@fragno-dev/tanstack-db-adapter 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE.md +16 -0
  2. package/README.md +207 -2
  3. package/dist/checkpoint.d.ts +9 -0
  4. package/dist/checkpoint.d.ts.map +1 -0
  5. package/dist/checkpoint.js +11 -0
  6. package/dist/checkpoint.js.map +1 -0
  7. package/dist/coordinator/fragno-browser-persistence-startup.d.ts +92 -0
  8. package/dist/coordinator/fragno-browser-persistence-startup.d.ts.map +1 -0
  9. package/dist/coordinator/fragno-browser-persistence-startup.js +366 -0
  10. package/dist/coordinator/fragno-browser-persistence-startup.js.map +1 -0
  11. package/dist/coordinator/fragno-collection-registry.js +85 -0
  12. package/dist/coordinator/fragno-collection-registry.js.map +1 -0
  13. package/dist/coordinator/fragno-internal-collection.d.ts +40 -0
  14. package/dist/coordinator/fragno-internal-collection.d.ts.map +1 -0
  15. package/dist/coordinator/fragno-internal-collection.js +142 -0
  16. package/dist/coordinator/fragno-internal-collection.js.map +1 -0
  17. package/dist/coordinator/fragno-internal-fetcher.d.ts +23 -0
  18. package/dist/coordinator/fragno-internal-fetcher.d.ts.map +1 -0
  19. package/dist/coordinator/fragno-internal-fetcher.js +60 -0
  20. package/dist/coordinator/fragno-internal-fetcher.js.map +1 -0
  21. package/dist/coordinator/fragno-ordered-persistence.js +66 -0
  22. package/dist/coordinator/fragno-ordered-persistence.js.map +1 -0
  23. package/dist/coordinator/fragno-outbox-synchronizer.js +193 -0
  24. package/dist/coordinator/fragno-outbox-synchronizer.js.map +1 -0
  25. package/dist/coordinator/fragno-table-collection.js +91 -0
  26. package/dist/coordinator/fragno-table-collection.js.map +1 -0
  27. package/dist/coordinator.d.ts +113 -0
  28. package/dist/coordinator.d.ts.map +1 -0
  29. package/dist/coordinator.js +231 -0
  30. package/dist/coordinator.js.map +1 -0
  31. package/dist/outbox-stream.js +85 -0
  32. package/dist/outbox-stream.js.map +1 -0
  33. package/dist/outbox-transport-error.js +12 -0
  34. package/dist/outbox-transport-error.js.map +1 -0
  35. package/dist/protocol.js +115 -0
  36. package/dist/protocol.js.map +1 -0
  37. package/package.json +83 -3
package/LICENSE.md ADDED
@@ -0,0 +1,16 @@
1
+ Copyright 2025 - present "ReJot Nederland B.V.", and individual contributors.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
4
+ associated documentation files (the “Software”), to deal in the Software without restriction,
5
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
6
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
7
+ furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial
10
+ portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
13
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
14
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
15
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,208 @@
1
- # Temporary Holding Version
1
+ # @fragno-dev/tanstack-db-adapter
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Ingest one Fragno database outbox into a durable, database-level set of eager TanStack DB
4
+ collections.
5
+
6
+ The adapter is intentionally one-way. It synchronizes server mutations into TanStack DB; client
7
+ mutations, optimistic overlays, command submission, and on-demand subsets are out of scope.
8
+
9
+ ## Collection setup
10
+
11
+ Create one coordinator for each physical Fragno outbox. Supply every schema represented by that
12
+ outbox, register the tables used by the application, and preload once after registration closes:
13
+
14
+ ```ts
15
+ import { createFragnoOutboxCoordinator } from "@fragno-dev/tanstack-db-adapter";
16
+
17
+ const coordinator = await createFragnoOutboxCoordinator({
18
+ baseUrl: "/api/app",
19
+ fetch: globalThis.fetch,
20
+ schemas: [appSchema, workflowsSchema] as const,
21
+ });
22
+
23
+ const users = coordinator.collection(appSchema, "users");
24
+ const posts = coordinator.collection(appSchema, "posts");
25
+ const workflowInstances = coordinator.collection(workflowsSchema, "workflow_instance");
26
+
27
+ await coordinator.preload();
28
+ ```
29
+
30
+ Collections can only be registered while the coordinator is `"idle"`. `preload()` opens one shared
31
+ stream, applies finite catch-up, and marks collections ready at its explicit `caught-up` frame. Live
32
+ entries continue on the same response. Readiness does not wait for disk persistence; use
33
+ `flushPersistence()` when you need all accepted writes to be durable.
34
+
35
+ Clean up the database-level resource when it is no longer used:
36
+
37
+ ```ts
38
+ await coordinator.cleanup();
39
+ ```
40
+
41
+ ## Authenticated and scoped Fetch
42
+
43
+ Pass a Fetch implementation that forwards authentication or routes requests to the correct scoped
44
+ backend:
45
+
46
+ ```ts
47
+ const coordinator = await createFragnoOutboxCoordinator({
48
+ baseUrl: "/api/app-scoped/org/acme",
49
+ fetch: (input, init) =>
50
+ fetch(input, {
51
+ ...init,
52
+ headers: {
53
+ ...init?.headers,
54
+ authorization: `Bearer ${token}`,
55
+ },
56
+ }),
57
+ schemas: [appSchema] as const,
58
+ });
59
+ ```
60
+
61
+ The coordinator derives these routes from `baseUrl`:
62
+
63
+ - `GET /_internal`
64
+ - `GET /_internal/outbox/stream?protocol=1`
65
+
66
+ The adapter never requests paginated `/outbox` data. Each stream starts strictly after the exact
67
+ checkpoint. Reloads and rotations with no new commits receive no entry payloads. Catch-up is applied
68
+ in bounded 50-entry batches. The response's `started` frame fixes the target; only `caught-up` makes
69
+ collections ready. Heartbeats are transport liveness, not readiness.
70
+
71
+ A final `rotate` frame requests immediate reconnection without backoff. Only classified network
72
+ failures and EOF without `rotate` transition through `"retrying"` and `"replaying"` with exponential
73
+ backoff. Request validation, HTTP errors (including 401/404 and 5xx), malformed protocol or
74
+ payloads, and application/callback errors transition to `"failed"` and reject pending `preload()`.
75
+ Only coordinator-owned disposal silently stops the stream loop; an unrelated `AbortError` is a
76
+ failure.
77
+
78
+ An interrupted partial catch-up batch is discarded and replayed; a planned rotation commits its
79
+ partial batch without marking it ready. Collections already ready remain usable during reconnection.
80
+ Custom Fetch implementations must preserve native Fetch network-error semantics; response parsing
81
+ and collection application are deliberately outside the network-error classification boundary.
82
+
83
+ To retrieve source metadata without opening browser persistence, use the same typed description
84
+ request:
85
+
86
+ ```ts
87
+ import { fetchFragnoOutboxDescription } from "@fragno-dev/tanstack-db-adapter";
88
+
89
+ const description = await fetchFragnoOutboxDescription({
90
+ baseUrl: "/api/app",
91
+ fetch: authenticatedFetch,
92
+ signal: request.signal,
93
+ });
94
+
95
+ console.log(description.adapterIdentity, description.currentVersionstamp);
96
+ ```
97
+
98
+ ## Coordinator state
99
+
100
+ The current lifecycle state is available directly and through the reactive internal collection:
101
+
102
+ ```ts
103
+ coordinator.state;
104
+ coordinator.internal.collection;
105
+ coordinator.internal.getCheckpoint();
106
+ ```
107
+
108
+ Lifecycle states are:
109
+
110
+ ```ts
111
+ "opening" |
112
+ "idle" |
113
+ "registering" |
114
+ "catching-up" |
115
+ "live" |
116
+ "rotating" |
117
+ "retrying" |
118
+ "replaying" |
119
+ "failed" |
120
+ "disposed";
121
+ ```
122
+
123
+ `coordinator.internal.collection` contains one `"coordinator"` row with the state, exact checkpoint,
124
+ and serialized error. It can be consumed by ordinary TanStack live queries.
125
+
126
+ ## Checkpoints and persistence
127
+
128
+ One physical outbox maps to one local persistence database and one shared exact checkpoint:
129
+
130
+ ```ts
131
+ type FragnoOutboxCheckpoint = {
132
+ versionstamp: string;
133
+ uowId: string;
134
+ };
135
+ ```
136
+
137
+ Every resumed request sends `afterVersionstamp` and `afterUowId`. The server verifies that exact
138
+ checkpoint before streaming, without downloading old entries. A missing or different UOW returns
139
+ HTTP 409 and stops synchronization. Adapter identity alone is insufficient because it survives a
140
+ backup restore, even when subsequent commits rewrite previously observed versionstamps. Collection
141
+ checkpoints also reject a changed UOW during partial-commit recovery.
142
+
143
+ During finite catch-up, each affected collection applies one bounded batch in one TanStack
144
+ transaction. Row changes and that collection's applied-entry checkpoint commit together. The shared
145
+ database checkpoint advances only after every affected collection accepts the page, and ordered
146
+ persistence prevents it from overtaking an earlier failed table write.
147
+
148
+ Separate TanStack collection commits remain independently observable. The coordinator provides
149
+ ordered, replay-safe convergence, not atomic cross-collection UI visibility.
150
+
151
+ Browser persistence is opened automatically with TanStack's `BrowserCollectionCoordinator`, so one
152
+ tab owns the SQLite writer. The OPFS database identity includes:
153
+
154
+ - normalized `baseUrl`;
155
+ - backend adapter identity;
156
+ - `FRAGNO_OUTBOX_LOCAL_SCHEMA_VERSION`.
157
+
158
+ Changing the backend adapter identity or local schema version opens a fresh local database and
159
+ replays the outbox. Increase `FRAGNO_OUTBOX_LOCAL_SCHEMA_VERSION` whenever a persisted materialized
160
+ row format changes. Local schema version 2 rebuilds version-1 caches that may lack checkpoint UOW
161
+ IDs.
162
+
163
+ ## Row update mode
164
+
165
+ Fragno update entries contain patches, so collections default to partial-row handling:
166
+
167
+ ```ts
168
+ const users = coordinator.collection(appSchema, "users", {
169
+ rowUpdateMode: "partial",
170
+ });
171
+ ```
172
+
173
+ Append-only tables that never receive Fragno update operations can use full-row handling:
174
+
175
+ ```ts
176
+ const emissions = coordinator.collection(workflowsSchema, "workflow_step_emission", {
177
+ rowUpdateMode: "full",
178
+ skipMissingTruncateDeletes: true,
179
+ });
180
+ ```
181
+
182
+ Do not use `"full"` for tables that receive updates. Their outbox values are patches rather than
183
+ complete rows.
184
+
185
+ `skipMissingTruncateDeletes` scans persisted rows once per collection synchronization registration
186
+ and skips catch-up truncate-derived deletes for keys that are already absent. The key index is
187
+ maintained by both catch-up and live changes, and cleared by explicit truncation. Stream rotations
188
+ and reconnects reuse it without rescanning; a replacement registration initializes a fresh index.
189
+ Ordinary deletes and live truncate delivery remain unchanged. Skipped deletes still advance the
190
+ collection checkpoint.
191
+
192
+ ## Catch-up progress
193
+
194
+ Use `onCatchUpProgress` for a loading indicator:
195
+
196
+ ```ts
197
+ const coordinator = await createFragnoOutboxCoordinator({
198
+ baseUrl: "/api/app",
199
+ fetch: globalThis.fetch,
200
+ schemas: [appSchema] as const,
201
+ onCatchUpProgress(progress) {
202
+ console.log(progress.completedPages, progress.totalPages, progress.percent);
203
+ },
204
+ });
205
+ ```
206
+
207
+ The percentage assumes contiguous database versions between the persisted checkpoint and the
208
+ server's current versionstamp.
@@ -0,0 +1,9 @@
1
+ //#region src/checkpoint.d.ts
2
+ /** The last applied entry identity, including its UOW to detect restored outbox history. */
3
+ type FragnoOutboxCheckpoint = {
4
+ versionstamp: string;
5
+ uowId: string;
6
+ };
7
+ //#endregion
8
+ export { FragnoOutboxCheckpoint };
9
+ //# sourceMappingURL=checkpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint.d.ts","names":[],"sources":["../src/checkpoint.ts"],"mappings":";;KAIY;EACV;EACA"}
@@ -0,0 +1,11 @@
1
+ //#region src/checkpoint.ts
2
+ const FRAGNO_OUTBOX_COLLECTION_CHECKPOINT_METADATA_KEY = "fragno.outbox.collection-checkpoint.v1";
3
+ /** Skips collection changes already committed before a shared checkpoint could advance. */
4
+ function shouldApplyOutboxCheckpoint(appliedCheckpoint, incomingCheckpoint) {
5
+ if (appliedCheckpoint?.versionstamp === incomingCheckpoint.versionstamp && appliedCheckpoint.uowId !== incomingCheckpoint.uowId) throw new Error(`Outbox checkpoint ${incomingCheckpoint.versionstamp} changed unit of work.`);
6
+ return !appliedCheckpoint || incomingCheckpoint.versionstamp > appliedCheckpoint.versionstamp;
7
+ }
8
+ //#endregion
9
+ export { FRAGNO_OUTBOX_COLLECTION_CHECKPOINT_METADATA_KEY, shouldApplyOutboxCheckpoint };
10
+
11
+ //# sourceMappingURL=checkpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint.js","names":[],"sources":["../src/checkpoint.ts"],"sourcesContent":["export const FRAGNO_OUTBOX_COLLECTION_CHECKPOINT_METADATA_KEY =\n \"fragno.outbox.collection-checkpoint.v1\";\n\n/** The last applied entry identity, including its UOW to detect restored outbox history. */\nexport type FragnoOutboxCheckpoint = {\n versionstamp: string;\n uowId: string;\n};\n\nexport type FragnoOutboxSource = {\n adapterIdentity: string;\n namespace: string;\n table: string;\n};\n\n/** Skips collection changes already committed before a shared checkpoint could advance. */\nexport function shouldApplyOutboxCheckpoint(\n appliedCheckpoint: FragnoOutboxCheckpoint | undefined,\n incomingCheckpoint: FragnoOutboxCheckpoint,\n): boolean {\n if (\n appliedCheckpoint?.versionstamp === incomingCheckpoint.versionstamp &&\n appliedCheckpoint.uowId !== incomingCheckpoint.uowId\n ) {\n throw new Error(`Outbox checkpoint ${incomingCheckpoint.versionstamp} changed unit of work.`);\n }\n return !appliedCheckpoint || incomingCheckpoint.versionstamp > appliedCheckpoint.versionstamp;\n}\n"],"mappings":";AAAA,MAAa,mDACX;;AAeF,SAAgB,4BACd,mBACA,oBACS;CACT,IACE,mBAAmB,iBAAiB,mBAAmB,gBACvD,kBAAkB,UAAU,mBAAmB,OAE/C,MAAM,IAAI,MAAM,qBAAqB,mBAAmB,aAAa,uBAAuB;CAE9F,OAAO,CAAC,qBAAqB,mBAAmB,eAAe,kBAAkB;AACnF"}
@@ -0,0 +1,92 @@
1
+ //#region src/coordinator/fragno-browser-persistence-startup.d.ts
2
+ type FragnoBrowserDiagnostic<TValue> = {
3
+ status: "available";
4
+ value: TValue;
5
+ } | {
6
+ status: "unavailable";
7
+ reason: string;
8
+ } | {
9
+ status: "failed";
10
+ error: string;
11
+ };
12
+ type FragnoBrowserLock = {
13
+ name: string;
14
+ mode: string;
15
+ clientId: string | null;
16
+ };
17
+ type FragnoBrowserLockSnapshot = {
18
+ held: FragnoBrowserLock[];
19
+ pending: FragnoBrowserLock[];
20
+ relevantHeld: FragnoBrowserLock[];
21
+ relevantPending: FragnoBrowserLock[];
22
+ interpretation: string;
23
+ };
24
+ type FragnoBrowserStorageEstimate = {
25
+ quota: number | null;
26
+ usage: number | null;
27
+ usageDetails: Record<string, number> | null;
28
+ };
29
+ type FragnoBrowserWorkerProbeSnapshot = {
30
+ status: "unsupported";
31
+ reason: string;
32
+ } | {
33
+ status: "starting";
34
+ elapsedMs: number;
35
+ } | {
36
+ status: "responsive";
37
+ responseMs: number;
38
+ runtime: {
39
+ timerAvailable: boolean;
40
+ opfsAvailable: boolean;
41
+ fileSystemFileHandleGlobalAvailable: boolean;
42
+ syncAccessHandlePrototypeAvailable: boolean;
43
+ };
44
+ } | {
45
+ status: "failed";
46
+ error: string;
47
+ };
48
+ type FragnoOPFSEntry = {
49
+ kind: "file";
50
+ name: string;
51
+ } | {
52
+ kind: "directory";
53
+ name: string;
54
+ entries: Array<{
55
+ name: string;
56
+ kind: string;
57
+ }>;
58
+ inspectionError: string | null;
59
+ };
60
+ /** Captures worker, runtime, storage, Web Locks, and OPFS state during a stalled browser startup. */
61
+ type FragnoBrowserPersistenceDiagnostics = {
62
+ databaseName: string;
63
+ elapsedMs: number;
64
+ timerDriftMs: number;
65
+ collectedAt: string;
66
+ likelyCause: string;
67
+ workerProbe: FragnoBrowserWorkerProbeSnapshot;
68
+ runtime: {
69
+ href: string | null;
70
+ origin: string | null;
71
+ userAgent: string | null;
72
+ visibilityState: DocumentVisibilityState | null;
73
+ readyState: DocumentReadyState | null;
74
+ documentHasFocus: boolean | null;
75
+ isSecureContext: boolean;
76
+ crossOriginIsolated: boolean;
77
+ online: boolean | null;
78
+ hardwareConcurrency: number | null;
79
+ workerAvailable: boolean;
80
+ broadcastChannelAvailable: boolean;
81
+ webLocksAvailable: boolean;
82
+ opfsAvailable: boolean;
83
+ fileSystemFileHandleGlobalAvailable: boolean;
84
+ };
85
+ webLocks: FragnoBrowserDiagnostic<FragnoBrowserLockSnapshot>;
86
+ storageEstimate: FragnoBrowserDiagnostic<FragnoBrowserStorageEstimate>;
87
+ persistedStorage: FragnoBrowserDiagnostic<boolean>;
88
+ opfsEntries: FragnoBrowserDiagnostic<FragnoOPFSEntry[]>;
89
+ };
90
+ //#endregion
91
+ export { FragnoBrowserPersistenceDiagnostics };
92
+ //# sourceMappingURL=fragno-browser-persistence-startup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fragno-browser-persistence-startup.d.ts","names":[],"sources":["../../src/coordinator/fragno-browser-persistence-startup.ts"],"mappings":";KAGK,wBAAwB;EACvB;EAAqB,OAAO;;EAC5B;EAAuB;;EACvB;EAAkB;;KAEnB;EACH;EACA;EACA;;KAGG;EACH,MAAM;EACN,SAAS;EACT,cAAc;EACd,iBAAiB;EACjB;;KAGG;EACH;EACA;EACA,cAAc;;KAGX;EACC;EAAuB;;EACvB;EAAoB;;EAEpB;EACA;EACA;IACE;IACA;IACA;IACA;;;EAGF;EAAkB;;KAOnB;EACC;EAAc;;EAEd;EACA;EACA,SAAS;IAAQ;IAAc;;EAC/B;;;KAeM;EACV;EACA;EACA;EACA;EACA;EACA,aAAa;EACb;IACE;IACA;IACA;IACA,iBAAiB;IACjB,YAAY;IACZ;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEF,UAAU,wBAAwB;EAClC,iBAAiB,wBAAwB;EACzC,kBAAkB;EAClB,aAAa,wBAAwB"}