@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
JavaScript
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 };