@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/AGENTS.md +254 -0
- package/dist/index.d.ts +551 -35
- package/dist/index.js +1202 -11
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
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
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
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 { }
|