UNPKG

@stainless-code/svelte-layers

Version:

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

248 lines (247 loc) 7.6 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 { createSubscriber } from "svelte/reactivity"; //#region src/index.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] No LayerClient in context — call setLayerClient() in a parent component."); return c; } function defaultSelector(states) { return states; } function makeSvelteStack(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; }; runSelect(getSource()); const subscribe = createSubscriber((update) => stack.subscribe(() => { const prevValue = cache?.value; runSelect(getSource()); if (prevValue === void 0 || !compare(prevValue, cache.value)) update(); })); return { get current() { subscribe(); return runSelect(getSource()); }, callFor(state, rootProps) { const layer = stack.getLayer(state.id); return layer ? createCallContext$1(stack, layer, state, rootProps) : null; } }; } /** * Exposes a {@link LayerClient} stack through Svelte 5 runes reactivity. * * @param opts Stack id, selector, and compare options. * @param client Optional client override; defaults to {@link useLayerClient}. * @returns A reactive stack accessor. * @default `stack` is `"default"`; `select` is identity; `compare` is * `Object.is`. * @example * ```svelte * <script> * import { useStack } from "@stainless-code/svelte-layers"; * * const stack = useStack({ stack: "confirm" }); * <\/script> * {#each stack.current as state (state.id)} * {@const call = stack.callFor(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 makeSvelteStack(stack, () => stack.getSnapshot(), opts.select ?? defaultSelector, opts.compare ?? Object.is); } /** * Exposes a stack's queued snapshot through Svelte 5 runes reactivity. */ function createQueuedStack(opts = {}, client) { const resolved = client ?? useLayerClient(); const stackId = opts.stack ?? "default"; const stack = resolved.getStack(stackId); return makeSvelteStack(stack, () => stack.getQueuedSnapshot(), opts.select ?? defaultSelector, opts.compare ?? Object.is); } /** * Observe all mounted layers matching a key. * * A {@link DataTag} key infers its response and error types. */ 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 stateObs = useStack({ stack: stackId, select: selectByKey, compare: shallowArrayEqual$1 }, resolved); const queuedObs = createQueuedStack({ stack: stackId, select: selectByKey, compare: shallowArrayEqual$1 }, resolved); const handle = createLayer$1(options, resolved); Object.defineProperties(handle, { state: { get() { return stateObs.current; }, enumerable: true }, queued: { get() { return queuedObs.current; }, enumerable: true }, top: { get() { return stateObs.current.at(-1) ?? null; }, enumerable: true } }); return handle; } function createLayer(options, client) { return createLayerImpl(options, client); } /** * 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"; * * let { call }: LayerComponentProps<void, boolean> = $props(); * const flow = useMutationFlow(call); * <\/script> * <button * disabled={flow.pending} * onclick={() => void flow.run(() => Promise.resolve()).orEnd(true)} * > * Confirm * </button> * ``` */ function useMutationFlow(call) { let pending = false; let notify = null; const subscribe = createSubscriber((update) => { notify = update; return () => { notify = null; }; }); return { get pending() { subscribe(); return pending; }, run: (fn) => ({ orEnd: async (response) => { pending = true; notify?.(); call.setRunning(true); try { await fn(); call.end(response); } finally { call.setRunning(false); pending = false; notify?.(); } } }) }; } /** * 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, childStackId, createCallContext, createLayer, createLayerGroup, createLayerHandle, createLayerQueuedState, createLayerState, createQueuedStack, hashKey, isPayloadValidationError, keySignature, layerKey, layerOptions, notifyManager, setLayerClient, shallowArrayEqual, useLayerClient, useLayerGroup, useMutationFlow, useStack };