UNPKG

@stainless-code/svelte-layers

Version:

Svelte adapter for @stainless-code/layers — call a layer like an async function and await its response.

228 lines (227 loc) 7.75 kB
import { ControlledPromise, Layer, LayerClient, LayerClient as LayerClient$1, LayerStack, PayloadValidationError, Subscribable, childStackId, childStackId as childStackId$1, createCallContext, createCallContext as createCallContext$1, createLayer as createLayer$1, createLayer as createLayerHandle, createLayerGroup, createLayerGroup as createLayerGroup$1, hashKey, isPayloadValidationError, keySignature, keySignature as keySignature$1, layerKey, layerOptions, notifyManager, shallowArrayEqual, shallowArrayEqual as shallowArrayEqual$1 } from "@stainless-code/layers"; import { getContext, onDestroy, setContext } from "svelte"; import { derived, readable, writable } from "svelte/store"; //#region src/store.ts const LAYER_CLIENT_KEY = Symbol("layers.client"); /** * Provides a {@link LayerClient} to descendant components. * * @param client Client to provide. A new client is created when omitted. * @returns The provided client. */ function setLayerClient(client) { const c = client ?? new LayerClient$1(); setContext(LAYER_CLIENT_KEY, c); return c; } /** * Reads the nearest {@link LayerClient} from Svelte context. * * @returns The nearest provided client. */ function useLayerClient() { const c = getContext(LAYER_CLIENT_KEY); if (!c) throw new Error("[layers/svelte-store] No LayerClient in context — call setLayerClient() in a parent component."); return c; } function defaultSelector(states) { return states; } function makeReadableStack(stack, getSource, select, compare) { let cache = null; const runSelect = (base) => { const prev = cache; if (prev && prev.base === base) return prev.value; const next = select(base); if (prev && compare(prev.value, next)) { cache = { base, value: prev.value }; return prev.value; } cache = { base, value: next }; return next; }; return readable(runSelect(getSource()), (set) => { const unsubscribe = stack.subscribe(() => { const prevValue = cache?.value; const next = runSelect(getSource()); if (prevValue === void 0 || !compare(prevValue, next)) set(next); }); set(runSelect(getSource())); return unsubscribe; }); } /** * Exposes a {@link LayerClient} stack as a Svelte readable store. * * Use `$stack` in components to subscribe. Pair each state with {@link callFor} * when rendering a layer. * * @param opts Stack id, selector, and compare options. * @param client Optional client override; defaults to {@link useLayerClient}. * @returns A readable store of the selected stack value. * @default `stack` is `"default"`; `select` is identity; `compare` is * `Object.is`. * @example * ```svelte * <script> * import { * callFor, * useLayerClient, * useStack, * } from "@stainless-code/svelte-layers/store"; * * const client = useLayerClient(); * const stack = useStack({ stack: "confirm" }); * <\/script> * {#each $stack as state (state.id)} * {@const call = callFor(client, "confirm", state)} * {#if call}<Confirm {call} />{/if} * {/each} * ``` */ function useStack(opts = {}, client) { const resolved = client ?? useLayerClient(); const stackId = opts.stack ?? "default"; const stack = resolved.getStack(stackId); return makeReadableStack(stack, () => stack.getSnapshot(), opts.select ?? defaultSelector, opts.compare ?? Object.is); } /** Exposes a stack's queued snapshot as a Svelte readable store. */ function createQueuedStack(opts = {}, client) { const resolved = client ?? useLayerClient(); const stackId = opts.stack ?? "default"; const stack = resolved.getStack(stackId); return makeReadableStack(stack, () => stack.getQueuedSnapshot(), opts.select ?? defaultSelector, opts.compare ?? Object.is); } /** Observe all mounted layers matching a key. */ function createLayerState(opts, client) { const sig = keySignature$1(opts.key); return useStack({ stack: opts.stack, select: (states) => { const filtered = states.filter((s) => keySignature$1(s.key) === sig); return opts.select ? opts.select(filtered) : filtered; }, compare: opts.compare ?? shallowArrayEqual$1 }, client); } /** Observe all queued layers matching a key. */ function createLayerQueuedState(opts, client) { const sig = keySignature$1(opts.key); return createQueuedStack({ stack: opts.stack, select: (states) => { const filtered = states.filter((s) => keySignature$1(s.key) === sig); return opts.select ? opts.select(filtered) : filtered; }, compare: opts.compare ?? shallowArrayEqual$1 }, client); } function createLayerImpl(options, client) { const resolved = client ?? useLayerClient(); const stackId = options.stack ?? "default"; const sig = keySignature$1(options.key); const selectByKey = (states) => states.filter((s) => keySignature$1(s.key) === sig); const handle = createLayer$1(options, resolved); const state = useStack({ stack: stackId, select: selectByKey, compare: shallowArrayEqual$1 }, resolved); const queued = createQueuedStack({ stack: stackId, select: selectByKey, compare: shallowArrayEqual$1 }, resolved); const top = derived(state, ($s) => $s.at(-1) ?? null); Object.assign(handle, { state, queued, top }); return handle; } function createLayer(options, client) { return createLayerImpl(options, client); } /** * Builds the call context for a layer state. * * @param client Client that owns the stack. * @param stackId Stack containing the state. * @param state State whose layer should receive the call. * @param rootProps Props supplied by the layer host. * @returns The call context, or `null` if the layer is no longer mounted. */ function callFor(client, stackId, state, rootProps) { const stack = client.getStack(stackId); const layer = stack.getLayer(state.id); return layer ? createCallContext$1(stack, layer, state, rootProps) : null; } /** * Coordinate a layer's pending state with an async mutation and end it on success. * * @example * ```svelte * <script lang="ts"> * import { type LayerComponentProps, useMutationFlow } from "@stainless-code/svelte-layers/store"; * * let { call }: LayerComponentProps<void, boolean> = $props(); * const { pending, run } = useMutationFlow(call); * <\/script> * <button disabled={$pending} onclick={() => void run(() => Promise.resolve()).orEnd(true)}> * Confirm * </button> * ``` */ function useMutationFlow(call) { const pending = writable(false); return { pending, run: (fn) => ({ orEnd: async (response) => { pending.set(true); call.setRunning(true); try { await fn(); call.end(response); } finally { call.setRunning(false); pending.set(false); } } }) }; } /** * Create a child stack scoped to the calling layer's lifetime. * * The child stack is disposed and cleared via `cancelAll` when its parent * layer unmounts (`LayerCancelledError`). */ function useLayerGroup(call, options) { const client = useLayerClient(); const stackId = childStackId$1(call, options?.name); const group = createLayerGroup$1(client, call, options); onDestroy(() => { group.dispose(); client.cancelAll(group.stackId, { reason: "groupDispose" }); }); const stack = useStack({ stack: stackId }); const open = ((opts) => client.open({ ...opts, stack: stackId })); const dismissAll = (response) => client.dismissAll(stackId, response); return { open, dismissAll, stack, stackId }; } //#endregion export { ControlledPromise, Layer, LayerClient, LayerStack, PayloadValidationError, Subscribable, callFor, childStackId, createCallContext, createLayer, createLayerGroup, createLayerHandle, createLayerQueuedState, createLayerState, createQueuedStack, hashKey, isPayloadValidationError, keySignature, layerKey, layerOptions, notifyManager, setLayerClient, shallowArrayEqual, useLayerClient, useLayerGroup, useMutationFlow, useStack };