@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
@@ -0,0 +1,142 @@
1
+ import { createCollection } from "@tanstack/db";
2
+ import { persistedCollectionOptions } from "@tanstack/db-sqlite-persistence-core";
3
+ //#region src/coordinator/fragno-internal-collection.ts
4
+ const FRAGNO_INTERNAL_COLLECTION_ID = "fragno.outbox.internal.v1";
5
+ const FRAGNO_INTERNAL_STATUS_ID = "coordinator";
6
+ const FRAGNO_INTERNAL_CHECKPOINT_METADATA_KEY = "fragno.outbox.checkpoint.v1";
7
+ const FRAGNO_INTERNAL_FLUSH_MARKER_METADATA_KEY = "fragno.outbox.flush-marker.v1";
8
+ /** Persisted and user-queryable coordinator state for one Fragno outbox database. */
9
+ var FragnoInternalCollection = class {
10
+ collection;
11
+ #controls;
12
+ #preloadPromise;
13
+ #checkpoint;
14
+ #status;
15
+ constructor(options) {
16
+ const persistence = resolveCollectionPersistence(options.persistence, FRAGNO_INTERNAL_COLLECTION_ID, options.schemaVersion);
17
+ this.#status = {
18
+ id: FRAGNO_INTERNAL_STATUS_ID,
19
+ state: options.state ?? "opening",
20
+ checkpoint: null,
21
+ error: null
22
+ };
23
+ this.collection = createCollection(persistedCollectionOptions({
24
+ id: FRAGNO_INTERNAL_COLLECTION_ID,
25
+ getKey: (row) => row.id,
26
+ persistence,
27
+ schemaVersion: options.schemaVersion,
28
+ syncMode: "eager",
29
+ sync: {
30
+ rowUpdateMode: "full",
31
+ sync: (controls) => {
32
+ if (!controls.metadata) throw new Error("Fragno internal collection requires TanStack sync metadata.");
33
+ this.#controls = controls;
34
+ this.#checkpoint = controls.metadata.collection.get(FRAGNO_INTERNAL_CHECKPOINT_METADATA_KEY);
35
+ this.#status = {
36
+ ...this.#status,
37
+ checkpoint: this.#checkpoint ?? null
38
+ };
39
+ controls.markReady();
40
+ return () => {
41
+ if (this.#controls === controls) this.#controls = void 0;
42
+ };
43
+ }
44
+ }
45
+ }));
46
+ }
47
+ preload() {
48
+ if (this.#preloadPromise) return this.#preloadPromise;
49
+ this.#preloadPromise = this.collection.preload().then(async () => {
50
+ await this.#publishStatus();
51
+ });
52
+ return this.#preloadPromise;
53
+ }
54
+ getCheckpoint() {
55
+ return this.#checkpoint;
56
+ }
57
+ setCheckpoint(checkpoint) {
58
+ this.#checkpoint = checkpoint;
59
+ this.#status = {
60
+ ...this.#status,
61
+ checkpoint
62
+ };
63
+ const controls = this.#requireControls();
64
+ controls.begin();
65
+ controls.metadata.collection.set(FRAGNO_INTERNAL_CHECKPOINT_METADATA_KEY, checkpoint);
66
+ controls.write({
67
+ type: "update",
68
+ value: this.#status
69
+ });
70
+ return controls.commit();
71
+ }
72
+ /**
73
+ * Settles once every earlier write in the shared persistence database is durable: a persisted
74
+ * commit's receipt settles with its durable write, and writes persist in order.
75
+ */
76
+ flushPersistence() {
77
+ const controls = this.#requireControls();
78
+ controls.begin();
79
+ controls.metadata.collection.set(FRAGNO_INTERNAL_FLUSH_MARKER_METADATA_KEY, globalThis.crypto.randomUUID());
80
+ return controls.commit();
81
+ }
82
+ clearCheckpoint() {
83
+ this.#checkpoint = void 0;
84
+ this.#status = {
85
+ ...this.#status,
86
+ checkpoint: null
87
+ };
88
+ const controls = this.#requireControls();
89
+ controls.begin();
90
+ controls.metadata.collection.delete(FRAGNO_INTERNAL_CHECKPOINT_METADATA_KEY);
91
+ controls.write({
92
+ type: "update",
93
+ value: this.#status
94
+ });
95
+ return controls.commit();
96
+ }
97
+ setState(state, error = null) {
98
+ this.#status = {
99
+ ...this.#status,
100
+ state,
101
+ error: state === "failed" ? coordinatorError(error) : null
102
+ };
103
+ return this.#publishStatus();
104
+ }
105
+ cleanup() {
106
+ return this.collection.cleanup().then(() => {});
107
+ }
108
+ #publishStatus() {
109
+ const controls = this.#requireControls();
110
+ controls.begin();
111
+ controls.write({
112
+ type: "update",
113
+ value: this.#status
114
+ });
115
+ return controls.commit();
116
+ }
117
+ #requireControls() {
118
+ if (!this.#controls) throw new Error("Fragno internal collection has not been preloaded.");
119
+ return this.#controls;
120
+ }
121
+ };
122
+ function coordinatorError(error) {
123
+ if (error instanceof Error) return {
124
+ name: error.name,
125
+ message: error.message
126
+ };
127
+ return {
128
+ name: "Error",
129
+ message: String(error)
130
+ };
131
+ }
132
+ function resolveCollectionPersistence(persistence, collectionId, schemaVersion) {
133
+ return persistence.resolvePersistenceForCollection?.({
134
+ collectionId,
135
+ mode: "sync-present",
136
+ schemaVersion
137
+ }) ?? persistence.resolvePersistenceForMode?.("sync-present") ?? persistence;
138
+ }
139
+ //#endregion
140
+ export { FRAGNO_INTERNAL_CHECKPOINT_METADATA_KEY, FRAGNO_INTERNAL_COLLECTION_ID, FRAGNO_INTERNAL_STATUS_ID, FragnoInternalCollection };
141
+
142
+ //# sourceMappingURL=fragno-internal-collection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fragno-internal-collection.js","names":["#status","#controls","#checkpoint","#preloadPromise","#publishStatus","#requireControls"],"sources":["../../src/coordinator/fragno-internal-collection.ts"],"sourcesContent":["import {\n createCollection,\n type Collection,\n type SyncAppliedReceipt,\n type SyncConfig,\n} from \"@tanstack/db\";\nimport {\n persistedCollectionOptions,\n type PersistedCollectionPersistence,\n} from \"@tanstack/db-sqlite-persistence-core\";\n\nimport type { FragnoOutboxCheckpoint } from \"../checkpoint\";\n\nexport const FRAGNO_INTERNAL_COLLECTION_ID = \"fragno.outbox.internal.v1\";\nexport const FRAGNO_INTERNAL_STATUS_ID = \"coordinator\" as const;\nexport const FRAGNO_INTERNAL_CHECKPOINT_METADATA_KEY = \"fragno.outbox.checkpoint.v1\";\nconst FRAGNO_INTERNAL_FLUSH_MARKER_METADATA_KEY = \"fragno.outbox.flush-marker.v1\";\n\nexport type FragnoOutboxCoordinatorState =\n | \"opening\"\n | \"idle\"\n | \"registering\"\n | \"catching-up\"\n | \"replaying\"\n | \"live\"\n | \"retrying\"\n | \"rotating\"\n | \"failed\"\n | \"disposed\";\n\nexport type FragnoOutboxCoordinatorError = {\n name: string;\n message: string;\n};\n\nexport type FragnoInternalRow = {\n id: typeof FRAGNO_INTERNAL_STATUS_ID;\n state: FragnoOutboxCoordinatorState;\n checkpoint: FragnoOutboxCheckpoint | null;\n error: FragnoOutboxCoordinatorError | null;\n};\n\ntype FragnoInternalSyncControls = Parameters<SyncConfig<FragnoInternalRow, string>[\"sync\"]>[0];\n\n/** Persisted and user-queryable coordinator state for one Fragno outbox database. */\nexport class FragnoInternalCollection {\n readonly collection: Collection<FragnoInternalRow, string>;\n\n #controls: FragnoInternalSyncControls | undefined;\n #preloadPromise: Promise<void> | undefined;\n #checkpoint: FragnoOutboxCheckpoint | undefined;\n #status: FragnoInternalRow;\n\n constructor(options: {\n persistence: PersistedCollectionPersistence;\n schemaVersion?: number;\n state?: FragnoOutboxCoordinatorState;\n }) {\n const persistence = resolveCollectionPersistence(\n options.persistence,\n FRAGNO_INTERNAL_COLLECTION_ID,\n options.schemaVersion,\n );\n this.#status = {\n id: FRAGNO_INTERNAL_STATUS_ID,\n state: options.state ?? \"opening\",\n checkpoint: null,\n error: null,\n };\n\n this.collection = createCollection(\n persistedCollectionOptions<FragnoInternalRow, string>({\n id: FRAGNO_INTERNAL_COLLECTION_ID,\n getKey: (row) => row.id,\n persistence,\n schemaVersion: options.schemaVersion,\n syncMode: \"eager\",\n sync: {\n rowUpdateMode: \"full\",\n sync: (controls) => {\n if (!controls.metadata) {\n throw new Error(\"Fragno internal collection requires TanStack sync metadata.\");\n }\n\n this.#controls = controls;\n this.#checkpoint = controls.metadata.collection.get(\n FRAGNO_INTERNAL_CHECKPOINT_METADATA_KEY,\n ) as FragnoOutboxCheckpoint | undefined;\n this.#status = {\n ...this.#status,\n checkpoint: this.#checkpoint ?? null,\n };\n controls.markReady();\n\n return () => {\n if (this.#controls === controls) {\n this.#controls = undefined;\n }\n };\n },\n },\n }),\n );\n }\n\n preload(): Promise<void> {\n if (this.#preloadPromise) {\n return this.#preloadPromise;\n }\n this.#preloadPromise = this.collection.preload().then(async () => {\n await this.#publishStatus();\n });\n return this.#preloadPromise;\n }\n\n getCheckpoint(): FragnoOutboxCheckpoint | undefined {\n return this.#checkpoint;\n }\n\n setCheckpoint(checkpoint: FragnoOutboxCheckpoint): SyncAppliedReceipt {\n this.#checkpoint = checkpoint;\n this.#status = { ...this.#status, checkpoint };\n\n const controls = this.#requireControls();\n controls.begin();\n controls.metadata!.collection.set(FRAGNO_INTERNAL_CHECKPOINT_METADATA_KEY, checkpoint);\n controls.write({ type: \"update\", value: this.#status });\n return controls.commit();\n }\n\n /**\n * Settles once every earlier write in the shared persistence database is durable: a persisted\n * commit's receipt settles with its durable write, and writes persist in order.\n */\n flushPersistence(): SyncAppliedReceipt {\n const controls = this.#requireControls();\n controls.begin();\n // A fresh marker gives the commit a change to persist.\n controls.metadata!.collection.set(\n FRAGNO_INTERNAL_FLUSH_MARKER_METADATA_KEY,\n globalThis.crypto.randomUUID(),\n );\n return controls.commit();\n }\n\n clearCheckpoint(): SyncAppliedReceipt {\n this.#checkpoint = undefined;\n this.#status = { ...this.#status, checkpoint: null };\n\n const controls = this.#requireControls();\n controls.begin();\n controls.metadata!.collection.delete(FRAGNO_INTERNAL_CHECKPOINT_METADATA_KEY);\n controls.write({ type: \"update\", value: this.#status });\n return controls.commit();\n }\n\n setState(state: FragnoOutboxCoordinatorState, error: unknown = null): SyncAppliedReceipt {\n this.#status = {\n ...this.#status,\n state,\n error: state === \"failed\" ? coordinatorError(error) : null,\n };\n return this.#publishStatus();\n }\n\n cleanup(): Promise<void> {\n return this.collection.cleanup().then(() => {});\n }\n\n #publishStatus(): SyncAppliedReceipt {\n const controls = this.#requireControls();\n controls.begin();\n controls.write({ type: \"update\", value: this.#status });\n return controls.commit();\n }\n\n #requireControls(): FragnoInternalSyncControls {\n if (!this.#controls) {\n throw new Error(\"Fragno internal collection has not been preloaded.\");\n }\n return this.#controls;\n }\n}\n\nfunction coordinatorError(error: unknown): FragnoOutboxCoordinatorError {\n if (error instanceof Error) {\n return { name: error.name, message: error.message };\n }\n\n return { name: \"Error\", message: String(error) };\n}\n\nfunction resolveCollectionPersistence(\n persistence: PersistedCollectionPersistence,\n collectionId: string,\n schemaVersion: number | undefined,\n): PersistedCollectionPersistence {\n return (\n persistence.resolvePersistenceForCollection?.({\n collectionId,\n mode: \"sync-present\",\n schemaVersion,\n }) ??\n persistence.resolvePersistenceForMode?.(\"sync-present\") ??\n persistence\n );\n}\n"],"mappings":";;;AAaA,MAAa,gCAAgC;AAC7C,MAAa,4BAA4B;AACzC,MAAa,0CAA0C;AACvD,MAAM,4CAA4C;;AA6BlD,IAAa,2BAAb,MAAsC;CACpC;CAEA;CACA;CACA;CACA;CAEA,YAAY,SAIT;EACD,MAAM,cAAc,6BAClB,QAAQ,aACR,+BACA,QAAQ,aACV;EACA,KAAKA,UAAU;GACb,IAAI;GACJ,OAAO,QAAQ,SAAS;GACxB,YAAY;GACZ,OAAO;EACT;EAEA,KAAK,aAAa,iBAChB,2BAAsD;GACpD,IAAI;GACJ,SAAS,QAAQ,IAAI;GACrB;GACA,eAAe,QAAQ;GACvB,UAAU;GACV,MAAM;IACJ,eAAe;IACf,OAAO,aAAa;KAClB,IAAI,CAAC,SAAS,UACZ,MAAM,IAAI,MAAM,6DAA6D;KAG/E,KAAKC,YAAY;KACjB,KAAKC,cAAc,SAAS,SAAS,WAAW,IAC9C,uCACF;KACA,KAAKF,UAAU;MACb,GAAG,KAAKA;MACR,YAAY,KAAKE,eAAe;KAClC;KACA,SAAS,UAAU;KAEnB,aAAa;MACX,IAAI,KAAKD,cAAc,UACrB,KAAKA,YAAY,KAAA;KAErB;IACF;GACF;EACF,CAAC,CACH;CACF;CAEA,UAAyB;EACvB,IAAI,KAAKE,iBACP,OAAO,KAAKA;EAEd,KAAKA,kBAAkB,KAAK,WAAW,QAAQ,CAAC,CAAC,KAAK,YAAY;GAChE,MAAM,KAAKC,eAAe;EAC5B,CAAC;EACD,OAAO,KAAKD;CACd;CAEA,gBAAoD;EAClD,OAAO,KAAKD;CACd;CAEA,cAAc,YAAwD;EACpE,KAAKA,cAAc;EACnB,KAAKF,UAAU;GAAE,GAAG,KAAKA;GAAS;EAAW;EAE7C,MAAM,WAAW,KAAKK,iBAAiB;EACvC,SAAS,MAAM;EACf,SAAS,SAAU,WAAW,IAAI,yCAAyC,UAAU;EACrF,SAAS,MAAM;GAAE,MAAM;GAAU,OAAO,KAAKL;EAAQ,CAAC;EACtD,OAAO,SAAS,OAAO;CACzB;;;;;CAMA,mBAAuC;EACrC,MAAM,WAAW,KAAKK,iBAAiB;EACvC,SAAS,MAAM;EAEf,SAAS,SAAU,WAAW,IAC5B,2CACA,WAAW,OAAO,WAAW,CAC/B;EACA,OAAO,SAAS,OAAO;CACzB;CAEA,kBAAsC;EACpC,KAAKH,cAAc,KAAA;EACnB,KAAKF,UAAU;GAAE,GAAG,KAAKA;GAAS,YAAY;EAAK;EAEnD,MAAM,WAAW,KAAKK,iBAAiB;EACvC,SAAS,MAAM;EACf,SAAS,SAAU,WAAW,OAAO,uCAAuC;EAC5E,SAAS,MAAM;GAAE,MAAM;GAAU,OAAO,KAAKL;EAAQ,CAAC;EACtD,OAAO,SAAS,OAAO;CACzB;CAEA,SAAS,OAAqC,QAAiB,MAA0B;EACvF,KAAKA,UAAU;GACb,GAAG,KAAKA;GACR;GACA,OAAO,UAAU,WAAW,iBAAiB,KAAK,IAAI;EACxD;EACA,OAAO,KAAKI,eAAe;CAC7B;CAEA,UAAyB;EACvB,OAAO,KAAK,WAAW,QAAQ,CAAC,CAAC,WAAW,CAAC,CAAC;CAChD;CAEA,iBAAqC;EACnC,MAAM,WAAW,KAAKC,iBAAiB;EACvC,SAAS,MAAM;EACf,SAAS,MAAM;GAAE,MAAM;GAAU,OAAO,KAAKL;EAAQ,CAAC;EACtD,OAAO,SAAS,OAAO;CACzB;CAEA,mBAA+C;EAC7C,IAAI,CAAC,KAAKC,WACR,MAAM,IAAI,MAAM,oDAAoD;EAEtE,OAAO,KAAKA;CACd;AACF;AAEA,SAAS,iBAAiB,OAA8C;CACtE,IAAI,iBAAiB,OACnB,OAAO;EAAE,MAAM,MAAM;EAAM,SAAS,MAAM;CAAQ;CAGpD,OAAO;EAAE,MAAM;EAAS,SAAS,OAAO,KAAK;CAAE;AACjD;AAEA,SAAS,6BACP,aACA,cACA,eACgC;CAChC,OACE,YAAY,kCAAkC;EAC5C;EACA,MAAM;EACN;CACF,CAAC,KACD,YAAY,4BAA4B,cAAc,KACtD;AAEJ"}
@@ -0,0 +1,23 @@
1
+ //#region src/coordinator/fragno-internal-fetcher.d.ts
2
+ type FragnoInternalDescription = {
3
+ adapterIdentity: string;
4
+ currentVersionstamp: string | null;
5
+ fragments: Array<{
6
+ name: string;
7
+ mountRoute: string;
8
+ }>;
9
+ schemas: Array<{
10
+ name: string;
11
+ namespace: string | null;
12
+ version: number;
13
+ tables: string[];
14
+ }>;
15
+ routes: {
16
+ internal: "/_internal";
17
+ outbox?: "/_internal/outbox";
18
+ outboxStream?: "/_internal/outbox/stream";
19
+ };
20
+ };
21
+ //#endregion
22
+ export { FragnoInternalDescription };
23
+ //# sourceMappingURL=fragno-internal-fetcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fragno-internal-fetcher.d.ts","names":[],"sources":["../../src/coordinator/fragno-internal-fetcher.ts"],"mappings":";KAMY;EACV;EACA;EACA,WAAW;IAAQ;IAAc;;EACjC,SAAS;IACP;IACA;IACA;IACA;;EAEF;IACE;IACA;IACA"}
@@ -0,0 +1,60 @@
1
+ import { rethrowOutboxNetworkFailure } from "../outbox-transport-error.js";
2
+ import { FragnoOutboxProtocolError } from "../outbox-stream.js";
3
+ import { FRAGNO_OUTBOX_PAGE_SIZE } from "@fragno-dev/db/outbox";
4
+ //#region src/coordinator/fragno-internal-fetcher.ts
5
+ /** Typed Fetch access to the trusted Fragno internal routes used by outbox synchronization. */
6
+ var FragnoInternalFetcher = class {
7
+ baseUrl;
8
+ #fetch;
9
+ #describeUrl;
10
+ #outboxStreamUrl;
11
+ constructor(options) {
12
+ const baseUrl = new URL(options.baseUrl, globalThis.location?.href);
13
+ if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") throw new Error("Fragno outbox synchronization requires an HTTP(S) base URL.");
14
+ baseUrl.hash = "";
15
+ baseUrl.pathname = baseUrl.pathname.replace(/\/+$/, "");
16
+ const internalUrl = new URL(baseUrl);
17
+ internalUrl.pathname = `${baseUrl.pathname}/_internal`;
18
+ const outboxStreamUrl = new URL(internalUrl);
19
+ outboxStreamUrl.pathname = `${internalUrl.pathname}/outbox/stream`;
20
+ this.baseUrl = baseUrl.toString();
21
+ this.#fetch = options.fetch;
22
+ this.#describeUrl = internalUrl;
23
+ this.#outboxStreamUrl = outboxStreamUrl;
24
+ }
25
+ async describe(options = {}) {
26
+ return await (await this.#get(this.#describeUrl, options.signal)).json();
27
+ }
28
+ async openOutboxStream(options) {
29
+ const url = new URL(this.#outboxStreamUrl);
30
+ url.searchParams.set("protocol", "1");
31
+ if (options.checkpoint) {
32
+ url.searchParams.set("afterVersionstamp", options.checkpoint.versionstamp);
33
+ url.searchParams.set("afterUowId", options.checkpoint.uowId);
34
+ }
35
+ url.searchParams.set("limit", String(FRAGNO_OUTBOX_PAGE_SIZE));
36
+ const request = new Request(url, { signal: options.signal });
37
+ let response;
38
+ try {
39
+ response = await this.#fetch(request);
40
+ } catch (cause) {
41
+ rethrowOutboxNetworkFailure(cause, options.signal);
42
+ }
43
+ if (!response.ok) {
44
+ await response.body?.cancel().catch(() => {});
45
+ if (response.status === 400 || response.status === 409) throw new FragnoOutboxProtocolError(`Fragno outbox stream rejected its protocol or cursor: ${response.status}.`);
46
+ throw new Error(`Fragno outbox stream request failed: ${response.status} ${response.statusText}`);
47
+ }
48
+ if (!response.body) throw new Error("Fragno outbox stream response has no body.");
49
+ return response.body;
50
+ }
51
+ async #get(url, signal) {
52
+ const response = await this.#fetch(url, { signal });
53
+ if (!response.ok) throw new Error(`Fragno internal request failed: ${response.status} ${response.statusText}`);
54
+ return response;
55
+ }
56
+ };
57
+ //#endregion
58
+ export { FragnoInternalFetcher };
59
+
60
+ //# sourceMappingURL=fragno-internal-fetcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fragno-internal-fetcher.js","names":["#fetch","#describeUrl","#outboxStreamUrl","#get"],"sources":["../../src/coordinator/fragno-internal-fetcher.ts"],"sourcesContent":["import { FRAGNO_OUTBOX_PAGE_SIZE } from \"@fragno-dev/db/outbox\";\n\nimport type { FragnoOutboxCheckpoint } from \"../checkpoint\";\nimport { FragnoOutboxProtocolError } from \"../outbox-stream\";\nimport { rethrowOutboxNetworkFailure } from \"../outbox-transport-error\";\n\nexport type FragnoInternalDescription = {\n adapterIdentity: string;\n currentVersionstamp: string | null;\n fragments: Array<{ name: string; mountRoute: string }>;\n schemas: Array<{\n name: string;\n namespace: string | null;\n version: number;\n tables: string[];\n }>;\n routes: {\n internal: \"/_internal\";\n outbox?: \"/_internal/outbox\";\n outboxStream?: \"/_internal/outbox/stream\";\n };\n};\n\n/** Typed Fetch access to the trusted Fragno internal routes used by outbox synchronization. */\nexport class FragnoInternalFetcher {\n readonly baseUrl: string;\n\n readonly #fetch: typeof globalThis.fetch;\n readonly #describeUrl: URL;\n readonly #outboxStreamUrl: URL;\n\n constructor(options: { baseUrl: string | URL; fetch: typeof globalThis.fetch }) {\n const baseUrl = new URL(options.baseUrl, globalThis.location?.href);\n if (baseUrl.protocol !== \"http:\" && baseUrl.protocol !== \"https:\") {\n throw new Error(\"Fragno outbox synchronization requires an HTTP(S) base URL.\");\n }\n baseUrl.hash = \"\";\n baseUrl.pathname = baseUrl.pathname.replace(/\\/+$/, \"\");\n\n const internalUrl = new URL(baseUrl);\n internalUrl.pathname = `${baseUrl.pathname}/_internal`;\n const outboxStreamUrl = new URL(internalUrl);\n outboxStreamUrl.pathname = `${internalUrl.pathname}/outbox/stream`;\n\n this.baseUrl = baseUrl.toString();\n this.#fetch = options.fetch;\n this.#describeUrl = internalUrl;\n this.#outboxStreamUrl = outboxStreamUrl;\n }\n\n async describe(options: { signal?: AbortSignal } = {}): Promise<FragnoInternalDescription> {\n const response = await this.#get(this.#describeUrl, options.signal);\n const description = (await response.json()) as FragnoInternalDescription;\n return description;\n }\n\n async openOutboxStream(options: {\n checkpoint: FragnoOutboxCheckpoint | undefined;\n signal?: AbortSignal;\n }): Promise<ReadableStream<Uint8Array>> {\n const url = new URL(this.#outboxStreamUrl);\n url.searchParams.set(\"protocol\", \"1\");\n if (options.checkpoint) {\n url.searchParams.set(\"afterVersionstamp\", options.checkpoint.versionstamp);\n url.searchParams.set(\"afterUowId\", options.checkpoint.uowId);\n }\n url.searchParams.set(\"limit\", String(FRAGNO_OUTBOX_PAGE_SIZE));\n const request = new Request(url, { signal: options.signal });\n let response: Response;\n try {\n response = await this.#fetch(request);\n } catch (cause) {\n rethrowOutboxNetworkFailure(cause, options.signal);\n }\n if (!response.ok) {\n await response.body?.cancel().catch(() => {});\n if (response.status === 400 || response.status === 409) {\n throw new FragnoOutboxProtocolError(\n `Fragno outbox stream rejected its protocol or cursor: ${response.status}.`,\n );\n }\n throw new Error(\n `Fragno outbox stream request failed: ${response.status} ${response.statusText}`,\n );\n }\n if (!response.body) {\n throw new Error(\"Fragno outbox stream response has no body.\");\n }\n return response.body;\n }\n\n async #get(url: URL, signal: AbortSignal | undefined): Promise<Response> {\n const response = await this.#fetch(url, { signal });\n if (!response.ok) {\n throw new Error(`Fragno internal request failed: ${response.status} ${response.statusText}`);\n }\n return response;\n }\n}\n"],"mappings":";;;;;AAwBA,IAAa,wBAAb,MAAmC;CACjC;CAEA;CACA;CACA;CAEA,YAAY,SAAoE;EAC9E,MAAM,UAAU,IAAI,IAAI,QAAQ,SAAS,WAAW,UAAU,IAAI;EAClE,IAAI,QAAQ,aAAa,WAAW,QAAQ,aAAa,UACvD,MAAM,IAAI,MAAM,6DAA6D;EAE/E,QAAQ,OAAO;EACf,QAAQ,WAAW,QAAQ,SAAS,QAAQ,QAAQ,EAAE;EAEtD,MAAM,cAAc,IAAI,IAAI,OAAO;EACnC,YAAY,WAAW,GAAG,QAAQ,SAAS;EAC3C,MAAM,kBAAkB,IAAI,IAAI,WAAW;EAC3C,gBAAgB,WAAW,GAAG,YAAY,SAAS;EAEnD,KAAK,UAAU,QAAQ,SAAS;EAChC,KAAKA,SAAS,QAAQ;EACtB,KAAKC,eAAe;EACpB,KAAKC,mBAAmB;CAC1B;CAEA,MAAM,SAAS,UAAoC,CAAC,GAAuC;EAGzF,OAAO,OADoB,MADJ,KAAKC,KAAK,KAAKF,cAAc,QAAQ,MAAM,EAAA,CAC9B,KAAK;CAE3C;CAEA,MAAM,iBAAiB,SAGiB;EACtC,MAAM,MAAM,IAAI,IAAI,KAAKC,gBAAgB;EACzC,IAAI,aAAa,IAAI,YAAY,GAAG;EACpC,IAAI,QAAQ,YAAY;GACtB,IAAI,aAAa,IAAI,qBAAqB,QAAQ,WAAW,YAAY;GACzE,IAAI,aAAa,IAAI,cAAc,QAAQ,WAAW,KAAK;EAC7D;EACA,IAAI,aAAa,IAAI,SAAS,OAAO,uBAAuB,CAAC;EAC7D,MAAM,UAAU,IAAI,QAAQ,KAAK,EAAE,QAAQ,QAAQ,OAAO,CAAC;EAC3D,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,KAAKF,OAAO,OAAO;EACtC,SAAS,OAAO;GACd,4BAA4B,OAAO,QAAQ,MAAM;EACnD;EACA,IAAI,CAAC,SAAS,IAAI;GAChB,MAAM,SAAS,MAAM,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;GAC5C,IAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KACjD,MAAM,IAAI,0BACR,yDAAyD,SAAS,OAAO,EAC3E;GAEF,MAAM,IAAI,MACR,wCAAwC,SAAS,OAAO,GAAG,SAAS,YACtE;EACF;EACA,IAAI,CAAC,SAAS,MACZ,MAAM,IAAI,MAAM,4CAA4C;EAE9D,OAAO,SAAS;CAClB;CAEA,MAAMG,KAAK,KAAU,QAAoD;EACvE,MAAM,WAAW,MAAM,KAAKH,OAAO,KAAK,EAAE,OAAO,CAAC;EAClD,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,mCAAmC,SAAS,OAAO,GAAG,SAAS,YAAY;EAE7F,OAAO;CACT;AACF"}
@@ -0,0 +1,66 @@
1
+ //#region src/coordinator/fragno-ordered-persistence.ts
2
+ /**
3
+ * A hydration scope hands TanStack an unscheduled inner adapter whose writes would bypass the
4
+ * queue, and queueing them would wait on writes blocked behind that same scope. Hiding the scope
5
+ * makes TanStack run hydration through the wrapped adapter.
6
+ */
7
+ const HIDDEN_ADAPTER_PROPERTIES = /* @__PURE__ */ new Set([
8
+ "runInHydrationScope",
9
+ "runInRegularScope",
10
+ "isHydrationScopeScheduled"
11
+ ]);
12
+ /** Orders every collection write before later writes to the shared Fragno persistence database. */
13
+ function orderFragnoPersistenceWrites(persistence) {
14
+ const queue = new FragnoPersistenceWriteQueue();
15
+ const persistenceCache = /* @__PURE__ */ new WeakMap();
16
+ const adapterCache = /* @__PURE__ */ new WeakMap();
17
+ const wrapAdapter = (adapter) => {
18
+ const cached = adapterCache.get(adapter);
19
+ if (cached) return cached;
20
+ const wrapped = forwardAdapterCapabilities(adapter);
21
+ wrapped.applyCommittedTx = (target, transaction) => queue.enqueue(() => adapter.applyCommittedTx(target, transaction));
22
+ const { reconcileCommittedTx } = adapter;
23
+ if (reconcileCommittedTx) wrapped.reconcileCommittedTx = (target, transaction, anchor) => queue.enqueue(() => reconcileCommittedTx.call(adapter, target, transaction, anchor));
24
+ adapterCache.set(adapter, wrapped);
25
+ return wrapped;
26
+ };
27
+ const wrapPersistence = (resolvedPersistence) => {
28
+ const cached = persistenceCache.get(resolvedPersistence);
29
+ if (cached) return cached;
30
+ const wrapped = { adapter: wrapAdapter(resolvedPersistence.adapter) };
31
+ persistenceCache.set(resolvedPersistence, wrapped);
32
+ if (resolvedPersistence.coordinator) wrapped.coordinator = resolvedPersistence.coordinator;
33
+ if (resolvedPersistence.resolvePersistenceForCollection) wrapped.resolvePersistenceForCollection = (options) => wrapPersistence(resolvedPersistence.resolvePersistenceForCollection(options));
34
+ if (resolvedPersistence.resolvePersistenceForMode) wrapped.resolvePersistenceForMode = (mode) => wrapPersistence(resolvedPersistence.resolvePersistenceForMode(mode));
35
+ return wrapped;
36
+ };
37
+ return Object.assign(wrapPersistence(persistence), { drain: () => queue.drain() });
38
+ }
39
+ /**
40
+ * Binds every adapter method, including class methods, so coordinators keep the capabilities they
41
+ * require, such as the browser coordinator's `reserveLeadershipTerm`.
42
+ */
43
+ function forwardAdapterCapabilities(adapter) {
44
+ const forwarded = {};
45
+ for (let source = adapter; source && source !== Object.prototype; source = Object.getPrototypeOf(source)) for (const property of Reflect.ownKeys(source)) {
46
+ if (property === "constructor" || HIDDEN_ADAPTER_PROPERTIES.has(property) || Object.hasOwn(forwarded, property)) continue;
47
+ const value = Object.getOwnPropertyDescriptor(source, property)?.value;
48
+ if (typeof value === "function") forwarded[property] = value.bind(adapter);
49
+ }
50
+ return forwarded;
51
+ }
52
+ var FragnoPersistenceWriteQueue = class {
53
+ #tail = Promise.resolve();
54
+ enqueue(write) {
55
+ const queuedWrite = this.#tail.then(write);
56
+ this.#tail = queuedWrite;
57
+ return queuedWrite;
58
+ }
59
+ async drain() {
60
+ await this.#tail;
61
+ }
62
+ };
63
+ //#endregion
64
+ export { orderFragnoPersistenceWrites };
65
+
66
+ //# sourceMappingURL=fragno-ordered-persistence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fragno-ordered-persistence.js","names":["#tail"],"sources":["../../src/coordinator/fragno-ordered-persistence.ts"],"sourcesContent":["import type {\n PersistedCollectionPersistence,\n PersistenceAdapter,\n} from \"@tanstack/db-sqlite-persistence-core\";\n\n/**\n * A hydration scope hands TanStack an unscheduled inner adapter whose writes would bypass the\n * queue, and queueing them would wait on writes blocked behind that same scope. Hiding the scope\n * makes TanStack run hydration through the wrapped adapter.\n */\nconst HIDDEN_ADAPTER_PROPERTIES: ReadonlySet<PropertyKey> = new Set([\n \"runInHydrationScope\",\n \"runInRegularScope\",\n \"isHydrationScopeScheduled\",\n]);\n\nexport type OrderedFragnoPersistence = PersistedCollectionPersistence & {\n drain(): Promise<void>;\n};\n\n/** Orders every collection write before later writes to the shared Fragno persistence database. */\nexport function orderFragnoPersistenceWrites(\n persistence: PersistedCollectionPersistence,\n): OrderedFragnoPersistence {\n const queue = new FragnoPersistenceWriteQueue();\n const persistenceCache = new WeakMap<\n PersistedCollectionPersistence,\n PersistedCollectionPersistence\n >();\n const adapterCache = new WeakMap<PersistenceAdapter, PersistenceAdapter>();\n\n const wrapAdapter = (adapter: PersistenceAdapter): PersistenceAdapter => {\n const cached = adapterCache.get(adapter);\n if (cached) {\n return cached;\n }\n\n const wrapped = forwardAdapterCapabilities(adapter);\n wrapped.applyCommittedTx = (target, transaction) =>\n queue.enqueue(() => adapter.applyCommittedTx(target, transaction));\n const { reconcileCommittedTx } = adapter;\n if (reconcileCommittedTx) {\n wrapped.reconcileCommittedTx = (target, transaction, anchor) =>\n queue.enqueue(() => reconcileCommittedTx.call(adapter, target, transaction, anchor));\n }\n\n adapterCache.set(adapter, wrapped);\n return wrapped;\n };\n\n const wrapPersistence = (\n resolvedPersistence: PersistedCollectionPersistence,\n ): PersistedCollectionPersistence => {\n const cached = persistenceCache.get(resolvedPersistence);\n if (cached) {\n return cached;\n }\n\n const wrapped: PersistedCollectionPersistence = {\n adapter: wrapAdapter(resolvedPersistence.adapter),\n };\n persistenceCache.set(resolvedPersistence, wrapped);\n\n if (resolvedPersistence.coordinator) {\n wrapped.coordinator = resolvedPersistence.coordinator;\n }\n if (resolvedPersistence.resolvePersistenceForCollection) {\n wrapped.resolvePersistenceForCollection = (options) =>\n wrapPersistence(resolvedPersistence.resolvePersistenceForCollection!(options));\n }\n if (resolvedPersistence.resolvePersistenceForMode) {\n wrapped.resolvePersistenceForMode = (mode) =>\n wrapPersistence(resolvedPersistence.resolvePersistenceForMode!(mode));\n }\n\n return wrapped;\n };\n\n return Object.assign(wrapPersistence(persistence), {\n drain: () => queue.drain(),\n });\n}\n\n/**\n * Binds every adapter method, including class methods, so coordinators keep the capabilities they\n * require, such as the browser coordinator's `reserveLeadershipTerm`.\n */\nfunction forwardAdapterCapabilities(adapter: PersistenceAdapter): PersistenceAdapter {\n const forwarded: Record<PropertyKey, unknown> = {};\n for (\n let source: object | null = adapter;\n source && source !== Object.prototype;\n source = Object.getPrototypeOf(source) as object | null\n ) {\n for (const property of Reflect.ownKeys(source)) {\n if (\n property === \"constructor\" ||\n HIDDEN_ADAPTER_PROPERTIES.has(property) ||\n Object.hasOwn(forwarded, property)\n ) {\n continue;\n }\n const value: unknown = Object.getOwnPropertyDescriptor(source, property)?.value;\n if (typeof value === \"function\") {\n forwarded[property] = value.bind(adapter);\n }\n }\n }\n return forwarded as unknown as PersistenceAdapter;\n}\n\nclass FragnoPersistenceWriteQueue {\n #tail: Promise<unknown> = Promise.resolve();\n\n enqueue<TResult>(write: () => Promise<TResult>): Promise<TResult> {\n const queuedWrite = this.#tail.then(write);\n // Keep a rejection in the chain so a later shared checkpoint can never pass a failed table\n // write and become the durable resume position.\n this.#tail = queuedWrite;\n return queuedWrite;\n }\n\n async drain(): Promise<void> {\n await this.#tail;\n }\n}\n"],"mappings":";;;;;;AAUA,MAAM,4CAAsD,IAAI,IAAI;CAClE;CACA;CACA;AACF,CAAC;;AAOD,SAAgB,6BACd,aAC0B;CAC1B,MAAM,QAAQ,IAAI,4BAA4B;CAC9C,MAAM,mCAAmB,IAAI,QAG3B;CACF,MAAM,+BAAe,IAAI,QAAgD;CAEzE,MAAM,eAAe,YAAoD;EACvE,MAAM,SAAS,aAAa,IAAI,OAAO;EACvC,IAAI,QACF,OAAO;EAGT,MAAM,UAAU,2BAA2B,OAAO;EAClD,QAAQ,oBAAoB,QAAQ,gBAClC,MAAM,cAAc,QAAQ,iBAAiB,QAAQ,WAAW,CAAC;EACnE,MAAM,EAAE,yBAAyB;EACjC,IAAI,sBACF,QAAQ,wBAAwB,QAAQ,aAAa,WACnD,MAAM,cAAc,qBAAqB,KAAK,SAAS,QAAQ,aAAa,MAAM,CAAC;EAGvF,aAAa,IAAI,SAAS,OAAO;EACjC,OAAO;CACT;CAEA,MAAM,mBACJ,wBACmC;EACnC,MAAM,SAAS,iBAAiB,IAAI,mBAAmB;EACvD,IAAI,QACF,OAAO;EAGT,MAAM,UAA0C,EAC9C,SAAS,YAAY,oBAAoB,OAAO,EAClD;EACA,iBAAiB,IAAI,qBAAqB,OAAO;EAEjD,IAAI,oBAAoB,aACtB,QAAQ,cAAc,oBAAoB;EAE5C,IAAI,oBAAoB,iCACtB,QAAQ,mCAAmC,YACzC,gBAAgB,oBAAoB,gCAAiC,OAAO,CAAC;EAEjF,IAAI,oBAAoB,2BACtB,QAAQ,6BAA6B,SACnC,gBAAgB,oBAAoB,0BAA2B,IAAI,CAAC;EAGxE,OAAO;CACT;CAEA,OAAO,OAAO,OAAO,gBAAgB,WAAW,GAAG,EACjD,aAAa,MAAM,MAAM,EAC3B,CAAC;AACH;;;;;AAMA,SAAS,2BAA2B,SAAiD;CACnF,MAAM,YAA0C,CAAC;CACjD,KACE,IAAI,SAAwB,SAC5B,UAAU,WAAW,OAAO,WAC5B,SAAS,OAAO,eAAe,MAAM,GAErC,KAAK,MAAM,YAAY,QAAQ,QAAQ,MAAM,GAAG;EAC9C,IACE,aAAa,iBACb,0BAA0B,IAAI,QAAQ,KACtC,OAAO,OAAO,WAAW,QAAQ,GAEjC;EAEF,MAAM,QAAiB,OAAO,yBAAyB,QAAQ,QAAQ,CAAC,EAAE;EAC1E,IAAI,OAAO,UAAU,YACnB,UAAU,YAAY,MAAM,KAAK,OAAO;CAE5C;CAEF,OAAO;AACT;AAEA,IAAM,8BAAN,MAAkC;CAChC,QAA0B,QAAQ,QAAQ;CAE1C,QAAiB,OAAiD;EAChE,MAAM,cAAc,KAAKA,MAAM,KAAK,KAAK;EAGzC,KAAKA,QAAQ;EACb,OAAO;CACT;CAEA,MAAM,QAAuB;EAC3B,MAAM,KAAKA;CACb;AACF"}
@@ -0,0 +1,193 @@
1
+ import { FragnoOutboxProtocolError, consumeNdjsonOutboxStream } from "../outbox-stream.js";
2
+ import { decodeFragnoOutboxPayload, fragnoOutboxOperationTarget, projectFragnoOutboxOperations } from "../protocol.js";
3
+ import { FRAGNO_OUTBOX_PAGE_SIZE } from "@fragno-dev/db/outbox";
4
+ //#region src/coordinator/fragno-outbox-synchronizer.ts
5
+ /** Owns shared outbox ordering and delivers target-specific changes to collection adapters. */
6
+ var FragnoOutboxSynchronizer = class {
7
+ #fetcher;
8
+ #checkpointStore;
9
+ #abortController = new AbortController();
10
+ #subscribers = /* @__PURE__ */ new Map();
11
+ #registrationWaiters = /* @__PURE__ */ new Set();
12
+ #sessionPromise;
13
+ #adapterIdentity;
14
+ #ready = false;
15
+ #disposed = false;
16
+ #onCatchUpPage;
17
+ constructor(options) {
18
+ this.#fetcher = options.fetcher;
19
+ this.#adapterIdentity = options.adapterIdentity;
20
+ this.#checkpointStore = options.checkpointStore;
21
+ this.#onCatchUpPage = options.onCatchUpPage;
22
+ }
23
+ register(subscriber) {
24
+ if (this.#disposed) throw new Error("Cannot register a collection after Fragno outbox synchronization disposal.");
25
+ this.#subscribers.set(subscriber.target.key, subscriber);
26
+ this.#resolveRegistrationWaiters();
27
+ return () => {
28
+ if (this.#subscribers.get(subscriber.target.key) === subscriber) this.#subscribers.delete(subscriber.target.key);
29
+ };
30
+ }
31
+ waitUntilRegistered(targets) {
32
+ if (this.#disposed) return Promise.reject(/* @__PURE__ */ new Error("Fragno outbox synchronization has been disposed."));
33
+ if (this.#hasEveryTarget(targets)) return Promise.resolve();
34
+ return new Promise((resolve, reject) => {
35
+ this.#registrationWaiters.add({
36
+ targets,
37
+ resolve,
38
+ reject
39
+ });
40
+ });
41
+ }
42
+ /** Runs catch-up and live delivery on one response; resolves only on planned rotation. */
43
+ streamSession(options) {
44
+ if (this.#disposed) return Promise.reject(new DOMException("Fragno outbox synchronization was disposed.", "AbortError"));
45
+ this.#sessionPromise ??= this.#runStreamSession(options).finally(() => {
46
+ this.#sessionPromise = void 0;
47
+ });
48
+ return this.#sessionPromise;
49
+ }
50
+ applyChanges(targetKey, delivery) {
51
+ return this.#requireSubscriber(targetKey).apply(delivery);
52
+ }
53
+ truncate(targetKey) {
54
+ return this.#requireSubscriber(targetKey).truncate();
55
+ }
56
+ markReady() {
57
+ for (const subscriber of this.#subscribers.values()) subscriber.markReady();
58
+ }
59
+ dispose() {
60
+ if (this.#disposed) return;
61
+ this.#disposed = true;
62
+ this.#abortController.abort();
63
+ this.#subscribers.clear();
64
+ const error = /* @__PURE__ */ new Error("Fragno outbox synchronization was disposed before registration.");
65
+ for (const waiter of this.#registrationWaiters) waiter.reject(error);
66
+ this.#registrationWaiters.clear();
67
+ }
68
+ async #runStreamSession(options) {
69
+ if (this.#disposed) throw new Error("Cannot catch up a disposed Fragno outbox synchronizer.");
70
+ await Promise.all([...this.#subscribers.values()].map((subscriber) => subscriber.prepareCatchUp?.()).filter((preparation) => preparation !== void 0));
71
+ const initialCheckpoint = this.#checkpointStore.getCheckpoint();
72
+ const afterVersionstamp = initialCheckpoint?.versionstamp;
73
+ let caughtUp = false;
74
+ let entries = [];
75
+ let completedBatches = 0;
76
+ const flushCatchUpBatch = async () => {
77
+ const page = entries;
78
+ entries = [];
79
+ await this.#applyAndAdvancePage(page);
80
+ completedBatches += 1;
81
+ this.#onCatchUpPage?.(this.#checkpointStore.getCheckpoint());
82
+ };
83
+ await consumeNdjsonOutboxStream(await this.#fetcher.openOutboxStream({
84
+ checkpoint: initialCheckpoint,
85
+ signal: this.#abortController.signal
86
+ }), {
87
+ signal: this.#abortController.signal,
88
+ afterVersionstamp,
89
+ onFrame: async (frame) => {
90
+ switch (frame.type) {
91
+ case "started":
92
+ if (frame.adapterIdentity !== this.#adapterIdentity) throw new FragnoOutboxProtocolError("Fragno outbox adapter identity changed during synchronization.");
93
+ if (initialCheckpoint && (frame.catchUpTargetVersionstamp === null || frame.catchUpTargetVersionstamp < initialCheckpoint.versionstamp)) throw new FragnoOutboxProtocolError("Fragno outbox source is behind the persisted checkpoint.");
94
+ options.onStarted(frame.catchUpTargetVersionstamp);
95
+ break;
96
+ case "entry":
97
+ if (caughtUp) await this.#applyAndAdvanceEntry(frame.entry);
98
+ else {
99
+ entries.push(frame.entry);
100
+ if (entries.length === FRAGNO_OUTBOX_PAGE_SIZE) await flushCatchUpBatch();
101
+ }
102
+ break;
103
+ case "caught-up":
104
+ if (entries.length > 0 || completedBatches === 0) await flushCatchUpBatch();
105
+ caughtUp = true;
106
+ if (!this.#ready) {
107
+ this.markReady();
108
+ this.#ready = true;
109
+ }
110
+ options.onCaughtUp();
111
+ break;
112
+ case "rotate":
113
+ if (!caughtUp && entries.length > 0) await flushCatchUpBatch();
114
+ break;
115
+ case "heartbeat": break;
116
+ }
117
+ }
118
+ });
119
+ }
120
+ async #applyAndAdvancePage(entries) {
121
+ const lastEntry = entries.at(-1);
122
+ if (!lastEntry) return;
123
+ const deliveriesByTarget = /* @__PURE__ */ new Map();
124
+ for (const entry of entries) this.#planEntryDeliveries(entry, deliveriesByTarget);
125
+ await Promise.all([...deliveriesByTarget].map(([targetKey, deliveries]) => Promise.resolve(this.#requireSubscriber(targetKey).applyBatch(deliveries))));
126
+ this.#checkpointStore.setCheckpoint({
127
+ versionstamp: lastEntry.versionstamp,
128
+ uowId: lastEntry.uowId
129
+ });
130
+ }
131
+ async #applyAndAdvanceEntry(entry) {
132
+ const deliveriesByTarget = /* @__PURE__ */ new Map();
133
+ this.#planEntryDeliveries(entry, deliveriesByTarget);
134
+ await Promise.all([...deliveriesByTarget].map(([targetKey, deliveries]) => Promise.resolve(this.#requireSubscriber(targetKey).apply(deliveries[0]))));
135
+ this.#checkpointStore.setCheckpoint({
136
+ versionstamp: entry.versionstamp,
137
+ uowId: entry.uowId
138
+ });
139
+ }
140
+ #planEntryDeliveries(entry, deliveriesByTarget) {
141
+ const payload = decodeFragnoOutboxPayload(entry.payload);
142
+ const operationsByTarget = /* @__PURE__ */ new Map();
143
+ for (const operation of payload.operations) {
144
+ const target = fragnoOutboxOperationTarget(operation);
145
+ const targetKey = fragnoOutboxTargetKey(target.namespace, target.table);
146
+ if (!this.#subscribers.has(targetKey)) continue;
147
+ const targetOperations = operationsByTarget.get(targetKey);
148
+ if (targetOperations) targetOperations.push(operation);
149
+ else operationsByTarget.set(targetKey, [operation]);
150
+ }
151
+ for (const [targetKey, operations] of operationsByTarget) {
152
+ const subscriber = this.#requireSubscriber(targetKey);
153
+ const changes = projectFragnoOutboxOperations(entry, operations, {
154
+ schema: subscriber.target.schema,
155
+ table: subscriber.target.tableName,
156
+ namespace: subscriber.target.namespace
157
+ });
158
+ const deliveries = deliveriesByTarget.get(targetKey) ?? [];
159
+ deliveries.push({
160
+ checkpoint: {
161
+ versionstamp: entry.versionstamp,
162
+ uowId: entry.uowId
163
+ },
164
+ changes
165
+ });
166
+ deliveriesByTarget.set(targetKey, deliveries);
167
+ }
168
+ }
169
+ #requireSubscriber(targetKey) {
170
+ const subscriber = this.#subscribers.get(targetKey);
171
+ if (!subscriber) throw new Error(`No Fragno collection is registered for outbox target ${targetKey}.`);
172
+ return subscriber;
173
+ }
174
+ #hasEveryTarget(targets) {
175
+ return targets.every((target) => this.#subscribers.has(target));
176
+ }
177
+ #resolveRegistrationWaiters() {
178
+ for (const waiter of this.#registrationWaiters) if (this.#hasEveryTarget(waiter.targets)) {
179
+ this.#registrationWaiters.delete(waiter);
180
+ waiter.resolve();
181
+ }
182
+ }
183
+ };
184
+ function fragnoOutboxTargetKey(namespace, tableName) {
185
+ return `${identifierSegment(namespace)}${identifierSegment(tableName)}`;
186
+ }
187
+ function identifierSegment(value) {
188
+ return `${value.length}:${value}`;
189
+ }
190
+ //#endregion
191
+ export { FragnoOutboxSynchronizer, fragnoOutboxTargetKey };
192
+
193
+ //# sourceMappingURL=fragno-outbox-synchronizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fragno-outbox-synchronizer.js","names":["#fetcher","#checkpointStore","#abortController","#subscribers","#registrationWaiters","#adapterIdentity","#onCatchUpPage","#disposed","#resolveRegistrationWaiters","#hasEveryTarget","#sessionPromise","#runStreamSession","#requireSubscriber","#applyAndAdvancePage","#applyAndAdvanceEntry","#ready","#planEntryDeliveries"],"sources":["../../src/coordinator/fragno-outbox-synchronizer.ts"],"sourcesContent":["import { FRAGNO_OUTBOX_PAGE_SIZE } from \"@fragno-dev/db/outbox\";\nimport type { AnySchema } from \"@fragno-dev/db/schema\";\n\nimport type { OutboxOperation } from \"@fragno-dev/db\";\n\nimport type { SyncAppliedReceipt } from \"@tanstack/db\";\n\nimport type { FragnoOutboxCheckpoint } from \"../checkpoint\";\nimport { consumeNdjsonOutboxStream, FragnoOutboxProtocolError } from \"../outbox-stream\";\nimport {\n decodeFragnoOutboxPayload,\n fragnoOutboxOperationTarget,\n projectFragnoOutboxOperations,\n type FragnoCollectionChange,\n type FragnoOutboxEntry,\n} from \"../protocol\";\ntype FragnoSynchronizedRow = Record<string, unknown>;\n\ntype FragnoOutboxFetcher = {\n openOutboxStream(options: {\n checkpoint: FragnoOutboxCheckpoint | undefined;\n signal?: AbortSignal;\n }): Promise<ReadableStream<Uint8Array>>;\n};\n\ntype FragnoOutboxCheckpointStore = {\n getCheckpoint(): FragnoOutboxCheckpoint | undefined;\n setCheckpoint(checkpoint: FragnoOutboxCheckpoint): void;\n};\n\nexport type FragnoOutboxTarget = {\n key: string;\n namespace: string;\n schema: AnySchema;\n tableName: string;\n};\n\nexport type FragnoOutboxDelivery = {\n checkpoint: FragnoOutboxCheckpoint;\n changes: FragnoCollectionChange<FragnoSynchronizedRow>[];\n};\n\nexport type FragnoOutboxSubscriber = {\n target: FragnoOutboxTarget;\n prepareCatchUp?(): Promise<void>;\n apply(delivery: FragnoOutboxDelivery): SyncAppliedReceipt;\n applyBatch(deliveries: readonly FragnoOutboxDelivery[]): SyncAppliedReceipt;\n truncate(): SyncAppliedReceipt;\n markReady(): void;\n};\n\ntype RegistrationWaiter = {\n targets: readonly string[];\n resolve(): void;\n reject(error: Error): void;\n};\n\n/** Owns shared outbox ordering and delivers target-specific changes to collection adapters. */\nexport class FragnoOutboxSynchronizer {\n readonly #fetcher: FragnoOutboxFetcher;\n readonly #checkpointStore: FragnoOutboxCheckpointStore;\n readonly #abortController = new AbortController();\n readonly #subscribers = new Map<string, FragnoOutboxSubscriber>();\n readonly #registrationWaiters = new Set<RegistrationWaiter>();\n\n #sessionPromise: Promise<void> | undefined;\n readonly #adapterIdentity: string;\n #ready = false;\n #disposed = false;\n\n readonly #onCatchUpPage?: (checkpoint: FragnoOutboxCheckpoint | undefined) => void;\n\n constructor(options: {\n fetcher: FragnoOutboxFetcher;\n adapterIdentity: string;\n checkpointStore: FragnoOutboxCheckpointStore;\n onCatchUpPage?: (checkpoint: FragnoOutboxCheckpoint | undefined) => void;\n }) {\n this.#fetcher = options.fetcher;\n this.#adapterIdentity = options.adapterIdentity;\n this.#checkpointStore = options.checkpointStore;\n this.#onCatchUpPage = options.onCatchUpPage;\n }\n\n register(subscriber: FragnoOutboxSubscriber): () => void {\n if (this.#disposed) {\n throw new Error(\"Cannot register a collection after Fragno outbox synchronization disposal.\");\n }\n\n this.#subscribers.set(subscriber.target.key, subscriber);\n this.#resolveRegistrationWaiters();\n\n return () => {\n if (this.#subscribers.get(subscriber.target.key) === subscriber) {\n this.#subscribers.delete(subscriber.target.key);\n }\n };\n }\n\n waitUntilRegistered(targets: readonly string[]): Promise<void> {\n if (this.#disposed) {\n return Promise.reject(new Error(\"Fragno outbox synchronization has been disposed.\"));\n }\n if (this.#hasEveryTarget(targets)) {\n return Promise.resolve();\n }\n\n return new Promise<void>((resolve, reject) => {\n this.#registrationWaiters.add({ targets, resolve, reject });\n });\n }\n\n /** Runs catch-up and live delivery on one response; resolves only on planned rotation. */\n streamSession(options: {\n onStarted(targetVersionstamp: string | null): void;\n onCaughtUp(): void;\n }): Promise<void> {\n if (this.#disposed) {\n return Promise.reject(\n new DOMException(\"Fragno outbox synchronization was disposed.\", \"AbortError\"),\n );\n }\n this.#sessionPromise ??= this.#runStreamSession(options).finally(() => {\n this.#sessionPromise = undefined;\n });\n return this.#sessionPromise;\n }\n\n applyChanges(targetKey: string, delivery: FragnoOutboxDelivery): SyncAppliedReceipt {\n return this.#requireSubscriber(targetKey).apply(delivery);\n }\n\n truncate(targetKey: string): SyncAppliedReceipt {\n return this.#requireSubscriber(targetKey).truncate();\n }\n\n markReady(): void {\n for (const subscriber of this.#subscribers.values()) {\n subscriber.markReady();\n }\n }\n\n dispose(): void {\n if (this.#disposed) {\n return;\n }\n\n this.#disposed = true;\n this.#abortController.abort();\n this.#subscribers.clear();\n const error = new Error(\"Fragno outbox synchronization was disposed before registration.\");\n for (const waiter of this.#registrationWaiters) {\n waiter.reject(error);\n }\n this.#registrationWaiters.clear();\n }\n\n async #runStreamSession(options: {\n onStarted(targetVersionstamp: string | null): void;\n onCaughtUp(): void;\n }): Promise<void> {\n if (this.#disposed) {\n throw new Error(\"Cannot catch up a disposed Fragno outbox synchronizer.\");\n }\n\n await Promise.all(\n [...this.#subscribers.values()]\n .map((subscriber) => subscriber.prepareCatchUp?.())\n .filter((preparation): preparation is Promise<void> => preparation !== undefined),\n );\n\n const initialCheckpoint = this.#checkpointStore.getCheckpoint();\n const afterVersionstamp = initialCheckpoint?.versionstamp;\n let caughtUp = false;\n let entries: FragnoOutboxEntry[] = [];\n let completedBatches = 0;\n const flushCatchUpBatch = async () => {\n const page = entries;\n entries = [];\n await this.#applyAndAdvancePage(page);\n completedBatches += 1;\n this.#onCatchUpPage?.(this.#checkpointStore.getCheckpoint());\n };\n const body = await this.#fetcher.openOutboxStream({\n checkpoint: initialCheckpoint,\n signal: this.#abortController.signal,\n });\n await consumeNdjsonOutboxStream(body, {\n signal: this.#abortController.signal,\n afterVersionstamp,\n onFrame: async (frame) => {\n switch (frame.type) {\n case \"started\":\n if (frame.adapterIdentity !== this.#adapterIdentity) {\n throw new FragnoOutboxProtocolError(\n \"Fragno outbox adapter identity changed during synchronization.\",\n );\n }\n if (\n initialCheckpoint &&\n (frame.catchUpTargetVersionstamp === null ||\n frame.catchUpTargetVersionstamp < initialCheckpoint.versionstamp)\n ) {\n throw new FragnoOutboxProtocolError(\n \"Fragno outbox source is behind the persisted checkpoint.\",\n );\n }\n options.onStarted(frame.catchUpTargetVersionstamp);\n break;\n case \"entry\":\n if (caughtUp) {\n await this.#applyAndAdvanceEntry(frame.entry);\n } else {\n entries.push(frame.entry);\n if (entries.length === FRAGNO_OUTBOX_PAGE_SIZE) {\n await flushCatchUpBatch();\n }\n }\n break;\n case \"caught-up\":\n if (entries.length > 0 || completedBatches === 0) {\n await flushCatchUpBatch();\n }\n caughtUp = true;\n if (!this.#ready) {\n this.markReady();\n this.#ready = true;\n }\n options.onCaughtUp();\n break;\n case \"rotate\":\n // A planned boundary is safe to commit; an interrupted partial batch is discarded.\n if (!caughtUp && entries.length > 0) {\n await flushCatchUpBatch();\n }\n break;\n case \"heartbeat\":\n break;\n }\n },\n });\n }\n\n // A persisted TanStack commit publishes asynchronously, and its receipt resolves once the write is\n // durable. Awaiting table receipts keeps \"caught up\" meaning \"visible\" and surfaces failed\n // commits. The shared checkpoint is not awaited so the coordinator can go live before it is\n // durable; a failed checkpoint write still fails every later table write in the ordered queue.\n async #applyAndAdvancePage(entries: readonly FragnoOutboxEntry[]): Promise<void> {\n const lastEntry = entries.at(-1);\n if (!lastEntry) {\n return;\n }\n\n const deliveriesByTarget = new Map<string, FragnoOutboxDelivery[]>();\n for (const entry of entries) {\n this.#planEntryDeliveries(entry, deliveriesByTarget);\n }\n\n await Promise.all(\n [...deliveriesByTarget].map(([targetKey, deliveries]) =>\n Promise.resolve(this.#requireSubscriber(targetKey).applyBatch(deliveries)),\n ),\n );\n this.#checkpointStore.setCheckpoint({\n versionstamp: lastEntry.versionstamp,\n uowId: lastEntry.uowId,\n });\n }\n\n async #applyAndAdvanceEntry(entry: FragnoOutboxEntry): Promise<void> {\n const deliveriesByTarget = new Map<string, FragnoOutboxDelivery[]>();\n this.#planEntryDeliveries(entry, deliveriesByTarget);\n\n await Promise.all(\n [...deliveriesByTarget].map(([targetKey, deliveries]) =>\n Promise.resolve(this.#requireSubscriber(targetKey).apply(deliveries[0])),\n ),\n );\n // Collection checkpoints make partial cross-collection commits safe to retry.\n this.#checkpointStore.setCheckpoint({ versionstamp: entry.versionstamp, uowId: entry.uowId });\n }\n\n #planEntryDeliveries(\n entry: FragnoOutboxEntry,\n deliveriesByTarget: Map<string, FragnoOutboxDelivery[]>,\n ): void {\n const payload = decodeFragnoOutboxPayload(entry.payload);\n const operationsByTarget = new Map<string, OutboxOperation[]>();\n\n for (const operation of payload.operations) {\n const target = fragnoOutboxOperationTarget(operation);\n const targetKey = fragnoOutboxTargetKey(target.namespace, target.table);\n if (!this.#subscribers.has(targetKey)) {\n continue;\n }\n\n const targetOperations = operationsByTarget.get(targetKey);\n if (targetOperations) {\n targetOperations.push(operation);\n } else {\n operationsByTarget.set(targetKey, [operation]);\n }\n }\n\n for (const [targetKey, operations] of operationsByTarget) {\n const subscriber = this.#requireSubscriber(targetKey);\n const changes = projectFragnoOutboxOperations(entry, operations, {\n schema: subscriber.target.schema,\n table: subscriber.target.tableName,\n namespace: subscriber.target.namespace,\n });\n const deliveries = deliveriesByTarget.get(targetKey) ?? [];\n deliveries.push({\n checkpoint: { versionstamp: entry.versionstamp, uowId: entry.uowId },\n changes: changes as FragnoCollectionChange<FragnoSynchronizedRow>[],\n });\n deliveriesByTarget.set(targetKey, deliveries);\n }\n }\n\n #requireSubscriber(targetKey: string): FragnoOutboxSubscriber {\n const subscriber = this.#subscribers.get(targetKey);\n if (!subscriber) {\n throw new Error(`No Fragno collection is registered for outbox target ${targetKey}.`);\n }\n return subscriber;\n }\n\n #hasEveryTarget(targets: readonly string[]): boolean {\n return targets.every((target) => this.#subscribers.has(target));\n }\n\n #resolveRegistrationWaiters(): void {\n for (const waiter of this.#registrationWaiters) {\n if (this.#hasEveryTarget(waiter.targets)) {\n this.#registrationWaiters.delete(waiter);\n waiter.resolve();\n }\n }\n }\n}\n\nexport function fragnoOutboxTargetKey(namespace: string, tableName: string): string {\n return `${identifierSegment(namespace)}${identifierSegment(tableName)}`;\n}\n\nfunction identifierSegment(value: string): string {\n return `${value.length}:${value}`;\n}\n"],"mappings":"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"}