@abloatai/ablo 0.25.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 (244) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +3 -3
  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 +328 -262
  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/examples/existing-python-backend.md +3 -3
  220. package/docs/identity.md +4 -4
  221. package/docs/integration-guide.md +1 -1
  222. package/docs/react.md +1 -1
  223. package/docs/sessions.md +5 -7
  224. package/package.json +15 -13
  225. package/dist/client/index.d.ts +0 -36
  226. package/dist/client/index.js +0 -33
  227. package/dist/config/index.d.ts +0 -10
  228. package/dist/config/index.js +0 -12
  229. package/dist/interfaces/headless.d.ts +0 -95
  230. package/dist/interfaces/headless.js +0 -41
  231. package/dist/query/index.d.ts +0 -6
  232. package/dist/query/index.js +0 -5
  233. package/dist/realtime/index.d.ts +0 -10
  234. package/dist/realtime/index.js +0 -9
  235. package/dist/schema/plane.d.ts +0 -23
  236. package/dist/schema/plane.js +0 -19
  237. package/dist/server/next.d.ts +0 -51
  238. package/dist/server/next.js +0 -47
  239. package/dist/sync/OfflineFlush.d.ts +0 -9
  240. package/dist/sync/OfflineFlush.js +0 -22
  241. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  242. package/dist/sync/OfflineTransactionStore.js +0 -263
  243. package/dist/transactions/index.d.ts +0 -16
  244. package/dist/transactions/index.js +0 -7
@@ -31,7 +31,7 @@ export function awaitClaimGrant(transport, claimId, options) {
31
31
  unsubs.push(transport.subscribe('claim_acquired', (p) => {
32
32
  if (p?.claimId === claimId) {
33
33
  getContext().logger.debug(`claim: acquired ${claimId} (target was free)`);
34
- settle(() => resolve({ waited: false }));
34
+ settle(() => { resolve({ waited: false }); });
35
35
  }
36
36
  }));
37
37
  unsubs.push(transport.subscribe('claim_granted', (p) => {
@@ -39,7 +39,7 @@ export function awaitClaimGrant(transport, claimId, options) {
39
39
  // Promoted to the head of the line — the creator's "it's the agent's
40
40
  // turn now" moment after waiting behind a holder.
41
41
  getContext().logger.info(`claim: granted ${claimId} — your turn (waited in queue)`);
42
- settle(() => resolve({ waited: true }));
42
+ settle(() => { resolve({ waited: true }); });
43
43
  }
44
44
  }));
45
45
  if (options?.maxQueueDepth !== undefined) {
@@ -49,7 +49,9 @@ export function awaitClaimGrant(transport, claimId, options) {
49
49
  return;
50
50
  const position = typeof p.position === 'number' ? p.position : 0;
51
51
  if (position >= max) {
52
- settle(() => reject(new AbloClaimedError(`Claim queue for ${claimId} is ${position} deep (max ${max}).`, { code: 'queue_too_deep' })));
52
+ settle(() => {
53
+ reject(new AbloClaimedError(`Claim queue for ${claimId} is ${position} deep (max ${max}).`, { code: 'queue_too_deep' }));
54
+ });
53
55
  }
54
56
  }));
55
57
  }
@@ -64,29 +66,35 @@ export function awaitClaimGrant(transport, claimId, options) {
64
66
  field: rejection.target.field,
65
67
  })
66
68
  : claimId;
67
- settle(() => reject(new AbloClaimedError(formatClaimedErrorMessage({
68
- targetLabel: target,
69
- heldBy: rejection.heldBy,
70
- claim: rejection.heldByClaim,
71
- policyReason: rejection.policyReason,
72
- fallback: `Claim rejected for ${target}.`,
73
- }), {
74
- code: rejection.reason === 'conflict'
75
- ? 'claim_conflict'
76
- : 'claim_lease_unavailable',
77
- claims: rejection.heldByClaim ? [rejection.heldByClaim] : undefined,
78
- })));
69
+ settle(() => {
70
+ reject(new AbloClaimedError(formatClaimedErrorMessage({
71
+ targetLabel: target,
72
+ heldBy: rejection.heldBy,
73
+ claim: rejection.heldByClaim,
74
+ policyReason: rejection.policyReason,
75
+ fallback: `Claim rejected for ${target}.`,
76
+ }), {
77
+ code: rejection.reason === 'conflict'
78
+ ? 'claim_conflict'
79
+ : 'claim_lease_unavailable',
80
+ claims: rejection.heldByClaim ? [rejection.heldByClaim] : undefined,
81
+ }));
82
+ });
79
83
  }));
80
84
  unsubs.push(transport.subscribe('claim_lost', (p) => {
81
85
  if (p?.claimId === claimId) {
82
- settle(() => reject(new AbloClaimedError(`Claim lost while queued for ${claimId}.`, {
83
- code: 'claim_lost',
84
- })));
86
+ settle(() => {
87
+ reject(new AbloClaimedError(`Claim lost while queued for ${claimId}.`, {
88
+ code: 'claim_lost',
89
+ }));
90
+ });
85
91
  }
86
92
  }));
87
93
  if (options?.timeoutMs && options.timeoutMs > 0) {
88
94
  timer = setTimeout(() => {
89
- settle(() => reject(new AbloClaimedError(`Timed out waiting for the queue grant on claim ${claimId}.`, { code: 'grant_timeout' })));
95
+ settle(() => {
96
+ reject(new AbloClaimedError(`Timed out waiting for the queue grant on claim ${claimId}.`, { code: 'grant_timeout' }));
97
+ });
90
98
  }, options.timeoutMs);
91
99
  }
92
100
  });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * bootstrapApply — applying bootstrap results to the in-memory pool.
3
+ *
4
+ * Extracted from BaseSyncedStore.ts as a cohesive leaf: routing a
5
+ * full/partial bootstrap result through the pool-write facade, collecting
6
+ * the delta-protected ids that must survive ghost removal, and replaying
7
+ * the deltas queued during an active bootstrap. The store keeps thin
8
+ * protected delegates with unchanged signatures and talks back through the
9
+ * minimal {@link PoolContext} — never the store's class type — so no
10
+ * module cycle forms. The heavy lifting (model creation, healing, upsert,
11
+ * ghost removal) stays owned by `SyncClient`.
12
+ */
13
+ import type { BootstrapResult } from '../Database.js';
14
+ import type { SyncDelta } from './SyncWebSocket.js';
15
+ /** Rehydration statistics from bootstrap */
16
+ export interface RehydrationStats {
17
+ added: number;
18
+ updated: number;
19
+ removed: number;
20
+ skipped: number;
21
+ healed: number;
22
+ elapsedMs: number;
23
+ }
24
+ /**
25
+ * What the bootstrap-apply path needs back from its host store. The two
26
+ * `SyncClient` facades come with enrichment pre-bound by the host, so the
27
+ * store's `enrichRelations` override point keeps its dynamic dispatch.
28
+ */
29
+ export interface PoolContext {
30
+ /** `SyncClient.applyDeltaBatchToPool` with the host's `enrichRelations` bound. */
31
+ applyDeltaBatchToPool(results: NonNullable<BootstrapResult['deltaResults']>): void;
32
+ /** `SyncClient.applyBootstrapDataToPool` — model creation, healing, pool upsert, ghost removal. */
33
+ applyBootstrapDataToPool(bootstrapData: {
34
+ models?: Record<string, unknown[]>;
35
+ failedModels?: string[];
36
+ }, protectedIds?: ReadonlySet<string>): {
37
+ added: number;
38
+ updated: number;
39
+ removed: number;
40
+ skipped: number;
41
+ healed: number;
42
+ };
43
+ /** Pool size — for the completion log line. */
44
+ getPoolSize(): number;
45
+ /** Every id currently in the pool — for delta-protected-id collection. */
46
+ getAllPoolIds(): string[];
47
+ /** Deltas queued during an active bootstrap; null when none is in flight.
48
+ * Backed by the host's `bootstrapDeltaQueue` field (get/set accessors). */
49
+ bootstrapDeltaQueue: SyncDelta[] | null;
50
+ /** The host's atomic frame apply — `applyDeltaFrame` deliberately stays
51
+ * in BaseSyncedStore (the authoritative-apply correctness seam). */
52
+ applyDeltaFrame(deltas: SyncDelta[]): void;
53
+ }
54
+ /** Apply bootstrap data to the ObjectPool with ghost removal */
55
+ /** Apply bootstrap data to the ObjectPool. Delegates pool writes to SyncClient. */
56
+ export declare function applyBootstrapToPool(ctx: PoolContext, bootstrapResult: BootstrapResult, protectedIds?: ReadonlySet<string>): RehydrationStats;
57
+ /** Collect IDs that must survive ghost removal (added by deltas during bootstrap) */
58
+ export declare function collectDeltaProtectedIds(ctx: PoolContext, preBootstrapIds: ReadonlySet<string>): Set<string>;
59
+ /** Replay deltas queued during bootstrap */
60
+ export declare function replayQueuedDeltas(ctx: PoolContext): void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * bootstrapApply — applying bootstrap results to the in-memory pool.
3
+ *
4
+ * Extracted from BaseSyncedStore.ts as a cohesive leaf: routing a
5
+ * full/partial bootstrap result through the pool-write facade, collecting
6
+ * the delta-protected ids that must survive ghost removal, and replaying
7
+ * the deltas queued during an active bootstrap. The store keeps thin
8
+ * protected delegates with unchanged signatures and talks back through the
9
+ * minimal {@link PoolContext} — never the store's class type — so no
10
+ * module cycle forms. The heavy lifting (model creation, healing, upsert,
11
+ * ghost removal) stays owned by `SyncClient`.
12
+ */
13
+ import { getContext } from '../context.js';
14
+ /** Apply bootstrap data to the ObjectPool with ghost removal */
15
+ /** Apply bootstrap data to the ObjectPool. Delegates pool writes to SyncClient. */
16
+ export function applyBootstrapToPool(ctx, bootstrapResult, protectedIds) {
17
+ const { bootstrapData } = bootstrapResult;
18
+ // Partial bootstrap: Database.processDeltaBatch already wrote the deltas
19
+ // to IDB. Route the same results through the delta-apply path so the
20
+ // in-memory pool evicts deleted entities (and updates modified ones).
21
+ // Without this, reconnect DELETEs persist to IDB but the canvas keeps
22
+ // showing ghost layers until a full reload.
23
+ if (bootstrapData.type === 'partial') {
24
+ const deltaResults = bootstrapResult.deltaResults;
25
+ if (deltaResults && deltaResults.length > 0) {
26
+ ctx.applyDeltaBatchToPool(deltaResults);
27
+ }
28
+ return { added: 0, updated: 0, removed: 0, skipped: 0, healed: 0, elapsedMs: 0 };
29
+ }
30
+ if (!bootstrapData.models) {
31
+ return { added: 0, updated: 0, removed: 0, skipped: 0, healed: 0, elapsedMs: 0 };
32
+ }
33
+ const start = typeof performance !== 'undefined' ? performance.now() : Date.now();
34
+ // SyncClient owns: model creation, healing, pool upsert, ghost removal
35
+ const stats = ctx.applyBootstrapDataToPool(bootstrapData, protectedIds);
36
+ const elapsedMs = Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - start);
37
+ getContext().logger.info('[BaseSyncedStore] Bootstrap applied', {
38
+ ...stats, elapsedMs, poolSize: ctx.getPoolSize(),
39
+ });
40
+ return { ...stats, elapsedMs };
41
+ }
42
+ /** Collect IDs that must survive ghost removal (added by deltas during bootstrap) */
43
+ export function collectDeltaProtectedIds(ctx, preBootstrapIds) {
44
+ const protectedIds = new Set();
45
+ for (const id of ctx.getAllPoolIds()) {
46
+ if (!preBootstrapIds.has(id))
47
+ protectedIds.add(id);
48
+ }
49
+ for (const delta of ctx.bootstrapDeltaQueue ?? []) {
50
+ if (delta.actionType !== 'D' && delta.modelId)
51
+ protectedIds.add(delta.modelId);
52
+ }
53
+ return protectedIds;
54
+ }
55
+ /** Replay deltas queued during bootstrap */
56
+ export function replayQueuedDeltas(ctx) {
57
+ const queue = ctx.bootstrapDeltaQueue;
58
+ ctx.bootstrapDeltaQueue = null;
59
+ if (!queue || queue.length === 0)
60
+ return;
61
+ // Deltas that landed during bootstrap are a complete frame — apply
62
+ // them atomically (one flush, one re-render) rather than dribbling
63
+ // each back through the live debounce path.
64
+ ctx.applyDeltaFrame(queue);
65
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Commit-path frame builders and claim instrumentation for the sync
3
+ * WebSocket — pure functions with no socket state, extracted from
4
+ * SyncWebSocket so the wire codec is a leaf the transport (and the frame
5
+ * dispatch table) can share.
6
+ */
7
+ import type { CommitMessage } from '../wire/index.js';
8
+ import type { MutationOperation, ClaimEvent } from '../interfaces/index.js';
9
+ import type { StaleNotification, ReadDependency } from '../coordination/schema.js';
10
+ /**
11
+ * Resolution value of a commit ack. `notifications` is present only when a
12
+ * guarded write (`onStale: 'notify') hit a concurrent change — the
13
+ * advisory self-heal signal, surfaced both here and via `conflict:notified`.
14
+ */
15
+ export interface CommitAck {
16
+ lastSyncId: number;
17
+ notifications?: StaleNotification[];
18
+ }
19
+ /**
20
+ * Project the SDK's `MutationOperation[]` onto the canonical wire
21
+ * `CommitMessage`. This is the single serialize boundary between the SDK op
22
+ * type (loose `type: string`, plus an SDK-internal `options` the server never
23
+ * reads) and the strict wire contract. The per-field map gives compile-time
24
+ * drift detection (a `CommitOperation` shape change breaks here) and the lone
25
+ * `as` narrows the validated op `type` to the wire union — the only
26
+ * loosening, localized to this boundary.
27
+ */
28
+ export declare function buildCommitFrame(operations: readonly MutationOperation[], clientTxId: string, causedByTaskId?: string | null, reads?: readonly ReadDependency[] | null): CommitMessage;
29
+ /**
30
+ * Defensively validate the optional `notifications` array off a commit ack.
31
+ * Untrusted wire data — a malformed entry is dropped rather than throwing,
32
+ * so a bad notification never sinks an otherwise-successful commit.
33
+ */
34
+ export declare function parseNotifications(raw: unknown): StaleNotification[] | undefined;
35
+ /**
36
+ * Single instrumentation point for claim events. Every `claim_*` frame routes
37
+ * through here so a developer debugging a collision gets one consistent trace
38
+ * — a console line AND a structured capture — without each dispatch case
39
+ * re-deriving the row/holder shape. The wire payload is loosely typed
40
+ * (`Record<string, unknown>`), so this is the one place that narrows it into
41
+ * a {@link ClaimEvent}.
42
+ */
43
+ export declare function recordClaim(phase: ClaimEvent['phase'], payload: Record<string, unknown>): void;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Commit-path frame builders and claim instrumentation for the sync
3
+ * WebSocket — pure functions with no socket state, extracted from
4
+ * SyncWebSocket so the wire codec is a leaf the transport (and the frame
5
+ * dispatch table) can share.
6
+ */
7
+ import { getContext } from '../context.js';
8
+ import { staleNotificationSchema, wireParticipantKindSchema, } from '../coordination/schema.js';
9
+ import { formatClaim } from '../coordination/trace.js';
10
+ /**
11
+ * Project the SDK's `MutationOperation[]` onto the canonical wire
12
+ * `CommitMessage`. This is the single serialize boundary between the SDK op
13
+ * type (loose `type: string`, plus an SDK-internal `options` the server never
14
+ * reads) and the strict wire contract. The per-field map gives compile-time
15
+ * drift detection (a `CommitOperation` shape change breaks here) and the lone
16
+ * `as` narrows the validated op `type` to the wire union — the only
17
+ * loosening, localized to this boundary.
18
+ */
19
+ export function buildCommitFrame(operations, clientTxId, causedByTaskId, reads) {
20
+ const payload = {
21
+ operations: operations.map((op) => ({
22
+ type: op.type,
23
+ model: op.model,
24
+ id: op.id,
25
+ input: op.input,
26
+ transactionId: op.transactionId,
27
+ readAt: op.readAt,
28
+ onStale: op.onStale,
29
+ })),
30
+ clientTxId,
31
+ };
32
+ if (causedByTaskId)
33
+ payload.causedByTaskId = causedByTaskId;
34
+ // Batch-level read-set (STORM layer): rows/groups the batch was premised on.
35
+ if (reads && reads.length > 0)
36
+ payload.reads = [...reads];
37
+ return { type: 'commit', payload };
38
+ }
39
+ /**
40
+ * Defensively validate the optional `notifications` array off a commit ack.
41
+ * Untrusted wire data — a malformed entry is dropped rather than throwing,
42
+ * so a bad notification never sinks an otherwise-successful commit.
43
+ */
44
+ export function parseNotifications(raw) {
45
+ if (!Array.isArray(raw) || raw.length === 0)
46
+ return undefined;
47
+ const out = [];
48
+ for (const entry of raw) {
49
+ const parsed = staleNotificationSchema.safeParse(entry);
50
+ if (parsed.success)
51
+ out.push(parsed.data);
52
+ }
53
+ return out.length > 0 ? out : undefined;
54
+ }
55
+ /**
56
+ * Single instrumentation point for claim events. Every `claim_*` frame routes
57
+ * through here so a developer debugging a collision gets one consistent trace
58
+ * — a console line AND a structured capture — without each dispatch case
59
+ * re-deriving the row/holder shape. The wire payload is loosely typed
60
+ * (`Record<string, unknown>`), so this is the one place that narrows it into
61
+ * a {@link ClaimEvent}.
62
+ */
63
+ export function recordClaim(phase, payload) {
64
+ const str = (v) => typeof v === 'string' ? v : undefined;
65
+ // Targets arrive flat ({ entityType, entityId }) or nested under `target`.
66
+ const target = payload.target && typeof payload.target === 'object'
67
+ ? payload.target
68
+ : payload;
69
+ const kind = wireParticipantKindSchema.safeParse(payload.participantKind);
70
+ const event = {
71
+ phase,
72
+ claimId: str(payload.claimId),
73
+ model: str(target.entityType) ?? str(target.model),
74
+ id: str(target.entityId) ?? str(target.id),
75
+ field: str(target.field),
76
+ actor: str(payload.actor) ?? str(payload.heldBy),
77
+ participantKind: kind.success ? kind.data : undefined,
78
+ position: typeof payload.position === 'number' ? payload.position : undefined,
79
+ reason: str(payload.policyReason) ?? str(payload.reason),
80
+ };
81
+ const message = formatClaim(event);
82
+ // A rejection or lost lease is the collision a developer is actively
83
+ // debugging → warn (shows at the default log level). The routine events
84
+ // (acquired/queued/granted/expired) are debug-only so they never drown the
85
+ // console until you opt in with `new Ablo({ debug: true })`.
86
+ const isCollision = phase === 'rejected' || phase === 'lost';
87
+ const ctx = getContext();
88
+ if (isCollision)
89
+ ctx.logger.warn(message);
90
+ else
91
+ ctx.logger.debug(message);
92
+ ctx.observability.breadcrumb(message, 'sync.coordination', isCollision ? 'warning' : 'info');
93
+ ctx.observability.captureClaim(event);
94
+ }
@@ -189,8 +189,9 @@ export function createClaimStream(config, transport = null) {
189
189
  // If WE are in this line, trace our position (the "agent queued behind a
190
190
  // claim" moment) — once per position change, so advancing is visible.
191
191
  const ourIndex = line.findIndex((c) => ownClaims.has(c.id));
192
- if (ourIndex >= 0) {
193
- const ourId = line[ourIndex].id;
192
+ const ourClaim = ourIndex >= 0 ? line[ourIndex] : undefined;
193
+ if (ourClaim) {
194
+ const ourId = ourClaim.id;
194
195
  if (lastLoggedQueuePos.get(ourId) !== ourIndex) {
195
196
  lastLoggedQueuePos.set(ourId, ourIndex);
196
197
  getContext().logger.info(`claim: queued for ${claimLabel(p.target.type, p.target.id)} — position ${ourIndex + 1} of ${line.length}, waiting`, { claimId: ourId });
@@ -26,8 +26,6 @@ export interface CreateSnapshotArgs<TSchema extends Schema = Schema, K extends k
26
26
  /** Returns the engine's current `lastSyncId`. Read at snapshot time
27
27
  * to stamp the watermark; not re-read after. */
28
28
  getLastSyncId: () => number;
29
- entities: {
30
- readonly [M in K]: string | readonly string[];
31
- };
29
+ entities: Readonly<Record<K, string | readonly string[]>>;
32
30
  }
33
31
  export declare function createSnapshot<TSchema extends Schema, K extends keyof TSchema['models'] & string>(args: CreateSnapshotArgs<TSchema, K>): Snapshot<TSchema, K>;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * credentialLifecycle — reconnect-credential re-mint + proactive pre-roll.
3
+ *
4
+ * Extracted from BaseSyncedStore.ts as a cohesive leaf: owns the re-mint
5
+ * hook, the single-flight guard, and the browser-only proactive refresh
6
+ * (timer + OS-wake listener). The store keeps thin public delegates
7
+ * (`setCredentialRefresher` / `performCredentialRefresh` /
8
+ * `startCredentialLifecycle`) so its published surface is unchanged, and
9
+ * talks back through the minimal {@link CredentialLifecycleContext} —
10
+ * never the store's class type — so no module cycle forms.
11
+ */
12
+ import type { RecoveryClass } from '../errorCodes.js';
13
+ /**
14
+ * Tri-state outcome of a credential re-mint, mirroring the `getToken`
15
+ * contract (see {@link CredentialLifecycle.refresh}).
16
+ */
17
+ export type CredentialRefreshOutcome = 'refreshed' | 'session_error' | 'network_error';
18
+ /**
19
+ * What an auth-rejected transport should do after the backbone has run:
20
+ * `'retry'` = a fresh credential is in the credential source, replay the
21
+ * request ONCE; `'stop'` = don't replay (terminal session loss, a rejection
22
+ * a re-mint can't cure, or a transient mint failure the caller's own
23
+ * retry/revalidation path will recover later).
24
+ */
25
+ export type CredentialRecoveryOutcome = 'retry' | 'stop';
26
+ /**
27
+ * What a credential refresher may resolve with. The plain-string form is the
28
+ * classic `getToken` contract; the object form additionally carries the mint
29
+ * response's `expiresAt` (ISO string / epoch ms / Date) so the proactive
30
+ * pre-roll can schedule off the credential's ACTUAL lifetime instead of
31
+ * assuming the server's default TTL.
32
+ */
33
+ export type CredentialRefreshResult = string | {
34
+ readonly token: string;
35
+ readonly expiresAt?: string | number | Date;
36
+ } | null;
37
+ /** A credential re-mint hook. `Promise<string | null>` resolvers remain
38
+ * assignable — the widened result type is a superset. */
39
+ export type CredentialRefresher = () => Promise<CredentialRefreshResult>;
40
+ /** Fallback pre-roll cadence AND ceiling — the historical 10-minute value,
41
+ * comfortably inside the server's 15m default `ek_` TTL. Used verbatim when
42
+ * the refresher reports no expiry. */
43
+ export declare const DEFAULT_PREROLL_INTERVAL_MS: number;
44
+ /** Floor so a very short (or already-elapsed) TTL can't hot-loop the mint. */
45
+ export declare const MIN_PREROLL_DELAY_MS: number;
46
+ /**
47
+ * When to pre-roll a credential that expires at `expiresAtMs`: after ~2/3 of
48
+ * the REMAINING lifetime (15m TTL → 10m, exactly the historical cadence),
49
+ * clamped to [{@link MIN_PREROLL_DELAY_MS}, {@link DEFAULT_PREROLL_INTERVAL_MS}].
50
+ * Unknown expiry (`null`) → the 10-minute fallback. Exported for unit tests.
51
+ */
52
+ export declare function computePrerollDelayMs(expiresAtMs: number | null, nowMs: number): number;
53
+ /**
54
+ * What the lifecycle needs back from its host store. Deliberately minimal —
55
+ * three callbacks, all resolved lazily at call time (the ConnectionManager
56
+ * behind two of them doesn't exist until `setupWebSocketSync`).
57
+ */
58
+ export interface CredentialLifecycleContext {
59
+ /** Push a freshly-minted access token into the shared credential source
60
+ * (no-op when the deployment wired no credential source). */
61
+ setAuthToken(token: string): void;
62
+ /** Nudge the connection FSM to re-probe with the current credential —
63
+ * the host's `nudgeReconnect()` (`CREDENTIAL_REFRESHED`). */
64
+ nudgeReconnect(): void;
65
+ /** Surface a terminal session loss to the connection FSM
66
+ * (`BOOTSTRAP_FAILED_SESSION`) — the long-lived login is gone. */
67
+ reportSessionExpired(): void;
68
+ }
69
+ export declare class CredentialLifecycle {
70
+ private readonly ctx;
71
+ /**
72
+ * Re-mint hook for the short-lived access credential (the Stripe-style
73
+ * `ek_`/`rk_`). Wired by the React provider from its `getToken`/`authEndpoint`
74
+ * — the engine owns WHEN to refresh (a stale-credential probe / an external
75
+ * nudge), the integrator owns HOW to mint. Mirrors the `getToken` contract:
76
+ * resolves a token string on success, `null` when the long-lived login is
77
+ * gone (terminal), and THROWS on a transient/offline failure. Used by
78
+ * {@link refresh}. Absent ⇒ no silent re-mint (e.g. a static
79
+ * `apiKey` deployment whose credential source refreshes out-of-band).
80
+ */
81
+ private credentialRefresher;
82
+ /** Single-flight guard so a wake nudge + an in-flight request + a probe don't
83
+ * all mint at once (the classic "token thrash → random logout" bug). */
84
+ private inFlightCredentialRefresh;
85
+ /** Teardown for the proactive credential lifecycle (refresh timer + wake/
86
+ * online/focus listeners) installed by {@link start};
87
+ * cleared on `BaseSyncedStore.disconnect`. Null when no resolver is wired. */
88
+ private credentialLifecycleTeardown;
89
+ /** Epoch ms the CURRENT credential expires, when the refresher reports it
90
+ * (object-form {@link CredentialRefreshResult}); `null` for the classic
91
+ * string-form resolver → the pre-roll falls back to its fixed cadence. */
92
+ private credentialExpiresAtMs;
93
+ /** Re-arm hook for the proactive pre-roll timer (set by {@link start}).
94
+ * Invoked after every successful mint so a REACTIVE refresh (probe, wake,
95
+ * the initial `ready()` mint) immediately re-anchors the schedule to the
96
+ * fresh credential's real expiry instead of a stale fixed delay. */
97
+ private prerollReschedule;
98
+ constructor(ctx: CredentialLifecycleContext);
99
+ /**
100
+ * Register the access-credential re-mint hook. Called by the React provider
101
+ * with a thunk that mints a fresh `ek_`/`rk_` (typically its `getToken`).
102
+ * See {@link credentialRefresher}.
103
+ */
104
+ setRefresher(refresher: CredentialRefresher | null): void;
105
+ /**
106
+ * Re-mint the short-lived access credential and push it into the credential
107
+ * source, reporting a tri-state outcome the `ConnectionManager` maps to
108
+ * its FSM. The contract mirrors `getToken` (and PowerSync's `fetchCredentials`
109
+ * / Liveblocks' `authEndpoint`, but made explicit instead of overloading
110
+ * return/throw):
111
+ * - token string → `'refreshed'` (fresh key in place; re-probe & reconnect)
112
+ * - `null` → `'session_error'` (login itself is gone → terminal, sign out)
113
+ * - throw → `'network_error'` (couldn't reach the mint endpoint → transient)
114
+ *
115
+ * SINGLE-FLIGHT: concurrent callers (a wake nudge, an in-flight request, the
116
+ * probe) share one in-flight promise so we never double-mint — the canonical
117
+ * fix for the "every 401 mints a token → thrash → spurious logout" anti-pattern.
118
+ *
119
+ * No refresher wired ⇒ `'refreshed'` (a no-op re-probe): a static-`apiKey`
120
+ * deployment has no session to re-mint from; its credential source refreshes
121
+ * out-of-band, so we just re-probe with whatever it currently holds.
122
+ */
123
+ refresh(): Promise<CredentialRefreshOutcome>;
124
+ /**
125
+ * Route a refresh outcome into the FSM — the ONE place the tri-state is
126
+ * interpreted, shared by the proactive pre-roll, the wake nudge, and the
127
+ * HTTP auth-recovery path so every trigger converges on identical behavior.
128
+ */
129
+ private routeRefreshOutcome;
130
+ /**
131
+ * THE recovery backbone for auth-rejected requests, whatever the transport.
132
+ * The WS probe already routes 401s through the FSM; HTTP consumers (the
133
+ * lazy-query lane, bootstrap helpers) call THIS instead of inventing their
134
+ * own handling — one single-flight mint, one outcome routing, one taxonomy.
135
+ *
136
+ * Mirrors `NetworkProbe`'s classification of the same codes:
137
+ * - `access_credential_expiry` — the routine case (an expired `ek_`/`rk_`):
138
+ * silently re-mint via the single-flight {@link refresh} and tell the
139
+ * caller to replay ONCE on success. Never a sign-out by itself — the
140
+ * only terminal path is the mint itself resolving `null`.
141
+ * - `session_expiry` — the login is gone. Report to the FSM (sign-out
142
+ * path) and stop; replaying is pointless.
143
+ * - `auth_blocked` / `permission` / everything else — re-minting produces
144
+ * the same rejected credential; stop without touching the FSM.
145
+ */
146
+ recoverFromAuthRejection(recovery: RecoveryClass): Promise<CredentialRecoveryOutcome>;
147
+ /**
148
+ * Install the access-credential lifecycle the CLIENT owns (this used to live
149
+ * in the React provider — wrong layer). Two parts:
150
+ * 1. REACTIVE — register `getToken` as the re-mint hook the FSM calls when a
151
+ * probe finds the key stale (`credential_stale`) or on a nudge.
152
+ * 2. PROACTIVE — keep the short-lived key fresh ahead of trouble: a refresh
153
+ * timer inside the TTL, plus re-mint on OS wake. The ENTIRE proactive
154
+ * block is browser-gated (`typeof window`): server/SSR has no socket to
155
+ * keep warm and the resolver is browser-oriented, so arming it in Node
156
+ * would fire a relative-URL fetch and throw. (Agents pass a static
157
+ * `apiKey` with no resolver, so this method is never called for them.)
158
+ *
159
+ * Config-driven and invisible, like Supabase's `autoRefreshToken` — consumers
160
+ * never call a refresh method. Idempotent (a second call replaces the first);
161
+ * torn down on `BaseSyncedStore.disconnect`.
162
+ *
163
+ * `opts.proactiveInNode` arms the refresh TIMER on a windowless host too —
164
+ * set for agent/system participants (long-lived server sockets whose
165
+ * `rk_`/`ek_` must renew BEFORE the hub's keepalive reaper closes them; the
166
+ * PowerSync/Ably "SDK renews everywhere" model). Node timers are `unref`ed
167
+ * so a finishing script is never kept alive by the pre-roll. The OS-wake
168
+ * listener remains browser-only regardless (no `window` to listen on).
169
+ */
170
+ start(getToken: CredentialRefresher, opts?: {
171
+ proactiveInNode?: boolean;
172
+ }): void;
173
+ /** Tear down the proactive credential lifecycle (idempotent). */
174
+ stop(): void;
175
+ }