@thomasfosterau/legation-browser 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,656 @@
1
+ import { IndexedDb } from "@effect/platform-browser";
2
+ import { Client, ClientStore, Collection, Identity, Mutation, Node, Query, StoreError, Transport, TransportError } from "@thomasfosterau/legation";
3
+ import * as Layer from "effect/Layer";
4
+ import { ClientEngine, Coverage, EngineHost, HttpCache, Mirror, MirrorChannel, MirrorChannel as MirrorChannel$1, MirrorProtocol } from "@thomasfosterau/legation/internal";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Scope from "effect/Scope";
7
+ import * as Stream from "effect/Stream";
8
+ import * as Duration from "effect/Duration";
9
+ import * as Schema from "effect/Schema";
10
+ declare namespace IndexedDbClientStore_d_exports {
11
+ export { IndexedDbClientStoreOptions, layer, layerNamespaced, layerWindow, namespacedPrefix };
12
+ }
13
+ /** Options for the IndexedDB-backed `ClientStore` layers. */
14
+ interface IndexedDbClientStoreOptions {
15
+ /** IndexedDB database name. Defaults to platform-browser's default. */
16
+ readonly database?: string;
17
+ /** Key namespace inside the store. Defaults to core's `"legation/"`. */
18
+ readonly prefix?: string;
19
+ /**
20
+ * The identity this replica belongs to — an opaque, app-supplied
21
+ * fingerprint, **never PII and never a credential** (see
22
+ * {@link Identity.Fingerprint}). Supplying it turns the identity boundary
23
+ * on: the store compares it with its stamp on open and resets the replica
24
+ * on a mismatch, quarantining unpushed writes under the superseded
25
+ * fingerprint. Unset leaves the boundary off.
26
+ */
27
+ readonly identity?: Identity.Fingerprint;
28
+ }
29
+ /**
30
+ * ClientStore over IndexedDB. Requires the `IndexedDb` primitives service;
31
+ * use {@link layerWindow} where browser globals are available, or provide
32
+ * `IndexedDb` yourself (tests use fake-indexeddb).
33
+ *
34
+ * A pull and a push acknowledgement commit as one IndexedDB transaction (#393),
35
+ * so a durable pull writes its rows once and writes no release record.
36
+ */
37
+ declare const layer: (options?: IndexedDbClientStoreOptions) => Layer.Layer<ClientStore.ClientStore, never, IndexedDb.IndexedDb>;
38
+ /** Self-contained layer using the browser's global `indexedDB`. */
39
+ declare const layerWindow: (options?: IndexedDbClientStoreOptions) => Layer.Layer<ClientStore.ClientStore>;
40
+ /**
41
+ * The key prefix {@link layerNamespaced} derives for a fingerprint:
42
+ * core's `"legation/"` namespace with a percent-encoded identity segment
43
+ * under it, so two principals' replicas cannot collide and neither can
44
+ * collide with an un-namespaced one.
45
+ */
46
+ declare const namespacedPrefix: (identity: Identity.Fingerprint) => string;
47
+ /**
48
+ * The namespacing alternative to resetting (#212): one replica per identity
49
+ * in a single IndexedDB database, keyed by fingerprint.
50
+ *
51
+ * A principal's rows are unreachable from another principal's store because
52
+ * they are under a different key prefix — no comparison, no reset, and a
53
+ * user who switches back finds their replica still warm. The fingerprint is
54
+ * still stamped, so a store reached through this layer keeps the same
55
+ * guarantee if it is ever opened un-namespaced.
56
+ *
57
+ * The cost is disk: every identity that has ever signed in on this device
58
+ * keeps a replica until something clears it. Prefer {@link layerWindow} with
59
+ * an `identity` where sign-ins are unbounded.
60
+ *
61
+ * Pass `database` to isolate at the database level instead of the key level
62
+ * — a heavier boundary (a whole IndexedDB database per identity, deletable
63
+ * in one call) for apps that want it.
64
+ */
65
+ declare const layerNamespaced: (identity: Identity.Fingerprint, options?: Omit<IndexedDbClientStoreOptions, "identity" | "prefix">) => Layer.Layer<ClientStore.ClientStore>;
66
+ declare namespace BroadcastMirrorChannel_d_exports {
67
+ export { make$1 as make };
68
+ }
69
+ /** A {@link MirrorChannel.MirrorChannel} over a `BroadcastChannel` name. */
70
+ declare const make$1: (name: string) => Effect.Effect<MirrorChannel$1.MirrorChannel, never, Scope.Scope>;
71
+ declare namespace BackgroundSync_d_exports {
72
+ export { FlushOptions, FlushOutcome, HostNotReady, IdentitySource, ListenOptions$1 as ListenOptions, Listener$1 as Listener, RegisterOnJournalOptions, RegisterOptions, RegisterOutcome, SyncEvent, SyncManager, SyncRegistration, SyncScope, defaultReadyTimeout, defaultTag, flush, identityUnchecked, listen$1 as listen, register, registerOnJournal };
73
+ }
74
+ /**
75
+ * The tag the library registers and answers. Short on purpose: Chromium
76
+ * refuses over-long tags.
77
+ */
78
+ declare const defaultTag = "legation-flush";
79
+ /** How long a `sync` event waits for a host to attach before giving up. */
80
+ declare const defaultReadyTimeout: Duration.Input;
81
+ /** `SyncManager`: absent where the browser has no Background Sync. */
82
+ interface SyncManager {
83
+ readonly register: (tag: string) => Promise<void>;
84
+ }
85
+ /**
86
+ * A `ServiceWorkerRegistration`, or any stand-in for one. Typed as `object`
87
+ * rather than a slice with an optional `sync`, because TypeScript's weak-type
88
+ * check would then refuse a real registration (whose declared type has no
89
+ * `sync`); the member is probed at runtime instead (see `syncManagerOf`).
90
+ */
91
+ type SyncRegistration = object;
92
+ /** The slice of the worker's `SyncEvent` the listener reads. */
93
+ interface SyncEvent {
94
+ readonly tag: string;
95
+ /** Chromium: `true` when the browser will not retry this registration again. */
96
+ readonly lastChance?: boolean;
97
+ readonly waitUntil: (promise: Promise<unknown>) => void;
98
+ }
99
+ /**
100
+ * The slice of `ServiceWorkerGlobalScope` the listener needs — `self` in a
101
+ * worker. `registration` is where the host registers the tag from.
102
+ */
103
+ interface SyncScope {
104
+ readonly addEventListener: (type: "sync", listener: (event: SyncEvent) => void) => void;
105
+ readonly registration?: SyncRegistration | undefined;
106
+ }
107
+ /**
108
+ * What asking for a sync did. None of these is a failure: registration is
109
+ * advisory, and the journal flushes on the connectivity path whatever
110
+ * happens here.
111
+ *
112
+ * - `Registered` — the browser will fire the tag's `sync` event.
113
+ * - `Unsupported` — no service-worker registration, or no `SyncManager` on
114
+ * it (Safari, Firefox): the connectivity path flushes.
115
+ * - `Refused` — the browser declined (permission withheld, no active worker,
116
+ * or — from inside a worker — no window of the origin open).
117
+ */
118
+ type RegisterOutcome = "Registered" | "Unsupported" | "Refused";
119
+ /** Options for {@link register}. */
120
+ interface RegisterOptions {
121
+ /** Defaults to {@link defaultTag}; must match the listener's. */
122
+ readonly tag?: string;
123
+ /**
124
+ * Where to register. Defaults to the page's own service-worker
125
+ * registration (`navigator.serviceWorker.getRegistration()` — never
126
+ * `ready`, which would wait forever on a page no worker controls).
127
+ */
128
+ readonly registration?: Effect.Effect<SyncRegistration | undefined>;
129
+ }
130
+ /**
131
+ * Ask the browser to wake the worker with the library's `sync` event once
132
+ * connectivity is back. Call it from the page after a journaled mutation, or
133
+ * let `ServiceWorkerHost` do it from the worker (see {@link ListenOptions}).
134
+ *
135
+ * Never fails and never logs above debug level: absence is the common case
136
+ * (Background Sync is Chromium-only), and the journal still flushes on the
137
+ * next connectivity signal. Registering a tag that is already pending is a
138
+ * no-op for the browser, so calling it once per mutation is fine.
139
+ */
140
+ declare const register: (options?: RegisterOptions) => Effect.Effect<RegisterOutcome>;
141
+ /** Options for {@link registerOnJournal}. */
142
+ interface RegisterOnJournalOptions extends RegisterOptions {
143
+ /**
144
+ * Whether the journal cannot drain right now. Defaults to
145
+ * `navigator.onLine === false` in the worker (`WorkerNavigator.onLine`).
146
+ */
147
+ readonly offline?: Effect.Effect<boolean>;
148
+ }
149
+ /**
150
+ * Register the tag for the scope's lifetime whenever `engine`'s journal
151
+ * cannot drain now — the host's side of {@link register}, since in the
152
+ * service-worker topology a page's mutation is journaled in the worker:
153
+ *
154
+ * - a mutation is journaled while {@link RegisterOnJournalOptions.offline}
155
+ * holds; and
156
+ * - a push fails terminally (`ClientEngine.syncFailures`, after the retry
157
+ * policy is exhausted) — reachable network, unreachable backend.
158
+ *
159
+ * Never on an online mutation: the sync loop drains those, debounced and
160
+ * compacted, and an online registration would fire `sync` at once and push
161
+ * each write on its own (#246). Chromium only accepts a registration from a
162
+ * worker while a window of the origin is open; a refusal is a debug line.
163
+ */
164
+ declare const registerOnJournal: (engine: ClientEngine.ClientEngine, options?: RegisterOnJournalOptions) => Effect.Effect<void, never, Scope.Scope>;
165
+ /**
166
+ * What a flush did. Each is a settled outcome — the `sync` event resolves and
167
+ * the browser does not retry:
168
+ *
169
+ * - `Flushed` — the push completed (possibly with nothing pending).
170
+ * Acknowledged entries are trimmed and their optimistic effects dropped; a
171
+ * permanently rejected one settles as `rejected` in `mutationStates`, with
172
+ * its reason.
173
+ * - `IdentityChanged` — the store is not stamped for whoever is signed in now
174
+ * (see {@link ListenOptions.identity}); nothing was pushed, and the journal
175
+ * waits for the next page boot to apply the boundary's transition.
176
+ * - `UpdateNeeded` — the server rejected this build's schema version; the
177
+ * journal is retained and `onUpdateNeeded` has fired. Retrying cannot help
178
+ * until the app updates.
179
+ */
180
+ type FlushOutcome = "Flushed" | "IdentityChanged" | "UpdateNeeded";
181
+ /**
182
+ * The explicit opt-out from the flush's identity check, for an app with **no
183
+ * signed-in identity at all** (every session is the same public principal).
184
+ * The flush then relies on the store's boundary alone: quarantined entries
185
+ * are still unreachable from push, but a store still stamped for a user who
186
+ * signed out in a tab that closed before `Client.reset` ran **would be
187
+ * pushed**. Never pass this in an app that has sign-in.
188
+ */
189
+ declare const identityUnchecked: unique symbol;
190
+ /**
191
+ * Who is signed in now, as the app resolves it inside the worker — or
192
+ * {@link identityUnchecked}. See {@link ListenOptions.identity}.
193
+ */
194
+ type IdentitySource = Effect.Effect<Identity.Fingerprint | undefined> | typeof identityUnchecked;
195
+ /** Options for {@link flush}. */
196
+ interface FlushOptions {
197
+ /** See {@link ListenOptions.identity}. */
198
+ readonly identity: IdentitySource;
199
+ }
200
+ /**
201
+ * Flush `engine`'s journal through the ordinary push path, behind the
202
+ * identity check. A journal larger than half the engine's unpulled bound goes
203
+ * in chunks with a pull before each chunk after the first, so a flush may
204
+ * pull (#429); a smaller journal is pushed without pulling. Fails — so the
205
+ * `sync` event rejects and the browser reschedules — on a transport or store
206
+ * failure, with the journal intact. A pull between chunks that fails is a
207
+ * transport failure of the same kind, so the flush never reports `Flushed`
208
+ * over a journal it stopped short of.
209
+ */
210
+ declare const flush: (engine: ClientEngine.ClientEngine, options: FlushOptions) => Effect.Effect<FlushOutcome, TransportError | StoreError>;
211
+ declare const HostNotReady_base: Schema.Class<HostNotReady, Schema.TaggedStruct<"HostNotReady", {
212
+ readonly tag: Schema.String;
213
+ readonly message: Schema.String;
214
+ }>, import("effect/Cause").YieldableError & {
215
+ readonly brand: unique symbol;
216
+ }>;
217
+ /** No host attached the listener within its `readyTimeout`. */
218
+ declare class HostNotReady extends HostNotReady_base {}
219
+ /** Options for {@link listen}. */
220
+ interface ListenOptions$1 {
221
+ /** The tag to answer, and to register from the host. Defaults to {@link defaultTag}. */
222
+ readonly tag?: string;
223
+ /** The worker's global scope. Defaults to `globalThis` (`self` in a worker). */
224
+ readonly scope?: SyncScope;
225
+ /**
226
+ * How long a `sync` event waits for a host to attach before rejecting, so
227
+ * the browser reschedules it. Defaults to {@link defaultReadyTimeout}. A
228
+ * worker that only boots its host on a page's handshake never attaches on
229
+ * a tab-less wake; boot the host at worker start for background flushing.
230
+ */
231
+ readonly readyTimeout?: Duration.Input;
232
+ /**
233
+ * **Required.** The fingerprint of whoever is signed in **now**, as the app
234
+ * resolves it from inside the worker (e.g. a session-subject hash it keeps
235
+ * beside its session). A flush pushes only if the store's stamp equals it
236
+ * exactly; `undefined` means "cannot tell" and skips.
237
+ *
238
+ * Required rather than optional because the store's own boundary covers
239
+ * only a reset that ran: a tab closed before `Client.reset` leaves the old
240
+ * identity's journal in the pushable keyspace, and a background flush would
241
+ * send it under whoever's cookies the worker now carries. An app with no
242
+ * sign-in passes {@link identityUnchecked}, which accepts that risk
243
+ * explicitly.
244
+ */
245
+ readonly identity: IdentitySource;
246
+ /**
247
+ * Register the tag from the worker when the attached engine's journal
248
+ * cannot drain now (see {@link registerOnJournal}). Defaults to `true`;
249
+ * turn it off when the page registers with {@link register} itself.
250
+ */
251
+ readonly registerOnJournal?: boolean;
252
+ /**
253
+ * Whether the journal cannot drain now, for {@link registerOnJournal}.
254
+ * Defaults to `navigator.onLine === false`.
255
+ */
256
+ readonly offline?: Effect.Effect<boolean>;
257
+ }
258
+ /**
259
+ * The worker's `sync` listener, created by {@link listen}. It answers its
260
+ * tag from the moment it exists; a host attaches an engine with
261
+ * {@link Listener.serve}, which `ServiceWorkerHost.start` does when given the
262
+ * listener as `backgroundSync`.
263
+ */
264
+ interface Listener$1 {
265
+ /** The tag this listener answers. */
266
+ readonly tag: string;
267
+ /**
268
+ * Attach `engine` for the scope's lifetime: `sync` events for the tag —
269
+ * including any that arrived before this call — flush its journal, run
270
+ * under the caller's services (logger, tracer). Closing the scope detaches
271
+ * it; a later event then waits for the next host.
272
+ */
273
+ readonly serve: (engine: ClientEngine.ClientEngine) => Effect.Effect<void, never, Scope.Scope>;
274
+ }
275
+ /**
276
+ * Install the `sync` listener on the worker's scope — **synchronously, at the
277
+ * top level of the worker script** (see the module docs for why). Deliberately
278
+ * a plain function rather than an Effect for that reason.
279
+ *
280
+ * ```ts
281
+ * // service-worker.ts, top level
282
+ * const backgroundSync = BackgroundSync.listen({ identity: currentFingerprint });
283
+ * // ... later, once the engine's layers are built:
284
+ * yield* ServiceWorkerHost.start({ backgroundSync, registry, nodes, urls });
285
+ * ```
286
+ */
287
+ declare const listen$1: (options: ListenOptions$1) => Listener$1;
288
+ declare namespace PushPoke_d_exports {
289
+ export { ListenOptions, Listener, PokeOutcome, PushEvent, PushScope, listen };
290
+ }
291
+ /** The slice of a worker's `PushEvent` the listener reads. */
292
+ interface PushEvent {
293
+ /** The push's data (`PushMessageData`, or `null` for a push without any). */
294
+ readonly data: unknown;
295
+ readonly waitUntil: (promise: Promise<unknown>) => void;
296
+ }
297
+ /** The slice of `ServiceWorkerGlobalScope` the listener needs — `self` in a worker. */
298
+ interface PushScope {
299
+ readonly addEventListener: (type: "push", listener: (event: PushEvent) => void) => void;
300
+ readonly registration?: SyncRegistration;
301
+ }
302
+ /**
303
+ * What a recognised push did.
304
+ *
305
+ * - `Registered` / `Unsupported` / `Refused` — the Background Sync
306
+ * registration's outcome (`action: "backgroundSync"`, the default).
307
+ * - `Pulled` / `PullFailed` — the engine pulled, or the pull failed and the
308
+ * next connectivity signal pulls instead (`action: "pull"`).
309
+ * - `IdentityChanged` — the replica is not stamped for whoever is signed in
310
+ * now, so nothing was done.
311
+ */
312
+ type PokeOutcome = RegisterOutcome | "Pulled" | "PullFailed" | "IdentityChanged";
313
+ /** Options for {@link listen}. */
314
+ interface ListenOptions {
315
+ /**
316
+ * **Required.** Who is signed in now, as `BackgroundSync.listen` takes it:
317
+ * a recognised push acts only if the replica's stamp equals it exactly.
318
+ * An app with no sign-in passes `BackgroundSync.identityUnchecked`.
319
+ */
320
+ readonly identity: IdentitySource;
321
+ /**
322
+ * What a recognised push does: `"backgroundSync"` (the default) registers the
323
+ * library's sync tag; `"pull"` pulls at once.
324
+ */
325
+ readonly action?: "backgroundSync" | "pull";
326
+ /** The sync tag to register; defaults to `BackgroundSync.defaultTag`. */
327
+ readonly tag?: string;
328
+ /** The worker's global scope. Defaults to `globalThis` (`self` in a worker). */
329
+ readonly scope?: PushScope;
330
+ /**
331
+ * How long a recognised push waits for a host to attach an engine. Defaults
332
+ * to `BackgroundSync.defaultReadyTimeout`.
333
+ */
334
+ readonly readyTimeout?: Duration.Input;
335
+ }
336
+ /** The `push` listener {@link listen} installs; a host attaches its engine with {@link Listener.serve}. */
337
+ interface Listener {
338
+ /**
339
+ * Attach `engine` for the scope's lifetime, which `ServiceWorkerHost.start`
340
+ * does when given the listener as `pushPoke`. A push that arrived before
341
+ * then waits for it, up to the ready timeout.
342
+ */
343
+ readonly serve: (engine: ClientEngine.ClientEngine) => Effect.Effect<void, never, Scope.Scope>;
344
+ }
345
+ /**
346
+ * Install the `push` listener on the worker's scope, synchronously at the top
347
+ * level of the worker script.
348
+ *
349
+ * ```ts
350
+ * // service-worker.ts, top level
351
+ * const backgroundSync = BackgroundSync.listen({ identity: currentFingerprint });
352
+ * const pushPoke = PushPoke.listen({ identity: currentFingerprint });
353
+ * // ... once the engine's layers are built:
354
+ * yield* ServiceWorkerHost.start({ backgroundSync, pushPoke, registry, nodes });
355
+ * ```
356
+ */
357
+ declare const listen: (options: ListenOptions) => Listener;
358
+ declare namespace ServiceWorkerHost_d_exports {
359
+ export { Host, ServiceWorkerHostOptions, defaultChannelName, run$1 as run, start };
360
+ }
361
+ /** Options for {@link start} and {@link run}. */
362
+ interface ServiceWorkerHostOptions {
363
+ /** BroadcastChannel name shared with the mirrors it serves. */
364
+ readonly channelName?: string;
365
+ /**
366
+ * The collection registry mirrors' `related` registrations resolve against
367
+ * (forwarded to {@link EngineHost.run}). Pass it whenever mirrors use
368
+ * relational reads.
369
+ */
370
+ readonly registry?: Collection.CollectionRegistry;
371
+ /**
372
+ * The node graph and URL-keyed reads for `SubscribeUrl` registrations
373
+ * (#159): a page with no `Client` posts `{ _tag: "SubscribeUrl",
374
+ * mirrorId, url }` on the channel and the host pins the URL's windows
375
+ * under that id — forwarded to {@link EngineHost.run}. Build `urls` with
376
+ * `JsonApiQuery.urlReads(nodes)` from `@thomasfosterau/legation-jsonapi`.
377
+ */
378
+ readonly nodes?: Node.NodeRegistry;
379
+ readonly urls?: Client.UrlReads;
380
+ /**
381
+ * The worker's Background Sync listener (#213), from
382
+ * `BackgroundSync.listen({ identity })` at the top level of the worker script. The host
383
+ * attaches its engine for as long as it serves: the listener's `sync`
384
+ * events — including one that woke the worker before the host had booted —
385
+ * flush the journal through `push`, and the tag is registered when the
386
+ * journal cannot drain now (offline, or a terminally failed push). Unset, no listener is served and `sync` events are not flushed.
387
+ */
388
+ readonly backgroundSync?: Listener$1;
389
+ /**
390
+ * Web Push as a poke carrier (#214): install `PushPoke.listen({ identity })`
391
+ * at the top level of the worker script and pass it here. The host attaches
392
+ * its engine, so a recognised push registers the Background Sync (or pulls,
393
+ * with `action: "pull"`) once the replica's identity checks out. Unset, the
394
+ * library's push is not answered.
395
+ */
396
+ readonly pushPoke?: Listener;
397
+ }
398
+ /** The default channel name; Mirror setups should use the same one. */
399
+ declare const defaultChannelName = "legation";
400
+ /** The serving host: the engine it serves and what it holds for the mirrors. */
401
+ interface Host {
402
+ /** The engine the host serves — the replica a façade reads (`ClientView.fromEngine(engine)`). */
403
+ readonly engine: ClientEngine.ClientEngine;
404
+ /**
405
+ * The registrations the host holds on its mirrors' behalf at this moment
406
+ * (`EngineHost.Host.registrations`): the ledger a façade's coverage check
407
+ * scans, `Coverage.coverageOf(nodes, view.query, host.registrations)`.
408
+ */
409
+ readonly registrations: Effect.Effect<ReadonlyArray<Coverage.Entry>>;
410
+ }
411
+ /**
412
+ * Start the host: serve mirrors over the broadcast channel and start the
413
+ * engine's sync loop (pull on poke, push+pull on reconnect), then return
414
+ * the {@link Host}. Both the host and `runSync` fork their fibers into the
415
+ * surrounding scope, so the caller must keep that scope open for as long
416
+ * as it serves (`Effect.never`, as {@link run} does); closing it stops
417
+ * serving.
418
+ */
419
+ declare const start: (options?: ServiceWorkerHostOptions) => Effect.Effect<Host, never, ClientEngine.ClientEngine | Scope.Scope>;
420
+ /**
421
+ * Run the host: {@link start}, then serve forever. NEVER returns — both the
422
+ * host and `runSync` fork their fibers into the surrounding scope, so
423
+ * returning would let a caller's `Effect.scoped` tear the whole host down
424
+ * immediately. Interrupt it / close the scope to stop serving.
425
+ */
426
+ declare const run$1: (options?: ServiceWorkerHostOptions) => Effect.Effect<never, never, ClientEngine.ClientEngine | Scope.Scope>;
427
+ declare namespace ElectedHost_d_exports {
428
+ export { ElectedHost, ElectedHostOptions, Role, run };
429
+ }
430
+ /** Whether this tab is currently serving the engine or mirroring another tab's. */
431
+ type Role = "host" | "mirror";
432
+ /** Options for {@link run}. */
433
+ interface ElectedHostOptions {
434
+ /** The Web Lock name AND the sync-protocol client group — one election per group. */
435
+ readonly clientGroupId: string;
436
+ readonly nodes: Node.NodeRegistry;
437
+ readonly mutations: Mutation.MutatorRegistry;
438
+ /**
439
+ * `ClientStore` (durable — pair {@link IndexedDbClientStore}) and
440
+ * `ClientTransport`, composed as a Layer so winning the election is what
441
+ * triggers connecting: nothing here opens a socket or an IndexedDB
442
+ * transaction until this tab is actually elected, keeping a losing (or
443
+ * still-waiting) tab's footprint to the mirror alone.
444
+ */
445
+ readonly dependencies: Layer.Layer<ClientStore.ClientStore | Transport.ClientTransport>;
446
+ /**
447
+ * The engine's client identity while this tab hosts. Defaults to
448
+ * `host-${clientGroupId}` — deterministic and shared by construction, so
449
+ * whichever tab wins resumes the SAME journal/rows/cookie regardless of
450
+ * which physical tab or random id it happens to be (a fresh random id
451
+ * per boot would fork the mutation-id sequence and orphan whatever the
452
+ * old host had already journaled).
453
+ */
454
+ readonly hostClientId?: string;
455
+ /** BroadcastChannel name shared with every tab in this group. Defaults to {@link ServiceWorkerHost.defaultChannelName}. */
456
+ readonly channelName?: string;
457
+ /** Forwarded to {@link EngineHost.run} for mirrors' `related` registrations. */
458
+ readonly registry?: Collection.CollectionRegistry;
459
+ /**
460
+ * URL-keyed reads (#159): lets the returned Client `subscribe(url)`, and
461
+ * — with `nodes` — lets the hosting tab answer `SubscribeUrl` from pages
462
+ * without a Client (forwarded to {@link EngineHost.run}).
463
+ */
464
+ readonly urls?: Client.UrlReads;
465
+ readonly queries?: Query.QueryRegistry;
466
+ readonly syncLoop?: ClientEngine.SyncLoopOptions;
467
+ /** Override for tests/non-browser injection. Defaults to `navigator.locks`. */
468
+ readonly locks?: LockManager;
469
+ }
470
+ /** What {@link run} returns: one stable Client for the tab, and a stream of its host/mirror role. */
471
+ interface ElectedHost {
472
+ /**
473
+ * One stable Client for the tab's whole lifetime: reads and writes go
474
+ * through the mirror until this tab is elected, and through the owned
475
+ * engine from that point on — the switch is transparent to callers (a
476
+ * live `observe`/`related` re-hydrates from the new tier; explicit
477
+ * `subscribe` pins are replayed onto it).
478
+ */
479
+ readonly client: Client.Client;
480
+ /** Purely observational — devtools/debug UI. Never drives application logic. */
481
+ readonly role: Stream.Stream<Role>;
482
+ }
483
+ /**
484
+ * Elect this tab into hosting `clientGroupId`'s engine — full main-thread
485
+ * host (ADR-0010) while the lock is held, a mirror over the same channel
486
+ * otherwise — and hand back one `Client` that works unchanged across the
487
+ * transition, plus a `role` stream for observability.
488
+ */
489
+ declare const run: (options: ElectedHostOptions) => Effect.Effect<ElectedHost, never, Scope.Scope>;
490
+ declare namespace CacheApiHttpCache_d_exports {
491
+ export { CacheLike, CacheStorageLike, FetchHandlerOptions, FetchHandlerResult, fetchHandler, make, metaHeader, open };
492
+ }
493
+ /** The subset of the Cache API this store uses. */
494
+ interface CacheLike {
495
+ readonly match: (request: Request) => Promise<Response | undefined>;
496
+ readonly put: (request: Request, response: Response) => Promise<void>;
497
+ readonly delete: (request: Request) => Promise<boolean>;
498
+ readonly keys: () => Promise<ReadonlyArray<Request>>;
499
+ }
500
+ /** The slice of the Cache API's `CacheStorage` that {@link open} uses (`caches` in a browser). */
501
+ interface CacheStorageLike {
502
+ readonly open: (name: string) => Promise<CacheLike>;
503
+ }
504
+ /** Header carrying the serialized CachedResponseMeta on stored copies. */
505
+ declare const metaHeader = "x-legation-cache-meta";
506
+ /** ResponseCacheStore over one named Cache API cache. */
507
+ declare const make: (cache: CacheLike) => HttpCache.ResponseCacheStore;
508
+ /** Open a named cache (defaults to the global `caches`). */
509
+ declare const open: (name: string, cacheStorage?: CacheStorageLike) => Effect.Effect<HttpCache.ResponseCacheStore, StoreError>;
510
+ /** Options for {@link fetchHandler}. */
511
+ interface FetchHandlerOptions {
512
+ readonly store: HttpCache.ResponseCacheStore;
513
+ /** Per-request policy; `undefined` bypasses the cache entirely. */
514
+ readonly policy: (request: Request) => HttpCache.HttpCachePolicy | undefined;
515
+ /** The network. Defaults to the global `fetch`. */
516
+ readonly fetch?: (request: Request) => Promise<Response>;
517
+ }
518
+ /** What {@link fetchHandler} answers a request with. */
519
+ interface FetchHandlerResult {
520
+ readonly response: Response;
521
+ /**
522
+ * Present when a stale entry was served: refreshes the cache in the
523
+ * background. Service workers pass it to `event.waitUntil`.
524
+ */
525
+ readonly revalidate?: Effect.Effect<void, StoreError | TransportError> | undefined;
526
+ }
527
+ /**
528
+ * Build the stale-while-revalidate handler: fresh entries are served from
529
+ * cache, stale-but-unexpired entries are served immediately with a
530
+ * background revalidation, and expired or missing entries reload from the
531
+ * network (caching ok GET responses).
532
+ */
533
+ declare const fetchHandler: (options: FetchHandlerOptions) => (request: Request) => Effect.Effect<FetchHandlerResult, StoreError | TransportError>;
534
+ declare namespace HttpCacheInvalidation_d_exports {
535
+ export { FINE_TAG_ROW_BUDGET, InvalidateOnChangesOptions, TagsForOptions, TagsForResult, Touched, collectionChanges, defaultFineTagFor, defaultTagFor, invalidateOnChanges, tagsFor, tagsForObserved };
536
+ }
537
+ /** The default coarse tag convention: `collection:{name}` (`HttpCache.tagFor`, shared with the edge, #206). */
538
+ declare const defaultTagFor: (collection: string) => string;
539
+ /** The default fine, per-row tag convention: `collection:{name}:{id}` (`HttpCache.fineTagFor`). */
540
+ declare const defaultFineTagFor: (collection: string, id: string) => string;
541
+ /**
542
+ * The fine-tag row budget: a response enumerating more rows than this is
543
+ * tagged coarse-only (see {@link tagsFor}) rather than carrying one fine tag
544
+ * per row — a document listing thousands of rows cannot carry thousands of
545
+ * tags on a bounded header.
546
+ *
547
+ * Derived against the real ceiling on the header this convention rides on —
548
+ * `x-legation-cache-meta` (`CacheApiHttpCache.metaHeader`), which carries
549
+ * `encodeURIComponent(JSON.stringify(meta))` and so must fit under Node's
550
+ * default `--max-http-header-size` of 16 KiB (16384 bytes) wherever responses are
551
+ * served by a Node HTTP server:
552
+ *
553
+ * - `encodeURIComponent` expands every byte outside its unreserved set to
554
+ * `%XX` (3 bytes), so 3x the raw byte length is a safe upper bound on
555
+ * encoded size regardless of which characters a collection name or row id
556
+ * use.
557
+ * - Reserve 4096 bytes (16 KiB / 4) for the rest of `CachedResponseMeta` —
558
+ * `url`, three numeric timestamps, the coarse tag, and JSON's own
559
+ * braces/keys/quotes — leaving `16384 - 4096 = 12288` bytes for fine tags.
560
+ * - One fine tag, worst case: a JSON array entry `"collection:{name}:{id}",`
561
+ * with a 40-char collection name and a 40-char row id (a UUIDv4 is 36
562
+ * chars) is `2 (quotes) + 11 ("collection:") + 40 + 1 (":") + 40 + 1 (",")
563
+ * = 95` raw bytes, i.e. at most `95 * 3 = 285` encoded bytes.
564
+ * - `N = floor(12288 / 285) = 43`.
565
+ */
566
+ declare const FINE_TAG_ROW_BUDGET = 43;
567
+ /** The collections and individual rows one engine event touched (`HttpCache.Touched`). */
568
+ type Touched = HttpCache.Touched;
569
+ /**
570
+ * Derive the change feed from an engine's event stream: the distinct
571
+ * collections and rows each `PullCompleted` (always present) and scoped
572
+ * `ViewRebuilt` (when present) touched, dropping events that touched
573
+ * nothing. Feed the result to {@link invalidateOnChanges}.
574
+ */
575
+ declare const collectionChanges: (events: Stream.Stream<ClientEngine.EngineEvent>) => Stream.Stream<Touched>;
576
+ /** Options for {@link invalidateOnChanges}. */
577
+ interface InvalidateOnChangesOptions {
578
+ readonly store: HttpCache.ResponseCacheStore;
579
+ /** Touched collections/rows per change (e.g. from {@link collectionChanges}). */
580
+ readonly changes: Stream.Stream<Touched>;
581
+ /** Map a collection name to its coarse cache tag; defaults to {@link defaultTagFor}. */
582
+ readonly tagFor?: (collection: string) => string;
583
+ /** Map a row to its fine cache tag; defaults to {@link defaultFineTagFor}. */
584
+ readonly fineTagFor?: (collection: string, id: string) => string;
585
+ }
586
+ /**
587
+ * Fork a scoped fibre that, per emitted change, evicts every cached response
588
+ * tagged for the touched collections (coarse) **and** the touched rows
589
+ * (fine) — `store.invalidateTags`. Invalidation cost stays proportional to
590
+ * the store's tag enumeration — this scans no more than `invalidateTags`
591
+ * already does. Runs until the scope closes; a store error is logged and
592
+ * swallowed, never fatal to the fibre, so a transient cache failure doesn't
593
+ * stop future invalidations.
594
+ */
595
+ declare const invalidateOnChanges: (options: InvalidateOnChangesOptions) => Effect.Effect<void, never, Scope.Scope>;
596
+ /** Options for {@link tagsFor}. */
597
+ interface TagsForOptions {
598
+ /** Map a collection name to its coarse cache tag; defaults to {@link defaultTagFor}. */
599
+ readonly tagFor?: (collection: string) => string;
600
+ /** Map a row to its fine cache tag; defaults to {@link defaultFineTagFor}. */
601
+ readonly fineTagFor?: (collection: string, id: string) => string;
602
+ }
603
+ /** What {@link tagsFor} returns. */
604
+ interface TagsForResult {
605
+ /** The tags to attach to the response (`HttpCachePolicy.tags`). */
606
+ readonly tags: ReadonlyArray<string>;
607
+ /**
608
+ * `true` when `rowIds.length` exceeded {@link FINE_TAG_ROW_BUDGET}, so
609
+ * `tags` carries the coarse tag only rather than one fine tag per row.
610
+ */
611
+ readonly budgetExceeded: boolean;
612
+ }
613
+ /**
614
+ * Build the cache tags for a response that enumerates `rowIds` of
615
+ * `collection`: the coarse tag, plus one fine tag per row when the row count
616
+ * is within {@link FINE_TAG_ROW_BUDGET} — beyond it, the coarse tag alone
617
+ * (`budgetExceeded: true`), since a fine tag per row would blow the header
618
+ * budget the tag rides on. Pure and synchronous, for use building
619
+ * `HttpCachePolicy.tags`; pair with {@link tagsForObserved} to also record
620
+ * the fallback when it happens.
621
+ */
622
+ declare const tagsFor: (collection: string, rowIds: ReadonlyArray<string>, options?: TagsForOptions) => TagsForResult;
623
+ /**
624
+ * {@link tagsFor}, plus an observable fallback: a response that exceeded the
625
+ * budget is never silently coarse — it logs a warning and increments
626
+ * `legation_cache_tag_budget_exceeded_total` (`Telemetry.cacheTagBudgetExceededTotal`),
627
+ * following the same "safely degraded, but counted" idiom as the engine's
628
+ * other bounded-hub fallbacks (`legation_hints_dropped_total`,
629
+ * `legation_change_collapses_total`).
630
+ */
631
+ declare const tagsForObserved: (collection: string, rowIds: ReadonlyArray<string>, options?: TagsForOptions) => Effect.Effect<TagsForResult>;
632
+ declare namespace BrowserConnectivity_d_exports {
633
+ export { BrowserConnectivityOptions, withBrowserConnectivity };
634
+ }
635
+ /**
636
+ * Options for {@link withBrowserConnectivity}.
637
+ */
638
+ interface BrowserConnectivityOptions {
639
+ /**
640
+ * How often a runtime with no `window` (a service worker) reads
641
+ * `navigator.onLine` to detect a flip, since it has no `online`/`offline`
642
+ * events. Defaults to `"5 seconds"`. The interval bounds how long a
643
+ * reconnect takes to reach the sync loop. Ignored where `window` exists.
644
+ */
645
+ readonly pollInterval?: Duration.Input | undefined;
646
+ }
647
+ /**
648
+ * Wrap a `ClientTransport` so its connection status combines
649
+ * `navigator.onLine` with the transport's own signal, and a return-to-visible
650
+ * surfaces a poke. Where there is no `window` (a service worker), the
651
+ * navigator signal is probed every `pollInterval` instead of listened for.
652
+ * Scoped: forked fibres and DOM listeners are torn down when the scope closes.
653
+ */
654
+ declare const withBrowserConnectivity: (transport: Transport.ClientTransport, options?: BrowserConnectivityOptions) => Effect.Effect<Transport.ClientTransport, never, Scope.Scope>;
655
+ export { BackgroundSync_d_exports as BackgroundSync, BroadcastMirrorChannel_d_exports as BroadcastMirrorChannel, BrowserConnectivity_d_exports as BrowserConnectivity, CacheApiHttpCache_d_exports as CacheApiHttpCache, ElectedHost_d_exports as ElectedHost, EngineHost, HttpCacheInvalidation_d_exports as HttpCacheInvalidation, IndexedDbClientStore_d_exports as IndexedDbClientStore, Mirror, MirrorChannel, MirrorProtocol, PushPoke_d_exports as PushPoke, ServiceWorkerHost_d_exports as ServiceWorkerHost };
656
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":["make","MirrorChannel","ListenOptions","Listener","listen","BackgroundSync.SyncRegistration","BackgroundSync.RegisterOutcome","BackgroundSync.IdentitySource","BackgroundSync.Listener","PushPoke.Listener","run"],"sources":["../src/IndexedDbClientStore.ts","../src/BroadcastMirrorChannel.ts","../src/BackgroundSync.ts","../src/PushPoke.ts","../src/ServiceWorkerHost.ts","../src/ElectedHost.ts","../src/CacheApiHttpCache.ts","../src/HttpCacheInvalidation.ts","../src/BrowserConnectivity.ts"],"mappings":"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"}