@abloatai/ablo 0.24.0 → 0.26.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 (246) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +33 -0
  3. package/README.md +45 -36
  4. package/dist/BaseSyncedStore.d.ts +72 -195
  5. package/dist/BaseSyncedStore.js +240 -625
  6. package/dist/Database.d.ts +8 -9
  7. package/dist/Database.js +9 -14
  8. package/dist/LazyReferenceCollection.js +5 -0
  9. package/dist/Model.js +6 -8
  10. package/dist/ModelRegistry.d.ts +25 -6
  11. package/dist/ModelRegistry.js +12 -6
  12. package/dist/NetworkMonitor.js +1 -1
  13. package/dist/ObjectPool.js +9 -3
  14. package/dist/SyncClient.d.ts +15 -5
  15. package/dist/SyncClient.js +152 -66
  16. package/dist/SyncEngineContext.d.ts +1 -3
  17. package/dist/SyncEngineContext.js +1 -2
  18. package/dist/agent/Agent.d.ts +21 -8
  19. package/dist/agent/Agent.js +26 -13
  20. package/dist/ai-sdk/coordination-context.js +9 -3
  21. package/dist/ai-sdk/wrap.d.ts +1 -1
  22. package/dist/ai-sdk/wrap.js +1 -1
  23. package/dist/auth/credentialPolicy.d.ts +10 -2
  24. package/dist/auth/credentialSource.d.ts +1 -13
  25. package/dist/auth/credentialSource.js +6 -13
  26. package/dist/auth/index.js +3 -3
  27. package/dist/cli.cjs +583 -287
  28. package/dist/client/Ablo.d.ts +35 -764
  29. package/dist/client/Ablo.js +64 -688
  30. package/dist/client/ApiClient.d.ts +14 -1
  31. package/dist/client/ApiClient.js +63 -9
  32. package/dist/client/auth.d.ts +8 -17
  33. package/dist/client/auth.js +42 -6
  34. package/dist/client/consoleLogger.d.ts +36 -0
  35. package/dist/client/consoleLogger.js +45 -0
  36. package/dist/client/createInternalComponents.js +1 -1
  37. package/dist/client/createModelProxy.js +10 -6
  38. package/dist/client/credentialEndpoint.d.ts +63 -0
  39. package/dist/client/credentialEndpoint.js +87 -0
  40. package/dist/client/hostedEndpoints.d.ts +24 -0
  41. package/dist/client/hostedEndpoints.js +24 -0
  42. package/dist/client/httpClient.d.ts +1 -1
  43. package/dist/client/identity.js +2 -1
  44. package/dist/client/modelRegistration.d.ts +14 -0
  45. package/dist/client/modelRegistration.js +321 -0
  46. package/dist/client/options.d.ts +396 -0
  47. package/dist/client/options.js +10 -0
  48. package/dist/client/resourceTypes.d.ts +344 -0
  49. package/dist/client/resourceTypes.js +13 -0
  50. package/dist/client/schemaConfig.d.ts +56 -0
  51. package/dist/client/schemaConfig.js +188 -0
  52. package/dist/client/sessionMint.d.ts +1 -1
  53. package/dist/client/validateAbloOptions.js +1 -1
  54. package/dist/client/wsMutationExecutor.d.ts +33 -0
  55. package/dist/client/wsMutationExecutor.js +77 -0
  56. package/dist/context.js +0 -3
  57. package/dist/coordination/index.d.ts +8 -1
  58. package/dist/coordination/index.js +24 -1
  59. package/dist/coordination/trace.d.ts +0 -1
  60. package/dist/coordination/trace.js +0 -1
  61. package/dist/core/DatabaseManager.d.ts +0 -1
  62. package/dist/core/DatabaseManager.js +26 -24
  63. package/dist/core/QueryProcessor.js +7 -8
  64. package/dist/core/QueryView.d.ts +14 -2
  65. package/dist/core/QueryView.js +4 -5
  66. package/dist/core/StoreManager.d.ts +2 -2
  67. package/dist/core/StoreManager.js +5 -1
  68. package/dist/core/index.d.ts +1 -1
  69. package/dist/core/openIDBWithTimeout.js +15 -11
  70. package/dist/core/query-utils.js +1 -1
  71. package/dist/core/storeContract.d.ts +143 -0
  72. package/dist/core/storeContract.js +16 -0
  73. package/dist/errorCodes.d.ts +11 -2
  74. package/dist/errorCodes.js +155 -137
  75. package/dist/errors.d.ts +18 -7
  76. package/dist/errors.js +14 -1
  77. package/dist/index.js +5 -0
  78. package/dist/interfaces/index.d.ts +6 -20
  79. package/dist/keys/index.js +18 -9
  80. package/dist/mutators/UndoManager.js +6 -1
  81. package/dist/mutators/defineMutators.d.ts +1 -3
  82. package/dist/policy/types.d.ts +1 -1
  83. package/dist/query/client.d.ts +11 -0
  84. package/dist/query/client.js +81 -52
  85. package/dist/react/AbloProvider.d.ts +6 -10
  86. package/dist/react/AbloProvider.js +22 -15
  87. package/dist/react/context.d.ts +2 -118
  88. package/dist/react/useAblo.js +1 -1
  89. package/dist/react/useErrorListener.js +1 -1
  90. package/dist/react/useMutationFailureListener.js +1 -1
  91. package/dist/react/useMutators.js +7 -1
  92. package/dist/react/useReactive.js +0 -1
  93. package/dist/react/useUndoScope.js +2 -2
  94. package/dist/schema/ddlLock.d.ts +39 -0
  95. package/dist/schema/ddlLock.js +51 -0
  96. package/dist/schema/field.js +1 -1
  97. package/dist/schema/index.d.ts +4 -3
  98. package/dist/schema/index.js +15 -5
  99. package/dist/schema/model.d.ts +3 -3
  100. package/dist/schema/model.js +2 -2
  101. package/dist/schema/openapi.js +5 -1
  102. package/dist/schema/queries.d.ts +1 -1
  103. package/dist/schema/queries.js +1 -0
  104. package/dist/schema/residency.d.ts +35 -0
  105. package/dist/schema/residency.js +27 -0
  106. package/dist/schema/roles.d.ts +25 -0
  107. package/dist/schema/roles.js +37 -0
  108. package/dist/schema/schema.d.ts +3 -3
  109. package/dist/schema/schema.js +11 -7
  110. package/dist/schema/serialize.d.ts +8 -8
  111. package/dist/schema/sync-delta-row.d.ts +2 -2
  112. package/dist/schema/sync-delta-row.js +2 -2
  113. package/dist/server/storage-mode.d.ts +8 -1
  114. package/dist/server/storage-mode.js +14 -4
  115. package/dist/source/adapter.d.ts +1 -1
  116. package/dist/source/adapters/drizzle.js +4 -3
  117. package/dist/source/adapters/kysely.js +4 -3
  118. package/dist/source/adapters/memory.js +1 -1
  119. package/dist/source/adapters/prisma.js +5 -4
  120. package/dist/source/conformance.js +10 -8
  121. package/dist/source/connector.js +4 -2
  122. package/dist/source/factory.d.ts +106 -0
  123. package/dist/source/factory.js +273 -0
  124. package/dist/source/index.d.ts +11 -461
  125. package/dist/source/index.js +14 -416
  126. package/dist/source/next.d.ts +1 -1
  127. package/dist/source/next.js +1 -1
  128. package/dist/source/pushQueue.d.ts +1 -1
  129. package/dist/source/pushQueue.js +6 -3
  130. package/dist/source/signing.d.ts +59 -0
  131. package/dist/source/signing.js +142 -0
  132. package/dist/source/types.d.ts +334 -0
  133. package/dist/source/types.js +44 -0
  134. package/dist/stores/ObjectStore.d.ts +1 -1
  135. package/dist/stores/ObjectStore.js +23 -23
  136. package/dist/stores/SyncActionStore.d.ts +1 -1
  137. package/dist/stores/SyncActionStore.js +70 -35
  138. package/dist/surface.d.ts +1 -1
  139. package/dist/surface.js +1 -0
  140. package/dist/sync/AreaOfInterestManager.d.ts +2 -2
  141. package/dist/sync/AreaOfInterestManager.js +1 -1
  142. package/dist/sync/BootstrapHelper.d.ts +1 -3
  143. package/dist/sync/BootstrapHelper.js +29 -8
  144. package/dist/sync/ConnectionManager.d.ts +8 -0
  145. package/dist/sync/ConnectionManager.js +19 -4
  146. package/dist/sync/HydrationCoordinator.d.ts +12 -0
  147. package/dist/sync/HydrationCoordinator.js +25 -5
  148. package/dist/sync/NetworkProbe.d.ts +3 -2
  149. package/dist/sync/NetworkProbe.js +7 -6
  150. package/dist/sync/SyncWebSocket.d.ts +71 -120
  151. package/dist/sync/SyncWebSocket.js +324 -630
  152. package/dist/sync/awaitClaimGrant.js +27 -19
  153. package/dist/sync/bootstrapApply.d.ts +60 -0
  154. package/dist/sync/bootstrapApply.js +65 -0
  155. package/dist/sync/commitFrames.d.ts +43 -0
  156. package/dist/sync/commitFrames.js +94 -0
  157. package/dist/sync/createClaimStream.js +3 -2
  158. package/dist/sync/createSnapshot.d.ts +1 -3
  159. package/dist/sync/credentialLifecycle.d.ts +175 -0
  160. package/dist/sync/credentialLifecycle.js +329 -0
  161. package/dist/sync/deltaPipeline.d.ts +111 -0
  162. package/dist/sync/deltaPipeline.js +262 -0
  163. package/dist/sync/groupChange.d.ts +111 -0
  164. package/dist/sync/groupChange.js +246 -0
  165. package/dist/sync/heartbeat.d.ts +62 -0
  166. package/dist/sync/heartbeat.js +91 -0
  167. package/dist/sync/participants.d.ts +8 -8
  168. package/dist/sync/syncCursor.d.ts +44 -0
  169. package/dist/sync/syncCursor.js +59 -0
  170. package/dist/sync/syncPlan.d.ts +62 -0
  171. package/dist/sync/syncPlan.js +51 -0
  172. package/dist/sync/wsFrameHandlers.d.ts +117 -0
  173. package/dist/sync/wsFrameHandlers.js +378 -0
  174. package/dist/testing/fixtures/bootstrap.d.ts +9 -11
  175. package/dist/testing/fixtures/deltas.d.ts +1 -1
  176. package/dist/testing/fixtures/models.js +1 -0
  177. package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
  178. package/dist/testing/helpers/sync-engine-harness.js +2 -1
  179. package/dist/testing/index.d.ts +1 -1
  180. package/dist/testing/index.js +1 -1
  181. package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
  182. package/dist/testing/mocks/MockSyncContext.js +1 -32
  183. package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
  184. package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
  185. package/dist/transactions/TransactionQueue.d.ts +20 -56
  186. package/dist/transactions/TransactionQueue.js +101 -487
  187. package/dist/transactions/TransactionStore.d.ts +18 -0
  188. package/dist/transactions/TransactionStore.js +51 -0
  189. package/dist/transactions/coalesceRules.d.ts +34 -0
  190. package/dist/transactions/coalesceRules.js +117 -0
  191. package/dist/transactions/commitPayload.d.ts +134 -0
  192. package/dist/transactions/commitPayload.js +152 -0
  193. package/dist/transactions/deltaConfirmation.d.ts +60 -0
  194. package/dist/transactions/deltaConfirmation.js +223 -0
  195. package/dist/transactions/optimistic.d.ts +24 -0
  196. package/dist/transactions/optimistic.js +45 -0
  197. package/dist/transactions/persistedReplay.d.ts +93 -0
  198. package/dist/transactions/persistedReplay.js +105 -0
  199. package/dist/types/index.d.ts +3 -3
  200. package/dist/types/index.js +0 -5
  201. package/dist/types/modelData.d.ts +12 -0
  202. package/dist/types/modelData.js +11 -0
  203. package/dist/types/participant.d.ts +21 -0
  204. package/dist/types/participant.js +12 -0
  205. package/dist/types/streams.d.ts +8 -14
  206. package/dist/utils/mobx-setup.d.ts +1 -1
  207. package/dist/utils/mobx-setup.js +2 -2
  208. package/dist/wire/errorEnvelope.d.ts +11 -2
  209. package/dist/wire/errorEnvelope.js +14 -2
  210. package/dist/wire/frames.d.ts +90 -0
  211. package/dist/wire/frames.js +55 -1
  212. package/dist/wire/index.d.ts +4 -1
  213. package/dist/wire/index.js +20 -1
  214. package/dist/wire/protocol.d.ts +45 -0
  215. package/dist/wire/protocol.js +45 -0
  216. package/dist/wire/protocolVersion.d.ts +56 -0
  217. package/dist/wire/protocolVersion.js +63 -0
  218. package/docs/api-keys.md +4 -3
  219. package/docs/data-sources.md +12 -5
  220. package/docs/examples/existing-python-backend.md +3 -3
  221. package/docs/identity.md +4 -4
  222. package/docs/integration-guide.md +1 -1
  223. package/docs/react.md +1 -1
  224. package/docs/sessions.md +5 -7
  225. package/llms.txt +4 -2
  226. package/package.json +15 -13
  227. package/dist/client/index.d.ts +0 -36
  228. package/dist/client/index.js +0 -33
  229. package/dist/config/index.d.ts +0 -10
  230. package/dist/config/index.js +0 -12
  231. package/dist/interfaces/headless.d.ts +0 -95
  232. package/dist/interfaces/headless.js +0 -41
  233. package/dist/query/index.d.ts +0 -6
  234. package/dist/query/index.js +0 -5
  235. package/dist/realtime/index.d.ts +0 -10
  236. package/dist/realtime/index.js +0 -9
  237. package/dist/schema/plane.d.ts +0 -23
  238. package/dist/schema/plane.js +0 -19
  239. package/dist/server/next.d.ts +0 -51
  240. package/dist/server/next.js +0 -47
  241. package/dist/sync/OfflineFlush.d.ts +0 -9
  242. package/dist/sync/OfflineFlush.js +0 -22
  243. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  244. package/dist/sync/OfflineTransactionStore.js +0 -263
  245. package/dist/transactions/index.d.ts +0 -16
  246. package/dist/transactions/index.js +0 -7
@@ -0,0 +1,18 @@
1
+ /**
2
+ * In-memory transaction index — id map plus a by-status secondary index so
3
+ * the queue's hot paths (`getByStatus('pending')` on every batch/coalesce
4
+ * decision) stay O(status bucket) instead of O(all transactions). Lifted out
5
+ * of `TransactionQueue.ts` as a stateful leaf with no queue coupling.
6
+ */
7
+ import type { Transaction } from './commitPayload.js';
8
+ export declare class TransactionStore {
9
+ private transactions;
10
+ private byStatus;
11
+ add(transaction: Transaction): void;
12
+ get(id: string): Transaction | undefined;
13
+ updateStatus(id: string, newStatus: Transaction['status']): void;
14
+ getByStatus(status: Transaction['status']): Transaction[];
15
+ remove(id: string): void;
16
+ clear(): void;
17
+ getAll(): Transaction[];
18
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * In-memory transaction index — id map plus a by-status secondary index so
3
+ * the queue's hot paths (`getByStatus('pending')` on every batch/coalesce
4
+ * decision) stay O(status bucket) instead of O(all transactions). Lifted out
5
+ * of `TransactionQueue.ts` as a stateful leaf with no queue coupling.
6
+ */
7
+ export class TransactionStore {
8
+ transactions = new Map();
9
+ byStatus = new Map();
10
+ add(transaction) {
11
+ this.transactions.set(transaction.id, transaction);
12
+ if (!this.byStatus.has(transaction.status)) {
13
+ this.byStatus.set(transaction.status, new Set());
14
+ }
15
+ this.byStatus.get(transaction.status).add(transaction.id);
16
+ }
17
+ get(id) {
18
+ return this.transactions.get(id);
19
+ }
20
+ updateStatus(id, newStatus) {
21
+ const tx = this.transactions.get(id);
22
+ if (!tx)
23
+ return;
24
+ this.byStatus.get(tx.status)?.delete(id);
25
+ tx.status = newStatus;
26
+ if (!this.byStatus.has(newStatus)) {
27
+ this.byStatus.set(newStatus, new Set());
28
+ }
29
+ this.byStatus.get(newStatus).add(id);
30
+ }
31
+ getByStatus(status) {
32
+ const ids = this.byStatus.get(status) || new Set();
33
+ return Array.from(ids)
34
+ .map((id) => this.transactions.get(id))
35
+ .filter(Boolean);
36
+ }
37
+ remove(id) {
38
+ const tx = this.transactions.get(id);
39
+ if (!tx)
40
+ return;
41
+ this.transactions.delete(id);
42
+ this.byStatus.get(tx.status)?.delete(id);
43
+ }
44
+ clear() {
45
+ this.transactions.clear();
46
+ this.byStatus.clear();
47
+ }
48
+ getAll() {
49
+ return Array.from(this.transactions.values());
50
+ }
51
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Coalescing rules — the queue's same-entity causality guards, lifted out of
3
+ * `TransactionQueue.ts` as a leaf over a minimal store-shaped interface:
4
+ *
5
+ * • create-then-delete cancellation (`takeUnsentCreateForModel`): a delete
6
+ * of a row whose create never left the client cancels both locally;
7
+ * • the create barrier (`findCreateBarrierForDelete`) + deferred-delete
8
+ * parking (`deferDeleteUntilCreateSettles` / `release…`): once a create
9
+ * HAS been sent, a delete must wait for it to settle or the server could
10
+ * see DELETE-before-CREATE;
11
+ * • update-payload merging (`mergeUpdateData`): collapses rapid same-entity
12
+ * updates into one wire op.
13
+ *
14
+ * The queue's `enqueue`/`delete` verbs stay in the host (they orchestrate
15
+ * optimistic state + events); the RULES live here.
16
+ */
17
+ import type { MutationInput, Transaction } from './commitPayload.js';
18
+ /** The slice of `TransactionStore` the coalescing rules read. */
19
+ export interface TransactionStoreLike {
20
+ get(id: string): Transaction | undefined;
21
+ getByStatus(status: Transaction['status']): Transaction[];
22
+ }
23
+ export declare const entityKey: (modelName: string, modelId: string) => string;
24
+ /**
25
+ * Find-and-remove a create for (model, id) that has never been sent —
26
+ * checking the microtask staging area, the execution queue, and the store's
27
+ * pending bucket, in that order. Splices the winner out of whichever queue
28
+ * held it so the caller can cancel it instead of shipping create+delete.
29
+ */
30
+ export declare function takeUnsentCreateForModel(staged: Transaction[], queued: Transaction[], store: Pick<TransactionStoreLike, 'getByStatus'>, modelName: string, modelId: string): Transaction | undefined;
31
+ export declare function findCreateBarrierForDelete(store: Pick<TransactionStoreLike, 'getByStatus'>, modelName: string, modelId: string): Transaction | undefined;
32
+ export declare function deferDeleteUntilCreateSettles(deferredDeletesByCreate: Map<string, Transaction[]>, createTransaction: Transaction, deleteTransaction: Transaction): void;
33
+ export declare function releaseDeferredDeletesForCreate(deferredDeletesByCreate: Map<string, Transaction[]>, store: Pick<TransactionStoreLike, 'get'>, enqueue: (transaction: Transaction) => void, createTransaction: Transaction): void;
34
+ export declare function mergeUpdateData(left: MutationInput | undefined, right: MutationInput | undefined, _modelName?: string): MutationInput;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Coalescing rules — the queue's same-entity causality guards, lifted out of
3
+ * `TransactionQueue.ts` as a leaf over a minimal store-shaped interface:
4
+ *
5
+ * • create-then-delete cancellation (`takeUnsentCreateForModel`): a delete
6
+ * of a row whose create never left the client cancels both locally;
7
+ * • the create barrier (`findCreateBarrierForDelete`) + deferred-delete
8
+ * parking (`deferDeleteUntilCreateSettles` / `release…`): once a create
9
+ * HAS been sent, a delete must wait for it to settle or the server could
10
+ * see DELETE-before-CREATE;
11
+ * • update-payload merging (`mergeUpdateData`): collapses rapid same-entity
12
+ * updates into one wire op.
13
+ *
14
+ * The queue's `enqueue`/`delete` verbs stay in the host (they orchestrate
15
+ * optimistic state + events); the RULES live here.
16
+ */
17
+ export const entityKey = (modelName, modelId) => `${modelName}:${modelId}`;
18
+ const isTransactionForModel = (transaction, modelName, modelId) => transaction.modelName === modelName && transaction.modelId === modelId;
19
+ /**
20
+ * Find-and-remove a create for (model, id) that has never been sent —
21
+ * checking the microtask staging area, the execution queue, and the store's
22
+ * pending bucket, in that order. Splices the winner out of whichever queue
23
+ * held it so the caller can cancel it instead of shipping create+delete.
24
+ */
25
+ export function takeUnsentCreateForModel(staged, queued, store, modelName, modelId) {
26
+ const isUnsentCreate = (tx) => tx.type === 'create' &&
27
+ tx.status === 'pending' &&
28
+ tx.attempts === 0 &&
29
+ isTransactionForModel(tx, modelName, modelId);
30
+ const stagedIndex = staged.findIndex(isUnsentCreate);
31
+ if (stagedIndex >= 0) {
32
+ return staged.splice(stagedIndex, 1)[0];
33
+ }
34
+ const queuedIndex = queued.findIndex(isUnsentCreate);
35
+ if (queuedIndex >= 0) {
36
+ return queued.splice(queuedIndex, 1)[0];
37
+ }
38
+ return store.getByStatus('pending').find(isUnsentCreate);
39
+ }
40
+ export function findCreateBarrierForDelete(store, modelName, modelId) {
41
+ const liveCreates = [
42
+ ...store.getByStatus('pending'),
43
+ ...store.getByStatus('executing'),
44
+ ...store.getByStatus('awaiting_delta'),
45
+ ].filter((tx) => tx.type === 'create' &&
46
+ isTransactionForModel(tx, modelName, modelId) &&
47
+ // A never-attempted pending create can be cancelled instead. Once the
48
+ // create has been sent, even a retry-pending state is a causal barrier:
49
+ // the server may already have applied it and only the response was lost.
50
+ (tx.status !== 'pending' || tx.attempts > 0));
51
+ return liveCreates.sort((a, b) => b.createdAt - a.createdAt)[0];
52
+ }
53
+ export function deferDeleteUntilCreateSettles(deferredDeletesByCreate, createTransaction, deleteTransaction) {
54
+ const key = entityKey(createTransaction.modelName, createTransaction.modelId);
55
+ const deferred = deferredDeletesByCreate.get(key) ?? [];
56
+ deferred.push(deleteTransaction);
57
+ deferredDeletesByCreate.set(key, deferred);
58
+ }
59
+ export function releaseDeferredDeletesForCreate(deferredDeletesByCreate, store, enqueue, createTransaction) {
60
+ const key = entityKey(createTransaction.modelName, createTransaction.modelId);
61
+ const deferred = deferredDeletesByCreate.get(key);
62
+ if (!deferred || deferred.length === 0)
63
+ return;
64
+ deferredDeletesByCreate.delete(key);
65
+ for (const deleteTransaction of deferred) {
66
+ if (store.get(deleteTransaction.id)?.status !== 'pending')
67
+ continue;
68
+ enqueue(deleteTransaction);
69
+ }
70
+ }
71
+ // Merge two GraphQL update payloads with special handling for metadata fields
72
+ export function mergeUpdateData(left, right, _modelName) {
73
+ const out = { ...(left || {}) };
74
+ const src = right || {};
75
+ for (const key of Object.keys(src)) {
76
+ // Special case: metadata payloads may be JSON strings; merge objects instead of clobbering
77
+ if (key === 'metadata') {
78
+ const l = out.metadata;
79
+ const r = src.metadata;
80
+ // If both sides undefined/null, continue
81
+ if (l == null && r == null) {
82
+ continue;
83
+ }
84
+ // Normalize to objects
85
+ const toObj = (v) => {
86
+ if (v == null)
87
+ return {};
88
+ if (typeof v === 'string') {
89
+ try {
90
+ return JSON.parse(v);
91
+ }
92
+ catch {
93
+ return {};
94
+ }
95
+ }
96
+ if (typeof v === 'object')
97
+ return v;
98
+ return {};
99
+ };
100
+ const lobj = toObj(l);
101
+ const robj = toObj(r);
102
+ const merged = { ...lobj, ...robj };
103
+ // Re-stringify to match schema input type
104
+ try {
105
+ out.metadata = JSON.stringify(merged);
106
+ }
107
+ catch {
108
+ // Fallback to right-hand side if stringify fails
109
+ out.metadata = typeof r === 'string' ? r : JSON.stringify(robj || {});
110
+ }
111
+ continue;
112
+ }
113
+ // Default: shallow overwrite with right-hand value
114
+ out[key] = src[key];
115
+ }
116
+ return out;
117
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Commit-payload projection — the queue's wire vocabulary, lifted out of
3
+ * `TransactionQueue.ts` as a pure leaf (no queue state, no timers).
4
+ *
5
+ * Owns the `Transaction` record shape plus every helper that turns a local
6
+ * transaction into a wire-safe commit operation: schema-field projection
7
+ * (`projectCommitPayload`), write-option projection (`applyWriteOptions`),
8
+ * FK-aware priority scoring, and the transport-error duck-typing used to
9
+ * classify server rejections. Because nothing here touches the queue's
10
+ * runtime, the HTTP commit path can share this projection too.
11
+ */
12
+ import { MutationOperationType } from '../types/index.js';
13
+ import type { MutationOptions, WriteOptions } from '../interfaces/index.js';
14
+ export interface UserContext {
15
+ userId: string;
16
+ organizationId: string;
17
+ role?: string;
18
+ teamIds?: string[];
19
+ }
20
+ /** Wire-format mutation payload (post-projection). */
21
+ export type MutationInput = Record<string, unknown>;
22
+ /**
23
+ * Project a Model's serialized data onto its schema-declared fields
24
+ * and return a wire-safe commit payload. Two jobs:
25
+ *
26
+ * 1. Drop framework internals (`__class`, `__typename`, `clientId`,
27
+ * `syncStatus`) and anything not declared on the model's schema.
28
+ * 2. JSON.stringify values typed as `field.json()` — TEXT columns
29
+ * storing JSON need explicit stringification; postgres.js won't
30
+ * auto-serialize for non-JSONB columns.
31
+ *
32
+ * For updates (`dropUndefined: true`), `undefined` values are also
33
+ * stripped so they don't translate to `SET column = NULL` on the
34
+ * server side.
35
+ *
36
+ * Fields are read from `ModelRegistry`, populated by
37
+ * `registerModelsFromSchema` at SDK initialization. If the model
38
+ * isn't registered with field metadata (edge case — e.g., tests or
39
+ * manually registered models), projection falls back to identity and
40
+ * the caller gets whatever the Model serialized.
41
+ */
42
+ export declare function projectCommitPayload(modelName: string, source: Record<string, unknown>, opts: {
43
+ dropUndefined: boolean;
44
+ }): MutationInput;
45
+ export interface Transaction {
46
+ id: string;
47
+ type: 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
48
+ modelName: string;
49
+ modelId: string;
50
+ modelKey: string;
51
+ data?: MutationInput;
52
+ previousData?: MutationInput | null;
53
+ context: UserContext;
54
+ status: 'pending' | 'executing' | 'awaiting_delta' | 'completed' | 'failed' | 'rolled_back';
55
+ createdAt: number;
56
+ attempts: number;
57
+ priority: 'normal' | 'high';
58
+ priorityScore: number;
59
+ writeOptions?: WriteOptions;
60
+ batchId?: string;
61
+ /** Completed locally without a server operation; no sync echo will arrive. */
62
+ localOnly?: boolean;
63
+ /** LINEAR PATTERN: syncId threshold - transaction confirms when delta.id >= this value */
64
+ syncIdNeededForCompletion?: number;
65
+ /**
66
+ * Resolves when the server has confirmed this transaction (delta arrived
67
+ * or HTTP ack). Rejects with the originating error if the transaction is
68
+ * permanently rolled back. Name matches the queue's existing `'confirmed'`
69
+ * status vocabulary (`commits.create({wait:'confirmed'})`,
70
+ * `waitForConfirmation`) — gives call sites a single `await` point for
71
+ * "did my write land?", so failures surface at the source instead of
72
+ * leaking via silent pool rollback. The rejection error is the same
73
+ * `AbloError` recorded on the queue's `transaction:failed` event.
74
+ */
75
+ confirmation?: Promise<void>;
76
+ }
77
+ export declare const normalizeModelKey: (modelName: string) => string;
78
+ export declare const stripModelSuffix: (modelName: string) => string;
79
+ /**
80
+ * FK-ordered create priority.
81
+ *
82
+ * Reads `config.modelCreatePriority` out of the runtime SyncEngineContext —
83
+ * this map is populated once at `createSyncEngine(...)` time by walking the
84
+ * schema's `belongsTo` graph (see `computeFKDepthPriority` in
85
+ * `client/createSyncEngine.ts`). The queue stays schema-agnostic: no model
86
+ * names appear here, and consumer applications can override specific
87
+ * priorities via `configOverrides.modelCreatePriority` without touching the
88
+ * SDK.
89
+ *
90
+ * Non-create ops (update/delete/archive/unarchive) don't need FK ordering
91
+ * because the row already exists, so they all share
92
+ * `config.defaultNonCreatePriority`.
93
+ */
94
+ export declare const computePriorityScore: (type: Transaction["type"], modelName: string) => number;
95
+ export declare const TX_TYPE_TO_MUTATION_OP: Record<Transaction['type'], MutationOperationType>;
96
+ export declare function hasStaleWriteOptions(options?: WriteOptions): boolean;
97
+ export interface WriteOperationFields {
98
+ readAt?: number | null;
99
+ onStale?: 'reject' | 'overwrite' | 'notify' | null;
100
+ options?: Pick<MutationOptions, 'idempotencyKey' | 'label'>;
101
+ }
102
+ /**
103
+ * Project a transaction's `writeOptions` onto the wire operation. Stale
104
+ * guards (`readAt`/`onStale`) ride at the op root; `idempotencyKey`/`label`
105
+ * ride in the op's `options` slot (`MutationOperation.options` — the
106
+ * mutation_log cache key + audit tag). This is the single place the
107
+ * caller-supplied write vocabulary crosses onto the wire.
108
+ */
109
+ export declare function applyWriteOptions<T extends object>(op: T, transaction: Transaction): T & WriteOperationFields;
110
+ /**
111
+ * Structural shape we duck-type against for transport-layer errors.
112
+ * Captures the union of GraphQL-style and HTTP-style error shapes the
113
+ * mutation executor surfaces — kept narrow on purpose so we don't
114
+ * pretend to know fields the runtime won't always supply.
115
+ */
116
+ export interface TransportError {
117
+ message?: string;
118
+ code?: string | number;
119
+ extensions?: Record<string, unknown>;
120
+ locations?: readonly unknown[];
121
+ path?: readonly (string | number)[];
122
+ response?: {
123
+ status?: number;
124
+ errors?: readonly {
125
+ extensions?: {
126
+ code?: string;
127
+ };
128
+ message?: string;
129
+ }[];
130
+ };
131
+ error?: string;
132
+ }
133
+ export declare function asTransportError(value: unknown): TransportError;
134
+ export declare function extractStatusCode(error: unknown): number | undefined;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Commit-payload projection — the queue's wire vocabulary, lifted out of
3
+ * `TransactionQueue.ts` as a pure leaf (no queue state, no timers).
4
+ *
5
+ * Owns the `Transaction` record shape plus every helper that turns a local
6
+ * transaction into a wire-safe commit operation: schema-field projection
7
+ * (`projectCommitPayload`), write-option projection (`applyWriteOptions`),
8
+ * FK-aware priority scoring, and the transport-error duck-typing used to
9
+ * classify server rejections. Because nothing here touches the queue's
10
+ * runtime, the HTTP commit path can share this projection too.
11
+ */
12
+ import { getContext } from '../context.js';
13
+ import { getActiveRegistry } from '../ModelRegistry.js';
14
+ import { MutationOperationType } from '../types/index.js';
15
+ /**
16
+ * Framework-internal keys added by `Model.toJSON()` that must never
17
+ * reach the wire. The server treats each top-level key as a target
18
+ * column, so shipping these would blow up the INSERT/UPDATE.
19
+ */
20
+ const FRAMEWORK_KEYS = new Set(['__class', '__typename', 'clientId', 'syncStatus']);
21
+ /**
22
+ * Project a Model's serialized data onto its schema-declared fields
23
+ * and return a wire-safe commit payload. Two jobs:
24
+ *
25
+ * 1. Drop framework internals (`__class`, `__typename`, `clientId`,
26
+ * `syncStatus`) and anything not declared on the model's schema.
27
+ * 2. JSON.stringify values typed as `field.json()` — TEXT columns
28
+ * storing JSON need explicit stringification; postgres.js won't
29
+ * auto-serialize for non-JSONB columns.
30
+ *
31
+ * For updates (`dropUndefined: true`), `undefined` values are also
32
+ * stripped so they don't translate to `SET column = NULL` on the
33
+ * server side.
34
+ *
35
+ * Fields are read from `ModelRegistry`, populated by
36
+ * `registerModelsFromSchema` at SDK initialization. If the model
37
+ * isn't registered with field metadata (edge case — e.g., tests or
38
+ * manually registered models), projection falls back to identity and
39
+ * the caller gets whatever the Model serialized.
40
+ */
41
+ export function projectCommitPayload(modelName, source, opts) {
42
+ const metadata = getActiveRegistry().getMetadata(modelName);
43
+ const fields = metadata?.fields;
44
+ const out = {};
45
+ if (!fields) {
46
+ // Unknown registration — strip framework keys and ship the rest.
47
+ for (const [k, v] of Object.entries(source)) {
48
+ if (FRAMEWORK_KEYS.has(k))
49
+ continue;
50
+ if (opts.dropUndefined && v === undefined)
51
+ continue;
52
+ out[k] = v;
53
+ }
54
+ return out;
55
+ }
56
+ for (const [key, meta] of Object.entries(fields)) {
57
+ if (!(key in source))
58
+ continue;
59
+ const value = source[key];
60
+ if (opts.dropUndefined && value === undefined)
61
+ continue;
62
+ // JSON-typed fields (`jsonb` on the server): ship as OBJECTS over
63
+ // the wire, not pre-stringified strings. Previously we stringified
64
+ // here, which round-tripped incorrectly:
65
+ //
66
+ // 1. Client stringifies `position: {x, y}` → `'{"x":...}'`
67
+ // 2. Server writes to jsonb column (parses string → jsonb object, fine)
68
+ // 3. Server's delta echoes `data: JSON.stringify(op.input)` where
69
+ // `op.input.position` is still the STRING from step 1
70
+ // 4. Client merges delta → `model.position = "{...}"` (STRING)
71
+ // 5. Next drag: `{ ...layer.position, x, y }` spreads the STRING
72
+ // char-by-char, producing corrupted char-indexed objects like
73
+ // `{"0":"{","1":"\"","2":"x",...,"x":null,"y":null,...}`
74
+ // 6. That corrupt object lands in the next commit, stored in jsonb.
75
+ //
76
+ // Sending objects avoids the round-trip mismatch: the wire carries
77
+ // the object through delta + commit unchanged, and `postgres-js`
78
+ // serializes JS objects to jsonb correctly via its own
79
+ // `json.serialize` (triggered by Postgres's ParameterDescription
80
+ // response identifying the column as type 3802 / jsonb).
81
+ out[key] = value;
82
+ }
83
+ return out;
84
+ }
85
+ export const normalizeModelKey = (modelName) => modelName.replace('Model', '').toLowerCase();
86
+ export const stripModelSuffix = (modelName) => modelName.replace('Model', '');
87
+ /**
88
+ * FK-ordered create priority.
89
+ *
90
+ * Reads `config.modelCreatePriority` out of the runtime SyncEngineContext —
91
+ * this map is populated once at `createSyncEngine(...)` time by walking the
92
+ * schema's `belongsTo` graph (see `computeFKDepthPriority` in
93
+ * `client/createSyncEngine.ts`). The queue stays schema-agnostic: no model
94
+ * names appear here, and consumer applications can override specific
95
+ * priorities via `configOverrides.modelCreatePriority` without touching the
96
+ * SDK.
97
+ *
98
+ * Non-create ops (update/delete/archive/unarchive) don't need FK ordering
99
+ * because the row already exists, so they all share
100
+ * `config.defaultNonCreatePriority`.
101
+ */
102
+ export const computePriorityScore = (type, modelName) => {
103
+ const { modelCreatePriority, defaultCreatePriority, defaultNonCreatePriority } = getContext().config;
104
+ if (type !== 'create')
105
+ return defaultNonCreatePriority;
106
+ return modelCreatePriority.get(modelName) ?? defaultCreatePriority;
107
+ };
108
+ export const TX_TYPE_TO_MUTATION_OP = {
109
+ create: MutationOperationType.CREATE,
110
+ update: MutationOperationType.UPDATE,
111
+ delete: MutationOperationType.DELETE,
112
+ archive: MutationOperationType.ARCHIVE,
113
+ unarchive: MutationOperationType.UNARCHIVE,
114
+ };
115
+ export function hasStaleWriteOptions(options) {
116
+ return (options?.readAt !== undefined ||
117
+ options?.onStale !== undefined);
118
+ }
119
+ /**
120
+ * Project a transaction's `writeOptions` onto the wire operation. Stale
121
+ * guards (`readAt`/`onStale`) ride at the op root; `idempotencyKey`/`label`
122
+ * ride in the op's `options` slot (`MutationOperation.options` — the
123
+ * mutation_log cache key + audit tag). This is the single place the
124
+ * caller-supplied write vocabulary crosses onto the wire.
125
+ */
126
+ export function applyWriteOptions(op, transaction) {
127
+ const operation = op;
128
+ const writeOptions = transaction.writeOptions;
129
+ if (!writeOptions)
130
+ return operation;
131
+ if (writeOptions.readAt !== undefined) {
132
+ operation.readAt = writeOptions.readAt;
133
+ }
134
+ if (writeOptions.onStale !== undefined) {
135
+ operation.onStale = writeOptions.onStale;
136
+ }
137
+ if (writeOptions.idempotencyKey != null || writeOptions.label !== undefined) {
138
+ operation.options = {
139
+ ...(writeOptions.idempotencyKey != null
140
+ ? { idempotencyKey: writeOptions.idempotencyKey }
141
+ : {}),
142
+ ...(writeOptions.label !== undefined ? { label: writeOptions.label } : {}),
143
+ };
144
+ }
145
+ return operation;
146
+ }
147
+ export function asTransportError(value) {
148
+ return (value && typeof value === 'object' ? value : {});
149
+ }
150
+ export function extractStatusCode(error) {
151
+ return asTransportError(error).response?.status;
152
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Delta-confirmation tracking — the queue's "did my write's echo arrive?"
3
+ * machinery, lifted out of `TransactionQueue.ts` as a stateful leaf.
4
+ *
5
+ * Owns the ack watermark (via the shared `SyncPosition`), the per-transaction
6
+ * confirmation timeout map, and the retry-with-backoff/reconciliation policy
7
+ * for `awaiting_delta` transactions. Talks back to the queue through the
8
+ * minimal `DeltaConfirmationContext` interface (never the host class type),
9
+ * so the leaf stays cycle-free and testable in isolation.
10
+ */
11
+ import type { SyncPosition } from '../sync/syncPosition.js';
12
+ import type { Transaction } from './commitPayload.js';
13
+ /**
14
+ * The slice of the queue a confirmation tracker needs: store lookups +
15
+ * status flips, dropping optimistic entries on confirm, the host's event
16
+ * surface (`transaction:completed`, `reconciliation:needed`, …), the
17
+ * connection check (timeouts re-schedule instead of escalating while
18
+ * offline), and the shared client position (`noteAck` advances `acked`;
19
+ * diagnostics read `applied`).
20
+ */
21
+ export interface DeltaConfirmationContext {
22
+ store: {
23
+ get(id: string): Transaction | undefined;
24
+ getByStatus(status: Transaction['status']): Transaction[];
25
+ updateStatus(id: string, status: Transaction['status']): void;
26
+ };
27
+ optimisticUpdates: {
28
+ delete(id: string): boolean;
29
+ };
30
+ emit(event: string, payload?: unknown): void;
31
+ isConnected(): boolean;
32
+ position: SyncPosition;
33
+ }
34
+ export declare class DeltaConfirmationTracker {
35
+ private readonly ctx;
36
+ private static readonly DELTA_MAX_RETRIES;
37
+ private static readonly DELTA_INITIAL_TIMEOUT_MS;
38
+ private static readonly DELTA_MAX_TIMEOUT_MS;
39
+ private deltaConfirmationTimeouts;
40
+ private deltaConfirmationRetries;
41
+ constructor(ctx: DeltaConfirmationContext);
42
+ /** Applied-cursor alias, kept so the read sites below stay legible. */
43
+ private get lastSeenSyncId();
44
+ noteAck(lastSyncId: number | undefined): void;
45
+ /**
46
+ * LINEAR PATTERN: Confirm all awaiting transactions when delta with syncId >= threshold arrives.
47
+ * This replaces clientMutationId echoing - transactions are confirmed by sync ID threshold.
48
+ * @param syncId - The sync ID of the received delta
49
+ */
50
+ onDeltaReceived(syncId: number): void;
51
+ scheduleDeltaConfirmationTimeout(tx: Transaction, timeoutMs: number): void;
52
+ private cancelDeltaConfirmationTimeout;
53
+ /**
54
+ * Tear down every armed confirmation timer (30–120s each, one per
55
+ * in-flight transaction). Called from `TransactionQueue.dispose()` —
56
+ * without it a disposed queue kept the Node process alive and fired
57
+ * callbacks against an already-cleared store (T1.19).
58
+ */
59
+ dispose(): void;
60
+ }