@happyvertical/smrt-web 0.51.31 → 0.51.33
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,15 @@
|
|
|
1
|
+
/** Internal coordination state shared by the smrt-web runtime modules. */
|
|
2
|
+
/**
|
|
3
|
+
* TanStack transactions resolve with an opaque transaction object rather than
|
|
4
|
+
* the value returned by the REST mutation handler. Keep the latter beside the
|
|
5
|
+
* public collection handle so WebMCP can return the server's canonical row
|
|
6
|
+
* without exposing engine internals.
|
|
7
|
+
*/
|
|
8
|
+
export declare const persistedMutationResults: WeakMap<object, Map<string, unknown>>;
|
|
9
|
+
/**
|
|
10
|
+
* Engine-owned hydration boundary. The implementation registers this callback
|
|
11
|
+
* where the typed query-collection API is available; WebMCP only asks the
|
|
12
|
+
* boundary to hydrate a row and never reaches into engine internals.
|
|
13
|
+
*/
|
|
14
|
+
export declare const mutationTargetHydrators: WeakMap<object, (row: Record<string, unknown>) => Promise<void>>;
|
|
15
|
+
//# sourceMappingURL=internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAE1E;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,uCAGlC,CAAC;AAEJ;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,wBAE5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAC9C,CAAC"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-web — the durable IndexedDB outbox queue (#1762).
|
|
3
|
+
*
|
|
4
|
+
* A raw IndexedDB FIFO queue: the persistence half of the offline outbox. One
|
|
5
|
+
* database per durable-store namespace ({@link durableStoreNamespace}), one
|
|
6
|
+
* object store (`outbox`) keyed by an AUTO-INCREMENTING `seq` — so insertion
|
|
7
|
+
* order IS replay order for free — plus a `state` index so the replay loop can
|
|
8
|
+
* scan only rows that still need work.
|
|
9
|
+
*
|
|
10
|
+
* ## Why raw IndexedDB, not `@tanstack/offline-transactions`
|
|
11
|
+
*
|
|
12
|
+
* The decisive finding (#1762): the TanStack offline layer's public API is
|
|
13
|
+
* engine-typed (`Collection<…>`, `mutationFns`), so importing it emits a
|
|
14
|
+
* `@tanstack/` specifier into `dist/offline.d.ts`, which
|
|
15
|
+
* `scripts/check-smrt-web-engine-boundary.mjs` scans for unconditionally — the
|
|
16
|
+
* build would fail. It also pins `@tanstack/db` exactly and competes with
|
|
17
|
+
* smrt-web's OWN mutation lifecycle. So the outbox is hand-rolled over browser
|
|
18
|
+
* globals (`indexedDB`, `navigator.locks`) with ZERO new runtime dependency.
|
|
19
|
+
*
|
|
20
|
+
* This module is engine-free (no `@tanstack/*` import): it stays inside the
|
|
21
|
+
* engine-absorption boundary (#1761) and speaks only the sync-apply contract's
|
|
22
|
+
* op shape (`packages/core/src/sync/apply.ts`).
|
|
23
|
+
*
|
|
24
|
+
* Row payloads are stored as structured-clone-safe plain objects; IndexedDB
|
|
25
|
+
* clones on write, so callers may keep mutating the source object after enqueue.
|
|
26
|
+
*/
|
|
27
|
+
import type { SyncApplyOp } from './types.js';
|
|
28
|
+
/** IndexedDB object-store name inside every outbox database. */
|
|
29
|
+
export declare const OUTBOX_STORE = "outbox";
|
|
30
|
+
/** Index over {@link OutboxRow.state} — the replay scan reads it. */
|
|
31
|
+
export declare const OUTBOX_STATE_INDEX = "state";
|
|
32
|
+
/** IndexedDB schema version. Bump only on a store/index migration. */
|
|
33
|
+
export declare const OUTBOX_DB_VERSION = 1;
|
|
34
|
+
/**
|
|
35
|
+
* Durable lifecycle state of one queued mutation. Deliberately a SUPERSET of
|
|
36
|
+
* the app-observable sync state: `pending` rows are due for (re)send, while
|
|
37
|
+
* `synced`/`failed` are terminal tombstones removed after each drain. The
|
|
38
|
+
* app-facing {@link OutboxSyncState} (`pending`/`uploading`/`synced`/`failed`)
|
|
39
|
+
* is derived by the engine — `uploading` is a transient in-flight marker the
|
|
40
|
+
* engine emits, never persisted; this enum is the on-disk truth.
|
|
41
|
+
*/
|
|
42
|
+
export type OutboxRowState = 'pending' | 'synced' | 'failed';
|
|
43
|
+
/**
|
|
44
|
+
* One durable outbox row — a single client-authored mutation awaiting replay
|
|
45
|
+
* through `POST {basePath}/sync/apply`. Fields mirror the sync-apply item so a
|
|
46
|
+
* row maps to a batch item with no translation, plus the durable bookkeeping the
|
|
47
|
+
* replay loop needs (`state`, `attempts`, `nextAttemptAt`, `lastError`).
|
|
48
|
+
*/
|
|
49
|
+
export interface OutboxRow {
|
|
50
|
+
/** Auto-incrementing primary key — monotonic, so `seq` order IS FIFO. */
|
|
51
|
+
seq?: number;
|
|
52
|
+
/**
|
|
53
|
+
* The sync-apply `itemId`: the client's idempotency/correlation handle. A
|
|
54
|
+
* fresh UUID minted at enqueue time, distinct from {@link id} (the row UUID),
|
|
55
|
+
* so it survives coalescing and is echoed back verbatim by the endpoint.
|
|
56
|
+
*/
|
|
57
|
+
itemId: string;
|
|
58
|
+
/** The collection route segment (e.g. `products`) — sync-apply `object`. */
|
|
59
|
+
object: string;
|
|
60
|
+
/** Which mutation kind — sync-apply `op`. */
|
|
61
|
+
op: SyncApplyOp;
|
|
62
|
+
/** The client-generated row UUID — sync-apply `id`. */
|
|
63
|
+
id: string;
|
|
64
|
+
/** Field data for create/update; omitted for delete. */
|
|
65
|
+
payload?: Record<string, unknown>;
|
|
66
|
+
/** The server `updated_at` the client last saw — the conflict guard base. */
|
|
67
|
+
baseUpdatedAt?: string;
|
|
68
|
+
/**
|
|
69
|
+
* The consumer-declared replay transport this row routes through (#3021),
|
|
70
|
+
* by name. Absent ⇒ the row replays through `sync/apply`. Present ⇒ it
|
|
71
|
+
* replays ONLY through the transport registered under this name, and is held
|
|
72
|
+
* (never re-routed to `sync/apply`) until one is. Additive: rows written
|
|
73
|
+
* before #3021 lack it and keep their sync-apply meaning, so no schema bump.
|
|
74
|
+
*/
|
|
75
|
+
transport?: string;
|
|
76
|
+
/**
|
|
77
|
+
* A value the row's transport pinned on its first attempt (#3021) — e.g. the
|
|
78
|
+
* built server request — so every retry under the same idempotency key
|
|
79
|
+
* resends an identical request. Structured-clone-safe.
|
|
80
|
+
*/
|
|
81
|
+
pinned?: unknown;
|
|
82
|
+
/** On-disk lifecycle state. */
|
|
83
|
+
state: OutboxRowState;
|
|
84
|
+
/** How many replay attempts this row has made (drives backoff). */
|
|
85
|
+
attempts: number;
|
|
86
|
+
/** Epoch ms before which this row must not be retried (backoff gate). */
|
|
87
|
+
nextAttemptAt: number;
|
|
88
|
+
/** Last replay error message, for diagnostics/`snapshot()`. */
|
|
89
|
+
lastError?: string;
|
|
90
|
+
/** Epoch ms the row was first enqueued — stable ordering tiebreak + age. */
|
|
91
|
+
enqueuedAt: number;
|
|
92
|
+
}
|
|
93
|
+
/** The fields a caller supplies to {@link DurableOutboxQueue.enqueue}. */
|
|
94
|
+
export interface EnqueueInput {
|
|
95
|
+
itemId: string;
|
|
96
|
+
object: string;
|
|
97
|
+
op: SyncApplyOp;
|
|
98
|
+
id: string;
|
|
99
|
+
payload?: Record<string, unknown>;
|
|
100
|
+
baseUpdatedAt?: string;
|
|
101
|
+
transport?: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Feature-detect a usable IndexedDB. Some environments expose `indexedDB` but
|
|
105
|
+
* throw on `open()` (private-mode Firefox historically, sandboxed iframes), so a
|
|
106
|
+
* real probe opens a throwaway database. Returns `false` rather than throwing so
|
|
107
|
+
* the engine can degrade to a no-op instead of crashing the host.
|
|
108
|
+
*/
|
|
109
|
+
export declare function probeIndexedDb(): Promise<boolean>;
|
|
110
|
+
/**
|
|
111
|
+
* The durable FIFO outbox queue over one IndexedDB database. Constructed via
|
|
112
|
+
* {@link openDurableOutboxQueue} (async open + schema upgrade), then used
|
|
113
|
+
* through the async methods below (each resolves once its txn commits).
|
|
114
|
+
*
|
|
115
|
+
* NOT concurrency-controlled across tabs on its own — the OutboxEngine layers
|
|
116
|
+
* Web Locks leader election on top so exactly one tab drains it. Within one tab,
|
|
117
|
+
* IndexedDB's own transaction serialization is sufficient.
|
|
118
|
+
*/
|
|
119
|
+
export declare class DurableOutboxQueue {
|
|
120
|
+
private readonly db;
|
|
121
|
+
/** The IndexedDB database name (== the durable-store namespace). */
|
|
122
|
+
readonly dbName: string;
|
|
123
|
+
constructor(db: IDBDatabase, dbName: string);
|
|
124
|
+
/**
|
|
125
|
+
* Append a mutation to the tail of the queue in state `pending`, due
|
|
126
|
+
* immediately (`nextAttemptAt = 0`, `attempts = 0`). Resolves with the
|
|
127
|
+
* assigned `seq` once the write is durably committed.
|
|
128
|
+
*/
|
|
129
|
+
enqueue(input: EnqueueInput): Promise<number>;
|
|
130
|
+
/**
|
|
131
|
+
* Persist a state transition (and any of attempts/backoff/error) for the row
|
|
132
|
+
* at `seq`, reading-then-writing inside ONE transaction so a concurrent drain
|
|
133
|
+
* in the same tab can't lose the update. A no-op if the row is already gone
|
|
134
|
+
* (removed by a prior terminal transition).
|
|
135
|
+
*/
|
|
136
|
+
markState(seq: number, patch: Partial<Pick<OutboxRow, 'state' | 'attempts' | 'nextAttemptAt' | 'lastError' | 'pinned'>>): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* All rows that are due to (re)send at `now`: state `pending` AND
|
|
139
|
+
* `nextAttemptAt <= now`, in ascending `seq` (FIFO). Uses the `state` index to
|
|
140
|
+
* avoid scanning terminal tombstones. Terminal rows (`synced`/`failed`) are
|
|
141
|
+
* excluded — they await removal, not replay.
|
|
142
|
+
*/
|
|
143
|
+
listPending(now: number): Promise<OutboxRow[]>;
|
|
144
|
+
/** Remove the row at `seq` (a terminal transition drops it). */
|
|
145
|
+
remove(seq: number): Promise<void>;
|
|
146
|
+
/** Every row currently in the queue (any state), ascending `seq`. */
|
|
147
|
+
all(): Promise<OutboxRow[]>;
|
|
148
|
+
/**
|
|
149
|
+
* Drop every row — the durable-store `clear()` for `wipeDurableStore`. Empties
|
|
150
|
+
* the store but keeps the database (and its `seq` autoincrement counter) so a
|
|
151
|
+
* subsequent enqueue still gets fresh monotonic ids.
|
|
152
|
+
*/
|
|
153
|
+
clear(): Promise<void>;
|
|
154
|
+
/** Close the underlying database handle (called on engine dispose). */
|
|
155
|
+
close(): void;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Open (creating/upgrading as needed) the outbox database for `dbName` and wrap
|
|
159
|
+
* it in a {@link DurableOutboxQueue}. `dbName` MUST be a
|
|
160
|
+
* {@link durableStoreNamespace} string — already `encodeURIComponent`-safe and
|
|
161
|
+
* carrying the api/tenant/identity/manifest isolation boundary, so two
|
|
162
|
+
* identities never share one queue database.
|
|
163
|
+
*
|
|
164
|
+
* The `onupgradeneeded` handler creates the `outbox` store keyed by
|
|
165
|
+
* autoincrementing `seq` and the `state` index. Idempotent: re-opening an
|
|
166
|
+
* existing database at the same version skips the upgrade — which is exactly the
|
|
167
|
+
* "reload" path (a new tab re-opens the same durable backing and sees prior
|
|
168
|
+
* rows).
|
|
169
|
+
*/
|
|
170
|
+
export declare function openDurableOutboxQueue(dbName: string): Promise<DurableOutboxQueue>;
|
|
171
|
+
//# sourceMappingURL=durable-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-queue.d.ts","sourceRoot":"","sources":["../../src/offline/durable-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-web — the shared, namespace-keyed outbox engine (#1762).
|
|
3
|
+
*
|
|
4
|
+
* The engine owns the durable queue, the leader lock, and the replay loop for
|
|
5
|
+
* ONE durable-store namespace, and is REF-COUNTED so that N collections sharing
|
|
6
|
+
* a namespace share exactly ONE engine — one IndexedDB database, one leader
|
|
7
|
+
* lock, one FIFO queue. This sharing is REQUIRED for correctness, not an
|
|
8
|
+
* optimization: if each collection held its OWN leader lock, two tabs could each
|
|
9
|
+
* win a different collection's lock and both replay the (shared) queue,
|
|
10
|
+
* double-POSTing. One lock per namespace route ⇒ one replayer per route across
|
|
11
|
+
* all tabs. Since #3021 a route is `sync-apply` (every sync-apply collection;
|
|
12
|
+
* keeps the pre-#3021 lock name) or one consumer-declared transport, and a tab
|
|
13
|
+
* leads only routes it has a binding for, so FIFO and backoff are per route.
|
|
14
|
+
*
|
|
15
|
+
* Replay maps sync-apply results onto durable transitions per the contract's
|
|
16
|
+
* "Web outbox (#1762)" consumer notes
|
|
17
|
+
* (docs/content/architecture/sync-apply-contract.md):
|
|
18
|
+
*
|
|
19
|
+
* | apply result | queue transition |
|
|
20
|
+
* |-------------------------------------|----------------------------------------|
|
|
21
|
+
* | `applied` | remove → `synced` |
|
|
22
|
+
* | `conflict` (stale_write/create_conflict) | remove + fire onConflict → `synced` (a RESOLVED outcome) |
|
|
23
|
+
* | `rejected` `write_failed` | keep, attempts++, backoff → `pending` |
|
|
24
|
+
* | `rejected` `auth_required`/`forbidden` | PAUSE the loop, keep queued → `pending` |
|
|
25
|
+
* | `rejected` other (`invalid_*`/`unknown_object`/`not_found`/`op_not_allowed`/`id_conflict`) | remove → `failed` (terminal) |
|
|
26
|
+
* | network reject / non-200 / lost response | whole batch stays `pending`, drain stops behind it (blind replay is safe by construction — idempotency) |
|
|
27
|
+
*
|
|
28
|
+
* Because items carry client-generated UUIDs and the endpoint is idempotent
|
|
29
|
+
* (`_insertOnly` create + no-op re-apply), a batch that was sent but whose
|
|
30
|
+
* response was lost can be blindly re-sent with no duplicate rows — which is
|
|
31
|
+
* why the network-failure path simply leaves the batch `pending`.
|
|
32
|
+
*
|
|
33
|
+
* Engine-free public surface: no `@tanstack/*` import — inside the boundary
|
|
34
|
+
* (#1761). The one engine-adjacent value it receives is the SMRT-owned
|
|
35
|
+
* durable-store namespace + registration hooks passed in `config`.
|
|
36
|
+
*/
|
|
37
|
+
import { type OutboxCommandTransport, type OutboxConflict, type ResolvedBackoff, type SyncApplyOp, type SyncStateEvent } from './types.js';
|
|
38
|
+
/** Maps a capability-seam envelope kind to a sync-apply op. */
|
|
39
|
+
export declare function envelopeKindToOp(kind: 'insert' | 'update' | 'delete'): SyncApplyOp;
|
|
40
|
+
/**
|
|
41
|
+
* The bookkeeping a namespace's durable-store registration needs, supplied by
|
|
42
|
+
* the public surface so the engine can key its queue and register for
|
|
43
|
+
* {@link wipeDurableStore} WITHOUT importing `durable-store.ts` itself (keeping
|
|
44
|
+
* this module's dependency surface minimal and the namespacing single-sourced in
|
|
45
|
+
* the caller). `namespace` is the {@link durableStoreNamespace} string.
|
|
46
|
+
*/
|
|
47
|
+
export interface OutboxEngineConfig {
|
|
48
|
+
/** The durable-store namespace string — the IDB dbName + lock-name root. */
|
|
49
|
+
namespace: string;
|
|
50
|
+
/** Resolved backoff parameters. */
|
|
51
|
+
backoff: ResolvedBackoff;
|
|
52
|
+
/**
|
|
53
|
+
* Register this engine's queue as a durable resource so `wipeDurableStore`
|
|
54
|
+
* can clear it; returns the unregister fn. Wraps `registerDurableResource`
|
|
55
|
+
* from the caller so the engine stays decoupled from that module.
|
|
56
|
+
*/
|
|
57
|
+
registerResource: (clear: () => Promise<void>) => () => void;
|
|
58
|
+
/** Injectable RNG for deterministic backoff jitter in tests. */
|
|
59
|
+
random?: () => number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A pending optimistic write to enqueue, in capability-seam terms. Callbacks are
|
|
63
|
+
* NOT carried here — they are registered per-`object` at
|
|
64
|
+
* {@link OutboxEngine.registerCollection} so that reloaded rows (which this
|
|
65
|
+
* session never enqueued) still route their events to the collection.
|
|
66
|
+
*/
|
|
67
|
+
export interface OutboxEnqueueRequest {
|
|
68
|
+
kind: 'insert' | 'update' | 'delete';
|
|
69
|
+
/** The collection route segment (definition.name). */
|
|
70
|
+
object: string;
|
|
71
|
+
/**
|
|
72
|
+
* The client-generated row UUID (the optimistic row's id). Empty ⇒ the
|
|
73
|
+
* minted item id stands in (a transport command with no target row).
|
|
74
|
+
*/
|
|
75
|
+
rowId: string;
|
|
76
|
+
/** Full row (insert) / changed fields (update) / ignored (delete). */
|
|
77
|
+
data: Record<string, unknown>;
|
|
78
|
+
/** The server updated_at last seen for this row, for the conflict guard. */
|
|
79
|
+
baseUpdatedAt?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Replay through the transport registered under this name instead of
|
|
82
|
+
* `sync/apply` (#3021). Absent ⇒ sync-apply.
|
|
83
|
+
*/
|
|
84
|
+
transport?: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Where a namespace's sync-apply rows are POSTed. Supplied by the binding that
|
|
88
|
+
* replays through sync-apply (an `offlineOutbox` without a `transport`), not by
|
|
89
|
+
* the engine's creator: a command-only queue may create the shared engine
|
|
90
|
+
* first, and must not pin a sync-apply base path it never declared.
|
|
91
|
+
*/
|
|
92
|
+
export interface OutboxSyncApplyTarget {
|
|
93
|
+
/** Absolute base path the sync-apply endpoint lives under (e.g. `/api/v1`). */
|
|
94
|
+
basePath: string;
|
|
95
|
+
/** Fetch implementation (injectable for tests/SSR). */
|
|
96
|
+
fetchFn: typeof fetch;
|
|
97
|
+
}
|
|
98
|
+
/** A read-only view of one queued item, for {@link OutboxEngine.snapshot}. */
|
|
99
|
+
export interface OutboxSnapshotItem {
|
|
100
|
+
itemId: string;
|
|
101
|
+
object: string;
|
|
102
|
+
op: SyncApplyOp;
|
|
103
|
+
rowId: string;
|
|
104
|
+
state: 'pending' | 'synced' | 'failed';
|
|
105
|
+
attempts: number;
|
|
106
|
+
nextAttemptAt: number;
|
|
107
|
+
lastError?: string;
|
|
108
|
+
/** The replay transport name, when the item replays through one (#3021). */
|
|
109
|
+
transport?: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The shared per-namespace outbox engine. Create/lookup via
|
|
113
|
+
* {@link getOrCreateOutboxEngine}; never construct directly (the module keeps
|
|
114
|
+
* the ref-counted registry).
|
|
115
|
+
*/
|
|
116
|
+
export declare class OutboxEngine {
|
|
117
|
+
private readonly config;
|
|
118
|
+
/**
|
|
119
|
+
* Per-collection callback sets, keyed by the collection route segment
|
|
120
|
+
* (`object`). Keyed by `object` — NOT by the queue row's `itemId` — precisely
|
|
121
|
+
* so replayed rows that were REHYDRATED from IndexedDB after a reload (whose
|
|
122
|
+
* itemIds this session never enqueued) still route their state/conflict events
|
|
123
|
+
* to the reloaded collection's callbacks. A `Set` per object so N collections
|
|
124
|
+
* sharing one engine+object each get every event (the common case is one
|
|
125
|
+
* collection per object, but the shared-engine model does not forbid more).
|
|
126
|
+
*/
|
|
127
|
+
private readonly listenersByObject;
|
|
128
|
+
/**
|
|
129
|
+
* The sync-apply endpoint, adopted from the first binding that declares one.
|
|
130
|
+
* A tab without one never leads the sync-apply route, so its rows wait for a
|
|
131
|
+
* tab that does (never dropped) — see `drainOnce`.
|
|
132
|
+
*/
|
|
133
|
+
private syncApply;
|
|
134
|
+
/** Ref count: number of collections currently attached to this engine. */
|
|
135
|
+
private refCount;
|
|
136
|
+
/** The durable queue, once opened. undefined while opening / if IDB absent. */
|
|
137
|
+
private queue;
|
|
138
|
+
/** Resolves once the async open settles (success or degraded). */
|
|
139
|
+
private readonly ready;
|
|
140
|
+
/** True when IndexedDB was unavailable and the engine is a durable no-op. */
|
|
141
|
+
private degraded;
|
|
142
|
+
/**
|
|
143
|
+
* Per-route leadership, requested when the first binding serving a route
|
|
144
|
+
* attaches and released when the last one detaches. Exactly one tab replays a
|
|
145
|
+
* given route; a tab never leads a route it has no binding for.
|
|
146
|
+
*/
|
|
147
|
+
private readonly leaders;
|
|
148
|
+
/** Unregister fn from the durable-store registry. */
|
|
149
|
+
private unregisterResource;
|
|
150
|
+
/** True once dispose() ran — guards late async continuations. */
|
|
151
|
+
private disposed;
|
|
152
|
+
/**
|
|
153
|
+
* Paused by an auth_required/forbidden result: the loop stops draining until
|
|
154
|
+
* a later enqueue (the app re-authenticated and is writing again) or an
|
|
155
|
+
* explicit retry wakes it. Items stay queued.
|
|
156
|
+
*/
|
|
157
|
+
private paused;
|
|
158
|
+
/** Wipes issued but not yet cleared; replay is suspended meanwhile. */
|
|
159
|
+
private wipesPending;
|
|
160
|
+
/** Those wipes, so `dispose()` can let them finish before closing. */
|
|
161
|
+
private readonly pendingWipes;
|
|
162
|
+
/** True while a drain pass is running, to coalesce concurrent triggers. */
|
|
163
|
+
private draining;
|
|
164
|
+
/** The running drain, while `draining`. */
|
|
165
|
+
private activeDrain;
|
|
166
|
+
/** A drain requested while one was in flight — run one more pass after. */
|
|
167
|
+
private drainQueued;
|
|
168
|
+
/** Timer for the next backoff-scheduled drain, if any. */
|
|
169
|
+
private backoffTimer;
|
|
170
|
+
/** The `online` event listener, so we can remove it on dispose. */
|
|
171
|
+
private onlineListener;
|
|
172
|
+
constructor(config: OutboxEngineConfig);
|
|
173
|
+
/** Open the durable queue (or mark degraded if IndexedDB is unusable). */
|
|
174
|
+
private open;
|
|
175
|
+
/**
|
|
176
|
+
* Register the queue as a durable resource. A wipe drops the namespace's
|
|
177
|
+
* registrations, so the clear callback forgets ours and the next enqueue
|
|
178
|
+
* registers again — otherwise a second wipe would miss rows written after
|
|
179
|
+
* the first.
|
|
180
|
+
*/
|
|
181
|
+
private registerForWipe;
|
|
182
|
+
/** Wake the drain loop immediately when connectivity returns. */
|
|
183
|
+
private wireOnlineListener;
|
|
184
|
+
/**
|
|
185
|
+
* Request cross-tab leadership for `route` (ref-counted per tab). The
|
|
186
|
+
* sync-apply route keeps the pre-#3021 lock name, so it stays mutually
|
|
187
|
+
* exclusive with tabs running an older build; each transport route has its
|
|
188
|
+
* own lock.
|
|
189
|
+
*/
|
|
190
|
+
private acquireRoute;
|
|
191
|
+
/** Drop one reference to `route`; the last one releases its lock. */
|
|
192
|
+
private releaseRoute;
|
|
193
|
+
/** Does this tab currently lead `route`? */
|
|
194
|
+
private leads;
|
|
195
|
+
/**
|
|
196
|
+
* Attach a collection: register its per-object callbacks and bump the ref
|
|
197
|
+
* count. Returns the exact callback record registered so the caller can pass
|
|
198
|
+
* it back to {@link unregisterCollection} for precise removal (two collections
|
|
199
|
+
* on the same object must each detach only their own callbacks). Registering
|
|
200
|
+
* by `object` is what lets rehydrated rows (reloaded from IDB) reach this
|
|
201
|
+
* collection's callbacks even though this session never enqueued them.
|
|
202
|
+
*/
|
|
203
|
+
registerCollection(binding: OutboxCollectionBinding): object;
|
|
204
|
+
/** The first live transport registered under `name`, if any. */
|
|
205
|
+
private transportFor;
|
|
206
|
+
/**
|
|
207
|
+
* Detach a collection: remove its callback record and decrement the ref count;
|
|
208
|
+
* when it reaches zero, dispose the engine (release the lock, unregister from
|
|
209
|
+
* the durable-store registry, close IndexedDB). The durable ROWS are NOT
|
|
210
|
+
* cleared — they must survive to replay after a reload; only the in-memory
|
|
211
|
+
* engine is torn down. Returns true if it disposed.
|
|
212
|
+
*/
|
|
213
|
+
unregisterCollection(object: string, record: object): Promise<boolean>;
|
|
214
|
+
/** Current ref count (test/introspection aid). */
|
|
215
|
+
get referenceCount(): number;
|
|
216
|
+
/**
|
|
217
|
+
* Enqueue an optimistic write into the durable queue and fire the initial
|
|
218
|
+
* `pending` state, then kick a drain. Resolves once the row is durably
|
|
219
|
+
* committed (so the caller's `wrapMutation` only reports handled after
|
|
220
|
+
* persistence). Replay events for this row (and for rows this session did not
|
|
221
|
+
* enqueue — reloaded from disk) route to the registered per-`object`
|
|
222
|
+
* callbacks, so a reload does not lose observability.
|
|
223
|
+
*
|
|
224
|
+
* In degraded (no-IndexedDB) mode the write is NOT durable, so this returns
|
|
225
|
+
* `undefined` and the capability falls through to the real fetcher instead
|
|
226
|
+
* of acknowledging an optimistic-only write.
|
|
227
|
+
*/
|
|
228
|
+
enqueue(request: OutboxEnqueueRequest): Promise<string | undefined>;
|
|
229
|
+
/**
|
|
230
|
+
* Force a retry of a specific queued item now: clears its backoff gate and
|
|
231
|
+
* wakes the loop. The bridge `OutboxHandle.retry(itemId)` calls this so an app
|
|
232
|
+
* "retry" button can flush a backed-off or auth-paused item. A no-op for an
|
|
233
|
+
* item that is not (or no longer) queued.
|
|
234
|
+
*/
|
|
235
|
+
retry(itemId: string): Promise<void>;
|
|
236
|
+
/**
|
|
237
|
+
* A read-only snapshot of the durable queue — the basis of
|
|
238
|
+
* `OutboxHandle.snapshot()`. Because the READ cache is NOT rehydrated after a
|
|
239
|
+
* reload in this slice (that's #1764's warmStart), the snapshot + the raw IDB
|
|
240
|
+
* store are how a test/app proves durability, not `collection.toArray()`.
|
|
241
|
+
*/
|
|
242
|
+
snapshot(): Promise<OutboxSnapshotItem[]>;
|
|
243
|
+
/**
|
|
244
|
+
* Deliver a state event to every callback registered for the event's
|
|
245
|
+
* collection `object` (best-effort). Routing by `object` — not `itemId` —
|
|
246
|
+
* means a row REHYDRATED from IndexedDB after a reload still reaches the
|
|
247
|
+
* reloaded collection's callback even though this session never enqueued it.
|
|
248
|
+
*/
|
|
249
|
+
private emit;
|
|
250
|
+
/** Deliver a conflict to every callback registered for its collection. */
|
|
251
|
+
private emitConflict;
|
|
252
|
+
/**
|
|
253
|
+
* The replay loop. Gated on: (a) leading at least one route, (b) not paused by an
|
|
254
|
+
* auth failure, (c) `navigator.onLine !== false`, (d) IndexedDB usable. Drains
|
|
255
|
+
* every led route's rows due now (`nextAttemptAt <= now`), oldest-first per
|
|
256
|
+
* route; sync-apply chunks into batches of ≤1000 per POST, one send at a time
|
|
257
|
+
* to preserve each route's FIFO. Concurrency-coalesced: a drain requested while one runs sets a flag
|
|
258
|
+
* to run exactly one more pass, so overlapping triggers never interleave.
|
|
259
|
+
*/
|
|
260
|
+
private drain;
|
|
261
|
+
/**
|
|
262
|
+
* Settles once the drain in flight (if any) has finished its request AND its
|
|
263
|
+
* durable result transition. A route's lock is released only after this, so
|
|
264
|
+
* another tab can never replay a row this tab is still settling.
|
|
265
|
+
*/
|
|
266
|
+
private settled;
|
|
267
|
+
/** One drain pass: send every currently-due batch, then schedule backoff. */
|
|
268
|
+
private drainOnce;
|
|
269
|
+
/**
|
|
270
|
+
* Replay one transport row through its consumer-declared transport (#3021)
|
|
271
|
+
* and map the outcome through the SAME transitions as a sync-apply result. A
|
|
272
|
+
* throw is the ambiguous path (request may or may not have landed): the row
|
|
273
|
+
* stays `pending` with backoff and is resent under the same idempotency key.
|
|
274
|
+
* Returns false when the row remains pending, stopping this route's pass
|
|
275
|
+
* (FIFO per route).
|
|
276
|
+
*/
|
|
277
|
+
private sendCommand;
|
|
278
|
+
/**
|
|
279
|
+
* Send one chunk through `POST {basePath}/sync/apply` and map results back
|
|
280
|
+
* onto durable transitions. On a network reject / non-200 / lost/mismatched
|
|
281
|
+
* response the WHOLE chunk stays `pending` (blind replay is safe) — every row
|
|
282
|
+
* goes back to `pending` with an incremented attempt + backoff so the loop
|
|
283
|
+
* doesn't hot-spin. Returns false when a retryable row remains pending, which
|
|
284
|
+
* stops this drain pass so newer FIFO chunks do not overtake it.
|
|
285
|
+
*/
|
|
286
|
+
private sendBatch;
|
|
287
|
+
/**
|
|
288
|
+
* POST a batch to `{basePath}/sync/apply`. Throws on a non-2xx or a network
|
|
289
|
+
* error (the caller treats a throw as "response lost → keep pending"). Returns
|
|
290
|
+
* the positional `results` array, or `undefined` on a malformed 200 body.
|
|
291
|
+
*/
|
|
292
|
+
private postBatch;
|
|
293
|
+
/**
|
|
294
|
+
* Map one positional apply result onto a durable transition + observable
|
|
295
|
+
* state, per the contract's consumer notes. See the class doc's mapping table.
|
|
296
|
+
*/
|
|
297
|
+
private applyResult;
|
|
298
|
+
/** Remove a successfully-applied (or conflict-resolved) row → `synced`. */
|
|
299
|
+
private finishSynced;
|
|
300
|
+
/** Re-queue every row of a failed batch (network path) with backoff. */
|
|
301
|
+
private requeueBatch;
|
|
302
|
+
/** Re-queue one row: attempts++, backoff gate, `pending` event. */
|
|
303
|
+
private requeueRow;
|
|
304
|
+
/**
|
|
305
|
+
* Schedule the next drain for the soonest backed-off row's `nextAttemptAt`.
|
|
306
|
+
* Only one timer is ever pending; a sooner schedule replaces a later one.
|
|
307
|
+
*/
|
|
308
|
+
private scheduleNextBackoff;
|
|
309
|
+
/**
|
|
310
|
+
* Tear down the in-memory engine: release leadership, remove the online
|
|
311
|
+
* listener, clear timers, unregister from the durable-store registry, and
|
|
312
|
+
* close IndexedDB. Does NOT clear the durable rows — they must survive to
|
|
313
|
+
* replay on the next load.
|
|
314
|
+
*/
|
|
315
|
+
private dispose;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get the shared engine for `config.namespace`, creating it on first use and
|
|
319
|
+
* ref-counting it. Every collection opting into the outbox under the same
|
|
320
|
+
* namespace gets the SAME engine — one IndexedDB db, one leader lock, one FIFO
|
|
321
|
+
* queue. The caller MUST pair each `getOrCreateOutboxEngine(...).registerCollection()`
|
|
322
|
+
* with a later `unregisterCollection()` (via `teardown`) so the engine disposes
|
|
323
|
+
* when its last collection detaches.
|
|
324
|
+
*/
|
|
325
|
+
export declare function getOrCreateOutboxEngine(config: OutboxEngineConfig): OutboxEngine;
|
|
326
|
+
/**
|
|
327
|
+
* The per-collection (or per-command-queue) binding registered on attach.
|
|
328
|
+
* `object` is the event-routing key AND, for a `transport` binding, the name
|
|
329
|
+
* transport rows are queued under.
|
|
330
|
+
*/
|
|
331
|
+
export interface OutboxCollectionBinding {
|
|
332
|
+
object: string;
|
|
333
|
+
onSyncStateChange?: (event: SyncStateEvent) => void;
|
|
334
|
+
onConflict?: (conflict: OutboxConflict) => void;
|
|
335
|
+
/** Replay rows queued under `object` through this transport (#3021). */
|
|
336
|
+
transport?: OutboxCommandTransport;
|
|
337
|
+
/** Declares the sync-apply endpoint (sync-apply bindings only). */
|
|
338
|
+
syncApply?: OutboxSyncApplyTarget;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Attach a collection to the namespace's engine (creating it if needed),
|
|
342
|
+
* register its per-`object` callbacks, and increment its ref count. Returns the
|
|
343
|
+
* engine plus the exact callback `record` to hand back to
|
|
344
|
+
* {@link releaseOutboxEngine} for precise removal. Pair with
|
|
345
|
+
* {@link releaseOutboxEngine}.
|
|
346
|
+
*/
|
|
347
|
+
export declare function acquireOutboxEngine(config: OutboxEngineConfig, binding: OutboxCollectionBinding): {
|
|
348
|
+
engine: OutboxEngine;
|
|
349
|
+
record: object;
|
|
350
|
+
};
|
|
351
|
+
/**
|
|
352
|
+
* Detach a collection from an engine (removing its callback record) and, if it
|
|
353
|
+
* was the last one, dispose it and evict it from the shared registry so the
|
|
354
|
+
* namespace starts fresh next time.
|
|
355
|
+
*/
|
|
356
|
+
export declare function releaseOutboxEngine(namespace: string, engine: OutboxEngine, object: string, record: object): Promise<void>;
|
|
357
|
+
/** Test-only: current engine count (for leak assertions). */
|
|
358
|
+
export declare function _outboxEngineCount(): number;
|
|
359
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/offline/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-web — Web Locks leader election for the outbox (#1762).
|
|
3
|
+
*
|
|
4
|
+
* With multiple tabs open, exactly ONE must replay the shared outbox queue, or
|
|
5
|
+
* two tabs would POST the same batch concurrently. The mechanism is the native
|
|
6
|
+
* Web Locks API (`navigator.locks`): a tab requests an EXCLUSIVE lock keyed by
|
|
7
|
+
* the durable-store namespace and holds it — via a promise that resolves only on
|
|
8
|
+
* release — for as long as it should be leader. The browser grants the lock to
|
|
9
|
+
* exactly one waiter at a time and AUTO-RELEASES it on tab crash/close/navigation
|
|
10
|
+
* with no heartbeat, so the next waiting tab becomes leader instantly and there
|
|
11
|
+
* is never a stuck lock.
|
|
12
|
+
*
|
|
13
|
+
* Why Web Locks over a BroadcastChannel election (v1 decision, locked): Web
|
|
14
|
+
* Locks gives crash-safe, race-free, browser-arbitrated exclusion for free —
|
|
15
|
+
* a hand-rolled BroadcastChannel election has to solve liveness (heartbeats,
|
|
16
|
+
* takeover on a silent crash) itself. BroadcastChannel is a possible future
|
|
17
|
+
* enhancement (cross-tab state fan-out), not the election primitive.
|
|
18
|
+
*
|
|
19
|
+
* ## Single-tab fallback (documented gap)
|
|
20
|
+
*
|
|
21
|
+
* When `navigator.locks` is absent (older Safari, non-browser hosts), this warns
|
|
22
|
+
* ONCE and falls back to acquiring leadership immediately and unconditionally.
|
|
23
|
+
* The outbox STILL replays — a lone tab is trivially the only replayer — but the
|
|
24
|
+
* multi-tab exactly-one-replayer guarantee does not hold across tabs that all
|
|
25
|
+
* took the fallback. This is a deliberate v1 limitation, NOT masked by a
|
|
26
|
+
* BroadcastChannel shim.
|
|
27
|
+
*
|
|
28
|
+
* Engine-free: no `@tanstack/*` import — stays inside the boundary (#1761).
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* A handle to relinquish leadership: call it to release the Web Lock (or cancel
|
|
32
|
+
* a still-pending request). Idempotent.
|
|
33
|
+
*/
|
|
34
|
+
export type LeadershipHandle = () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Acquire cross-tab leadership for `lockName` (the outbox's
|
|
37
|
+
* `smrt-web-outbox-leader:<namespace>`).
|
|
38
|
+
*
|
|
39
|
+
* When the lock is granted this tab becomes leader and `onAcquired()` fires; the
|
|
40
|
+
* tab stays leader — holding the lock — until the returned handle is called (or
|
|
41
|
+
* the tab dies, when the browser auto-releases). On release, `onReleased()`
|
|
42
|
+
* fires. Requesting is non-blocking: this returns synchronously with the
|
|
43
|
+
* release handle while the request waits in the background for its turn.
|
|
44
|
+
*
|
|
45
|
+
* Contract:
|
|
46
|
+
* - The lock is requested EXCLUSIVE; the callback returns a promise that stays
|
|
47
|
+
* pending until the release handle resolves it, so the browser considers the
|
|
48
|
+
* lock held for exactly that window.
|
|
49
|
+
* - Calling the handle before the lock is even granted aborts the pending
|
|
50
|
+
* request (via `AbortSignal`) so a torn-down engine never becomes leader
|
|
51
|
+
* later. `onReleased` still fires so callers can settle their own state.
|
|
52
|
+
* - No Web Locks support → single-tab fallback: `onAcquired()` fires on the next
|
|
53
|
+
* microtask (leadership is immediate and unconditional), the handle just fires
|
|
54
|
+
* `onReleased()`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function acquireLeadership(lockName: string, onAcquired: () => void, onReleased: () => void): LeadershipHandle;
|
|
57
|
+
//# sourceMappingURL=leader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leader.d.ts","sourceRoot":"","sources":["../../src/offline/leader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,IAAI,CAAC;AAsB1C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,IAAI,EACtB,UAAU,EAAE,MAAM,IAAI,GACrB,gBAAgB,CA6ElB"}
|