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