@thomasfosterau/legation-svelte 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+ import * as ManagedRuntime from "effect/ManagedRuntime";
4
+ import { Client, ClientStore, Hydration } from "@thomasfosterau/legation";
5
+ import * as P from "effect/Predicate";
6
+ // src/kit/clientGroup.ts
7
+ /** The client-group cookie's default name. */
8
+ const CLIENT_GROUP_COOKIE = "legation-client-group";
9
+ const ONE_YEAR = 31536e3;
10
+ /**
11
+ * Read the browser's client group id from its cookie, minting one when
12
+ * absent. Call it in a server `load` (or `handle`) and render the page's
13
+ * snapshot under the returned id; the client boots its engine with the same
14
+ * id (it can read the cookie itself, or take the value from the load data).
15
+ *
16
+ * The cookie is deliberately not `httpOnly` — the client engine needs the
17
+ * same group id. It carries no authority: identity is resolved server-side
18
+ * through the `IdentityResolver` seam, never from this value.
19
+ */
20
+ const clientGroupId = (cookies, options) => {
21
+ const name = options?.name ?? "legation-client-group";
22
+ const existing = cookies.get(name);
23
+ if (existing !== void 0 && existing !== "") return existing;
24
+ const id = (options?.generate ?? (() => crypto.randomUUID()))();
25
+ cookies.set(name, id, {
26
+ path: options?.path ?? "/",
27
+ httpOnly: false,
28
+ sameSite: "lax",
29
+ maxAge: options?.maxAge ?? ONE_YEAR
30
+ });
31
+ return id;
32
+ };
33
+ // src/kit/load.ts
34
+ function loadSnapshot(cookies, options) {
35
+ return Effect.suspend(() => {
36
+ const group = clientGroupId(cookies, options.cookie);
37
+ return Hydration.snapshot({
38
+ ...options,
39
+ clientGroupId: group
40
+ }).pipe(Effect.map((snapshot) => ({
41
+ snapshot,
42
+ clientGroupId: group
43
+ })));
44
+ });
45
+ }
46
+ // src/kit/boot.ts
47
+ /**
48
+ * Client boot
49
+ *
50
+ * `clientRuntime` is the browser half of the SSR flow: hydrate the page's
51
+ * snapshot into the durable store — but only when the store is empty
52
+ * (`Hydration.hydrateIfEmpty`: a repeat visit's durable rows, cookie, and
53
+ * journalled offline mutations are all fresher than the snapshot) — then build
54
+ * the Client over that store.
55
+ *
56
+ * Both halves are core: `Hydration` does the moving of state, `Client.make`
57
+ * does the wiring. This is only the Svelte-shaped assembly of them, producing
58
+ * the `ManagedRuntime` {@link setClientRuntime} expects, so a root layout can
59
+ * hand it straight to context.
60
+ */
61
+ const hydrated = (store, snapshot) => snapshot === void 0 ? store : Layer.effect(ClientStore.ClientStore)(Effect.gen(function* () {
62
+ const built = yield* ClientStore.ClientStore;
63
+ yield* Hydration.hydrateIfEmpty(built, snapshot).pipe(Effect.orDie);
64
+ return built;
65
+ })).pipe(Layer.provide(store));
66
+ /**
67
+ * Build the tab's Client runtime: store (snapshot-hydrated when empty) →
68
+ * transport → `Client.make`, which starts the sync loop itself. Hand the
69
+ * result to `setClientRuntime`, and dispose it when the app tears down.
70
+ */
71
+ const clientRuntime = (options) => {
72
+ const { store, transport, snapshot, subscribe, ...client } = options;
73
+ return ManagedRuntime.make(Layer.effect(Client.Client)(Effect.gen(function* () {
74
+ const { client: built } = yield* Client.make(client);
75
+ if (subscribe !== void 0) yield* Effect.forEach(subscribe, (read) => built.subscribe(read), { discard: true }).pipe(Effect.orDie);
76
+ return built;
77
+ })).pipe(Layer.provide(transport), Layer.provide(hydrated(store, snapshot))));
78
+ };
79
+ // src/kit/formMutation.ts
80
+ /** The hidden field carrying the rendered form's form client id. */
81
+ const FORM_CLIENT_FIELD = "legation-form-client";
82
+ /** The hidden field carrying the form client's mutation id. */
83
+ const FORM_MUTATION_FIELD = "legation-form-mutation";
84
+ /** The key of the action result's data under which the receipt travels. */
85
+ const RECEIPT_KEY = "legationReceipt";
86
+ /**
87
+ * Mint a form client for one rendered form: a fresh id with mutation id `1`
88
+ * (ADR-0030 #215 §2). The page's server `load` mints one per form and renders
89
+ * it in the {@link FORM_CLIENT_FIELD} and {@link FORM_MUTATION_FIELD} hidden
90
+ * fields, so a resubmission of the same rendered form is the same mutation.
91
+ */
92
+ const mintFormClient = (generate = () => crypto.randomUUID()) => ({
93
+ clientId: `form-${generate()}`,
94
+ mutationId: 1
95
+ });
96
+ /** The form client a posted form names, or `undefined` when its fields are missing or malformed. */
97
+ const formClientOf = (data) => {
98
+ const clientId = data.get(FORM_CLIENT_FIELD);
99
+ const mutationId = Number(data.get(FORM_MUTATION_FIELD));
100
+ return P.isString(clientId) && clientId !== "" && Number.isInteger(mutationId) && mutationId >= 1 ? {
101
+ clientId,
102
+ mutationId
103
+ } : void 0;
104
+ };
105
+ /**
106
+ * Read where a form action's Mutation runs: the client group from its cookie
107
+ * (never minted here) and the form client from the posted fields. `undefined`
108
+ * means the action cannot name the group or the form: it stays an out-of-band
109
+ * writer.
110
+ */
111
+ const formMutationTarget = (cookies, data, options) => {
112
+ const group = cookies.get(options?.cookieName ?? "legation-client-group");
113
+ const form = formClientOf(data);
114
+ return group === void 0 || group === "" || form === void 0 ? void 0 : {
115
+ clientGroupId: group,
116
+ ...form
117
+ };
118
+ };
119
+ /**
120
+ * Run the form layer's decoded args as the target's Mutation on the server
121
+ * (`Server.mutate`) and answer the receipt. Return it from the action as
122
+ * `{ [RECEIPT_KEY]: receipt }`, so the enhanced submit settles on it; a
123
+ * rejected receipt's `reason` is the form layer's to route to a message.
124
+ * Args that do not fit the Mutation's schema are a defect, not a field error.
125
+ */
126
+ const runFormMutation = (server, target, mutation, args) => server.mutate({
127
+ clientGroupId: target.clientGroupId,
128
+ clientId: target.clientId,
129
+ mutationId: target.mutationId,
130
+ mutation,
131
+ args
132
+ });
133
+ /** The receipt an action result carries under {@link RECEIPT_KEY}, or `undefined`. */
134
+ const receiptOf = (result) => {
135
+ if (result.type !== "success" && result.type !== "failure") return void 0;
136
+ const receipt = result.data?.[RECEIPT_KEY];
137
+ if (!P.hasProperty(receipt, "clientId") || !P.hasProperty(receipt, "mutationId") || !P.hasProperty(receipt, "phase") || !P.isString(receipt.clientId) || !P.isNumber(receipt.mutationId) || receipt.phase !== "acknowledged" && receipt.phase !== "rejected") return;
138
+ if (receipt.phase === "acknowledged") return {
139
+ clientId: receipt.clientId,
140
+ mutationId: receipt.mutationId,
141
+ phase: "acknowledged"
142
+ };
143
+ const reason = P.hasProperty(receipt, "reason") && P.isString(receipt.reason) ? receipt.reason : "";
144
+ return {
145
+ clientId: receipt.clientId,
146
+ mutationId: receipt.mutationId,
147
+ phase: "rejected",
148
+ reason
149
+ };
150
+ };
151
+ /**
152
+ * The enhanced submit for a form-action Mutation: pass it to SvelteKit's
153
+ * `use:enhance`. Each submit mints a fresh form client and writes it into the
154
+ * posted fields, overlays the Mutation's local effect under it without
155
+ * journaling it (the action delivers it), and settles on the receipt the
156
+ * action returns under {@link RECEIPT_KEY}: acknowledged moves the effect to
157
+ * the unpulled tail, where the pull that covers the form client releases it;
158
+ * rejected drops it. A result without a receipt (an error, a redirect, a
159
+ * failed network) abandons the effect, and the next pull shows the server's
160
+ * state. While offline the submit fails as the network fails (ADR-0030 open
161
+ * point 6).
162
+ */
163
+ const enhanceFormMutation = (runtime, options) => ({ formData }) => {
164
+ const form = mintFormClient(options.generate);
165
+ formData.set(FORM_CLIENT_FIELD, form.clientId);
166
+ formData.set(FORM_MUTATION_FIELD, String(form.mutationId));
167
+ const begun = runtime.runPromise(Effect.gen(function* () {
168
+ return yield* (yield* Client.Client).beginFormMutation(form, options.mutation.name, options.args(formData));
169
+ }));
170
+ return async ({ result, update }) => {
171
+ const submission = await begun;
172
+ const receipt = receiptOf(result);
173
+ await runtime.runPromise(receipt === void 0 ? submission.abandon : submission.settle(receipt).pipe(Effect.orDie));
174
+ if (receipt !== void 0) options.onReceipt?.(receipt);
175
+ await update({ refreshAll: options.refreshAll ?? false });
176
+ };
177
+ };
178
+ export { CLIENT_GROUP_COOKIE, FORM_CLIENT_FIELD, FORM_MUTATION_FIELD, RECEIPT_KEY, clientGroupId, clientRuntime, enhanceFormMutation, formClientOf, formMutationTarget, loadSnapshot, mintFormClient, receiptOf, runFormMutation };
179
+
180
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/kit/clientGroup.ts","../../src/kit/load.ts","../../src/kit/boot.ts","../../src/kit/formMutation.ts"],"sourcesContent":["/**\n * Client-group cookie\n *\n * The SSR flow renders under the *browser's* client group id so the snapshot\n * cookie continues that group's CVR conversation (see `Hydration.snapshot` in\n * `@thomasfosterau/legation`). The group id therefore has to be stable per browser and\n * visible to both the server (each `load`) and the client (the engine's\n * `clientGroupId` option) — an ordinary, non-`httpOnly` HTTP cookie.\n *\n * `clientGroupId(cookies)` reads it, minting and setting it on first contact.\n */\nimport type { Cookies } from \"@sveltejs/kit\";\n\n/** Options for {@link clientGroupId}. */\nexport interface ClientGroupOptions {\n /** Cookie name. Default `\"legation-client-group\"`. */\n readonly name?: string;\n /** Generate a fresh id on first contact. Default `crypto.randomUUID`. */\n readonly generate?: () => string;\n /** Cookie path. Default `\"/\"`. */\n readonly path?: string;\n /** Cookie lifetime in seconds. Default one year. */\n readonly maxAge?: number;\n}\n\n/** The client-group cookie's default name. */\nexport const CLIENT_GROUP_COOKIE = \"legation-client-group\";\nconst ONE_YEAR = 60 * 60 * 24 * 365;\n\n/**\n * Read the browser's client group id from its cookie, minting one when\n * absent. Call it in a server `load` (or `handle`) and render the page's\n * snapshot under the returned id; the client boots its engine with the same\n * id (it can read the cookie itself, or take the value from the load data).\n *\n * The cookie is deliberately not `httpOnly` — the client engine needs the\n * same group id. It carries no authority: identity is resolved server-side\n * through the `IdentityResolver` seam, never from this value.\n */\nexport const clientGroupId = (cookies: Cookies, options?: ClientGroupOptions): string => {\n const name = options?.name ?? CLIENT_GROUP_COOKIE;\n const existing = cookies.get(name);\n if (existing !== undefined && existing !== \"\") return existing;\n const id = (options?.generate ?? (() => crypto.randomUUID()))();\n cookies.set(name, id, {\n path: options?.path ?? \"/\",\n httpOnly: false,\n sameSite: \"lax\",\n maxAge: options?.maxAge ?? ONE_YEAR,\n });\n return id;\n};\n","/**\n * Server `load` sugar\n *\n * `loadSnapshot` combines the client-group cookie with `Hydration.snapshot`:\n * resolve (or mint) the browser's group id, render the page's subsets over a\n * request-scoped engine, and return `{ snapshot, clientGroupId }` for the\n * page data. Run it inside any Effect-based server `load` (e.g.\n * `@thomasfosterau/effect-sveltekit`'s `serverLoad`). The snapshot itself is\n * core (`Hydration.snapshot`); this only folds the cookie into it.\n *\n * Where the backend lives decides what the `load` supplies. Same process:\n * provide the app's sync server as `ServerHandler` and pass no transport.\n * Separate Worker (the usual Cloudflare shape — a SvelteKit Worker in front\n * of a binding-only API Worker): pass a `transport` built from the request's\n * service binding (`BindingTransport` in `@thomasfosterau/legation-alchemy-cloudflare`), and nothing\n * is required from context.\n */\nimport type { Cookies } from \"@sveltejs/kit\";\nimport * as Effect from \"effect/Effect\";\nimport { Hydration, type Transport } from \"@thomasfosterau/legation\";\nimport type {\n MutatorError,\n StoreError,\n TransportError,\n VersionNotSupportedError,\n} from \"@thomasfosterau/legation\";\nimport { clientGroupId, type ClientGroupOptions } from \"./clientGroup.js\";\n\n/** Options for {@link loadSnapshot}. */\nexport interface LoadSnapshotOptions extends Omit<Hydration.SnapshotOptions, \"clientGroupId\"> {\n /** Client-group cookie options (name, generation, lifetime). */\n readonly cookie?: ClientGroupOptions;\n}\n\n/** {@link LoadSnapshotOptions} with the carrier supplied — no `ServerHandler` needed. */\nexport interface RemoteLoadSnapshotOptions extends LoadSnapshotOptions {\n readonly transport: Transport.ClientTransport;\n}\n\n/** What {@link loadSnapshot} returns — put both in the page data. */\nexport interface LoadedSnapshot {\n readonly snapshot: Hydration.Snapshot;\n readonly clientGroupId: string;\n}\n\ntype LoadSnapshotError = StoreError | TransportError | MutatorError | VersionNotSupportedError;\n\n/**\n * Resolve the request's client group id from its cookie and produce the SSR\n * snapshot for the given subsets under it.\n *\n * The group id is the point of the cookie: the render happens under the\n * *browser's* group, so the snapshot's pull cookie continues that group's CVR\n * conversation. That holds over a remote `transport` too — the id travels on\n * the pull request and the server's fresh cookie travels back.\n */\nexport function loadSnapshot(\n cookies: Cookies,\n options: RemoteLoadSnapshotOptions\n): Effect.Effect<LoadedSnapshot, LoadSnapshotError>;\n/** The in-process form: renders against the app's sync server, the `ServerHandler` in context. */\nexport function loadSnapshot(\n cookies: Cookies,\n options: LoadSnapshotOptions\n): Effect.Effect<LoadedSnapshot, LoadSnapshotError, Transport.ServerHandler>;\nexport function loadSnapshot(\n cookies: Cookies,\n options: LoadSnapshotOptions\n): Effect.Effect<LoadedSnapshot, LoadSnapshotError, Transport.ServerHandler> {\n return Effect.suspend(() => {\n const group = clientGroupId(cookies, options.cookie);\n return Hydration.snapshot({ ...options, clientGroupId: group }).pipe(\n Effect.map((snapshot) => ({ snapshot, clientGroupId: group }))\n );\n });\n}\n","/**\n * Client boot\n *\n * `clientRuntime` is the browser half of the SSR flow: hydrate the page's\n * snapshot into the durable store — but only when the store is empty\n * (`Hydration.hydrateIfEmpty`: a repeat visit's durable rows, cookie, and\n * journalled offline mutations are all fresher than the snapshot) — then build\n * the Client over that store.\n *\n * Both halves are core: `Hydration` does the moving of state, `Client.make`\n * does the wiring. This is only the Svelte-shaped assembly of them, producing\n * the `ManagedRuntime` {@link setClientRuntime} expects, so a root layout can\n * hand it straight to context.\n */\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport * as ManagedRuntime from \"effect/ManagedRuntime\";\nimport { Client, ClientStore, Hydration, type Transport } from \"@thomasfosterau/legation\";\nimport type { ClientRuntime } from \"../Runtime.js\";\n\n/** Options for {@link clientRuntime}. */\nexport interface ClientRuntimeOptions extends Omit<Client.MakeOptions, \"store\" | \"transport\"> {\n /** Fully-provided client store layer (e.g. `IndexedDbClientStore.layerWindow()`). */\n readonly store: Layer.Layer<ClientStore.ClientStore>;\n /** Fully-provided transport layer (RPC client, or an in-memory pair in tests). */\n readonly transport: Layer.Layer<Transport.ClientTransport>;\n /** The page's SSR snapshot, hydrated into an empty store before the Client boots. */\n readonly snapshot?: Hydration.Snapshot;\n /** Reads to pin for the runtime's lifetime (the app shell's working set). */\n readonly subscribe?: ReadonlyArray<Parameters<Client.Client[\"subscribe\"]>[0]>;\n}\n\nconst hydrated = (\n store: Layer.Layer<ClientStore.ClientStore>,\n snapshot: Hydration.Snapshot | undefined\n): Layer.Layer<ClientStore.ClientStore> =>\n snapshot === undefined\n ? store\n : Layer.effect(ClientStore.ClientStore)(\n Effect.gen(function* () {\n const built = yield* ClientStore.ClientStore;\n // A hydration failure is a boot-integrity defect, not a recoverable\n // runtime error — surface it as such.\n yield* Hydration.hydrateIfEmpty(built, snapshot).pipe(Effect.orDie);\n return built;\n })\n ).pipe(Layer.provide(store));\n\n/**\n * Build the tab's Client runtime: store (snapshot-hydrated when empty) →\n * transport → `Client.make`, which starts the sync loop itself. Hand the\n * result to `setClientRuntime`, and dispose it when the app tears down.\n */\nexport const clientRuntime = (options: ClientRuntimeOptions): ClientRuntime => {\n const { store, transport, snapshot, subscribe, ...client } = options;\n return ManagedRuntime.make(\n Layer.effect(Client.Client)(\n Effect.gen(function* () {\n const { client: built } = yield* Client.make(client);\n if (subscribe !== undefined) {\n yield* Effect.forEach(subscribe, (read) => built.subscribe(read), {\n discard: true,\n }).pipe(Effect.orDie);\n }\n return built;\n })\n ).pipe(Layer.provide(transport), Layer.provide(hydrated(store, snapshot)))\n );\n};\n","/**\n * Form-action Mutations (ADR-0030, amendment #215)\n *\n * A SvelteKit form action that can name the client group runs a named\n * Mutation as one of that group's mutations, under a **form client** id\n * minted for the rendered form with mutation id `1`. The mechanism is core\n * (`Server.mutate`); this module adds only what binds to SvelteKit:\n *\n * - Server: {@link mintFormClient} for the page that renders the form,\n * {@link formMutationTarget} to read the group cookie and the posted\n * form-client fields, and {@link runFormMutation} to hand the form layer's\n * decoded args to `Server.mutate`. The action returns the receipt under\n * {@link RECEIPT_KEY}.\n * - Client: {@link enhanceFormMutation}, the enhanced submit for `use:enhance`.\n * It overlays the Mutation's effect under a fresh form client, then settles\n * on the receipt: acknowledged moves it to the unpulled tail until the pull\n * that covers it, rejected drops it.\n *\n * An action that cannot name the group (no cookie) is not routed here: it\n * stays an out-of-band writer (ADR-0020, amended), committing through the\n * app's API and announcing the change with `Server.broadcast`.\n *\n * One path per write: a page writes a submission either through its form\n * action (this module) or through a façade write (#169, ADR-0030 §1–§2),\n * never both for the same submission, or the write would be applied twice.\n */\nimport type { Cookies } from \"@sveltejs/kit\";\nimport * as Effect from \"effect/Effect\";\nimport * as P from \"effect/Predicate\";\nimport {\n Client,\n type Mutation,\n type Server,\n type TransportError,\n type VersionNotSupportedError,\n} from \"@thomasfosterau/legation\";\nimport type { ClientRuntime } from \"../Runtime.js\";\nimport { CLIENT_GROUP_COOKIE } from \"./clientGroup.js\";\n\n/** The hidden field carrying the rendered form's form client id. */\nexport const FORM_CLIENT_FIELD = \"legation-form-client\";\n/** The hidden field carrying the form client's mutation id. */\nexport const FORM_MUTATION_FIELD = \"legation-form-mutation\";\n/** The key of the action result's data under which the receipt travels. */\nexport const RECEIPT_KEY = \"legationReceipt\";\n\n/** A form client: the id minted for one rendered form, and its mutation id (`1`). */\nexport interface FormClient {\n readonly clientId: string;\n readonly mutationId: number;\n}\n\n/**\n * Mint a form client for one rendered form: a fresh id with mutation id `1`\n * (ADR-0030 #215 §2). The page's server `load` mints one per form and renders\n * it in the {@link FORM_CLIENT_FIELD} and {@link FORM_MUTATION_FIELD} hidden\n * fields, so a resubmission of the same rendered form is the same mutation.\n */\nexport const mintFormClient = (generate: () => string = () => crypto.randomUUID()): FormClient => ({\n clientId: `form-${generate()}`,\n mutationId: 1,\n});\n\n/** The form client a posted form names, or `undefined` when its fields are missing or malformed. */\nexport const formClientOf = (data: FormData): FormClient | undefined => {\n const clientId = data.get(FORM_CLIENT_FIELD);\n const mutationId = Number(data.get(FORM_MUTATION_FIELD));\n return P.isString(clientId) && clientId !== \"\" && Number.isInteger(mutationId) && mutationId >= 1\n ? { clientId, mutationId }\n : undefined;\n};\n\n/** Where a form action's Mutation runs: the client group and the form client. */\nexport interface FormMutationTarget extends FormClient {\n readonly clientGroupId: string;\n}\n\n/** Options for {@link formMutationTarget}. */\nexport interface FormMutationTargetOptions {\n /** The client-group cookie's name. Default `\"legation-client-group\"`, as `clientGroupId` uses. */\n readonly cookieName?: string | undefined;\n}\n\n/**\n * Read where a form action's Mutation runs: the client group from its cookie\n * (never minted here) and the form client from the posted fields. `undefined`\n * means the action cannot name the group or the form: it stays an out-of-band\n * writer.\n */\nexport const formMutationTarget = (\n cookies: Cookies,\n data: FormData,\n options?: FormMutationTargetOptions\n): FormMutationTarget | undefined => {\n const group = cookies.get(options?.cookieName ?? CLIENT_GROUP_COOKIE);\n const form = formClientOf(data);\n return group === undefined || group === \"\" || form === undefined\n ? undefined\n : { clientGroupId: group, ...form };\n};\n\n/**\n * Run the form layer's decoded args as the target's Mutation on the server\n * (`Server.mutate`) and answer the receipt. Return it from the action as\n * `{ [RECEIPT_KEY]: receipt }`, so the enhanced submit settles on it; a\n * rejected receipt's `reason` is the form layer's to route to a message.\n * Args that do not fit the Mutation's schema are a defect, not a field error.\n */\nexport const runFormMutation = <Args, ArgsEncoded>(\n server: Server.Server,\n target: FormMutationTarget,\n mutation: Mutation.Mutator<string, Args, ArgsEncoded>,\n args: Args\n): Effect.Effect<Server.Receipt, TransportError | VersionNotSupportedError> =>\n server.mutate({\n clientGroupId: target.clientGroupId,\n clientId: target.clientId,\n mutationId: target.mutationId,\n mutation,\n args,\n });\n\n/**\n * The part of SvelteKit's `ActionResult` (from `$app/forms`) read here: its\n * type, and the data a `success` or `failure` result carries.\n */\nexport interface FormActionResult {\n readonly type: string;\n readonly data?: Readonly<Record<string, unknown>> | undefined;\n}\n\n/**\n * The submit function {@link enhanceFormMutation} returns, typed by the parts\n * of SvelteKit's `SubmitFunction` it uses, so it can be passed to `enhance`.\n */\nexport type FormSubmitFunction = (input: {\n readonly formData: FormData;\n}) => (opts: {\n readonly result: FormActionResult;\n readonly update: (options?: { refreshAll?: boolean }) => Promise<void>;\n}) => Promise<void>;\n\n/** The receipt an action result carries under {@link RECEIPT_KEY}, or `undefined`. */\nexport const receiptOf = (result: FormActionResult): Server.Receipt | undefined => {\n if (result.type !== \"success\" && result.type !== \"failure\") return undefined;\n const receipt: unknown = result.data?.[RECEIPT_KEY];\n if (\n !P.hasProperty(receipt, \"clientId\") ||\n !P.hasProperty(receipt, \"mutationId\") ||\n !P.hasProperty(receipt, \"phase\") ||\n !P.isString(receipt.clientId) ||\n !P.isNumber(receipt.mutationId) ||\n (receipt.phase !== \"acknowledged\" && receipt.phase !== \"rejected\")\n ) {\n return undefined;\n }\n if (receipt.phase === \"acknowledged\") {\n return { clientId: receipt.clientId, mutationId: receipt.mutationId, phase: \"acknowledged\" };\n }\n const reason =\n P.hasProperty(receipt, \"reason\") && P.isString(receipt.reason) ? receipt.reason : \"\";\n return { clientId: receipt.clientId, mutationId: receipt.mutationId, phase: \"rejected\", reason };\n};\n\n/** Options for {@link enhanceFormMutation}. */\nexport interface EnhanceFormMutationOptions<Args, ArgsEncoded> {\n /** The Mutation the form's action runs. */\n readonly mutation: Mutation.Mutator<string, Args, ArgsEncoded>;\n /** The Mutation's args, read from the submitted form data (the form layer's decode). */\n readonly args: (data: FormData) => Args;\n /**\n * Re-run the page's `load` functions after the action, SvelteKit's default.\n * Default `false`: the overlay already shows the write, and the pull that\n * covers it brings the confirmed rows (ADR-0030 #215 §4).\n */\n readonly refreshAll?: boolean | undefined;\n /** Mint the per-submit form client id. Default `crypto.randomUUID`. */\n readonly generate?: (() => string) | undefined;\n /** Called with the receipt once the submission settles, e.g. to show a rejection's reason. */\n readonly onReceipt?: ((receipt: Server.Receipt) => void) | undefined;\n}\n\n/**\n * The enhanced submit for a form-action Mutation: pass it to SvelteKit's\n * `use:enhance`. Each submit mints a fresh form client and writes it into the\n * posted fields, overlays the Mutation's local effect under it without\n * journaling it (the action delivers it), and settles on the receipt the\n * action returns under {@link RECEIPT_KEY}: acknowledged moves the effect to\n * the unpulled tail, where the pull that covers the form client releases it;\n * rejected drops it. A result without a receipt (an error, a redirect, a\n * failed network) abandons the effect, and the next pull shows the server's\n * state. While offline the submit fails as the network fails (ADR-0030 open\n * point 6).\n */\nexport const enhanceFormMutation =\n <Args, ArgsEncoded>(\n runtime: ClientRuntime,\n options: EnhanceFormMutationOptions<Args, ArgsEncoded>\n ): FormSubmitFunction =>\n ({ formData }) => {\n const form = mintFormClient(options.generate);\n formData.set(FORM_CLIENT_FIELD, form.clientId);\n formData.set(FORM_MUTATION_FIELD, String(form.mutationId));\n const begun = runtime.runPromise(\n Effect.gen(function* () {\n const client = yield* Client.Client;\n return yield* client.beginFormMutation(form, options.mutation.name, options.args(formData));\n })\n );\n return async ({ result, update }) => {\n const submission = await begun;\n const receipt = receiptOf(result);\n await runtime.runPromise(\n receipt === undefined ? submission.abandon : submission.settle(receipt).pipe(Effect.orDie)\n );\n if (receipt !== undefined) options.onReceipt?.(receipt);\n await update({ refreshAll: options.refreshAll ?? false });\n };\n };\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@thomasfosterau/legation-svelte",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Svelte bindings for the legation engine: rune hooks over the Client, plus the SvelteKit integration on /kit",
5
+ "keywords": [
6
+ "effect",
7
+ "runes",
8
+ "ssr",
9
+ "svelte",
10
+ "sveltekit",
11
+ "sync-engine",
12
+ "typescript"
13
+ ],
14
+ "homepage": "https://github.com/thomasfosterau/legation/tree/trunk/packages/svelte#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/thomasfosterau/legation/issues"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Thomas Foster",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/thomasfosterau/legation.git",
23
+ "directory": "packages/svelte"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE",
29
+ "CHANGELOG.md"
30
+ ],
31
+ "type": "module",
32
+ "sideEffects": false,
33
+ "main": "./dist/index.svelte.js",
34
+ "types": "./dist/index.svelte.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.svelte.d.ts",
38
+ "svelte": "./dist/index.svelte.js",
39
+ "import": "./dist/index.svelte.js"
40
+ },
41
+ "./kit": {
42
+ "types": "./dist/kit/index.d.ts",
43
+ "import": "./dist/kit/index.js"
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "dependencies": {
51
+ "@thomasfosterau/effect-svelte": "^0.6.0",
52
+ "@thomasfosterau/legation": "^0.1.0"
53
+ },
54
+ "devDependencies": {
55
+ "@effect/vitest": "4.1.2",
56
+ "@sveltejs/kit": "^3.1.1",
57
+ "effect": "4.1.2",
58
+ "svelte": "^5.57.3",
59
+ "vitest": "5.0.5"
60
+ },
61
+ "peerDependencies": {
62
+ "@sveltejs/kit": "^2.0.0 || ^3.0.0",
63
+ "effect": "^4.0.2",
64
+ "svelte": ">=5.40"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "@sveltejs/kit": {
68
+ "optional": true
69
+ }
70
+ }
6
71
  }