@happyvertical/smrt-web 0.51.32 → 0.51.34
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/__typechecks__/webmcp-tool-definition.d.ts +2 -0
- package/dist/__typechecks__/webmcp-tool-definition.d.ts.map +1 -0
- package/dist/ai.d.ts +330 -358
- package/dist/ai.d.ts.map +1 -0
- package/dist/capability-classification.d.ts +89 -0
- package/dist/capability-classification.d.ts.map +1 -0
- package/dist/capability.d.ts +191 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/data-query.d.ts +121 -0
- package/dist/data-query.d.ts.map +1 -0
- package/dist/data-surface-actions.d.ts +118 -0
- package/dist/data-surface-actions.d.ts.map +1 -0
- package/dist/durable-store.d.ts +88 -0
- package/dist/durable-store.d.ts.map +1 -0
- package/dist/index.d.ts +468 -2245
- package/dist/index.d.ts.map +1 -0
- package/dist/intents.d.ts +273 -300
- package/dist/intents.d.ts.map +1 -0
- package/dist/internal.d.ts +15 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/offline/durable-queue.d.ts +171 -0
- package/dist/offline/durable-queue.d.ts.map +1 -0
- package/dist/offline/engine.d.ts +359 -0
- package/dist/offline/engine.d.ts.map +1 -0
- package/dist/offline/leader.d.ts +57 -0
- package/dist/offline/leader.d.ts.map +1 -0
- package/dist/offline/types.d.ts +232 -0
- package/dist/offline/types.d.ts.map +1 -0
- package/dist/offline.d.ts +258 -0
- package/dist/offline.d.ts.map +1 -0
- package/dist/persistence/snapshot-store.d.ts +121 -0
- package/dist/persistence/snapshot-store.d.ts.map +1 -0
- package/dist/persistence.d.ts +157 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/remote-query.d.ts +61 -0
- package/dist/remote-query.d.ts.map +1 -0
- package/dist/sse-client.d.ts +182 -0
- package/dist/sse-client.d.ts.map +1 -0
- package/dist/update-state/meta-store.d.ts +66 -0
- package/dist/update-state/meta-store.d.ts.map +1 -0
- package/dist/update-state.d.ts +115 -0
- package/dist/update-state.d.ts.map +1 -0
- package/dist/webmcp-tool-names.d.ts +106 -112
- package/dist/webmcp-tool-names.d.ts.map +1 -0
- package/dist/webmcp.d.ts +193 -653
- package/dist/webmcp.d.ts.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-web — offline outbox shared types (#1762).
|
|
3
|
+
*
|
|
4
|
+
* The SMRT-owned vocabulary the outbox modules (`durable-queue`, `leader`,
|
|
5
|
+
* `engine`) and the public surface (`offline.ts`) share. Kept in its own leaf
|
|
6
|
+
* module — imported by everything, importing nothing from the package — so there
|
|
7
|
+
* is no cycle back through `index.ts` and the whole outbox stays inside the
|
|
8
|
+
* engine-absorption boundary (#1761): NOT ONE `@tanstack/*` type appears here.
|
|
9
|
+
*
|
|
10
|
+
* The sync-apply op/status/reason names mirror
|
|
11
|
+
* `packages/core/src/sync/apply.ts` (the replay target) verbatim, so a queue row
|
|
12
|
+
* maps to a batch item and a batch result maps to a state transition with no
|
|
13
|
+
* translation layer. smrt-web has no inter-smrt dependency (dependency-DAG
|
|
14
|
+
* guardrails), so these are re-declared here rather than imported from core.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Mutation kinds the sync-apply batch endpoint accepts. Mirrors core's
|
|
18
|
+
* `SyncApplyOp`. The outbox maps the capability seam's insert/update/delete
|
|
19
|
+
* envelope kinds onto these (`insert → create`).
|
|
20
|
+
*/
|
|
21
|
+
export type SyncApplyOp = 'create' | 'update' | 'delete';
|
|
22
|
+
/**
|
|
23
|
+
* Per-item outcome status from a sync-apply batch result. Mirrors core's
|
|
24
|
+
* `SyncApplyStatus`. The engine maps each onto a durable transition per the
|
|
25
|
+
* contract's "Web outbox (#1762)" consumer notes.
|
|
26
|
+
*/
|
|
27
|
+
export type SyncApplyStatus = 'applied' | 'conflict' | 'rejected';
|
|
28
|
+
/**
|
|
29
|
+
* Machine-readable reasons a sync-apply item was `conflict` or `rejected`.
|
|
30
|
+
* Mirrors core's `SyncApplyReason` union. Drives the engine's result mapping:
|
|
31
|
+
* `auth_required`/`forbidden` pause the loop; `write_failed` retries; the rest
|
|
32
|
+
* are terminal.
|
|
33
|
+
*/
|
|
34
|
+
export type SyncApplyReason = 'invalid_item' | 'invalid_id' | 'invalid_payload' | 'unknown_object' | 'op_not_allowed' | 'auth_required' | 'forbidden' | 'not_found' | 'id_conflict' | 'write_failed' | 'stale_write' | 'create_conflict';
|
|
35
|
+
/**
|
|
36
|
+
* One item in a `POST {basePath}/sync/apply` request body. Mirrors core's
|
|
37
|
+
* `SyncApplyItem`. Built from an {@link OutboxRow} 1:1.
|
|
38
|
+
*/
|
|
39
|
+
export interface SyncApplyItem {
|
|
40
|
+
itemId: string;
|
|
41
|
+
object: string;
|
|
42
|
+
op: SyncApplyOp;
|
|
43
|
+
id: string;
|
|
44
|
+
payload?: Record<string, unknown>;
|
|
45
|
+
baseUpdatedAt?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One entry in a sync-apply batch response's `results[]`. Mirrors core's
|
|
49
|
+
* `SyncApplyItemResult`. `results[i]` corresponds to `items[i]` positionally;
|
|
50
|
+
* `itemId` is echoed for convenience.
|
|
51
|
+
*/
|
|
52
|
+
export interface SyncApplyItemResult {
|
|
53
|
+
itemId: string | null;
|
|
54
|
+
id: string | null;
|
|
55
|
+
status: SyncApplyStatus;
|
|
56
|
+
reason?: SyncApplyReason;
|
|
57
|
+
updatedAt?: string;
|
|
58
|
+
}
|
|
59
|
+
/** The HTTP-200 response body of `POST {basePath}/sync/apply`. */
|
|
60
|
+
export interface SyncApplyBatchResponse {
|
|
61
|
+
results: SyncApplyItemResult[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Maximum items the endpoint accepts in one batch (core's
|
|
65
|
+
* `MAX_SYNC_APPLY_BATCH_SIZE`). The engine chunks a larger backlog into
|
|
66
|
+
* successive POSTs, oldest-first, so FIFO order is preserved across chunks.
|
|
67
|
+
*/
|
|
68
|
+
export declare const MAX_SYNC_APPLY_BATCH_SIZE = 1000;
|
|
69
|
+
/**
|
|
70
|
+
* The URL segments of the batch apply endpoint relative to the API base path:
|
|
71
|
+
* `POST {basePath}/sync/apply` (core's `SYNC_APPLY_ROUTE_SEGMENTS`).
|
|
72
|
+
*/
|
|
73
|
+
export declare const SYNC_APPLY_ROUTE_SEGMENTS: readonly ["sync", "apply"];
|
|
74
|
+
/**
|
|
75
|
+
* The app-observable sync state of a queued mutation — the SAME four-state
|
|
76
|
+
* machine the KMP mobile foundation exposes (ADR 0001), so web and mobile
|
|
77
|
+
* outbox indicators render identically:
|
|
78
|
+
*
|
|
79
|
+
* - `pending` — enqueued, awaiting (re)send.
|
|
80
|
+
* - `uploading` — currently in a sync-apply POST.
|
|
81
|
+
* - `synced` — the mutation's effect is confirmed on the server (a terminal
|
|
82
|
+
* success — INCLUDING a surfaced conflict, which is a RESOLVED outcome: the
|
|
83
|
+
* server state won and the item left the queue).
|
|
84
|
+
* - `failed` — a terminal rejection the client cannot resolve by retrying
|
|
85
|
+
* (`invalid_*`, `not_found`, `id_conflict`, `op_not_allowed`, …) — surfaced
|
|
86
|
+
* for app-level handling.
|
|
87
|
+
*/
|
|
88
|
+
export type OutboxSyncState = 'pending' | 'uploading' | 'synced' | 'failed';
|
|
89
|
+
/**
|
|
90
|
+
* A sync-state transition delivered to {@link OfflineOutboxConfig.onSyncStateChange}.
|
|
91
|
+
* A PUSH callback (not a subscribable store): smrt-svelte wraps it into a
|
|
92
|
+
* reactive binding later (#1762 follow-on). Carries enough for an app to render
|
|
93
|
+
* a per-row indicator and a retry affordance.
|
|
94
|
+
*/
|
|
95
|
+
export interface SyncStateEvent {
|
|
96
|
+
/** The queue row's `itemId` (its idempotency handle) this state is for. */
|
|
97
|
+
itemId: string;
|
|
98
|
+
/** The client-generated row UUID the mutation targets. */
|
|
99
|
+
rowId: string;
|
|
100
|
+
/** The collection route segment (e.g. `products`). */
|
|
101
|
+
object: string;
|
|
102
|
+
/** The new observable state. */
|
|
103
|
+
state: OutboxSyncState;
|
|
104
|
+
/** How many replay attempts have run (0 until the first send). */
|
|
105
|
+
attempts: number;
|
|
106
|
+
/** The last replay error message, when `state` is `pending` after a retry. */
|
|
107
|
+
error?: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A conflict surfaced to {@link OfflineOutboxConfig.onConflict} when a replayed
|
|
111
|
+
* item comes back `conflict`. Per the contract this is a RESOLVED outcome — the
|
|
112
|
+
* server state won, the item is removed, and its terminal observable state is
|
|
113
|
+
* `synced` — so an app treats it as "your write was superseded; here is the
|
|
114
|
+
* server's `updatedAt` to rebase from", NOT as a failure to retry.
|
|
115
|
+
*/
|
|
116
|
+
export interface OutboxConflict {
|
|
117
|
+
/** The queue row's `itemId`. */
|
|
118
|
+
itemId: string;
|
|
119
|
+
/** The collection route segment. */
|
|
120
|
+
object: string;
|
|
121
|
+
/** The client-generated row UUID that conflicted. */
|
|
122
|
+
rowId: string;
|
|
123
|
+
/**
|
|
124
|
+
* Why the item conflicted: `stale_write` (an update/delete whose
|
|
125
|
+
* `baseUpdatedAt` was older than the server row) or `create_conflict` (a
|
|
126
|
+
* create landing on an existing, diverged row).
|
|
127
|
+
*/
|
|
128
|
+
reason: 'stale_write' | 'create_conflict';
|
|
129
|
+
/**
|
|
130
|
+
* The server row's `updated_at` after processing — the value to persist as
|
|
131
|
+
* the new `baseUpdatedAt` before re-editing. Present when the endpoint
|
|
132
|
+
* returned it.
|
|
133
|
+
*/
|
|
134
|
+
serverUpdatedAt?: string;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* One queued write handed to a consumer-declared {@link OutboxCommandTransport}
|
|
138
|
+
* at replay time (#3021). The transport replays it through a server operation
|
|
139
|
+
* the consumer owns — a data-surface action, a named remote command, a
|
|
140
|
+
* hand-written permission-gated service — instead of the generated
|
|
141
|
+
* `sync/apply`, which calls `collection.create()` directly.
|
|
142
|
+
*/
|
|
143
|
+
export interface OutboxCommand {
|
|
144
|
+
/**
|
|
145
|
+
* The durable, client-minted idempotency key (the queue row's `itemId`). It
|
|
146
|
+
* is IDENTICAL on every attempt of this write — including a blind resend
|
|
147
|
+
* after a lost response — so the server operation MUST dedupe on it; that is
|
|
148
|
+
* what makes the outbox's ambiguous-failure retries safe on this path.
|
|
149
|
+
*/
|
|
150
|
+
idempotencyKey: string;
|
|
151
|
+
/** The transport name the write was queued under. */
|
|
152
|
+
name: string;
|
|
153
|
+
/** The mutation kind in sync-apply terms. */
|
|
154
|
+
op: SyncApplyOp;
|
|
155
|
+
/** The client-generated row id the write targets. */
|
|
156
|
+
rowId: string;
|
|
157
|
+
/** Row / changed fields (create, update); absent for delete. */
|
|
158
|
+
payload?: Record<string, unknown>;
|
|
159
|
+
/** The server `updated_at` the client last saw, when the caller supplied one. */
|
|
160
|
+
baseUpdatedAt?: string;
|
|
161
|
+
/** 1-based number of this attempt (`1` on the first send). */
|
|
162
|
+
attempt: number;
|
|
163
|
+
/** The value this write's transport pinned on an earlier attempt, if any. */
|
|
164
|
+
pinned?: unknown;
|
|
165
|
+
/**
|
|
166
|
+
* Durably pin a structured-clone-safe value to this write before sending,
|
|
167
|
+
* so a later attempt (including after a reload) sees it as `pinned`. A
|
|
168
|
+
* transport whose server fingerprints the request per idempotency key pins
|
|
169
|
+
* the request it built, so a resend after a lost response is identical.
|
|
170
|
+
*/
|
|
171
|
+
pin(value: unknown): Promise<void>;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The server's outcome for one {@link OutboxCommand}, in the SAME vocabulary
|
|
175
|
+
* the sync-apply replay maps — so the four-state machine and backoff are
|
|
176
|
+
* shared, not re-derived:
|
|
177
|
+
*
|
|
178
|
+
* - `applied` → removed, `synced`.
|
|
179
|
+
* - `conflict` → removed, `onConflict` fired, `synced` (a RESOLVED outcome).
|
|
180
|
+
* - `rejected` with `write_failed` → kept, backoff, `pending` (retryable).
|
|
181
|
+
* - `rejected` with `auth_required` / `forbidden` → the whole loop pauses until
|
|
182
|
+
* re-auth (a later enqueue or `retry()`), kept `pending`.
|
|
183
|
+
* - `rejected` with any other reason → removed, `failed` (terminal).
|
|
184
|
+
*
|
|
185
|
+
* A transport that THROWS (network error, lost response, 5xx) is the ambiguous
|
|
186
|
+
* path: the write stays `pending` with backoff and is blindly resent later
|
|
187
|
+
* under the same {@link OutboxCommand.idempotencyKey}.
|
|
188
|
+
*/
|
|
189
|
+
export type OutboxCommandResult = {
|
|
190
|
+
status: 'applied';
|
|
191
|
+
updatedAt?: string;
|
|
192
|
+
} | {
|
|
193
|
+
status: 'conflict';
|
|
194
|
+
reason?: 'stale_write' | 'create_conflict';
|
|
195
|
+
updatedAt?: string;
|
|
196
|
+
} | {
|
|
197
|
+
status: 'rejected';
|
|
198
|
+
reason: SyncApplyReason | (string & {});
|
|
199
|
+
};
|
|
200
|
+
/** Replays one queued write through a consumer-declared server operation. */
|
|
201
|
+
export type OutboxCommandTransport = (command: OutboxCommand) => Promise<OutboxCommandResult>;
|
|
202
|
+
/** Exponential-backoff tuning for retryable replay failures. */
|
|
203
|
+
export interface OutboxBackoff {
|
|
204
|
+
/** Delay before the first retry, ms (default 1000). */
|
|
205
|
+
initialDelayMs?: number;
|
|
206
|
+
/** Multiplier applied per attempt (default 2). */
|
|
207
|
+
multiplier?: number;
|
|
208
|
+
/** Ceiling on the computed delay, ms (default 60000). */
|
|
209
|
+
maxDelayMs?: number;
|
|
210
|
+
}
|
|
211
|
+
/** Resolved backoff config (defaults filled in). */
|
|
212
|
+
export interface ResolvedBackoff {
|
|
213
|
+
initialDelayMs: number;
|
|
214
|
+
multiplier: number;
|
|
215
|
+
maxDelayMs: number;
|
|
216
|
+
}
|
|
217
|
+
/** Backoff defaults, per the blueprint (initial=1s, ×2, cap 60s). */
|
|
218
|
+
export declare const DEFAULT_BACKOFF: ResolvedBackoff;
|
|
219
|
+
/**
|
|
220
|
+
* Compute the backoff delay before the NEXT attempt given how many attempts
|
|
221
|
+
* have already been made. Exponential with a ceiling, then multiplied by a
|
|
222
|
+
* jitter factor in `[0.5, 1.0)` to spread a thundering herd of tabs/rows
|
|
223
|
+
* reconnecting at once: `min(maxDelay, initial * mult ** attempts) * jitter`.
|
|
224
|
+
*
|
|
225
|
+
* `attempts` is the count BEFORE this retry (so the first retry, after attempts
|
|
226
|
+
* became 1, uses `initial * mult ** 1`? no — see below). We pass the attempt
|
|
227
|
+
* count AFTER incrementing, and index the exponent off `attempts - 1` so the
|
|
228
|
+
* first retry waits ~`initialDelayMs`. `random` is injectable for deterministic
|
|
229
|
+
* tests.
|
|
230
|
+
*/
|
|
231
|
+
export declare function computeBackoffDelay(attempts: number, backoff: ResolvedBackoff, random?: () => number): number;
|
|
232
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/offline/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-web — the durable offline outbox capability (#1762).
|
|
3
|
+
*
|
|
4
|
+
* The web twin of the KMP mobile write queue: mutations against opted-in
|
|
5
|
+
* collections are captured in a durable browser-side outbox (IndexedDB) that
|
|
6
|
+
* survives reloads and crashes, then replayed FIFO against the idempotent
|
|
7
|
+
* sync-apply batch contract when connectivity returns, with exponential-backoff
|
|
8
|
+
* retries. Because items carry client-generated UUIDs and the endpoint is
|
|
9
|
+
* idempotent (`_insertOnly` create + no-op re-apply), blind retries after an
|
|
10
|
+
* ambiguous failure (request sent, response lost) can never duplicate rows — see
|
|
11
|
+
* the "Idempotency" section of docs/content/architecture/sync-apply-contract.md.
|
|
12
|
+
*
|
|
13
|
+
* This is a {@link SmrtWebCapability} plug-in, so it lives in its OWN module and
|
|
14
|
+
* an app opts a collection in by passing `offlineOutbox(config)` in that
|
|
15
|
+
* collection's `capabilities` array. A collection WITHOUT it is byte-for-byte
|
|
16
|
+
* the collection of today (the seam's no-op guarantee), which is exactly the
|
|
17
|
+
* "offline is opt-in per model" acceptance criterion.
|
|
18
|
+
*
|
|
19
|
+
* ## How it plugs into the seam
|
|
20
|
+
*
|
|
21
|
+
* - `wrapMutation(envelope, ctx)` — BEFORE the fetcher, it enqueues the write
|
|
22
|
+
* durably and returns `{ handled: true, result: envelope.data }`. The factory
|
|
23
|
+
* then SUPPRESSES the post-mutation refetch + `invalidateRelated()` (its
|
|
24
|
+
* `{ refetch: false }` path), so the optimistic row the app just inserted
|
|
25
|
+
* STANDS instead of being dropped by a refetch of a server list that has never
|
|
26
|
+
* seen the offline write. If the durable queue is unavailable, it returns
|
|
27
|
+
* `{ handled: false }` so the normal fetcher path runs instead of silently
|
|
28
|
+
* acknowledging a non-durable write. A handled write never touches
|
|
29
|
+
* `ctx.fetchers.create` — it replays ONLY through `sync/apply`, which is
|
|
30
|
+
* load-bearing: the normal REST create strips the client id (#1540) and would
|
|
31
|
+
* mint a NEW server id, orphaning the optimistic row; sync/apply's
|
|
32
|
+
* strict-insert path preserves the client UUID (#1540, #1540's
|
|
33
|
+
* mass-assignment guard on the sync path).
|
|
34
|
+
* - `onAttach(ctx)` — attaches this collection to the shared, namespace-keyed
|
|
35
|
+
* {@link OutboxEngine} (ref-counted so N collections share ONE engine / IDB
|
|
36
|
+
* db / leader lock / FIFO queue).
|
|
37
|
+
* - `teardown(ctx)` — detaches; the last detach disposes the engine (the durable
|
|
38
|
+
* ROWS survive for the next load).
|
|
39
|
+
*
|
|
40
|
+
* ## Observable state (push callbacks, not a store)
|
|
41
|
+
*
|
|
42
|
+
* Per-mutation sync state — `pending → uploading → synced` (and `→ failed` /
|
|
43
|
+
* conflict) — is delivered through {@link OfflineOutboxConfig.onSyncStateChange}
|
|
44
|
+
* and {@link OfflineOutboxConfig.onConflict}, PUSH callbacks matching the KMP
|
|
45
|
+
* foundation's `pending/uploading/synced/failed` state machine. This is
|
|
46
|
+
* deliberately not a subscribable store: smrt-svelte wraps it into a reactive
|
|
47
|
+
* binding in a later slice. Programmatic access to the durable queue is via
|
|
48
|
+
* {@link getOutboxHandle} (a bridge like `getEngineCollection`).
|
|
49
|
+
*
|
|
50
|
+
* ## Known gap (this slice's scope)
|
|
51
|
+
*
|
|
52
|
+
* The outbox does NOT rehydrate the READ cache after a reload — a reloaded tab's
|
|
53
|
+
* `collection.toArray()` will NOT show captured-offline rows until a fetch runs;
|
|
54
|
+
* that read-side rehydrate is #1764's `warmStart`. So the outbox's durability is
|
|
55
|
+
* proven via {@link OutboxHandle.snapshot} / the raw IndexedDB store, NOT via
|
|
56
|
+
* `collection.toArray()`. The WRITE side (capture → durable → exactly-once
|
|
57
|
+
* replay) is complete here.
|
|
58
|
+
*
|
|
59
|
+
* Engine-free public surface: no `@tanstack/*` type appears — inside the
|
|
60
|
+
* engine-absorption boundary (#1761), verified by
|
|
61
|
+
* scripts/check-smrt-web-engine-boundary.mjs.
|
|
62
|
+
*/
|
|
63
|
+
import type { SmrtWebCapability } from './capability.js';
|
|
64
|
+
import { type DurableStoreKey } from './durable-store.js';
|
|
65
|
+
import { type OutboxBackoff, type OutboxCommandTransport, type OutboxConflict, type SyncApplyOp, type SyncStateEvent } from './offline/types.js';
|
|
66
|
+
export type { OutboxBackoff, OutboxCommand, OutboxCommandResult, OutboxCommandTransport, OutboxConflict, OutboxSyncState, SyncStateEvent, } from './offline/types.js';
|
|
67
|
+
/**
|
|
68
|
+
* Configuration for {@link offlineOutbox}. Generic in the collection's row type
|
|
69
|
+
* `TData` so the capability matches the collection it plugs into.
|
|
70
|
+
*/
|
|
71
|
+
export interface OfflineOutboxConfig<TData extends object = object> {
|
|
72
|
+
/**
|
|
73
|
+
* The generated collection definition this outbox serves — its `name` is the
|
|
74
|
+
* sync-apply `object` route segment, so it MUST be the SAME definition passed
|
|
75
|
+
* to {@link createSmrtCollection}. (Named `object` to mirror the capability
|
|
76
|
+
* context; carries the collection's REST route segment.)
|
|
77
|
+
*/
|
|
78
|
+
object: {
|
|
79
|
+
name: string;
|
|
80
|
+
_row?: TData;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* The durable-store identity this outbox's queue is namespaced under — folds
|
|
84
|
+
* api base / tenant / identity / manifest hash, so a logout or tenant switch
|
|
85
|
+
* lands on a different IndexedDB database (never cross-identity reuse). Shared
|
|
86
|
+
* with #1764's persistence slice via the same {@link durableStoreNamespace}.
|
|
87
|
+
* `manifestHash` is opaque caller-supplied config for #1762; its canonical
|
|
88
|
+
* source is #1764's call (a schema-shape digest) — comment your call site.
|
|
89
|
+
*/
|
|
90
|
+
namespace: DurableStoreKey;
|
|
91
|
+
/**
|
|
92
|
+
* API base path the sync-apply endpoint lives under (`POST
|
|
93
|
+
* {syncApplyBasePath}/sync/apply`). Defaults to `/api/v1`, matching the REST
|
|
94
|
+
* generator's default. Set to the SvelteKit route base (`/api`) when replaying
|
|
95
|
+
* against the generated SvelteKit `sync/apply/+server.ts`. Ignored when
|
|
96
|
+
* {@link transport} is set.
|
|
97
|
+
*/
|
|
98
|
+
syncApplyBasePath?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Replay this collection's queued writes through a consumer-declared server
|
|
101
|
+
* operation instead of the generated `sync/apply` (#3021) — for a model whose
|
|
102
|
+
* generated write verbs are closed because a hand-written, permission-gated
|
|
103
|
+
* service owns its write rules. Rows are queued under `object.name` and replay
|
|
104
|
+
* ONLY through this transport (never `sync/apply`), each carrying its durable
|
|
105
|
+
* `idempotencyKey`, with the same four-state machine, per-route FIFO, and
|
|
106
|
+
* backoff.
|
|
107
|
+
* See {@link offlineCommandQueue} for writes with no collection at all.
|
|
108
|
+
*/
|
|
109
|
+
transport?: OutboxCommandTransport;
|
|
110
|
+
/** Fetch implementation override (tests, SSR). Defaults to global fetch. */
|
|
111
|
+
fetchFn?: typeof fetch;
|
|
112
|
+
/** Exponential-backoff tuning for retryable replay failures. */
|
|
113
|
+
backoff?: OutboxBackoff;
|
|
114
|
+
/**
|
|
115
|
+
* Called on every observable sync-state transition of a captured mutation
|
|
116
|
+
* (`pending → uploading → synced`, `→ failed`, and the `pending` re-arm after
|
|
117
|
+
* a retryable failure). A push callback; smrt-svelte turns it into a reactive
|
|
118
|
+
* binding later.
|
|
119
|
+
*/
|
|
120
|
+
onSyncStateChange?: (event: SyncStateEvent) => void;
|
|
121
|
+
/**
|
|
122
|
+
* Called when a replayed mutation comes back `conflict` — a RESOLVED outcome
|
|
123
|
+
* (server state won; the item leaves the queue with terminal state `synced`).
|
|
124
|
+
* Persist `serverUpdatedAt` as the new `baseUpdatedAt`, refetch the row, and
|
|
125
|
+
* surface the conflict to the user as appropriate.
|
|
126
|
+
*/
|
|
127
|
+
onConflict?: (conflict: OutboxConflict) => void;
|
|
128
|
+
/** Test-only: inject a deterministic RNG for backoff jitter. */
|
|
129
|
+
random?: () => number;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A programmatic handle to one namespace's outbox — the bridge pattern (like
|
|
133
|
+
* `getEngineCollection`) for trusted callers (an app's outbox panel, a
|
|
134
|
+
* smrt-svelte binding, tests). Fetched via {@link getOutboxHandle}.
|
|
135
|
+
*/
|
|
136
|
+
export interface OutboxHandle {
|
|
137
|
+
/**
|
|
138
|
+
* A read-only snapshot of the durable queue (every state). The way to prove
|
|
139
|
+
* durability after a reload, since this slice does not rehydrate the read
|
|
140
|
+
* cache (that's #1764).
|
|
141
|
+
*/
|
|
142
|
+
snapshot(): Promise<OutboxSnapshotItem[]>;
|
|
143
|
+
/**
|
|
144
|
+
* Force a retry of a specific queued item now (clears its backoff gate, wakes
|
|
145
|
+
* the loop, and clears an auth pause). A no-op for an item no longer queued.
|
|
146
|
+
*/
|
|
147
|
+
retry(itemId: string): Promise<void>;
|
|
148
|
+
}
|
|
149
|
+
/** One item in an {@link OutboxHandle.snapshot} result. */
|
|
150
|
+
export interface OutboxSnapshotItem {
|
|
151
|
+
/** The transport name, when the item replays through one (#3021). */
|
|
152
|
+
transport?: string;
|
|
153
|
+
/** The queue row's idempotency handle. */
|
|
154
|
+
itemId: string;
|
|
155
|
+
/** The collection route segment. */
|
|
156
|
+
object: string;
|
|
157
|
+
/** The mutation kind, in sync-apply terms. */
|
|
158
|
+
op: 'create' | 'update' | 'delete';
|
|
159
|
+
/** The client-generated row UUID. */
|
|
160
|
+
rowId: string;
|
|
161
|
+
/** The on-disk lifecycle state. */
|
|
162
|
+
state: 'pending' | 'synced' | 'failed';
|
|
163
|
+
/** Replay attempts so far. */
|
|
164
|
+
attempts: number;
|
|
165
|
+
/** Epoch ms before which this row won't be retried (backoff). */
|
|
166
|
+
nextAttemptAt: number;
|
|
167
|
+
/** Last replay error, if any. */
|
|
168
|
+
lastError?: string;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Build a durable offline-outbox capability for a collection. Add the returned
|
|
172
|
+
* capability to the collection's `capabilities` array; a collection without it
|
|
173
|
+
* is unaffected (the seam's no-op guarantee — the "opt-in per model" AC).
|
|
174
|
+
*
|
|
175
|
+
* The same `namespace` across multiple collections shares ONE engine (one IDB
|
|
176
|
+
* db, one queue) — so cross-collection ordering and the multi-tab
|
|
177
|
+
* single-replayer guarantee hold across every sync-apply collection of a given
|
|
178
|
+
* identity (one route). A collection with a `transport` is its own route.
|
|
179
|
+
*/
|
|
180
|
+
export declare function offlineOutbox<TData extends object = object>(config: OfflineOutboxConfig<TData>): SmrtWebCapability<TData>;
|
|
181
|
+
/** Configuration for {@link offlineCommandQueue}. */
|
|
182
|
+
export interface OfflineCommandQueueConfig {
|
|
183
|
+
/**
|
|
184
|
+
* The queue's name — the key queued writes are stored and routed under. It
|
|
185
|
+
* shares the namespace's durable queue and per-object event routing with
|
|
186
|
+
* every collection outbox, so it MUST NOT equal a sibling collection's
|
|
187
|
+
* `definition.name` (their events would cross). Durable: a write queued under a name replays only once a queue
|
|
188
|
+
* with that name (and a transport) is attached again, e.g. after a reload.
|
|
189
|
+
*/
|
|
190
|
+
name: string;
|
|
191
|
+
/** The durable-store identity — the SAME key the app's other outboxes use. */
|
|
192
|
+
namespace: DurableStoreKey;
|
|
193
|
+
/** Replays each queued write through the consumer's own server operation. */
|
|
194
|
+
transport: OutboxCommandTransport;
|
|
195
|
+
/** Exponential-backoff tuning for retryable replay failures. */
|
|
196
|
+
backoff?: OutboxBackoff;
|
|
197
|
+
/** Push callback for every sync-state transition (see {@link offlineOutbox}). */
|
|
198
|
+
onSyncStateChange?: (event: SyncStateEvent) => void;
|
|
199
|
+
/** Push callback for a `conflict` outcome (a RESOLVED outcome). */
|
|
200
|
+
onConflict?: (conflict: OutboxConflict) => void;
|
|
201
|
+
/** Test-only: inject a deterministic RNG for backoff jitter. */
|
|
202
|
+
random?: () => number;
|
|
203
|
+
}
|
|
204
|
+
/** One write for {@link OfflineCommandQueue.enqueue}. */
|
|
205
|
+
export interface OfflineCommandInput {
|
|
206
|
+
/** Command arguments — structured-clone-safe; persisted verbatim. */
|
|
207
|
+
payload?: Record<string, unknown>;
|
|
208
|
+
/** The row the write targets, when there is one. Defaults to the item id. */
|
|
209
|
+
rowId?: string;
|
|
210
|
+
/** Mutation kind, passed through to the transport. Defaults to `create`. */
|
|
211
|
+
op?: SyncApplyOp;
|
|
212
|
+
/** The server `updated_at` last seen, passed through to the transport. */
|
|
213
|
+
baseUpdatedAt?: string;
|
|
214
|
+
}
|
|
215
|
+
/** A durable queue of consumer-declared server operations (#3021). */
|
|
216
|
+
export interface OfflineCommandQueue {
|
|
217
|
+
/**
|
|
218
|
+
* Durably queue one write and kick replay. Resolves to its idempotency key
|
|
219
|
+
* once the write is committed to IndexedDB, or `undefined` when the durable
|
|
220
|
+
* queue is unavailable (no IndexedDB) or the queue was disposed — the write
|
|
221
|
+
* was NOT captured, so the caller must perform it online itself or surface
|
|
222
|
+
* the failure; it is never acknowledged non-durably.
|
|
223
|
+
*/
|
|
224
|
+
enqueue(input: OfflineCommandInput): Promise<string | undefined>;
|
|
225
|
+
/** The namespace's durable queue (every route), like {@link OutboxHandle}. */
|
|
226
|
+
snapshot(): Promise<OutboxSnapshotItem[]>;
|
|
227
|
+
/** Force a retry of a queued item now (clears backoff and an auth pause). */
|
|
228
|
+
retry(itemId: string): Promise<void>;
|
|
229
|
+
/** Detach. Queued writes stay on disk and replay on the next attach. */
|
|
230
|
+
dispose(): Promise<void>;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* A durable offline queue for writes that go through a consumer-declared
|
|
234
|
+
* server operation — a data-surface action, a named remote command, a
|
|
235
|
+
* permission-gated service — rather than a generated collection (#3021).
|
|
236
|
+
*
|
|
237
|
+
* Shares the namespace's outbox engine with every {@link offlineOutbox}: one
|
|
238
|
+
* IndexedDB queue, a cross-tab leader per route, FIFO within the route, the
|
|
239
|
+
* `pending → uploading → synced | failed` state machine, backoff, auth pause,
|
|
240
|
+
* and the `wipeDurableStore` registration. Each replay carries the write's
|
|
241
|
+
* durable `idempotencyKey`, which the server operation must dedupe on — that
|
|
242
|
+
* is what makes a blind resend after a lost response safe.
|
|
243
|
+
*
|
|
244
|
+
* Attaches immediately; call `dispose()` when the owning view unmounts.
|
|
245
|
+
*/
|
|
246
|
+
export declare function offlineCommandQueue(config: OfflineCommandQueueConfig): OfflineCommandQueue;
|
|
247
|
+
/**
|
|
248
|
+
* Retrieve the programmatic {@link OutboxHandle} for a durable-store namespace,
|
|
249
|
+
* or `undefined` if no opted-in collection is currently attached under it. The
|
|
250
|
+
* bridge for trusted callers (outbox UI, smrt-svelte binding, tests) to read the
|
|
251
|
+
* durable queue and force retries. Pass the SAME
|
|
252
|
+
* `durableStoreNamespace(config.namespace)` string the capability used.
|
|
253
|
+
*
|
|
254
|
+
* Mirrors the `getEngineCollection` bridge convention: an escape hatch that
|
|
255
|
+
* returns a narrow SMRT-owned surface rather than the engine itself.
|
|
256
|
+
*/
|
|
257
|
+
export declare function getOutboxHandle(namespace: string): OutboxHandle | undefined;
|
|
258
|
+
//# sourceMappingURL=offline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offline.d.ts","sourceRoot":"","sources":["../src/offline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-web — the durable collection snapshot store (#1764).
|
|
3
|
+
*
|
|
4
|
+
* The read-side twin of the offline outbox's write queue: a raw IndexedDB store
|
|
5
|
+
* that persists ONE blob per (namespace, collection) — the collection's last
|
|
6
|
+
* known rows — so a reload can warm-start (render stale instantly) before the
|
|
7
|
+
* engine revalidates in the background. One database per durable-store namespace
|
|
8
|
+
* ({@link durableStoreNamespace}), one object store (`snapshots`) keyed by the
|
|
9
|
+
* collection name, each record `{ collection, rows }`.
|
|
10
|
+
*
|
|
11
|
+
* ## Why raw IndexedDB, mirroring offline/durable-queue.ts
|
|
12
|
+
*
|
|
13
|
+
* Same decisive finding as #1762's outbox: the TanStack persistence layer's
|
|
14
|
+
* public API is engine-typed, so importing it would emit a `@tanstack/`
|
|
15
|
+
* specifier into a `.d.ts` and fail `scripts/check-smrt-web-engine-boundary.mjs`.
|
|
16
|
+
* So persistence is hand-rolled over `indexedDB` with ZERO new runtime
|
|
17
|
+
* dependency and stays inside the engine-absorption boundary (#1761) — this
|
|
18
|
+
* module speaks only plain rows, never an engine type.
|
|
19
|
+
*
|
|
20
|
+
* Rows are stored as structured-clone-safe plain objects (the public DTOs the
|
|
21
|
+
* factory already projects); IndexedDB clones on write, so callers may keep
|
|
22
|
+
* mutating the source array after a save.
|
|
23
|
+
*
|
|
24
|
+
* The namespace already folds api base / tenant / identity / manifest hash (see
|
|
25
|
+
* {@link durableStoreNamespace}), so a logout, tenant switch, or contract-
|
|
26
|
+
* changing deploy lands on a DIFFERENT database — a stale-schema snapshot is
|
|
27
|
+
* simply never found (the read returns nothing and the engine fetches fresh),
|
|
28
|
+
* which is exactly the "manifest-hash change drops persisted caches" guarantee
|
|
29
|
+
* without any explicit invalidation.
|
|
30
|
+
*/
|
|
31
|
+
/** IndexedDB object-store name inside every snapshot database. */
|
|
32
|
+
export declare const SNAPSHOT_STORE = "snapshots";
|
|
33
|
+
/** IndexedDB schema version. Bump only on a store/index migration. */
|
|
34
|
+
export declare const SNAPSHOT_DB_VERSION = 1;
|
|
35
|
+
/**
|
|
36
|
+
* Suffix appended to the durable-store namespace to form this store's OWN
|
|
37
|
+
* IndexedDB database name. Distinct DBs per store are REQUIRED for correctness:
|
|
38
|
+
* the merged outbox (#1762) already opens the BARE namespace as a v1 database
|
|
39
|
+
* with only its `outbox` object store. If persistence opened the same bare
|
|
40
|
+
* namespace at v1, whichever store opened SECOND would find the existing v1 DB,
|
|
41
|
+
* `onupgradeneeded` would NOT fire, and `objectStore('snapshots')` would throw
|
|
42
|
+
* `NotFoundError` — silently disabling persistence (or the outbox). A distinct
|
|
43
|
+
* DB name sidesteps the collision entirely; `wipeDurableStore` still clears both
|
|
44
|
+
* because it fans out via each registered resource's `clear()`, never by DB name.
|
|
45
|
+
*/
|
|
46
|
+
export declare const SNAPSHOT_DB_SUFFIX = "::snapshots";
|
|
47
|
+
/** One persisted collection snapshot record. */
|
|
48
|
+
export interface SnapshotRecord {
|
|
49
|
+
/** The REST collection name — the store key. */
|
|
50
|
+
collection: string;
|
|
51
|
+
/** The rows last seen for this collection (plain public DTOs). */
|
|
52
|
+
rows: Array<Record<string, unknown>>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Feature-detect a usable IndexedDB. Some environments expose `indexedDB` but
|
|
56
|
+
* throw on `open()` (private-mode Firefox historically, sandboxed iframes), so a
|
|
57
|
+
* real probe opens a throwaway database. Returns `false` rather than throwing so
|
|
58
|
+
* the persistence capability can degrade to non-persistent instead of crashing
|
|
59
|
+
* the host — matching the outbox's `probeIndexedDb()` posture.
|
|
60
|
+
*/
|
|
61
|
+
export declare function probeIndexedDb(): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* The durable snapshot store over one IndexedDB database. Constructed via
|
|
64
|
+
* {@link openSnapshotStore} (async open + schema upgrade), then used through the
|
|
65
|
+
* async methods below (each resolves once its txn commits).
|
|
66
|
+
*
|
|
67
|
+
* One store backs EVERY opted-in collection under a namespace, each keyed by its
|
|
68
|
+
* collection name — so N persisted collections of one identity share one
|
|
69
|
+
* database (like the outbox's shared queue), and the durable-store `clear()`
|
|
70
|
+
* wipes them all at once.
|
|
71
|
+
*/
|
|
72
|
+
export declare class SnapshotStore {
|
|
73
|
+
private readonly db;
|
|
74
|
+
/** The IndexedDB database name (== the durable-store namespace). */
|
|
75
|
+
readonly dbName: string;
|
|
76
|
+
constructor(db: IDBDatabase, dbName: string);
|
|
77
|
+
/**
|
|
78
|
+
* Read the persisted rows for `collection`, or `undefined` if none were ever
|
|
79
|
+
* saved (or the record is malformed). `undefined` is the warm-start "nothing
|
|
80
|
+
* on disk" signal — the engine then fetches fresh.
|
|
81
|
+
*/
|
|
82
|
+
load(collection: string): Promise<Array<Record<string, unknown>> | undefined>;
|
|
83
|
+
/**
|
|
84
|
+
* Write (replacing) the snapshot for `collection`. Resolves once the write is
|
|
85
|
+
* durably committed. A single blob per collection — the whole current row set,
|
|
86
|
+
* not a delta — so a restore is one read with no reconciliation.
|
|
87
|
+
*/
|
|
88
|
+
save(collection: string, rows: Array<Record<string, unknown>>): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Drop the snapshot for a single `collection` (its capability's own teardown
|
|
91
|
+
* does NOT clear — the persisted rows must survive for the next load; this is
|
|
92
|
+
* only for an explicit targeted purge). Kept for completeness / tests.
|
|
93
|
+
*/
|
|
94
|
+
remove(collection: string): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Drop EVERY snapshot — the durable-store `clear()` for `wipeDurableStore`.
|
|
97
|
+
* Empties the store but keeps the database so a subsequent save still works.
|
|
98
|
+
*/
|
|
99
|
+
clear(): Promise<void>;
|
|
100
|
+
/** Close the underlying database handle (called on the last detach). */
|
|
101
|
+
close(): void;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Open (creating/upgrading as needed) the snapshot database for `namespace` and
|
|
105
|
+
* wrap it in a {@link SnapshotStore}. `namespace` MUST be a
|
|
106
|
+
* {@link durableStoreNamespace} string — already `encodeURIComponent`-safe and
|
|
107
|
+
* carrying the api/tenant/identity/manifest isolation boundary, so two
|
|
108
|
+
* identities never share one snapshot database.
|
|
109
|
+
*
|
|
110
|
+
* The actual IndexedDB database name is `${namespace}${SNAPSHOT_DB_SUFFIX}` — a
|
|
111
|
+
* DISTINCT database from the merged outbox's bare-namespace one, so persistence +
|
|
112
|
+
* outbox under a shared namespace never collide on a single-store v1 DB (see
|
|
113
|
+
* {@link SNAPSHOT_DB_SUFFIX}).
|
|
114
|
+
*
|
|
115
|
+
* The `onupgradeneeded` handler creates the `snapshots` store keyed by
|
|
116
|
+
* `collection`. Idempotent: re-opening an existing database at the same version
|
|
117
|
+
* skips the upgrade — the reload path (a new tab re-opens the same durable
|
|
118
|
+
* backing and sees prior snapshots).
|
|
119
|
+
*/
|
|
120
|
+
export declare function openSnapshotStore(namespace: string): Promise<SnapshotStore>;
|
|
121
|
+
//# sourceMappingURL=snapshot-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-store.d.ts","sourceRoot":"","sources":["../../src/persistence/snapshot-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,kEAAkE;AAClE,eAAO,MAAM,cAAc,cAAc,CAAC;AAC1C,sEAAsE;AACtE,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAEhD,gDAAgD;AAChD,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACtC;AA8BD;;;;;;GAMG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAsBvD;AAED;;;;;;;;;GASG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAc;IACjC,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM;IAK3C;;;;OAIG;IACG,IAAI,CACR,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAYtD;;;;OAIG;IACG,IAAI,CACR,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACnC,OAAO,CAAC,IAAI,CAAC;IAQhB;;;;OAIG;IACG,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM/C;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B,wEAAwE;IACxE,KAAK,IAAI,IAAI;CAGd;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA8B3E"}
|