Components
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>
}