@happyvertical/smrt-web 0.38.2 → 0.38.3

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/dist/index.d.ts CHANGED
@@ -93,6 +93,16 @@ export declare interface CreateSmrtCollectionOptions<TData extends object = obje
93
93
  * `staleTimeMs` window.
94
94
  */
95
95
  initialData?: SmrtWebRow<TData>[];
96
+ /**
97
+ * Capability plug-ins hooking the collection lifecycle (#1755) — the seam
98
+ * that lets the offline outbox (#1762), persistence (#1764), and live SSE
99
+ * invalidation (#1763-client) slices each live in their own module instead of
100
+ * contending on this factory. Capabilities run in array order at six fixed
101
+ * points (see {@link SmrtWebCapability}). Additive and defaulting to none: an
102
+ * undefined or empty array is byte-for-byte the collection of today (the
103
+ * no-op guarantee), so this ships zero concrete capabilities by design.
104
+ */
105
+ capabilities?: SmrtWebCapability<TData>[];
96
106
  }
97
107
 
98
108
  /**
@@ -102,6 +112,86 @@ export declare interface CreateSmrtCollectionOptions<TData extends object = obje
102
112
  */
103
113
  export declare function createSmrtWebClient(): SmrtWebClient;
104
114
 
115
+ /**
116
+ * Create the app-wide live-updates subscriber. Feature-detects ONCE at
117
+ * construction: an available EventSource connects the SSE stream; otherwise it
118
+ * starts the `_changes` poll loop. A fatal SSE error later downgrades to
119
+ * polling for the rest of the subscriber's life (no flap-back).
120
+ */
121
+ export declare function createSmrtWebEventSubscriber(config: SmrtWebEventSubscriberConfig): SmrtWebEventSubscriber;
122
+
123
+ /**
124
+ * A durable artifact registered under a namespace — the outbox queue (#1762) or
125
+ * a persisted collection store (#1764). Each owns its storage engine and
126
+ * exposes only a `clear()` so {@link wipeDurableStore} can tear it down without
127
+ * knowing which engine backs it.
128
+ */
129
+ export declare interface DurableResource {
130
+ /** Which slice owns this artifact — for diagnostics and selective sweeps. */
131
+ readonly kind: 'outbox' | 'persisted-collection';
132
+ /** Drop this artifact's durable storage. Best-effort; may reject. */
133
+ clear(): Promise<void>;
134
+ }
135
+
136
+ /**
137
+ * @happyvertical/smrt-web — shared durable-store foundation (#1755).
138
+ *
139
+ * The ONE SMRT-layer namespacing + wipe registry that the future offline
140
+ * outbox (#1762) and persistence (#1764) slices both build on. Pure bookkeeping
141
+ * — ZERO client-data-engine (`@tanstack/*`) imports — so it stays inside the
142
+ * engine-absorption boundary and ships now, before either consumer exists, so
143
+ * the two slices agree on it from day one.
144
+ *
145
+ * Why a SMRT-layer registry rather than one storage engine: TanStack DB
146
+ * persistence (SQLite-WASM / OPFS) and `@tanstack/offline-transactions`
147
+ * (IndexedDB) are SEPARATE storage engines. There is no single primitive that
148
+ * spans both, so the shared foundation lives one level up — a deterministic
149
+ * namespace both slices key their own storage under, plus a registry so
150
+ * {@link wipeDurableStore} can clear BOTH through one call without the outbox
151
+ * and persistence modules importing each other. A logout / tenant-switch wipes
152
+ * every durable artifact for a namespace in one place.
153
+ *
154
+ * Nothing in this package calls these yet — the seam ships ahead of its
155
+ * consumers by design (see PRD #1755).
156
+ */
157
+ /**
158
+ * The identity a durable namespace is derived from. Combining the API base with
159
+ * the tenant + identity + manifest hash means a logout, a tenant switch, or a
160
+ * schema change each land on a DIFFERENT namespace, so durable artifacts are
161
+ * never reused across those boundaries.
162
+ *
163
+ * `manifestHash` is supplied by the caller (its source is #1764's call — a
164
+ * schema-shape digest); this module is source-agnostic and treats it as an
165
+ * opaque discriminator.
166
+ */
167
+ export declare interface DurableStoreKey {
168
+ /** API base path the durable data was fetched against (e.g. `/api/v1`). */
169
+ apiBase: string;
170
+ /** Active tenant id, if any — absent means the single-tenant / global scope. */
171
+ tenantId?: string;
172
+ /** Authenticated identity id, if any — absent means anonymous. */
173
+ identityId?: string;
174
+ /** Opaque schema-shape digest; a change forces a fresh namespace. */
175
+ manifestHash: string;
176
+ }
177
+
178
+ /**
179
+ * Compute the deterministic storage namespace for a {@link DurableStoreKey}.
180
+ * The same key always yields the same string; **any differing segment yields a
181
+ * different one** (injective) — this is critical because the namespace IS the
182
+ * tenant/identity/api isolation + wipe boundary, so a collision would reuse or
183
+ * wipe durable data ACROSS those boundaries.
184
+ *
185
+ * Injectivity is guaranteed three ways: every segment is `encodeURIComponent`-
186
+ * encoded, so a raw `:` in a value becomes `%3A` and can never be mistaken for a
187
+ * separator; an ABSENT `tenantId` / `identityId` maps to the empty string while
188
+ * a PRESENT value is prefixed with `_` (so undefined→``, ``→`_`, `x`→`_x`) — so
189
+ * an explicitly-EMPTY id no longer collides with "no id", nor does a real id of
190
+ * `-`. Both future slices key their own storage primitive (IndexedDB store name,
191
+ * OPFS path, …) under this string.
192
+ */
193
+ export declare function durableStoreNamespace(key: DurableStoreKey): string;
194
+
105
195
  /**
106
196
  * Retrieve the underlying engine collection backing a handle — an advanced
107
197
  * bridge for trusted framework bindings (e.g. the smrt-svelte live-query
@@ -112,6 +202,50 @@ export declare function createSmrtWebClient(): SmrtWebClient;
112
202
  */
113
203
  export declare function getEngineCollection<TData extends object>(handle: SmrtWebCollection<TData>): unknown;
114
204
 
205
+ /**
206
+ * Retrieve the programmatic {@link OutboxHandle} for a durable-store namespace,
207
+ * or `undefined` if no opted-in collection is currently attached under it. The
208
+ * bridge for trusted callers (outbox UI, smrt-svelte binding, tests) to read the
209
+ * durable queue and force retries. Pass the SAME
210
+ * `durableStoreNamespace(config.namespace)` string the capability used.
211
+ *
212
+ * Mirrors the `getEngineCollection` bridge convention: an escape hatch that
213
+ * returns a narrow SMRT-owned surface rather than the engine itself.
214
+ */
215
+ export declare function getOutboxHandle(namespace: string): OutboxHandle | undefined;
216
+
217
+ /**
218
+ * A thin per-collection capability that subscribes THIS collection to live
219
+ * signals for its `tableName`. On attach it registers `ctx.invalidate` (the
220
+ * factory's relationship-derived refetch primitive) with the shared subscriber;
221
+ * on teardown it unregisters. All transport, reconnection, and fan-out live in
222
+ * the {@link createSmrtWebEventSubscriber}; this capability is just the wire
223
+ * between one collection and that one subscriber.
224
+ */
225
+ export declare function liveInvalidation<TData extends object = object>(config: LiveInvalidationConfig): SmrtWebCapability<TData>;
226
+
227
+ /** Configuration for the {@link liveInvalidation} capability. */
228
+ export declare interface LiveInvalidationConfig {
229
+ /** The one app-wide subscriber from {@link createSmrtWebEventSubscriber}. */
230
+ subscriber: Pick<SmrtWebEventSubscriber, 'registerTable' | 'invalidateAll' | 'transport'>;
231
+ /**
232
+ * The PHYSICAL table name this collection reads. EXPLICIT because a
233
+ * `SmrtWebCollectionDefinition` has no physical-table field and STI children
234
+ * share one base table — the subscriber keys signals by physical table, so a
235
+ * guess would mis-route invalidations.
236
+ */
237
+ tableName: string;
238
+ }
239
+
240
+ /** The settle outcome handed to {@link SmrtWebCapability.onSettled}. */
241
+ export declare type MutationSettleOutcome = {
242
+ ok: true;
243
+ result: unknown;
244
+ } | {
245
+ ok: false;
246
+ error: unknown;
247
+ };
248
+
115
249
  /**
116
250
  * Generate a client-local id for optimistic inserts. The generated REST layer
117
251
  * strips client-supplied ids on create (mass-assignment guard #1540), so this
@@ -120,6 +254,187 @@ export declare function getEngineCollection<TData extends object>(handle: SmrtWe
120
254
  */
121
255
  export declare function newLocalId(): string;
122
256
 
257
+ /**
258
+ * Build a durable offline-outbox capability for a collection. Add the returned
259
+ * capability to the collection's `capabilities` array; a collection without it
260
+ * is unaffected (the seam's no-op guarantee — the "opt-in per model" AC).
261
+ *
262
+ * The same `namespace` across multiple collections shares ONE engine (one IDB
263
+ * db, one leader lock, one FIFO queue) — so cross-collection ordering and the
264
+ * multi-tab single-replayer guarantee hold across every opted-in collection of a
265
+ * given identity.
266
+ */
267
+ export declare function offlineOutbox<TData extends object = object>(config: OfflineOutboxConfig<TData>): SmrtWebCapability<TData>;
268
+
269
+ /**
270
+ * Configuration for {@link offlineOutbox}. Generic in the collection's row type
271
+ * `TData` so the capability matches the collection it plugs into.
272
+ */
273
+ export declare interface OfflineOutboxConfig<TData extends object = object> {
274
+ /**
275
+ * The generated collection definition this outbox serves — its `name` is the
276
+ * sync-apply `object` route segment, so it MUST be the SAME definition passed
277
+ * to {@link createSmrtCollection}. (Named `object` to mirror the capability
278
+ * context; carries the collection's REST route segment.)
279
+ */
280
+ object: {
281
+ name: string;
282
+ _row?: TData;
283
+ };
284
+ /**
285
+ * The durable-store identity this outbox's queue is namespaced under — folds
286
+ * api base / tenant / identity / manifest hash, so a logout or tenant switch
287
+ * lands on a different IndexedDB database (never cross-identity reuse). Shared
288
+ * with #1764's persistence slice via the same {@link durableStoreNamespace}.
289
+ * `manifestHash` is opaque caller-supplied config for #1762; its canonical
290
+ * source is #1764's call (a schema-shape digest) — comment your call site.
291
+ */
292
+ namespace: DurableStoreKey;
293
+ /**
294
+ * API base path the sync-apply endpoint lives under (`POST
295
+ * {syncApplyBasePath}/sync/apply`). Defaults to `/api/v1`, matching the REST
296
+ * generator's default. Set to the SvelteKit route base (`/api`) when replaying
297
+ * against the generated SvelteKit `sync/apply/+server.ts`.
298
+ */
299
+ syncApplyBasePath?: string;
300
+ /** Fetch implementation override (tests, SSR). Defaults to global fetch. */
301
+ fetchFn?: typeof fetch;
302
+ /** Exponential-backoff tuning for retryable replay failures. */
303
+ backoff?: OutboxBackoff;
304
+ /**
305
+ * Called on every observable sync-state transition of a captured mutation
306
+ * (`pending → uploading → synced`, `→ failed`, and the `pending` re-arm after
307
+ * a retryable failure). A push callback; smrt-svelte turns it into a reactive
308
+ * binding later.
309
+ */
310
+ onSyncStateChange?: (event: SyncStateEvent) => void;
311
+ /**
312
+ * Called when a replayed mutation comes back `conflict` — a RESOLVED outcome
313
+ * (server state won; the item leaves the queue with terminal state `synced`).
314
+ * Persist `serverUpdatedAt` as the new `baseUpdatedAt`, refetch the row, and
315
+ * surface the conflict to the user as appropriate.
316
+ */
317
+ onConflict?: (conflict: OutboxConflict) => void;
318
+ /** Test-only: inject a deterministic RNG for backoff jitter. */
319
+ random?: () => number;
320
+ }
321
+
322
+ /** Exponential-backoff tuning for retryable replay failures. */
323
+ export declare interface OutboxBackoff {
324
+ /** Delay before the first retry, ms (default 1000). */
325
+ initialDelayMs?: number;
326
+ /** Multiplier applied per attempt (default 2). */
327
+ multiplier?: number;
328
+ /** Ceiling on the computed delay, ms (default 60000). */
329
+ maxDelayMs?: number;
330
+ }
331
+
332
+ /**
333
+ * A conflict surfaced to {@link OfflineOutboxConfig.onConflict} when a replayed
334
+ * item comes back `conflict`. Per the contract this is a RESOLVED outcome — the
335
+ * server state won, the item is removed, and its terminal observable state is
336
+ * `synced` — so an app treats it as "your write was superseded; here is the
337
+ * server's `updatedAt` to rebase from", NOT as a failure to retry.
338
+ */
339
+ export declare interface OutboxConflict {
340
+ /** The queue row's `itemId`. */
341
+ itemId: string;
342
+ /** The collection route segment. */
343
+ object: string;
344
+ /** The client-generated row UUID that conflicted. */
345
+ rowId: string;
346
+ /**
347
+ * Why the item conflicted: `stale_write` (an update/delete whose
348
+ * `baseUpdatedAt` was older than the server row) or `create_conflict` (a
349
+ * create landing on an existing, diverged row).
350
+ */
351
+ reason: 'stale_write' | 'create_conflict';
352
+ /**
353
+ * The server row's `updated_at` after processing — the value to persist as
354
+ * the new `baseUpdatedAt` before re-editing. Present when the endpoint
355
+ * returned it.
356
+ */
357
+ serverUpdatedAt?: string;
358
+ }
359
+
360
+ /**
361
+ * A programmatic handle to one namespace's outbox — the bridge pattern (like
362
+ * `getEngineCollection`) for trusted callers (an app's outbox panel, a
363
+ * smrt-svelte binding, tests). Fetched via {@link getOutboxHandle}.
364
+ */
365
+ export declare interface OutboxHandle {
366
+ /**
367
+ * A read-only snapshot of the durable queue (every state). The way to prove
368
+ * durability after a reload, since this slice does not rehydrate the read
369
+ * cache (that's #1764).
370
+ */
371
+ snapshot(): Promise<OutboxSnapshotItem[]>;
372
+ /**
373
+ * Force a retry of a specific queued item now (clears its backoff gate, wakes
374
+ * the loop, and clears an auth pause). A no-op for an item no longer queued.
375
+ */
376
+ retry(itemId: string): Promise<void>;
377
+ }
378
+
379
+ /** One item in an {@link OutboxHandle.snapshot} result. */
380
+ export declare interface OutboxSnapshotItem {
381
+ /** The queue row's idempotency handle. */
382
+ itemId: string;
383
+ /** The collection route segment. */
384
+ object: string;
385
+ /** The mutation kind, in sync-apply terms. */
386
+ op: 'create' | 'update' | 'delete';
387
+ /** The client-generated row UUID. */
388
+ rowId: string;
389
+ /** The on-disk lifecycle state. */
390
+ state: 'pending' | 'synced' | 'failed';
391
+ /** Replay attempts so far. */
392
+ attempts: number;
393
+ /** Epoch ms before which this row won't be retried (backoff). */
394
+ nextAttemptAt: number;
395
+ /** Last replay error, if any. */
396
+ lastError?: string;
397
+ }
398
+
399
+ /**
400
+ * The app-observable sync state of a queued mutation — the SAME four-state
401
+ * machine the KMP mobile foundation exposes (ADR 0001), so web and mobile
402
+ * outbox indicators render identically:
403
+ *
404
+ * - `pending` — enqueued, awaiting (re)send.
405
+ * - `uploading` — currently in a sync-apply POST.
406
+ * - `synced` — the mutation's effect is confirmed on the server (a terminal
407
+ * success — INCLUDING a surfaced conflict, which is a RESOLVED outcome: the
408
+ * server state won and the item left the queue).
409
+ * - `failed` — a terminal rejection the client cannot resolve by retrying
410
+ * (`invalid_*`, `not_found`, `id_conflict`, `op_not_allowed`, …) — surfaced
411
+ * for app-level handling.
412
+ */
413
+ export declare type OutboxSyncState = 'pending' | 'uploading' | 'synced' | 'failed';
414
+
415
+ /**
416
+ * Register a durable artifact under a namespace so {@link wipeDurableStore} can
417
+ * later clear it. Returns an unregister function that removes just this
418
+ * resource — call it when the artifact is disposed on its own (before any
419
+ * namespace-wide wipe) so it is not cleared twice.
420
+ */
421
+ export declare function registerDurableResource(namespace: string, resource: DurableResource): () => void;
422
+
423
+ /**
424
+ * Run the `wrapMutation` hook across `capabilities` in array order for one
425
+ * mutation, short-circuiting on the FIRST capability that returns `{ handled:
426
+ * true }` (later capabilities' `wrapMutation` are then skipped). A capability
427
+ * that returns `{ handled: false }`, `undefined`, or omits the hook declines,
428
+ * and the next is tried. When every capability declines, resolves `{ handled:
429
+ * false }` so the factory falls through to the real fetcher.
430
+ */
431
+ export declare function runWrapMutation<TData extends object>(capabilities: readonly SmrtWebCapability<TData>[], envelope: SmrtWebMutationEnvelope, ctx: SmrtWebCapabilityContext<TData>): Promise<{
432
+ handled: true;
433
+ result: unknown;
434
+ } | {
435
+ handled: false;
436
+ }>;
437
+
123
438
  /**
124
439
  * The per-collection CRUD surface of the generated REST client
125
440
  * (`createClient(basePath).<collection>` from `@happyvertical/smrt-virt-client`).
@@ -136,6 +451,92 @@ export declare interface SmrtCrudFetchers {
136
451
  delete?(id: string): Promise<unknown>;
137
452
  }
138
453
 
454
+ /**
455
+ * A capability plugged into {@link createSmrtCollection}. Every hook is
456
+ * optional; a capability implements only the points its slice needs. Hooks fire
457
+ * in these fixed places, and capabilities run in ARRAY ORDER:
458
+ *
459
+ * - `contributeCacheKey` — ONCE, before the engine collection is constructed.
460
+ * - `warmStart` — ONCE, before the first read (seeds the cache).
461
+ * - `wrapMutation` — per mutation, BEFORE the fetcher, with a chance to handle
462
+ * the write itself (offline).
463
+ * - `onSettled` — per mutation, AFTER it settles (success AND fetcher-throw).
464
+ * - `onAttach` — ONCE, right after the engine collection is constructed; the
465
+ * ONLY place a capability registers an external (non-mutation) trigger such as
466
+ * an SSE subscription.
467
+ * - `teardown` — in `cleanup()`, after the engine's own cleanup.
468
+ */
469
+ export declare interface SmrtWebCapability<TData extends object = object> {
470
+ /** Diagnostic name (e.g. `'offline-outbox'`). */
471
+ readonly name: string;
472
+ /**
473
+ * Contribute extra cache-key segments so this capability can partition the
474
+ * collection's cache (e.g. an outbox variant). Runs ONCE, before construction;
475
+ * returned segments are appended to the collection's cache key/id. Return
476
+ * `undefined` (or omit) to contribute nothing.
477
+ */
478
+ contributeCacheKey?(ctx: SmrtWebCapabilityContext<TData>): string[] | undefined;
479
+ /**
480
+ * Provide rows to seed the cache before the first read — the persistence
481
+ * slice's rehydrate-from-disk path. Runs ONCE, before construction. NOTE:
482
+ * caller-supplied `initialData` (fresher same-request SSR truth) WINS over any
483
+ * capability `warmStart`. Return `undefined` to contribute no seed.
484
+ */
485
+ warmStart?(ctx: SmrtWebCapabilityContext<TData>): Promise<SmrtWebRow<TData>[] | undefined> | SmrtWebRow<TData>[] | undefined;
486
+ /**
487
+ * Intercept a mutation BEFORE its fetcher runs. Return `{ handled: true,
488
+ * result }` to take over the write (the fetcher is skipped and `result`
489
+ * reconciles the optimistic row — the offline path); return `{ handled: false
490
+ * }` or `undefined` to fall through to the real fetcher. With multiple
491
+ * capabilities the FIRST `{ handled: true }` wins and later capabilities'
492
+ * `wrapMutation` are skipped.
493
+ */
494
+ wrapMutation?(envelope: SmrtWebMutationEnvelope, ctx: SmrtWebCapabilityContext<TData>): Promise<WrapMutationOutcome> | WrapMutationOutcome;
495
+ /**
496
+ * Observe a mutation after it settles — on BOTH a successful persist and a
497
+ * fetcher throw (before the optimistic rollback propagates). Never swallows
498
+ * the error: a thrown fetcher error still rolls the transaction back.
499
+ */
500
+ onSettled?(envelope: SmrtWebMutationEnvelope, outcome: MutationSettleOutcome, ctx: SmrtWebCapabilityContext<TData>): void;
501
+ /**
502
+ * Run ONCE, right after the engine collection is constructed. The ONLY hook
503
+ * where a capability wires an external trigger (SSE subscription, focus
504
+ * listener) — `ctx.invalidate()` is callable here.
505
+ */
506
+ onAttach?(ctx: SmrtWebCapabilityContext<TData>): void;
507
+ /** Run inside `cleanup()`, AFTER the engine's own cleanup. Awaited if async. */
508
+ teardown?(ctx: SmrtWebCapabilityContext<TData>): void | Promise<void>;
509
+ }
510
+
511
+ /**
512
+ * The context every capability hook receives — a stable, engine-free view of
513
+ * the collection being built. All fields are the exact values the factory uses
514
+ * internally (the resolved cache key/id, the same fetchers), so a capability
515
+ * keys its own storage or subscriptions off the identical discriminators.
516
+ */
517
+ export declare interface SmrtWebCapabilityContext<TData extends object = object> {
518
+ /** The generated definition this collection materializes. */
519
+ readonly definition: SmrtWebCollectionDefinition<TData>;
520
+ /** The CRUD fetchers the collection persists through. */
521
+ readonly fetchers: SmrtCrudFetchers;
522
+ /**
523
+ * The `queryKey` segments the collection's reads use — a LIVE view: from
524
+ * `onAttach`/`warmStart` onward it is the final key (including every
525
+ * capability-contributed segment); read during `contributeCacheKey` it omits
526
+ * that capability's own not-yet-applied segment. Treat as read-only.
527
+ */
528
+ readonly cacheKey: readonly string[];
529
+ /** The engine-collection id string (mirrors {@link cacheKey}). */
530
+ readonly cacheId: string;
531
+ /**
532
+ * Enter the SAME relationship-derived invalidation the factory runs after a
533
+ * settled mutation. A capability calls this to refetch this collection and its
534
+ * manifest-related collections in response to an EXTERNAL trigger (e.g. an SSE
535
+ * message) without reaching into the engine.
536
+ */
537
+ invalidate(): void;
538
+ }
539
+
139
540
  /**
140
541
  * Opaque handle to the shared client cache / request-dedup layer. Create one
141
542
  * with {@link createSmrtWebClient} and pass the SAME instance to every
@@ -209,42 +610,88 @@ export declare interface SmrtWebCollectionDefinition<TData extends object = obje
209
610
  }
210
611
 
211
612
  /**
212
- * @happyvertical/smrt-webbrowser client data runtime (#1761).
213
- *
214
- * A typed collection factory that materializes the manifest-generated web
215
- * collection definitions (`@happyvertical/smrt-virt-web`) as cached, reactive
216
- * collections over the generated SMRT REST surface.
217
- *
218
- * This package is the **engine-absorption boundary**: the client-data engine
219
- * (currently TanStack DB) is an implementation detail held entirely inside
220
- * this module. Its types never appear on the public API — collections are
221
- * handed back as the SMRT-owned {@link SmrtWebCollection}, and the shared cache
222
- * as the opaque {@link SmrtWebClient} — so the engine stays swappable without a
223
- * consumer-visible break. Consumers never import `@tanstack/*` directly.
224
- *
225
- * Framework-agnostic by construction: this entry imports no UI framework.
226
- * Svelte live-query bindings ship separately (see PRD #1755) so this core never
227
- * pulls the Svelte-only `@tanstack/svelte-db` export condition.
228
- *
229
- * Scope of this slice:
230
- * - stale-while-revalidate reads (a `staleTimeMs` window, background revalidation)
231
- * - concurrent-read dedup (one network request per in-flight collection load)
232
- * - optimistic create that persists through the generated REST surface and
233
- * rolls back automatically when the server errors
234
- * - relationship-derived invalidation (#1761): a settled mutation invalidates
235
- * the caches of the collections related to the mutated one, with the edges
236
- * derived from the manifest (`definition.relationships`) — no hand-wired
237
- * cache keys. Cross-collection reach requires a shared client from
238
- * {@link createSmrtWebClient}; with a private client only the mutated
239
- * collection refetches.
240
- * - hydration seeding (#1761): rows fetched server-side (a SvelteKit
241
- * `+page.server.ts` load) seed the shared cache via
242
- * {@link CreateSmrtCollectionOptions.initialData}, so the first client read
243
- * serves them WITHOUT a duplicate first-render fetch.
244
- *
245
- * Deliberately NOT here yet (see PRD #1755): offline outbox, SSE invalidation,
246
- * persistence, version awareness.
613
+ * The minimal `EventSource` surface the subscriber uses declared here so a
614
+ * test injects a fake without a real DOM, and so this module compiles without
615
+ * DOM `lib` beyond the ambient global. A real `EventSource` satisfies it
616
+ * structurally.
247
617
  */
618
+ export declare interface SmrtWebEventSource {
619
+ /** Native reconnect fires this after (re)connect. */
620
+ onopen: ((this: unknown, ev: unknown) => unknown) | null;
621
+ /**
622
+ * Fires on a stream error. A transient drop leaves `readyState` at OPEN
623
+ * (0→…) and the browser auto-reconnects with `Last-Event-ID`; a fatal error
624
+ * (server 401 / route disabled) leaves it CLOSED.
625
+ */
626
+ onerror: ((this: unknown, ev: unknown) => unknown) | null;
627
+ /**
628
+ * Fires only for UNNAMED (`message`) events. The `_events` frames are NAMED
629
+ * (`change` / `resync`), so this never fires for them — the subscriber uses
630
+ * {@link addEventListener} instead. Present only to satisfy the structural
631
+ * type of a real EventSource.
632
+ */
633
+ onmessage: ((this: unknown, ev: unknown) => unknown) | null;
634
+ /** Register a listener for a NAMED event (`change`, `resync`). */
635
+ addEventListener(type: string, listener: (ev: {
636
+ data: string;
637
+ lastEventId: string;
638
+ }) => void): void;
639
+ /** Close the stream (stops reconnection). */
640
+ close(): void;
641
+ /** `CONNECTING` (0), `OPEN` (1), or `CLOSED` (2). */
642
+ readonly readyState: number;
643
+ }
644
+
645
+ /** Factory that constructs an {@link SmrtWebEventSource} for a URL. */
646
+ export declare type SmrtWebEventSourceFactory = (url: string, init: {
647
+ withCredentials: boolean;
648
+ }) => SmrtWebEventSource | null | undefined;
649
+
650
+ /**
651
+ * The ONE app-wide live-updates subscriber. Construct once (see
652
+ * {@link createSmrtWebEventSubscriber}) and pass it to every
653
+ * {@link liveInvalidation} capability.
654
+ */
655
+ export declare interface SmrtWebEventSubscriber {
656
+ /**
657
+ * The live transport: `'sse'` while the EventSource is the source of truth,
658
+ * `'polling'` when running (or downgraded to) the `_changes` fallback,
659
+ * `'idle'` before any transport starts.
660
+ */
661
+ readonly transport: SmrtWebSubscriberTransport;
662
+ /**
663
+ * Register an invalidator for a physical table. Returns an unregister
664
+ * function. Multiple invalidators may share a table (two live collections);
665
+ * a `change` for that table fires them all.
666
+ */
667
+ registerTable(table: string, invalidate: () => void): () => void;
668
+ /** Invalidate every registered table (a `resync` / `resyncRequired`). */
669
+ invalidateAll(): void;
670
+ /** Tear down the live transport and drop all registrations. */
671
+ close(): void;
672
+ }
673
+
674
+ /** Configuration for {@link createSmrtWebEventSubscriber}. */
675
+ export declare interface SmrtWebEventSubscriberConfig {
676
+ /** Absolute or same-origin URL of the generated `_events` SSE route. */
677
+ eventsUrl: string;
678
+ /** Absolute or same-origin URL of the generated `_changes` route (fallback). */
679
+ changesUrl: string;
680
+ /** `fetch` implementation for the polling fallback. Defaults to global fetch. */
681
+ fetchFn?: typeof fetch;
682
+ /**
683
+ * Factory for the EventSource. Defaults to `new globalThis.EventSource(url,
684
+ * { withCredentials })`, guarded by a `typeof` check — when EventSource is
685
+ * absent (or the factory yields nothing) the subscriber starts on the polling
686
+ * fallback instead.
687
+ */
688
+ eventSourceFactory?: SmrtWebEventSourceFactory;
689
+ /** Poll interval for the `_changes` fallback (ms). Default 5000. */
690
+ pollIntervalMs?: number;
691
+ /** `withCredentials` for the EventSource (cookie auth). Default true. */
692
+ withCredentials?: boolean;
693
+ }
694
+
248
695
  /**
249
696
  * Field metadata emitted per column by the `@happyvertical/smrt-virt-web`
250
697
  * virtual module (generated from the package manifest).
@@ -255,6 +702,31 @@ export declare interface SmrtWebFieldDefinition {
255
702
  default?: unknown;
256
703
  }
257
704
 
705
+ /**
706
+ * A single mutation described in SMRT-owned terms — handed to
707
+ * {@link SmrtWebCapability.wrapMutation} and {@link SmrtWebCapability.onSettled}
708
+ * so a capability can inspect or intercept a write without touching the engine's
709
+ * transaction type.
710
+ */
711
+ export declare interface SmrtWebMutationEnvelope {
712
+ /** Which mutation kind this describes. */
713
+ readonly kind: 'insert' | 'update' | 'delete';
714
+ /** The row key: the client-local id on insert, else the target row's id. */
715
+ readonly key: string;
716
+ /**
717
+ * The write payload: the full row on insert, the changed fields on update,
718
+ * and an empty object on delete (the key carries the target).
719
+ */
720
+ readonly data: Record<string, unknown>;
721
+ /**
722
+ * The server `updated_at` / `updatedAt` value the mutation is based on, when
723
+ * known. Update/delete handlers capture this from the original row so offline
724
+ * replay can preserve the sync/apply conflict guard even when the write
725
+ * payload only carries changed fields (or no fields for delete).
726
+ */
727
+ readonly baseUpdatedAt?: string;
728
+ }
729
+
258
730
  /**
259
731
  * A manifest-derived edge from this collection to a sibling REST collection,
260
732
  * emitted by the `@happyvertical/smrt-virt-web` virtual module. When a mutation
@@ -293,6 +765,9 @@ export declare type SmrtWebRow<TData extends object> = TData & {
293
765
  id: string;
294
766
  };
295
767
 
768
+ /** Which transport a subscriber is currently using. */
769
+ export declare type SmrtWebSubscriberTransport = 'sse' | 'polling' | 'idle';
770
+
296
771
  /** A change-subscription handle. Call {@link unsubscribe} to detach. */
297
772
  export declare interface SmrtWebSubscription {
298
773
  unsubscribe(): void;
@@ -309,6 +784,27 @@ export declare interface SmrtWebTransaction {
309
784
  };
310
785
  }
311
786
 
787
+ /**
788
+ * A sync-state transition delivered to {@link OfflineOutboxConfig.onSyncStateChange}.
789
+ * A PUSH callback (not a subscribable store): smrt-svelte wraps it into a
790
+ * reactive binding later (#1762 follow-on). Carries enough for an app to render
791
+ * a per-row indicator and a retry affordance.
792
+ */
793
+ export declare interface SyncStateEvent {
794
+ /** The queue row's `itemId` (its idempotency handle) this state is for. */
795
+ itemId: string;
796
+ /** The client-generated row UUID the mutation targets. */
797
+ rowId: string;
798
+ /** The collection route segment (e.g. `products`). */
799
+ object: string;
800
+ /** The new observable state. */
801
+ state: OutboxSyncState;
802
+ /** How many replay attempts have run (0 until the first send). */
803
+ attempts: number;
804
+ /** The last replay error message, when `state` is `pending` after a retry. */
805
+ error?: string;
806
+ }
807
+
312
808
  /**
313
809
  * Normalize a generated-client item result (create/update) to a row.
314
810
  * `{ error }` payloads become failures — inside mutation handlers this is what
@@ -325,4 +821,24 @@ export declare function unwrapItemResult(result: unknown, context: string): Reco
325
821
  */
326
822
  export declare function unwrapListResult(result: unknown, collectionName: string): Array<Record<string, unknown>>;
327
823
 
824
+ /**
825
+ * Clear every durable artifact registered under `namespace`, then drop the
826
+ * namespace. This is the single teardown point a logout / tenant-switch calls:
827
+ * it fans out across BOTH the outbox and persistence slices via the registry,
828
+ * so neither module needs to import the other.
829
+ *
830
+ * Best-effort: a resource whose `clear()` rejects does not abort the sweep —
831
+ * every registered resource is still cleared (a wipe is a teardown, not a
832
+ * transaction). A safe no-op on an unknown or empty namespace.
833
+ */
834
+ export declare function wipeDurableStore(namespace: string): Promise<void>;
835
+
836
+ /** The outcome handed to {@link SmrtWebCapability.wrapMutation}. */
837
+ export declare type WrapMutationOutcome = {
838
+ handled: true;
839
+ result: unknown;
840
+ } | {
841
+ handled: false;
842
+ } | undefined;
843
+
328
844
  export { }