@thomasfosterau/legation-svelte 0.0.0 → 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.
- package/CHANGELOG.md +644 -0
- package/LICENSE +21 -0
- package/README.md +389 -1
- package/dist/Runtime-DAQq0HCa.d.ts +66 -0
- package/dist/Runtime-DAQq0HCa.d.ts.map +1 -0
- package/dist/index.svelte.d.ts +177 -0
- package/dist/index.svelte.d.ts.map +1 -0
- package/dist/index.svelte.js +401 -0
- package/dist/index.svelte.js.map +1 -0
- package/dist/kit/index.d.ts +171 -0
- package/dist/kit/index.d.ts.map +1 -0
- package/dist/kit/index.js +180 -0
- package/dist/kit/index.js.map +1 -0
- package/package.json +60 -3
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { createRuntimeContext, useLiveStream } from "@thomasfosterau/effect-svelte";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as ManagedRuntime from "effect/ManagedRuntime";
|
|
5
|
+
import * as Option from "effect/Option";
|
|
6
|
+
import { getContext, hasContext, setContext } from "svelte";
|
|
7
|
+
import { Aggregate, Client, Query } from "@thomasfosterau/legation";
|
|
8
|
+
import * as Schema from "effect/Schema";
|
|
9
|
+
import * as Context from "effect/Context";
|
|
10
|
+
import * as Fiber from "effect/Fiber";
|
|
11
|
+
import * as HashMap from "effect/HashMap";
|
|
12
|
+
import * as P from "effect/Predicate";
|
|
13
|
+
import * as R from "effect/Record";
|
|
14
|
+
import * as Stream from "effect/Stream";
|
|
15
|
+
import { AsyncResult } from "effect/reactivity";
|
|
16
|
+
// src/unavailable.ts
|
|
17
|
+
/**
|
|
18
|
+
* The failure of a hook that reaches for a Client where none was set (#147):
|
|
19
|
+
* `setClient(undefined)`, as on the server. Kept apart from `Runtime.ts` so the
|
|
20
|
+
* hooks can raise it without importing the context module's public surface.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* A hook reached for the Client where none was set: `setClient(undefined)`,
|
|
24
|
+
* as on the server, where a hook renders from its `initial` seed and fails
|
|
25
|
+
* only when it needs the Client itself.
|
|
26
|
+
*/
|
|
27
|
+
var ClientUnavailable = class extends Schema.TaggedError()("ClientUnavailable", { message: Schema.String }) {};
|
|
28
|
+
/** The {@link ClientUnavailable} a hook fails with when no Client was set. */
|
|
29
|
+
const unavailable = () => new ClientUnavailable({ message: "@thomasfosterau/legation-svelte: no Client here (setClient was given none, as on the server). A hook renders from its `initial` seed without one; this one needed the Client." });
|
|
30
|
+
// src/Runtime.ts
|
|
31
|
+
/**
|
|
32
|
+
* Svelte client context
|
|
33
|
+
*
|
|
34
|
+
* The rune hooks reach the {@link Client.Client} — ADR-0022's one-per-thread
|
|
35
|
+
* object, so where a read is answered (this thread's engine, a worker-hosted
|
|
36
|
+
* one, the server) is configuration the components never see.
|
|
37
|
+
*
|
|
38
|
+
* An app builds a {@link ClientHandle} with {@link createClient} (#147) and
|
|
39
|
+
* hands it to the component tree with {@link setClient}; the hooks read it
|
|
40
|
+
* back, so they must be called during component initialisation (the same
|
|
41
|
+
* rule Svelte's `getContext` follows). The runtime the hooks run over is an
|
|
42
|
+
* implementation detail of the handle. An app with its own layers (an
|
|
43
|
+
* in-page server, a service-worker mirror, a simulated-offline transport)
|
|
44
|
+
* builds the runtime itself and wraps it with {@link fromRuntime}, or passes
|
|
45
|
+
* it to {@link setClientRuntime}.
|
|
46
|
+
*
|
|
47
|
+
* On the server there may be no Client at all: `setClient(undefined)` renders
|
|
48
|
+
* every hook from its `initial` seed, and only a hook that actually reaches
|
|
49
|
+
* for the Client (a write, an unseeded read) fails, with
|
|
50
|
+
* {@link ClientUnavailable}.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Build the Client a Svelte app boots with (#147): `Client.make`'s options in,
|
|
54
|
+
* a {@link ClientHandle} out, with no Effect runtime for the app to build. The
|
|
55
|
+
* Client is built on first use, so creating the handle does no work; with
|
|
56
|
+
* `transport: { url }` it is the standard remote setup.
|
|
57
|
+
*/
|
|
58
|
+
const createClient = (options) => fromRuntime(ManagedRuntime.make(Layer.effect(Client.Client)(Effect.map(Client.make(options), (made) => made.client))));
|
|
59
|
+
/**
|
|
60
|
+
* Wrap a runtime the app built itself, for the custom-layer path: an in-page
|
|
61
|
+
* server, a service-worker mirror, a simulated-offline transport.
|
|
62
|
+
*/
|
|
63
|
+
const fromRuntime = (runtime) => ({
|
|
64
|
+
runtime,
|
|
65
|
+
dispose: () => runtime.dispose()
|
|
66
|
+
});
|
|
67
|
+
const clientContext = createRuntimeContext("@thomasfosterau/legation-svelte — call setClient(createClient(…)) higher in the component tree");
|
|
68
|
+
const handleKey = Symbol.for("@thomasfosterau/legation-svelte/ClientHandle");
|
|
69
|
+
/**
|
|
70
|
+
* The runtime a tree with no Client runs its hooks over. Building it is lazy,
|
|
71
|
+
* so it costs nothing until a hook reaches for the Client, and then it fails.
|
|
72
|
+
*/
|
|
73
|
+
const unavailableRuntime = /* @__PURE__ */ ManagedRuntime.make(Layer.effect(Client.Client)(Effect.die(unavailable())));
|
|
74
|
+
/**
|
|
75
|
+
* Provide the Client to descendant components. Call once, high in the tree,
|
|
76
|
+
* before any rune hook. `undefined` (on the server, say) renders every hook
|
|
77
|
+
* from its `initial` seed; a hook that needs the Client then fails with
|
|
78
|
+
* {@link ClientUnavailable}.
|
|
79
|
+
*/
|
|
80
|
+
const setClient = (client) => {
|
|
81
|
+
setContext(handleKey, Option.fromUndefinedOr(client));
|
|
82
|
+
clientContext.provide(client === void 0 ? unavailableRuntime : client.runtime);
|
|
83
|
+
};
|
|
84
|
+
/** The Client put in context by {@link setClient}: `None` when it was given none. */
|
|
85
|
+
const getClient = () => hasContext(handleKey) ? getContext(handleKey) : Option.none();
|
|
86
|
+
/**
|
|
87
|
+
* Provide a Client runtime the app built itself: the escape hatch beside
|
|
88
|
+
* {@link setClient}, the same as `setClient(fromRuntime(runtime))`.
|
|
89
|
+
*/
|
|
90
|
+
const setClientRuntime = (runtime) => {
|
|
91
|
+
setClient(fromRuntime(runtime));
|
|
92
|
+
};
|
|
93
|
+
/** Read the Client runtime put in context by {@link setClient} or {@link setClientRuntime}. */
|
|
94
|
+
const getClientRuntime = () => clientContext.get();
|
|
95
|
+
// src/hooks.svelte.ts
|
|
96
|
+
/**
|
|
97
|
+
* Rune-based live queries (Svelte 5)
|
|
98
|
+
*
|
|
99
|
+
* The `use*` hooks speak **Query over the Client** (ADR-0022): reads are
|
|
100
|
+
* declarative `Query.Read`s — collection, `where`, `include`d edges with
|
|
101
|
+
* per-edge windows, `order`, cursor pagination — and where a read is
|
|
102
|
+
* answered (replica, worker-hosted engine, server fallthrough) is the
|
|
103
|
+
* Client's business, never the component's. Each hook returns an object
|
|
104
|
+
* whose reactive `current` is an `AsyncResult` (the same result vocabulary
|
|
105
|
+
* as the engine's reactivity atoms and the React binding), plus an
|
|
106
|
+
* awaitable `promise` for Async Svelte. Inputs are thunks called inside
|
|
107
|
+
* `$effect`, so a hook re-subscribes whenever the reactive state its thunk
|
|
108
|
+
* reads changes — a read whose cursor lives in `$state` just works — while
|
|
109
|
+
* the previous value stays visible as a waiting result
|
|
110
|
+
* (stale-while-revalidate).
|
|
111
|
+
*
|
|
112
|
+
* Joins need no separate hook: `include` resolves edges onto each row
|
|
113
|
+
* (`row.related`), windowed per the edge's declared order (ADR-0024), over
|
|
114
|
+
* owned and worker-hosted engines alike.
|
|
115
|
+
*
|
|
116
|
+
* Re-exported from the package root (`@thomasfosterau/legation-svelte`), which is the only
|
|
117
|
+
* read/write surface. Requires Svelte 5. Hooks must be called during
|
|
118
|
+
* component initialisation — they read the Client runtime from context
|
|
119
|
+
* ({@link setClientRuntime}) and register a `$effect`.
|
|
120
|
+
*/
|
|
121
|
+
const EMPTY_ROWS = [];
|
|
122
|
+
/** A `promise` that never settles — `.promise` while a read thunk opts out. */
|
|
123
|
+
const NEVER_PROMISE = new Promise(() => {});
|
|
124
|
+
/**
|
|
125
|
+
* Core plumbing — effect-svelte's `useLiveStream` does the work (thunk
|
|
126
|
+
* tracked inside `$effect`, interrupt-and-resubscribe on input change with
|
|
127
|
+
* the previous value kept as a waiting result, emissions in `$state.raw`,
|
|
128
|
+
* teardown interruption never surfacing as a failure, `initial` seeding a
|
|
129
|
+
* synchronous `Success`). This adapter resolves the Client from the
|
|
130
|
+
* context runtime and keeps the SSR seam below.
|
|
131
|
+
*
|
|
132
|
+
* `makeStream` returning `undefined` is the read thunk's opt-out (#141):
|
|
133
|
+
* "nothing right now". No window is ever registered and no request is
|
|
134
|
+
* issued, because the closure that would call `client.observe`/
|
|
135
|
+
* `view.observeNamed` never runs — `Stream.never` stands in, so
|
|
136
|
+
* `useLiveStream`'s own subscription does nothing. `optedOut` then forces
|
|
137
|
+
* `current`/`promise` back to their pre-subscription state regardless of
|
|
138
|
+
* what that never-emitting subscription leaves behind in `useLiveStream`'s
|
|
139
|
+
* state (stale-while-revalidate would otherwise keep a prior success
|
|
140
|
+
* visible, tagged "waiting", forever) — so an opted-out read stays
|
|
141
|
+
* `Initial`, distinguishable from a `Success` over zero rows, whether the
|
|
142
|
+
* thunk never ran or ran once and opted back out.
|
|
143
|
+
*/
|
|
144
|
+
const live = (runtime, makeStream, options) => {
|
|
145
|
+
let optedOut = $state(false);
|
|
146
|
+
const pick = () => {
|
|
147
|
+
const toStream = makeStream();
|
|
148
|
+
optedOut = toStream === void 0;
|
|
149
|
+
return toStream;
|
|
150
|
+
};
|
|
151
|
+
const seed = options?.initial === void 0 ? {} : { initial: options.initial };
|
|
152
|
+
const result = Option.isNone(getClient()) ? useLiveStream(() => pick() === void 0 ? Stream.never : Stream.die(unavailable()), {
|
|
153
|
+
runtime: Context.empty(),
|
|
154
|
+
...seed
|
|
155
|
+
}) : useLiveStream(() => {
|
|
156
|
+
const toStream = pick();
|
|
157
|
+
return toStream === void 0 ? Stream.never : Stream.unwrap(Effect.map(Client.Client, (client) => toStream(client)));
|
|
158
|
+
}, {
|
|
159
|
+
runtime,
|
|
160
|
+
...seed
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
get current() {
|
|
164
|
+
return optedOut ? AsyncResult.initial() : result.current;
|
|
165
|
+
},
|
|
166
|
+
get promise() {
|
|
167
|
+
if (P.isUndefined(globalThis.document) && options?.initial !== void 0) return options.initial;
|
|
168
|
+
return optedOut ? NEVER_PROMISE : result.promise;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Live result of a declarative read, re-subscribing when the thunk's
|
|
174
|
+
* reactive inputs change. `include`d edges resolve onto each row's
|
|
175
|
+
* `related`, windowed per the edge's declared order (ADR-0024). Observing
|
|
176
|
+
* IS subscribing: the read's windows are registered for the hook's
|
|
177
|
+
* lifetime through the Client's ledger.
|
|
178
|
+
*
|
|
179
|
+
* The thunk may return `undefined` to opt out (#141) — "nothing right now",
|
|
180
|
+
* for a read keyed on a selection that starts empty. No window is
|
|
181
|
+
* registered and no request is issued; `current` stays `Initial` rather
|
|
182
|
+
* than becoming a `Success` over zero rows, so an opted-out read stays
|
|
183
|
+
* distinguishable from one that legitimately matched nothing. The thunk
|
|
184
|
+
* stays reactive either way: flipping the condition subscribes/unsubscribes
|
|
185
|
+
* with the same stale-while-revalidate behaviour as any other input change.
|
|
186
|
+
*/
|
|
187
|
+
const useQuery = (read, options) => live(getClientRuntime(), () => {
|
|
188
|
+
const value = read();
|
|
189
|
+
if (value === void 0) return void 0;
|
|
190
|
+
return (client) => client.observe(value);
|
|
191
|
+
}, options);
|
|
192
|
+
/** The base read minus its window — the hook owns `first`/`after`. */
|
|
193
|
+
const connectionKey = (read) => {
|
|
194
|
+
const { first: _first, after: _after, ...base } = read;
|
|
195
|
+
return JSON.stringify(base);
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Connection-style paging over a read's root window: a live result that
|
|
199
|
+
* *grows* rather than jumping between detached pages. `loadMore` widens the
|
|
200
|
+
* one window by a page (ADR-0024 — "load more widens the subset"), so each
|
|
201
|
+
* page's rows accumulate and the previous ones stay on screen through the
|
|
202
|
+
* widen (stale-while-revalidate); the engine only ever syncs the current
|
|
203
|
+
* window, never the whole collection.
|
|
204
|
+
*
|
|
205
|
+
* Per-parent paging falls out of instantiating the hook per parent: scope
|
|
206
|
+
* the read's `where` to a parent id and give each parent its own
|
|
207
|
+
* `useConnection` (one per `{#each parents}` row), and each parent's list
|
|
208
|
+
* pages independently, with its own `hasMore` and window. The read thunk
|
|
209
|
+
* stays reactive — when its base (collection/where/order/include) changes the
|
|
210
|
+
* connection re-bases to the first page automatically; the window it owns
|
|
211
|
+
* (`first`/`after`) is ignored on the passed read.
|
|
212
|
+
*
|
|
213
|
+
* ```svelte
|
|
214
|
+
* const reviews = useConnection(() => productReviews(id), { pageSize: 10 });
|
|
215
|
+
* // {#each await reviews.promise as row (row.value.id)} … {/each}
|
|
216
|
+
* // <button onclick={reviews.loadMore} disabled={!reviews.hasMore}>More</button>
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* The read thunk may return `undefined` to opt out (#141): no window is
|
|
220
|
+
* registered and no request issued, `current` stays `Initial`, and paging
|
|
221
|
+
* state (`hasMore`/`loading`) reads as idle until the thunk starts returning
|
|
222
|
+
* a read again.
|
|
223
|
+
*
|
|
224
|
+
* **Engine-level retargeting (#133).** When the read has no `include`d edges
|
|
225
|
+
* and the view is engine-backed (`ClientView.observeWindow` present — a
|
|
226
|
+
* `Mirror` has none, see `docs/observing-row-changes.md`), `loadMore`/`reset`
|
|
227
|
+
* widen or shrink the SAME open observation's window via
|
|
228
|
+
* `ClientEngine.observeWindow`'s `retarget` — reusing `LiveView`'s maintained
|
|
229
|
+
* order instead of tearing the stream down and resubscribing wider each page.
|
|
230
|
+
* The stream itself is still built once per *base* change (collection/where/
|
|
231
|
+
* order/include), exactly as before; only `pages` no longer forces a rebuild.
|
|
232
|
+
* A joined read, or a mirror-backed connection, falls back to the original
|
|
233
|
+
* resubscribe-per-page behavior, unchanged.
|
|
234
|
+
*/
|
|
235
|
+
const useConnection = (read, options) => {
|
|
236
|
+
const pageSize = options?.pageSize ?? 20;
|
|
237
|
+
const runtime = getClientRuntime();
|
|
238
|
+
let pages = $state(1);
|
|
239
|
+
let baseKey;
|
|
240
|
+
$effect(() => {
|
|
241
|
+
const value = read();
|
|
242
|
+
const key = value === void 0 ? void 0 : connectionKey(value);
|
|
243
|
+
if (key !== baseKey) {
|
|
244
|
+
baseKey = key;
|
|
245
|
+
pages = 1;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
let retargetHandle;
|
|
249
|
+
const windowed = live(runtime, () => {
|
|
250
|
+
const value = read();
|
|
251
|
+
if (value === void 0) {
|
|
252
|
+
retargetHandle = void 0;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const { first: _first, after: _after, ...base } = value;
|
|
256
|
+
const key = connectionKey(value);
|
|
257
|
+
return (client) => Stream.unwrap(Effect.gen(function* () {
|
|
258
|
+
const observeWindow = client.view.observeWindow;
|
|
259
|
+
if (!R.isEmptyReadonlyRecord(base.include ?? {}) || observeWindow === void 0) {
|
|
260
|
+
retargetHandle = void 0;
|
|
261
|
+
return client.observe({
|
|
262
|
+
...base,
|
|
263
|
+
first: pages * pageSize
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const opened = yield* observeWindow(Query.plan(client.nodes, {
|
|
267
|
+
...base,
|
|
268
|
+
first: pageSize
|
|
269
|
+
}).root);
|
|
270
|
+
retargetHandle = {
|
|
271
|
+
baseKey: key,
|
|
272
|
+
retarget: (limit) => {
|
|
273
|
+
runtime.runFork(opened.retarget({ limit }));
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
if (pages > 1) retargetHandle.retarget(pages * pageSize);
|
|
277
|
+
return opened.rows.pipe(Stream.orDie, Stream.map((rows) => rows.map((row) => ({
|
|
278
|
+
...row,
|
|
279
|
+
related: {}
|
|
280
|
+
}))));
|
|
281
|
+
}).pipe(Effect.orDie));
|
|
282
|
+
}, options);
|
|
283
|
+
const rows = () => AsyncResult.getOrElse(windowed.current, () => EMPTY_ROWS);
|
|
284
|
+
const widenTo = (nextPages) => {
|
|
285
|
+
pages = nextPages;
|
|
286
|
+
const handle = retargetHandle;
|
|
287
|
+
if (handle !== void 0 && handle.baseKey === baseKey) handle.retarget(pages * pageSize);
|
|
288
|
+
};
|
|
289
|
+
return {
|
|
290
|
+
get current() {
|
|
291
|
+
return windowed.current;
|
|
292
|
+
},
|
|
293
|
+
get promise() {
|
|
294
|
+
return windowed.promise;
|
|
295
|
+
},
|
|
296
|
+
get hasMore() {
|
|
297
|
+
return rows().length >= pages * pageSize;
|
|
298
|
+
},
|
|
299
|
+
get loading() {
|
|
300
|
+
return AsyncResult.isWaiting(windowed.current);
|
|
301
|
+
},
|
|
302
|
+
loadMore: () => {
|
|
303
|
+
if (!AsyncResult.isWaiting(windowed.current) && rows().length >= pages * pageSize) widenTo(pages + 1);
|
|
304
|
+
},
|
|
305
|
+
reset: () => widenTo(1)
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Live result of a named query (server-resolved), with reactive name/args.
|
|
310
|
+
* The thunk may return `undefined` to opt out (#141): see {@link useQuery}.
|
|
311
|
+
*/
|
|
312
|
+
const useNamedQuery = (query, options) => live(getClientRuntime(), () => {
|
|
313
|
+
const value = query();
|
|
314
|
+
if (value === void 0) return void 0;
|
|
315
|
+
const { name, args } = value;
|
|
316
|
+
return (client) => client.view.observeNamed(name, args).pipe(Stream.orDie);
|
|
317
|
+
}, options);
|
|
318
|
+
/**
|
|
319
|
+
* Live aggregate over a read's root window (count/sum/min/max/avg). Local
|
|
320
|
+
* tier only — the aggregate sees the rows this client has synced. The read
|
|
321
|
+
* thunk may return `undefined` to opt out (#141): see {@link useQuery}.
|
|
322
|
+
*/
|
|
323
|
+
const useAggregate = (read, aggregate, options) => live(getClientRuntime(), () => {
|
|
324
|
+
const value = read();
|
|
325
|
+
if (value === void 0) return void 0;
|
|
326
|
+
return (client) => Aggregate.observe(client.view.observe(Query.plan(client.nodes, value).root).pipe(Stream.orDie), aggregate);
|
|
327
|
+
}, options);
|
|
328
|
+
/**
|
|
329
|
+
* Live grouped aggregate over a read's root window, keyed by
|
|
330
|
+
* `String(row.value[groupBy])`. The read thunk may return `undefined` to
|
|
331
|
+
* opt out (#141): see {@link useQuery}.
|
|
332
|
+
*/
|
|
333
|
+
const useAggregateGrouped = (read, aggregate, groupBy, options) => live(getClientRuntime(), () => {
|
|
334
|
+
const value = read();
|
|
335
|
+
if (value === void 0) return void 0;
|
|
336
|
+
return (client) => Aggregate.observeGrouped(client.view.observe(Query.plan(client.nodes, value).root).pipe(Stream.orDie), aggregate, groupBy);
|
|
337
|
+
}, options);
|
|
338
|
+
/**
|
|
339
|
+
* A `mutate(mutation, args)` bound to the context Client, resolving on
|
|
340
|
+
* journal (the write is applied optimistically before the promise settles).
|
|
341
|
+
*/
|
|
342
|
+
const useMutation = () => {
|
|
343
|
+
const runtime = getClientRuntime();
|
|
344
|
+
return (mutation, args) => runtime.runPromise(Effect.flatMap(Client.Client, (client) => client.mutate(mutation, args)));
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* Like {@link useMutation}, but resolving with the mutation's authoritative
|
|
348
|
+
* outcome once the backend settles it.
|
|
349
|
+
*/
|
|
350
|
+
const useMutationWithAck = () => {
|
|
351
|
+
const runtime = getClientRuntime();
|
|
352
|
+
return (mutation, args) => runtime.runPromise(Effect.flatMap(Client.Client, (client) => client.mutateWithAck(mutation, args)));
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* Reactive mutation-state list (#116) — TanStack Query's `useMutationState`
|
|
356
|
+
* analogue: every journaled mutation's current lifecycle state (`pending` →
|
|
357
|
+
* `acknowledged` | `rejected`), oldest-journaled first. Pass a `mutator`
|
|
358
|
+
* thunk to scope to one mutator (reactive, like {@link useQuery}'s `read`);
|
|
359
|
+
* omit it (or return `undefined`) for every mutation. Works identically over
|
|
360
|
+
* an owned engine and a connected host (ADR-0021) — the same states either
|
|
361
|
+
* way.
|
|
362
|
+
*/
|
|
363
|
+
const useMutationState = (mutator) => live(getClientRuntime(), () => {
|
|
364
|
+
const scope = mutator?.();
|
|
365
|
+
return (client) => client.mutationStates.pipe(Stream.map((states) => [...HashMap.values(states)].filter((state) => scope === void 0 || state.mutator === scope).toSorted((a, b) => a.mutationId - b.mutationId)));
|
|
366
|
+
}, { initial: [] });
|
|
367
|
+
/** Drain a boolean stream into `$state`, interrupting on teardown. */
|
|
368
|
+
const useFlag = (changes) => {
|
|
369
|
+
let value = $state(false);
|
|
370
|
+
$effect(() => {
|
|
371
|
+
const fiber = Effect.runFork(Stream.runForEach(changes, (next) => Effect.sync(() => value = next)));
|
|
372
|
+
return () => {
|
|
373
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
return { get current() {
|
|
377
|
+
return value;
|
|
378
|
+
} };
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Undo/redo over a manager's history as runes. Build one manager per client
|
|
382
|
+
* with `Undo.make(handle)` — a `Client` or a `ClientEngine` both work
|
|
383
|
+
* (issue #223) — and call this during component initialisation.
|
|
384
|
+
*/
|
|
385
|
+
const useUndo = (manager) => {
|
|
386
|
+
const canUndo = useFlag(manager.canUndoChanges);
|
|
387
|
+
const canRedo = useFlag(manager.canRedoChanges);
|
|
388
|
+
return {
|
|
389
|
+
get canUndo() {
|
|
390
|
+
return canUndo.current;
|
|
391
|
+
},
|
|
392
|
+
get canRedo() {
|
|
393
|
+
return canRedo.current;
|
|
394
|
+
},
|
|
395
|
+
undo: () => Effect.runPromise(Effect.ignore(manager.undo)),
|
|
396
|
+
redo: () => Effect.runPromise(Effect.ignore(manager.redo))
|
|
397
|
+
};
|
|
398
|
+
};
|
|
399
|
+
export { ClientUnavailable, createClient, fromRuntime, getClient, getClientRuntime, setClient, setClientRuntime, useAggregate, useAggregateGrouped, useConnection, useMutation, useMutationState, useMutationWithAck, useNamedQuery, useQuery, useUndo };
|
|
400
|
+
|
|
401
|
+
//# sourceMappingURL=index.svelte.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.svelte.js","names":[],"sources":["../src/unavailable.ts","../src/Runtime.ts","../src/hooks.svelte.ts"],"sourcesContent":["/**\n * The failure of a hook that reaches for a Client where none was set (#147):\n * `setClient(undefined)`, as on the server. Kept apart from `Runtime.ts` so the\n * hooks can raise it without importing the context module's public surface.\n */\nimport * as Schema from \"effect/Schema\";\n\n/**\n * A hook reached for the Client where none was set: `setClient(undefined)`,\n * as on the server, where a hook renders from its `initial` seed and fails\n * only when it needs the Client itself.\n */\nexport class ClientUnavailable extends Schema.TaggedError<\n ClientUnavailable,\n { readonly brand: unique symbol }\n>()(\"ClientUnavailable\", { message: Schema.String }) {}\n\n/** The {@link ClientUnavailable} a hook fails with when no Client was set. */\nexport const unavailable = (): ClientUnavailable =>\n new ClientUnavailable({\n message:\n \"@thomasfosterau/legation-svelte: no Client here (setClient was given none, as on the server). A hook renders from its `initial` seed without one; this one needed the Client.\",\n });\n","/**\n * Svelte client context\n *\n * The rune hooks reach the {@link Client.Client} — ADR-0022's one-per-thread\n * object, so where a read is answered (this thread's engine, a worker-hosted\n * one, the server) is configuration the components never see.\n *\n * An app builds a {@link ClientHandle} with {@link createClient} (#147) and\n * hands it to the component tree with {@link setClient}; the hooks read it\n * back, so they must be called during component initialisation (the same\n * rule Svelte's `getContext` follows). The runtime the hooks run over is an\n * implementation detail of the handle. An app with its own layers (an\n * in-page server, a service-worker mirror, a simulated-offline transport)\n * builds the runtime itself and wraps it with {@link fromRuntime}, or passes\n * it to {@link setClientRuntime}.\n *\n * On the server there may be no Client at all: `setClient(undefined)` renders\n * every hook from its `initial` seed, and only a hook that actually reaches\n * for the Client (a write, an unseeded read) fails, with\n * {@link ClientUnavailable}.\n */\nimport { createRuntimeContext } from \"@thomasfosterau/effect-svelte\";\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport * as ManagedRuntime from \"effect/ManagedRuntime\";\nimport * as Option from \"effect/Option\";\nimport { getContext, hasContext, setContext } from \"svelte\";\nimport { Client } from \"@thomasfosterau/legation\";\nimport { unavailable } from \"./unavailable.js\";\n\nexport { ClientUnavailable } from \"./unavailable.js\";\n\n/**\n * A runtime carrying the {@link Client.Client} — ADR-0022's one-per-thread\n * object. The rune hooks run over it: reads are declarative `Query.Read`s\n * and where they are answered (replica, host, server fallthrough) is the\n * Client's business, never the component's.\n */\nexport type ClientRuntime = ManagedRuntime.ManagedRuntime<Client.Client, never>;\n\n/**\n * The Client an app hands to its component tree (#147): what {@link setClient}\n * takes, and what {@link createClient} and {@link fromRuntime} build.\n */\nexport interface ClientHandle {\n /** The runtime the hooks run over, for Effect code that needs the Client directly. */\n readonly runtime: ClientRuntime;\n /**\n * Release the Client: stop its sync loop and close its store and transport.\n * Call it when the app tears down (a root layout's cleanup). Calling it again\n * does nothing more.\n */\n readonly dispose: () => Promise<void>;\n}\n\n/**\n * Build the Client a Svelte app boots with (#147): `Client.make`'s options in,\n * a {@link ClientHandle} out, with no Effect runtime for the app to build. The\n * Client is built on first use, so creating the handle does no work; with\n * `transport: { url }` it is the standard remote setup.\n */\nexport const createClient = (options: Client.MakeOptions): ClientHandle =>\n fromRuntime(\n ManagedRuntime.make(\n Layer.effect(Client.Client)(Effect.map(Client.make(options), (made) => made.client))\n )\n );\n\n/**\n * Wrap a runtime the app built itself, for the custom-layer path: an in-page\n * server, a service-worker mirror, a simulated-offline transport.\n */\nexport const fromRuntime = (runtime: ClientRuntime): ClientHandle => ({\n runtime,\n dispose: () => runtime.dispose(),\n});\n\nconst clientContext = createRuntimeContext<Client.Client>(\n \"@thomasfosterau/legation-svelte — call setClient(createClient(…)) higher in the component tree\"\n);\n\nconst handleKey = Symbol.for(\"@thomasfosterau/legation-svelte/ClientHandle\");\n\n/**\n * The runtime a tree with no Client runs its hooks over. Building it is lazy,\n * so it costs nothing until a hook reaches for the Client, and then it fails.\n */\nconst unavailableRuntime: ClientRuntime = /* @__PURE__ */ ManagedRuntime.make(\n Layer.effect(Client.Client)(Effect.die(unavailable()))\n);\n\n/**\n * Provide the Client to descendant components. Call once, high in the tree,\n * before any rune hook. `undefined` (on the server, say) renders every hook\n * from its `initial` seed; a hook that needs the Client then fails with\n * {@link ClientUnavailable}.\n */\nexport const setClient = (client?: ClientHandle): void => {\n setContext(handleKey, Option.fromUndefinedOr(client));\n clientContext.provide(client === undefined ? unavailableRuntime : client.runtime);\n};\n\n/** The Client put in context by {@link setClient}: `None` when it was given none. */\nexport const getClient = (): Option.Option<ClientHandle> =>\n hasContext(handleKey) ? getContext<Option.Option<ClientHandle>>(handleKey) : Option.none();\n\n/**\n * Provide a Client runtime the app built itself: the escape hatch beside\n * {@link setClient}, the same as `setClient(fromRuntime(runtime))`.\n */\nexport const setClientRuntime = (runtime: ClientRuntime): void => {\n setClient(fromRuntime(runtime));\n};\n\n/** Read the Client runtime put in context by {@link setClient} or {@link setClientRuntime}. */\nexport const getClientRuntime = (): ClientRuntime =>\n // Narrowing as in {@link getEngineRuntime}: `setClient` is the only writer.\n clientContext.get() as ClientRuntime;\n","/**\n * Rune-based live queries (Svelte 5)\n *\n * The `use*` hooks speak **Query over the Client** (ADR-0022): reads are\n * declarative `Query.Read`s — collection, `where`, `include`d edges with\n * per-edge windows, `order`, cursor pagination — and where a read is\n * answered (replica, worker-hosted engine, server fallthrough) is the\n * Client's business, never the component's. Each hook returns an object\n * whose reactive `current` is an `AsyncResult` (the same result vocabulary\n * as the engine's reactivity atoms and the React binding), plus an\n * awaitable `promise` for Async Svelte. Inputs are thunks called inside\n * `$effect`, so a hook re-subscribes whenever the reactive state its thunk\n * reads changes — a read whose cursor lives in `$state` just works — while\n * the previous value stays visible as a waiting result\n * (stale-while-revalidate).\n *\n * Joins need no separate hook: `include` resolves edges onto each row\n * (`row.related`), windowed per the edge's declared order (ADR-0024), over\n * owned and worker-hosted engines alike.\n *\n * Re-exported from the package root (`@thomasfosterau/legation-svelte`), which is the only\n * read/write surface. Requires Svelte 5. Hooks must be called during\n * component initialisation — they read the Client runtime from context\n * ({@link setClientRuntime}) and register a `$effect`.\n */\nimport { useLiveStream } from \"@thomasfosterau/effect-svelte\";\nimport * as Context from \"effect/Context\";\nimport * as Effect from \"effect/Effect\";\nimport * as Fiber from \"effect/Fiber\";\nimport * as HashMap from \"effect/HashMap\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport * as R from \"effect/Record\";\nimport * as Stream from \"effect/Stream\";\nimport { AsyncResult } from \"effect/reactivity\";\nimport { Aggregate, Client, Query } from \"@thomasfosterau/legation\";\nimport { ClientEngine, type MutationState, type Undo } from \"@thomasfosterau/legation/internal\";\nimport { getClient, getClientRuntime, type ClientRuntime } from \"./Runtime.js\";\nimport { unavailable } from \"./unavailable.js\";\n\n/** A live result: `current` re-renders whatever reads it on every emission. */\nexport interface Live<A, E = never> {\n /**\n * The current result. `Initial` until the first emission, then `Success`\n * per emission; while re-subscribing after an input change the previous\n * result is kept with `waiting: true` (stale-while-revalidate). An\n * `initial` seed starts as a non-waiting `Success` — \"we have data\", no\n * spinner over server-rendered content — until the first real emission.\n */\n readonly current: AsyncResult.AsyncResult<A, E>;\n /**\n * The awaitable view of the same result, for Async Svelte\n * (`experimental.async`): pending until the first data arrives — so\n * `await rows.promise` in a derived or template suspends the enclosing\n * `<svelte:boundary>`'s `pending` snippet — then an already-resolved\n * promise replaced on every emission (and on input change), so awaiting\n * expressions re-run with fresh rows while Svelte keeps the previous UI\n * on screen (stale-while-revalidate). A failed stream rejects it, which\n * the boundary's `failed` snippet catches.\n */\n readonly promise: Promise<A>;\n}\n\n/** Options shared by the live hooks. */\nexport interface LiveOptions<A> {\n /**\n * Seed `current` with a synchronous `Success` before the first emission —\n * during SSR (where `$effect` never runs) this is what renders, and on the\n * client it makes the first render match the server markup (no loading\n * flash). Compute it from the rows the page's load already fetched.\n */\n readonly initial?: A;\n}\n\n/** The rows a read resolves to: node values with their included edges. */\nexport type ResolvedRows = ReadonlyArray<Client.ResolvedRow>;\nconst EMPTY_ROWS: ResolvedRows = [];\n\n/** A `promise` that never settles — `.promise` while a read thunk opts out. */\nconst NEVER_PROMISE: Promise<never> = new Promise(() => {});\n\n/**\n * Core plumbing — effect-svelte's `useLiveStream` does the work (thunk\n * tracked inside `$effect`, interrupt-and-resubscribe on input change with\n * the previous value kept as a waiting result, emissions in `$state.raw`,\n * teardown interruption never surfacing as a failure, `initial` seeding a\n * synchronous `Success`). This adapter resolves the Client from the\n * context runtime and keeps the SSR seam below.\n *\n * `makeStream` returning `undefined` is the read thunk's opt-out (#141):\n * \"nothing right now\". No window is ever registered and no request is\n * issued, because the closure that would call `client.observe`/\n * `view.observeNamed` never runs — `Stream.never` stands in, so\n * `useLiveStream`'s own subscription does nothing. `optedOut` then forces\n * `current`/`promise` back to their pre-subscription state regardless of\n * what that never-emitting subscription leaves behind in `useLiveStream`'s\n * state (stale-while-revalidate would otherwise keep a prior success\n * visible, tagged \"waiting\", forever) — so an opted-out read stays\n * `Initial`, distinguishable from a `Success` over zero rows, whether the\n * thunk never ran or ran once and opted back out.\n */\nconst live = <A, E>(\n runtime: ClientRuntime,\n makeStream: () => ((client: Client.Client) => Stream.Stream<A, E>) | undefined,\n options?: LiveOptions<A>\n): Live<A, E> => {\n let optedOut = $state(false);\n // The thunk, read inside `$effect` for its reactive inputs; `undefined` opts out.\n const pick = () => {\n const toStream = makeStream();\n optedOut = toStream === undefined;\n return toStream;\n };\n const seed = options?.initial === undefined ? {} : { initial: options.initial };\n // With no Client set (#147), a read that is not opted out fails at once with\n // `ClientUnavailable`, run where nothing needs the Client; one that renders\n // only from its seed (SSR, where no `$effect` runs) never gets this far.\n const result = Option.isNone(getClient())\n ? useLiveStream(() => (pick() === undefined ? Stream.never : Stream.die(unavailable())), {\n runtime: Context.empty(),\n ...seed,\n })\n : useLiveStream(\n () => {\n const toStream = pick();\n return toStream === undefined\n ? Stream.never\n : Stream.unwrap(Effect.map(Client.Client, (client) => toStream(client)));\n },\n { runtime, ...seed }\n );\n return {\n get current() {\n return optedOut ? AsyncResult.initial() : result.current;\n },\n get promise() {\n // During SSR (no `$effect`, so no live stream) a seeded hook exposes\n // its value DIRECTLY: Svelte's server renderer defers real promises to\n // the boundary's `pending` snippet but renders awaited plain values\n // synchronously — so seeded pages fully server-render, and unseeded\n // ones legitimately show `pending`.\n if (P.isUndefined(globalThis.document) && options?.initial !== undefined) {\n return options.initial as Promise<A>;\n }\n return optedOut ? NEVER_PROMISE : result.promise;\n },\n };\n};\n\n/**\n * Live result of a declarative read, re-subscribing when the thunk's\n * reactive inputs change. `include`d edges resolve onto each row's\n * `related`, windowed per the edge's declared order (ADR-0024). Observing\n * IS subscribing: the read's windows are registered for the hook's\n * lifetime through the Client's ledger.\n *\n * The thunk may return `undefined` to opt out (#141) — \"nothing right now\",\n * for a read keyed on a selection that starts empty. No window is\n * registered and no request is issued; `current` stays `Initial` rather\n * than becoming a `Success` over zero rows, so an opted-out read stays\n * distinguishable from one that legitimately matched nothing. The thunk\n * stays reactive either way: flipping the condition subscribes/unsubscribes\n * with the same stale-while-revalidate behaviour as any other input change.\n */\nexport const useQuery = (\n read: () => Query.Read | undefined,\n options?: LiveOptions<ResolvedRows>\n): Live<ResolvedRows> =>\n live(\n getClientRuntime(),\n () => {\n const value = read();\n if (value === undefined) return undefined;\n return (client) => client.observe(value);\n },\n options\n );\n\n/** A live, growing connection over a read's root window (ADR-0024). */\nexport interface Connection extends Live<ResolvedRows> {\n /**\n * Widen the window by one page. A no-op while a widen is still resolving\n * or once {@link hasMore} is false — call it freely from a click handler.\n */\n readonly loadMore: () => void;\n /**\n * Whether another page may exist: `true` while the current window is\n * saturated (it holds as many rows as it asked for). Reactive. At a dataset\n * whose size is an exact multiple of the page size this stays `true` for one\n * more `loadMore`, which then resolves empty and flips it `false` — the\n * standard cost of limit-based paging without a separate total count.\n */\n readonly hasMore: boolean;\n /** Whether a page (the first or a widen) is currently resolving. Reactive. */\n readonly loading: boolean;\n /** Collapse back to the first page. */\n readonly reset: () => void;\n}\n\n/** Options for {@link useConnection}. */\nexport interface ConnectionOptions extends LiveOptions<ResolvedRows> {\n /** Rows per page — the window grows by this on each `loadMore`. Default 20. */\n readonly pageSize?: number;\n}\n\n/** The base read minus its window — the hook owns `first`/`after`. */\nconst connectionKey = (read: Query.Read): string => {\n const { first: _first, after: _after, ...base } = read;\n return JSON.stringify(base);\n};\n\n/**\n * Connection-style paging over a read's root window: a live result that\n * *grows* rather than jumping between detached pages. `loadMore` widens the\n * one window by a page (ADR-0024 — \"load more widens the subset\"), so each\n * page's rows accumulate and the previous ones stay on screen through the\n * widen (stale-while-revalidate); the engine only ever syncs the current\n * window, never the whole collection.\n *\n * Per-parent paging falls out of instantiating the hook per parent: scope\n * the read's `where` to a parent id and give each parent its own\n * `useConnection` (one per `{#each parents}` row), and each parent's list\n * pages independently, with its own `hasMore` and window. The read thunk\n * stays reactive — when its base (collection/where/order/include) changes the\n * connection re-bases to the first page automatically; the window it owns\n * (`first`/`after`) is ignored on the passed read.\n *\n * ```svelte\n * const reviews = useConnection(() => productReviews(id), { pageSize: 10 });\n * // {#each await reviews.promise as row (row.value.id)} … {/each}\n * // <button onclick={reviews.loadMore} disabled={!reviews.hasMore}>More</button>\n * ```\n *\n * The read thunk may return `undefined` to opt out (#141): no window is\n * registered and no request issued, `current` stays `Initial`, and paging\n * state (`hasMore`/`loading`) reads as idle until the thunk starts returning\n * a read again.\n *\n * **Engine-level retargeting (#133).** When the read has no `include`d edges\n * and the view is engine-backed (`ClientView.observeWindow` present — a\n * `Mirror` has none, see `docs/observing-row-changes.md`), `loadMore`/`reset`\n * widen or shrink the SAME open observation's window via\n * `ClientEngine.observeWindow`'s `retarget` — reusing `LiveView`'s maintained\n * order instead of tearing the stream down and resubscribing wider each page.\n * The stream itself is still built once per *base* change (collection/where/\n * order/include), exactly as before; only `pages` no longer forces a rebuild.\n * A joined read, or a mirror-backed connection, falls back to the original\n * resubscribe-per-page behavior, unchanged.\n */\nexport const useConnection = (\n read: () => Query.Read | undefined,\n options?: ConnectionOptions\n): Connection => {\n const pageSize = options?.pageSize ?? 20;\n const runtime = getClientRuntime();\n let pages = $state(1);\n // Non-reactive: tracks the base read so a reused hook re-bases to page 1\n // when its inputs change (a per-parent instance never trips this).\n // `undefined` also covers the opted-out state (#141).\n let baseKey: string | undefined;\n\n $effect(() => {\n const value = read();\n const key = value === undefined ? undefined : connectionKey(value);\n if (key !== baseKey) {\n baseKey = key;\n pages = 1;\n }\n });\n\n // The current base's retarget handle, set once per base change from inside\n // the stream thunk below. `loadMore`/`reset` widen/shrink through it\n // instead of relying on `pages` to force a resubscribe — `pages` is\n // deliberately read only inside the thunk's *async* body below, which\n // Svelte's effect tracking never sees, so changing it alone no longer\n // retriggers `useLiveStream`'s interrupt-and-resubscribe. `undefined` when\n // the current base has no engine-level retarget available (falls back to\n // the pre-#133 behavior).\n let retargetHandle:\n | { readonly baseKey: string; readonly retarget: (limit: number) => void }\n | undefined;\n\n const windowed = live(\n runtime,\n () => {\n const value = read();\n if (value === undefined) {\n retargetHandle = undefined;\n return undefined;\n }\n const { first: _first, after: _after, ...base } = value;\n const key = connectionKey(value);\n return (client) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const observeWindow = client.view.observeWindow;\n const hasEdges = !R.isEmptyReadonlyRecord(base.include ?? {});\n if (hasEdges || observeWindow === undefined) {\n retargetHandle = undefined;\n return client.observe({ ...base, first: pages * pageSize });\n }\n const compiled = Query.plan(client.nodes, { ...base, first: pageSize });\n const opened = yield* observeWindow(compiled.root);\n retargetHandle = {\n baseKey: key,\n retarget: (limit) => {\n runtime.runFork(opened.retarget({ limit }));\n },\n };\n // Defensive: `pages` is always freshly reset to 1 by the base-key\n // effect above before this thunk re-runs, but this keeps the\n // observation correct even if that ordering ever changes.\n if (pages > 1) retargetHandle.retarget(pages * pageSize);\n return opened.rows.pipe(\n Stream.orDie,\n Stream.map((rows) => rows.map((row) => ({ ...row, related: {} })))\n );\n }).pipe(Effect.orDie)\n );\n },\n options\n );\n\n const rows = (): ResolvedRows => AsyncResult.getOrElse(windowed.current, () => EMPTY_ROWS);\n\n const widenTo = (nextPages: number) => {\n pages = nextPages;\n const handle = retargetHandle;\n if (handle !== undefined && handle.baseKey === baseKey) handle.retarget(pages * pageSize);\n };\n\n return {\n get current() {\n return windowed.current;\n },\n get promise() {\n return windowed.promise;\n },\n get hasMore() {\n return rows().length >= pages * pageSize;\n },\n get loading() {\n return AsyncResult.isWaiting(windowed.current);\n },\n loadMore: () => {\n if (!AsyncResult.isWaiting(windowed.current) && rows().length >= pages * pageSize) {\n widenTo(pages + 1);\n }\n },\n reset: () => widenTo(1),\n };\n};\n\n/**\n * Live result of a named query (server-resolved), with reactive name/args.\n * The thunk may return `undefined` to opt out (#141): see {@link useQuery}.\n */\nexport const useNamedQuery = (\n query: () => { readonly name: string; readonly args: unknown } | undefined,\n options?: LiveOptions<ReadonlyArray<ClientEngine.LocalRow>>\n): Live<ReadonlyArray<ClientEngine.LocalRow>> =>\n live(\n getClientRuntime(),\n () => {\n const value = query();\n if (value === undefined) return undefined;\n const { name, args } = value;\n return (client) => client.view.observeNamed(name, args).pipe(Stream.orDie);\n },\n options\n );\n\n/**\n * Live aggregate over a read's root window (count/sum/min/max/avg). Local\n * tier only — the aggregate sees the rows this client has synced. The read\n * thunk may return `undefined` to opt out (#141): see {@link useQuery}.\n */\nexport const useAggregate = (\n read: () => Query.Read | undefined,\n aggregate: Aggregate.Aggregate,\n options?: LiveOptions<Aggregate.AggregateValue>\n): Live<Aggregate.AggregateValue> =>\n live(\n getClientRuntime(),\n () => {\n const value = read();\n if (value === undefined) return undefined;\n return (client) =>\n Aggregate.observe(\n client.view.observe(Query.plan(client.nodes, value).root).pipe(Stream.orDie),\n aggregate\n );\n },\n options\n );\n\n/**\n * Live grouped aggregate over a read's root window, keyed by\n * `String(row.value[groupBy])`. The read thunk may return `undefined` to\n * opt out (#141): see {@link useQuery}.\n */\nexport const useAggregateGrouped = (\n read: () => Query.Read | undefined,\n aggregate: Aggregate.Aggregate,\n groupBy: string,\n options?: LiveOptions<Record<string, Aggregate.AggregateValue>>\n): Live<Record<string, Aggregate.AggregateValue>> =>\n live(\n getClientRuntime(),\n () => {\n const value = read();\n if (value === undefined) return undefined;\n return (client) =>\n Aggregate.observeGrouped(\n client.view.observe(Query.plan(client.nodes, value).root).pipe(Stream.orDie),\n aggregate,\n groupBy\n );\n },\n options\n );\n\n/**\n * A `mutate(mutation, args)` bound to the context Client, resolving on\n * journal (the write is applied optimistically before the promise settles).\n */\nexport const useMutation = (): ((mutation: string, args: unknown) => Promise<void>) => {\n const runtime = getClientRuntime();\n return (mutation, args) =>\n runtime.runPromise(Effect.flatMap(Client.Client, (client) => client.mutate(mutation, args)));\n};\n\n/**\n * Like {@link useMutation}, but resolving with the mutation's authoritative\n * outcome once the backend settles it.\n */\nexport const useMutationWithAck = (): ((\n mutation: string,\n args: unknown\n) => Promise<ClientEngine.MutationOutcome>) => {\n const runtime = getClientRuntime();\n return (mutation, args) =>\n runtime.runPromise(\n Effect.flatMap(Client.Client, (client) => client.mutateWithAck(mutation, args))\n );\n};\n\n/**\n * Reactive mutation-state list (#116) — TanStack Query's `useMutationState`\n * analogue: every journaled mutation's current lifecycle state (`pending` →\n * `acknowledged` | `rejected`), oldest-journaled first. Pass a `mutator`\n * thunk to scope to one mutator (reactive, like {@link useQuery}'s `read`);\n * omit it (or return `undefined`) for every mutation. Works identically over\n * an owned engine and a connected host (ADR-0021) — the same states either\n * way.\n */\nexport const useMutationState = (\n mutator?: () => string | undefined\n): Live<ReadonlyArray<MutationState.MutationState>> =>\n live(\n getClientRuntime(),\n () => {\n const scope = mutator?.();\n return (client) =>\n client.mutationStates.pipe(\n Stream.map((states) =>\n [...HashMap.values(states)]\n .filter((state) => scope === undefined || state.mutator === scope)\n .toSorted((a, b) => a.mutationId - b.mutationId)\n )\n );\n },\n { initial: [] }\n );\n\n/** Live undo/redo availability plus the two commands. */\nexport interface UndoControls {\n /** Whether an undo is currently possible. Reactive. */\n readonly canUndo: boolean;\n /** Whether a redo is currently possible. Reactive. */\n readonly canRedo: boolean;\n /** Run the manager's `undo` (a no-op when nothing is undoable). */\n readonly undo: () => Promise<void>;\n /** Run the manager's `redo` (a no-op when nothing is redoable). */\n readonly redo: () => Promise<void>;\n}\n\n/** Drain a boolean stream into `$state`, interrupting on teardown. */\nconst useFlag = (changes: Stream.Stream<boolean>): { readonly current: boolean } => {\n let value = $state(false);\n $effect(() => {\n // The manager's streams close over the engine (no `R`), so they run on the\n // default runtime — `useUndo` needs no Client in context.\n const fiber = Effect.runFork(\n Stream.runForEach(changes, (next) => Effect.sync(() => (value = next)))\n );\n return () => {\n Effect.runFork(Fiber.interrupt(fiber));\n };\n });\n return {\n get current() {\n return value;\n },\n };\n};\n\n/**\n * Undo/redo over a manager's history as runes. Build one manager per client\n * with `Undo.make(handle)` — a `Client` or a `ClientEngine` both work\n * (issue #223) — and call this during component initialisation.\n */\nexport const useUndo = <E>(manager: Undo.UndoManager<E>): UndoControls => {\n const canUndo = useFlag(manager.canUndoChanges);\n const canRedo = useFlag(manager.canRedoChanges);\n return {\n get canUndo() {\n return canUndo.current;\n },\n get canRedo() {\n return canRedo.current;\n },\n undo: () => Effect.runPromise(Effect.ignore(manager.undo)),\n redo: () => Effect.runPromise(Effect.ignore(manager.redo)),\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { n as ClientRuntime } from "../Runtime-DAQq0HCa.js";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import { Client, ClientStore, Hydration, Mutation, MutatorError, Server, StoreError, Transport, TransportError, VersionNotSupportedError } from "@thomasfosterau/legation";
|
|
5
|
+
import { Cookies } from "@sveltejs/kit";
|
|
6
|
+
/** Options for {@link clientGroupId}. */
|
|
7
|
+
export interface ClientGroupOptions {
|
|
8
|
+
/** Cookie name. Default `"legation-client-group"`. */
|
|
9
|
+
readonly name?: string;
|
|
10
|
+
/** Generate a fresh id on first contact. Default `crypto.randomUUID`. */
|
|
11
|
+
readonly generate?: () => string;
|
|
12
|
+
/** Cookie path. Default `"/"`. */
|
|
13
|
+
readonly path?: string;
|
|
14
|
+
/** Cookie lifetime in seconds. Default one year. */
|
|
15
|
+
readonly maxAge?: number;
|
|
16
|
+
}
|
|
17
|
+
/** The client-group cookie's default name. */
|
|
18
|
+
export declare const CLIENT_GROUP_COOKIE = "legation-client-group";
|
|
19
|
+
/**
|
|
20
|
+
* Read the browser's client group id from its cookie, minting one when
|
|
21
|
+
* absent. Call it in a server `load` (or `handle`) and render the page's
|
|
22
|
+
* snapshot under the returned id; the client boots its engine with the same
|
|
23
|
+
* id (it can read the cookie itself, or take the value from the load data).
|
|
24
|
+
*
|
|
25
|
+
* The cookie is deliberately not `httpOnly` — the client engine needs the
|
|
26
|
+
* same group id. It carries no authority: identity is resolved server-side
|
|
27
|
+
* through the `IdentityResolver` seam, never from this value.
|
|
28
|
+
*/
|
|
29
|
+
export declare const clientGroupId: (cookies: Cookies, options?: ClientGroupOptions) => string;
|
|
30
|
+
/** Options for {@link loadSnapshot}. */
|
|
31
|
+
export interface LoadSnapshotOptions extends Omit<Hydration.SnapshotOptions, "clientGroupId"> {
|
|
32
|
+
/** Client-group cookie options (name, generation, lifetime). */
|
|
33
|
+
readonly cookie?: ClientGroupOptions;
|
|
34
|
+
}
|
|
35
|
+
/** {@link LoadSnapshotOptions} with the carrier supplied — no `ServerHandler` needed. */
|
|
36
|
+
export interface RemoteLoadSnapshotOptions extends LoadSnapshotOptions {
|
|
37
|
+
readonly transport: Transport.ClientTransport;
|
|
38
|
+
}
|
|
39
|
+
/** What {@link loadSnapshot} returns — put both in the page data. */
|
|
40
|
+
export interface LoadedSnapshot {
|
|
41
|
+
readonly snapshot: Hydration.Snapshot;
|
|
42
|
+
readonly clientGroupId: string;
|
|
43
|
+
}
|
|
44
|
+
type LoadSnapshotError = StoreError | TransportError | MutatorError | VersionNotSupportedError;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the request's client group id from its cookie and produce the SSR
|
|
47
|
+
* snapshot for the given subsets under it.
|
|
48
|
+
*
|
|
49
|
+
* The group id is the point of the cookie: the render happens under the
|
|
50
|
+
* *browser's* group, so the snapshot's pull cookie continues that group's CVR
|
|
51
|
+
* conversation. That holds over a remote `transport` too — the id travels on
|
|
52
|
+
* the pull request and the server's fresh cookie travels back.
|
|
53
|
+
*/
|
|
54
|
+
export declare function loadSnapshot(cookies: Cookies, options: RemoteLoadSnapshotOptions): Effect.Effect<LoadedSnapshot, LoadSnapshotError>;
|
|
55
|
+
/** The in-process form: renders against the app's sync server, the `ServerHandler` in context. */
|
|
56
|
+
export declare function loadSnapshot(cookies: Cookies, options: LoadSnapshotOptions): Effect.Effect<LoadedSnapshot, LoadSnapshotError, Transport.ServerHandler>;
|
|
57
|
+
/** Options for {@link clientRuntime}. */
|
|
58
|
+
export interface ClientRuntimeOptions extends Omit<Client.MakeOptions, "store" | "transport"> {
|
|
59
|
+
/** Fully-provided client store layer (e.g. `IndexedDbClientStore.layerWindow()`). */
|
|
60
|
+
readonly store: Layer.Layer<ClientStore.ClientStore>;
|
|
61
|
+
/** Fully-provided transport layer (RPC client, or an in-memory pair in tests). */
|
|
62
|
+
readonly transport: Layer.Layer<Transport.ClientTransport>;
|
|
63
|
+
/** The page's SSR snapshot, hydrated into an empty store before the Client boots. */
|
|
64
|
+
readonly snapshot?: Hydration.Snapshot;
|
|
65
|
+
/** Reads to pin for the runtime's lifetime (the app shell's working set). */
|
|
66
|
+
readonly subscribe?: ReadonlyArray<Parameters<Client.Client["subscribe"]>[0]>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build the tab's Client runtime: store (snapshot-hydrated when empty) →
|
|
70
|
+
* transport → `Client.make`, which starts the sync loop itself. Hand the
|
|
71
|
+
* result to `setClientRuntime`, and dispose it when the app tears down.
|
|
72
|
+
*/
|
|
73
|
+
export declare const clientRuntime: (options: ClientRuntimeOptions) => ClientRuntime;
|
|
74
|
+
/** The hidden field carrying the rendered form's form client id. */
|
|
75
|
+
export declare const FORM_CLIENT_FIELD = "legation-form-client";
|
|
76
|
+
/** The hidden field carrying the form client's mutation id. */
|
|
77
|
+
export declare const FORM_MUTATION_FIELD = "legation-form-mutation";
|
|
78
|
+
/** The key of the action result's data under which the receipt travels. */
|
|
79
|
+
export declare const RECEIPT_KEY = "legationReceipt";
|
|
80
|
+
/** A form client: the id minted for one rendered form, and its mutation id (`1`). */
|
|
81
|
+
export interface FormClient {
|
|
82
|
+
readonly clientId: string;
|
|
83
|
+
readonly mutationId: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Mint a form client for one rendered form: a fresh id with mutation id `1`
|
|
87
|
+
* (ADR-0030 #215 §2). The page's server `load` mints one per form and renders
|
|
88
|
+
* it in the {@link FORM_CLIENT_FIELD} and {@link FORM_MUTATION_FIELD} hidden
|
|
89
|
+
* fields, so a resubmission of the same rendered form is the same mutation.
|
|
90
|
+
*/
|
|
91
|
+
export declare const mintFormClient: (generate?: () => string) => FormClient;
|
|
92
|
+
/** The form client a posted form names, or `undefined` when its fields are missing or malformed. */
|
|
93
|
+
export declare const formClientOf: (data: FormData) => FormClient | undefined;
|
|
94
|
+
/** Where a form action's Mutation runs: the client group and the form client. */
|
|
95
|
+
export interface FormMutationTarget extends FormClient {
|
|
96
|
+
readonly clientGroupId: string;
|
|
97
|
+
}
|
|
98
|
+
/** Options for {@link formMutationTarget}. */
|
|
99
|
+
export interface FormMutationTargetOptions {
|
|
100
|
+
/** The client-group cookie's name. Default `"legation-client-group"`, as `clientGroupId` uses. */
|
|
101
|
+
readonly cookieName?: string | undefined;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Read where a form action's Mutation runs: the client group from its cookie
|
|
105
|
+
* (never minted here) and the form client from the posted fields. `undefined`
|
|
106
|
+
* means the action cannot name the group or the form: it stays an out-of-band
|
|
107
|
+
* writer.
|
|
108
|
+
*/
|
|
109
|
+
export declare const formMutationTarget: (cookies: Cookies, data: FormData, options?: FormMutationTargetOptions) => FormMutationTarget | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Run the form layer's decoded args as the target's Mutation on the server
|
|
112
|
+
* (`Server.mutate`) and answer the receipt. Return it from the action as
|
|
113
|
+
* `{ [RECEIPT_KEY]: receipt }`, so the enhanced submit settles on it; a
|
|
114
|
+
* rejected receipt's `reason` is the form layer's to route to a message.
|
|
115
|
+
* Args that do not fit the Mutation's schema are a defect, not a field error.
|
|
116
|
+
*/
|
|
117
|
+
export declare const runFormMutation: <Args, ArgsEncoded>(server: Server.Server, target: FormMutationTarget, mutation: Mutation.Mutator<string, Args, ArgsEncoded>, args: Args) => Effect.Effect<Server.Receipt, TransportError | VersionNotSupportedError>;
|
|
118
|
+
/**
|
|
119
|
+
* The part of SvelteKit's `ActionResult` (from `$app/forms`) read here: its
|
|
120
|
+
* type, and the data a `success` or `failure` result carries.
|
|
121
|
+
*/
|
|
122
|
+
export interface FormActionResult {
|
|
123
|
+
readonly type: string;
|
|
124
|
+
readonly data?: Readonly<Record<string, unknown>> | undefined;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The submit function {@link enhanceFormMutation} returns, typed by the parts
|
|
128
|
+
* of SvelteKit's `SubmitFunction` it uses, so it can be passed to `enhance`.
|
|
129
|
+
*/
|
|
130
|
+
export type FormSubmitFunction = (input: {
|
|
131
|
+
readonly formData: FormData;
|
|
132
|
+
}) => (opts: {
|
|
133
|
+
readonly result: FormActionResult;
|
|
134
|
+
readonly update: (options?: {
|
|
135
|
+
refreshAll?: boolean;
|
|
136
|
+
}) => Promise<void>;
|
|
137
|
+
}) => Promise<void>;
|
|
138
|
+
/** The receipt an action result carries under {@link RECEIPT_KEY}, or `undefined`. */
|
|
139
|
+
export declare const receiptOf: (result: FormActionResult) => Server.Receipt | undefined;
|
|
140
|
+
/** Options for {@link enhanceFormMutation}. */
|
|
141
|
+
export interface EnhanceFormMutationOptions<Args, ArgsEncoded> {
|
|
142
|
+
/** The Mutation the form's action runs. */
|
|
143
|
+
readonly mutation: Mutation.Mutator<string, Args, ArgsEncoded>;
|
|
144
|
+
/** The Mutation's args, read from the submitted form data (the form layer's decode). */
|
|
145
|
+
readonly args: (data: FormData) => Args;
|
|
146
|
+
/**
|
|
147
|
+
* Re-run the page's `load` functions after the action, SvelteKit's default.
|
|
148
|
+
* Default `false`: the overlay already shows the write, and the pull that
|
|
149
|
+
* covers it brings the confirmed rows (ADR-0030 #215 §4).
|
|
150
|
+
*/
|
|
151
|
+
readonly refreshAll?: boolean | undefined;
|
|
152
|
+
/** Mint the per-submit form client id. Default `crypto.randomUUID`. */
|
|
153
|
+
readonly generate?: (() => string) | undefined;
|
|
154
|
+
/** Called with the receipt once the submission settles, e.g. to show a rejection's reason. */
|
|
155
|
+
readonly onReceipt?: ((receipt: Server.Receipt) => void) | undefined;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The enhanced submit for a form-action Mutation: pass it to SvelteKit's
|
|
159
|
+
* `use:enhance`. Each submit mints a fresh form client and writes it into the
|
|
160
|
+
* posted fields, overlays the Mutation's local effect under it without
|
|
161
|
+
* journaling it (the action delivers it), and settles on the receipt the
|
|
162
|
+
* action returns under {@link RECEIPT_KEY}: acknowledged moves the effect to
|
|
163
|
+
* the unpulled tail, where the pull that covers the form client releases it;
|
|
164
|
+
* rejected drops it. A result without a receipt (an error, a redirect, a
|
|
165
|
+
* failed network) abandons the effect, and the next pull shows the server's
|
|
166
|
+
* state. While offline the submit fails as the network fails (ADR-0030 open
|
|
167
|
+
* point 6).
|
|
168
|
+
*/
|
|
169
|
+
export declare const enhanceFormMutation: <Args, ArgsEncoded>(runtime: ClientRuntime, options: EnhanceFormMutationOptions<Args, ArgsEncoded>) => FormSubmitFunction;
|
|
170
|
+
export {};
|
|
171
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/kit/clientGroup.ts","../../src/kit/load.ts","../../src/kit/boot.ts","../../src/kit/formMutation.ts"],"mappings":";;;;;;iBAciB;;WAEN;;WAEA;;WAEA;;WAEA;;;qBAIE;;;;;;;;;;;qBAaA,gBAAiB,SAAS,SAAS,UAAU;;iBCVzC,4BAA4B,KAAK,UAAU;;WAEjD,SAAS;;;iBAIH,kCAAkC;WACxC,WAAW,UAAU;;;iBAIf;WACN,UAAU,UAAU;WACpB;;KAGN,oBAAoB,aAAa,iBAAiB,eAAe;;;;;;;;;;wBAWtD,aACd,SAAS,SACT,SAAS,4BACR,OAAO,OAAO,gBAAgB;;wBAEjB,aACd,SAAS,SACT,SAAS,sBACR,OAAO,OAAO,gBAAgB,mBAAmB,UAAU;;iBC3C7C,6BAA6B,KAAK,OAAO;;WAE/C,OAAO,MAAM,MAAM,YAAY;;WAE/B,WAAW,MAAM,MAAM,UAAU;;WAEjC,WAAW,UAAU;;WAErB,YAAY,cAAc,WAAW,OAAO;;;;;;;qBAwB1C,gBAAiB,SAAS,yBAAuB;;qBCbjD;;qBAEA;;qBAEA;;iBAGI;WACN;WACA;;;;;;;;qBASE,iBAAkB,4BAAqD;;qBAMvE,eAAgB,MAAM,aAAW;;iBAS7B,2BAA2B;WACjC;;;iBAIM;;WAEN;;;;;;;;qBASE,qBACX,SAAS,SACT,MAAM,UACN,UAAU,8BACT;;;;;;;;qBAeU,kBAAmB,MAAM,aACpC,QAAQ,OAAO,QACf,QAAQ,oBACR,UAAU,SAAS,gBAAgB,MAAM,cACzC,MAAM,SACL,OAAO,OAAO,OAAO,SAAS,iBAAiB;;;;;iBAajC;WACN;WACA,OAAO,SAAS;;;;;;YAOf,sBAAsB;WACvB,UAAU;OACd;WACI,QAAQ;WACR,SAAS;IAAY;QAA2B;MACrD;;qBAGO,YAAa,QAAQ,qBAAmB,OAAO;;iBAsB3C,2BAA2B,MAAM;;WAEvC,UAAU,SAAS,gBAAgB,MAAM;;WAEzC,OAAO,MAAM,aAAa;;;;;;WAM1B;;WAEA;;WAEA,cAAc,SAAS,OAAO;;;;;;;;;;;;;;qBAe5B,sBACV,MAAM,aACL,SAAS,eACT,SAAS,2BAA2B,MAAM,iBACzC"}
|