Skip to content

Components

Ember
gts
import type { TOC } from '@ember/component/template-only';
import { service } from '@ember/service';
import Component from '@glimmer/component';

import { importSync, macroCondition, moduleExists } from '@embroider/macros';
import type { ComponentLike } from '@glint/template';

import type { RequestManager, Store } from '@warp-drive/core';
import { assert } from '@warp-drive/core/build-config/macros';
import type {
  ContentFeatures,
  RecoveryFeatures,
  RequestArgs,
  RequestLoadingState,
  RequestState,
  RequestSubscription,
} from '@warp-drive/core/store/-private';
import { createRequestSubscription, DISPOSE, memoized } from '@warp-drive/core/store/-private';
import type { StructuredErrorDocument } from '@warp-drive/core/types/request';

import { and, Throw } from './await.gts';

export type { ContentFeatures, RecoveryFeatures };

function notNull(x: null): never;
function notNull<T>(x: T): Exclude<T, null>;
function notNull<T>(x: T | null) {
  assert('Expected a non-null value, but got null', x !== null);
  return x;
}

const not = (x: unknown) => !x;
const IdleBlockMissingError = new Error(
  'No idle block provided for <Request> component, and no query or request was provided.'
);

let consume = service;
if (macroCondition(moduleExists('ember-provide-consume-context'))) {
  const { consume: contextConsume } = importSync('ember-provide-consume-context') as { consume: typeof service };
  consume = contextConsume;
}

const DefaultChrome: TOC<{
  Blocks: {
    default: [];
  };
}> = <template>{{yield}}</template>;

export interface EmberRequestArgs<RT, E> extends RequestArgs<RT, E> {
  chrome?: ComponentLike<{
    Blocks: { default: [] };
    Args: { state: RequestState | null; features: ContentFeatures<RT> };
  }>;
}

interface RequestSignature<RT, E> {
  Args: EmberRequestArgs<RT, E>;
  Blocks: {
    idle: [];
    loading: [state: RequestLoadingState];
    cancelled: [
      error: StructuredErrorDocument<E>,
      features: RecoveryFeatures,
    ];

    error: [
      error: StructuredErrorDocument<E>,
      features: RecoveryFeatures,
    ];

    content: [value: RT, features: ContentFeatures<RT>];
    always: [state: RequestState<RT, StructuredErrorDocument<E>>];
  };
}

export class Request<RT, E> extends Component<RequestSignature<RT, E>> {
  @consume('store') declare _store: Store;

  get store(): Store | RequestManager {
    const store = this.args.store || this._store;
    assert(
      moduleExists('ember-provide-consume-context')
        ? `No store was provided to the <Request> component. Either provide a store via the @store arg or via the context API provided by ember-provide-consume-context.`
        : `No store was provided to the <Request> component. Either provide a store via the @store arg or by registering a store service.`,
      store
    );
    return store;
  }

  _state: RequestSubscription<RT, E> | null = null;
  get state(): RequestSubscription<RT, E> {
    let { _state } = this;
    const { store } = this;
    const { subscription } = this.args;
    if (_state && (_state.store !== store || subscription)) {
      _state[DISPOSE]();
      _state = null;
    }

    if (subscription) {
      return subscription;
    }

    if (!_state) {
      this._state = _state = createRequestSubscription(store, this.args);
    }

    return _state;
  }

  @memoized
  get Chrome(): ComponentLike<{
    Blocks: { default: [] };
    Args: { state: RequestState | null; features: ContentFeatures<RT> };
  }> {
    return this.args.chrome || DefaultChrome;
  }

  willDestroy(): void {
    if (this._state) {
      this._state[DISPOSE]();
      this._state = null;
    }
  }

  <template>
    <this.Chrome @state={{if this.state.isIdle null this.state.reqState}} @features={{this.state.contentFeatures}}>
      {{#if (and this.state.isIdle (has-block "idle"))}}
        {{yield to="idle"}}

      {{else if this.state.isIdle}}
        <Throw @error={{IdleBlockMissingError}} />

      {{else if this.state.reqState.isLoading}}
        {{yield this.state.reqState.loadingState to="loading"}}

      {{else if (and this.state.reqState.isCancelled (has-block "cancelled"))}}
        {{yield (notNull this.state.reqState.reason) this.state.errorFeatures to="cancelled"}}

      {{else if (and this.state.reqState.isError (has-block "error"))}}
        {{yield (notNull this.state.reqState.reason) this.state.errorFeatures to="error"}}

      {{else if this.state.reqState.isSuccess}}
        {{yield this.state.result this.state.contentFeatures to="content"}}

      {{else if (not this.state.reqState.isCancelled)}}
        <Throw @error={{(notNull this.state.reqState.reason)}} />
      {{/if}}

      {{yield this.state.reqState to="always"}}
    </this.Chrome>
  </template>
}

Released under the MIT License.