@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,344 @@
1
+ /**
2
+ * The shared resource-type surface of the client — `ModelRead`, `ModelClient`,
3
+ * the `Commit*` / `Claim*` shapes, the session-mint params/resource, and the
4
+ * `HttpClaimApi` derivation.
5
+ *
6
+ * Extracted from `Ablo.ts` so `ApiClient.ts` / `httpClient.ts` /
7
+ * `sessionMint.ts` can take the wire-facing types WITHOUT importing the
8
+ * factory module that runtime-imports them back (the 4-cycle cluster madge
9
+ * flagged). This module is type-only — ZERO runtime imports — so importing it
10
+ * can never create a cycle. `Ablo.ts` re-exports everything here, so existing
11
+ * import paths keep resolving.
12
+ */
13
+ import type { StaleNotification, ReadDependency } from '../coordination/schema.js';
14
+ import type { ModelTarget, ModelClaim } from '../coordination/schema.js';
15
+ export type { ModelTarget, ModelClaim };
16
+ import type { SchemaRecord } from '../schema/schema.js';
17
+ import type { SyncGroupInput } from '../schema/roles.js';
18
+ import type { Claim, ClaimStream, ClaimWaitOptions, Duration, HeldClaim } from '../types/streams.js';
19
+ import type { ModelUpdater, ContentionOptions } from './functionalUpdate.js';
20
+ import type { ClaimOptions, ClaimParams, ClaimReadApi, AwaitedClaimMethod, ServerReadOptions } from './createModelProxy.js';
21
+ /**
22
+ * Operations available on each model in the sync engine.
23
+ *
24
+ * Naming aligns with Stripe / OpenAI / Anthropic conventions:
25
+ * `retrieve({ id })` — async single-row server read
26
+ * `list({ where })` — async collection server read
27
+ * `get(id)` / `getAll(...)` / `getCount(...)` — local graph snapshots
28
+ * `create({ data })` / `update({ id, data })` / `delete({ id })` — writes
29
+ * `claim({ id })` — durable claim handle for coordinated writes
30
+ */
31
+ export type { LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, Claim, HeldClaim, ModelOperations, } from './createModelProxy.js';
32
+ export type ModelOperationAction = 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
33
+ export type CommitWait = 'queued' | 'confirmed';
34
+ export interface ModelRead<T = Record<string, unknown>> {
35
+ /**
36
+ * The row, or `undefined` when no row matched the id (or it's outside the
37
+ * caller's scope). A miss is data-absence, not an error — `retrieve` never
38
+ * throws "not found", mirroring the WebSocket client's `T | undefined`.
39
+ * Branch on it: `const deal = (await ablo.deals.retrieve({ id })).data; if (!deal) …`.
40
+ */
41
+ readonly data: T | undefined;
42
+ readonly stamp: number;
43
+ readonly claims: readonly ModelClaim[];
44
+ }
45
+ export type IfClaimedPolicy = 'return' | 'fail';
46
+ export interface ClaimedOptions {
47
+ /**
48
+ * What to do when another participant has claimed the target: `return`
49
+ * includes active claim metadata in the response; `fail` throws
50
+ * `AbloClaimedError`. Waiting for a claim to clear is a claim-side concern —
51
+ * take `ablo.<model>.claim({ id })` (it queues fairly); reads never block.
52
+ */
53
+ readonly ifClaimed?: IfClaimedPolicy;
54
+ }
55
+ export type { ClaimWaitOptions } from '../types/streams.js';
56
+ export interface ModelReadOptions extends ClaimedOptions {
57
+ }
58
+ export interface ClaimCreateOptions {
59
+ readonly target: ModelTarget;
60
+ /** Human-readable phase shown to peers — `'editing'`, `'writing'`. The same
61
+ * word on every claim surface. */
62
+ readonly reason: string;
63
+ readonly ttl?: Duration;
64
+ /**
65
+ * Join the server's fair FIFO queue when the target is already claimed,
66
+ * rather than failing immediately. `create` then resolves only once the
67
+ * lease is actually ours (the server pushes `claim_acquired` if the target
68
+ * was free, or `claim_granted` when we reach the head of the line). Without
69
+ * this, a contended claim throws. Used by `ablo.<model>.claim` so writers
70
+ * serialize instead of racing.
71
+ */
72
+ readonly queue?: boolean;
73
+ /** Cap on how long to wait for a queued grant before rejecting. */
74
+ readonly waitTimeoutMs?: number;
75
+ /**
76
+ * Backpressure: reject with `AbloClaimedError('queue_too_deep')` instead of
77
+ * waiting if the queue is already `>= maxQueueDepth` when we join.
78
+ */
79
+ readonly maxQueueDepth?: number;
80
+ }
81
+ export interface CommitOperationInput {
82
+ readonly action: ModelOperationAction;
83
+ /** The model name — matches `ablo.<model>` and the schema's `model()`. */
84
+ readonly model?: string;
85
+ readonly target?: ModelTarget;
86
+ readonly id?: string | null;
87
+ readonly data?: Record<string, unknown> | null;
88
+ readonly transactionId?: string | null;
89
+ readonly readAt?: number | null;
90
+ readonly onStale?: 'reject' | 'overwrite' | 'notify' | null;
91
+ }
92
+ export interface CommitCreateOptions {
93
+ readonly claimRef?: string | {
94
+ readonly id: string;
95
+ } | null;
96
+ readonly idempotencyKey?: string | null;
97
+ readonly readAt?: number | null;
98
+ readonly onStale?: 'reject' | 'overwrite' | 'notify' | null;
99
+ /**
100
+ * A claim handle from `ablo.<model>.claim({ id })` (or the HTTP claim
101
+ * surface). Same vocabulary as the per-model writes: the handle's
102
+ * snapshot watermark becomes the batch `readAt` default and `onStale`
103
+ * defaults to `'reject'`, so a commit that follows a claim is guarded
104
+ * against concurrent edits without re-stating the watermark by hand.
105
+ * Explicit `readAt`/`onStale` on the options win.
106
+ */
107
+ readonly claim?: Claim | null;
108
+ readonly operation?: CommitOperationInput;
109
+ readonly operations?: readonly CommitOperationInput[];
110
+ readonly wait?: CommitWait;
111
+ /**
112
+ * Batch-level read dependencies (the STORM "did anything I looked at change?"
113
+ * layer). Declare the rows (`{model,id,readAt,fields?}`) or sync groups
114
+ * (`{group,readAt}`, e.g. `deck:abc`) this batch was premised on; the server
115
+ * validates none moved since `readAt` and fires the entry's `onStale` over the
116
+ * batch. Distinct from the write-target `readAt` — this guards what you READ,
117
+ * not what you write.
118
+ */
119
+ readonly reads?: readonly ReadDependency[] | null;
120
+ }
121
+ export interface CommitReceipt {
122
+ readonly id: string;
123
+ readonly status: CommitWait;
124
+ readonly lastSyncId?: number;
125
+ /**
126
+ * Stale-context notifications (notify-instead-of-abort, non-coercion). Present
127
+ * only when this commit guarded a write with `onStale: 'notify' and
128
+ * the premise moved concurrently — the conflicting field's current value,
129
+ * handed back as data instead of a forced `AbloStaleContextError`. The engine
130
+ * surfaces state; the intelligent actor (agent or human) decides how to
131
+ * resolve. Also fires on `conflict:notified`.
132
+ */
133
+ readonly notifications?: readonly StaleNotification[];
134
+ /**
135
+ * Ids of UPDATE/DELETE targets in this commit that matched ZERO rows (the row
136
+ * doesn't exist, or is outside the caller's org). Present (non-empty) only
137
+ * when a write missed. Typed resource wrappers turn this into a loud
138
+ * `AbloNotFoundError`; a raw `commits.create` caller can inspect it directly.
139
+ */
140
+ readonly missingIds?: readonly string[];
141
+ }
142
+ export interface CommitResource {
143
+ create(options: CommitCreateOptions): Promise<CommitReceipt>;
144
+ }
145
+ export interface ClaimResource extends ClaimStream {
146
+ create(options: ClaimCreateOptions): Promise<Claim>;
147
+ list(target?: Partial<ModelTarget>): readonly ModelClaim[];
148
+ waitFor(target: Partial<ModelTarget>, options?: ClaimWaitOptions): Promise<void>;
149
+ }
150
+ export interface ModelMutationOptions extends ClaimedOptions {
151
+ readonly claimRef?: string | {
152
+ readonly id: string;
153
+ } | null;
154
+ readonly idempotencyKey?: string | null;
155
+ readonly readAt?: number | null;
156
+ readonly onStale?: 'reject' | 'overwrite' | 'notify' | null;
157
+ readonly wait?: CommitWait;
158
+ readonly claim?: Claim | ClaimOptions | null;
159
+ }
160
+ /**
161
+ * The HTTP/stateless claim surface. Normal tools usually put `claim` directly
162
+ * on the write (`update({ id, data, claim })`) and let the SDK release it. Use
163
+ * this namespace for multi-step handles and coordination screens.
164
+ *
165
+ * Same surface as the reactive {@link ClaimApi}, but every read is a server
166
+ * round-trip, so `state`/`queue`/`reorder` are **awaited** here (the WebSocket
167
+ * client resolves them synchronously from its local pool — which is what lets
168
+ * `useAblo((ablo) => ablo.x.claim.state({ id }))` work inside a React render; a
169
+ * stateless client has no pool to read, so the `Promise` is unavoidable).
170
+ *
171
+ * Mechanically DERIVED from `ClaimReadApi` via {@link AwaitedClaimMethod} so the
172
+ * two transports can never drift: the ONLY difference is the uniform `Promise`
173
+ * wrapper that statelessness forces. `claim({ id })` is identical (already async
174
+ * on both); `state`/`queue`/`reorder`/`release` are the awaited form.
175
+ */
176
+ export type HttpClaimApi<T = Record<string, unknown>> = ((params: ClaimParams<T>) => Promise<HeldClaim<T>>) & {
177
+ [K in keyof ClaimReadApi<T>]: AwaitedClaimMethod<ClaimReadApi<T>[K]>;
178
+ };
179
+ export interface ModelClient<T = Record<string, unknown>> {
180
+ /**
181
+ * Single-row read over HTTP. **Returns an envelope, not the bare row** — the
182
+ * row is on `.data`, alongside the `.stamp` watermark (for stale-context
183
+ * guards on the following write) and any active `.claims`. A stateless HTTP
184
+ * client can't synthesize the watermark from a local snapshot, so the
185
+ * envelope is load-bearing here (the WebSocket client's `retrieve` returns
186
+ * `T | undefined` because it reads from the hydrated pool).
187
+ *
188
+ * ```ts
189
+ * const deal = await ablo.deals.retrieve({ id });
190
+ * deal.data?.recommendation; // ← the row is on .data
191
+ * deal.stamp; // watermark — pass to the next write's readAt
192
+ * ```
193
+ */
194
+ retrieve(params: ModelReadOptions & {
195
+ readonly id: string;
196
+ }): Promise<ModelRead<T>>;
197
+ /**
198
+ * Collection read over HTTP (server round-trip). Equality `where`, `orderBy`,
199
+ * `limit`. Present on the stateless protocol client; the store-backed
200
+ * `.model(name)` accessor omits it (use the typed `ablo.<model>.list` there).
201
+ */
202
+ list?(options?: ServerReadOptions<T>): Promise<T[]>;
203
+ /**
204
+ * Create a row and return it — the confirmed, authoritative server row (with
205
+ * framework defaults like `createdAt`/`createdBy`), mirroring the WebSocket
206
+ * client's `create`. A re-create of an existing caller-supplied id is
207
+ * idempotent and returns the EXISTING row, not the input.
208
+ */
209
+ create(params: ModelMutationOptions & {
210
+ readonly data: Record<string, unknown>;
211
+ readonly id?: string | null;
212
+ }): Promise<T>;
213
+ update(params: ModelMutationOptions & {
214
+ readonly id: string;
215
+ readonly data: Record<string, unknown>;
216
+ }): Promise<CommitReceipt>;
217
+ /**
218
+ * Update under contention with a function of the latest state —
219
+ * `update(id, current => next)`, the `setState(prev => next)` of the data
220
+ * layer. The SDK reads the freshest row, runs your updater, writes it as a
221
+ * compare-and-swap against the row's watermark, and re-reads + re-runs on any
222
+ * concurrent write. No claim, no identity, no conflict codes surface: the
223
+ * write either lands or throws {@link AbloContentionError} once its reconcile
224
+ * budget is spent. Return `null`/`undefined` from the updater to skip the
225
+ * write (resolves to `undefined`).
226
+ */
227
+ update(id: string, updater: ModelUpdater<T>, options?: ContentionOptions): Promise<CommitReceipt | undefined>;
228
+ delete(params: ModelMutationOptions & {
229
+ readonly id: string;
230
+ }): Promise<CommitReceipt>;
231
+ /**
232
+ * Durable lease + FIFO wait-line over HTTP — coordination without a socket.
233
+ * Present on the stateless protocol client (`Ablo({ schema: null })` /
234
+ * `createAbloHttpClient`); the store-backed `.model(name)` accessor omits it
235
+ * (the typed `ablo.<model>.claim` proxy is the full reactive namespace there).
236
+ */
237
+ claim?: HttpClaimApi<T>;
238
+ }
239
+ /** A single data operation a scoped **agent** session may perform on a model. */
240
+ export type SessionOperation = 'read' | 'create' | 'update' | 'delete';
241
+ /** Mint params for an **end-user** session — full data authority within the
242
+ * org (the Stripe `ephemeralKeys.create` / Supabase session shape). Mints an
243
+ * `ek_` token. `user.id` is your end user's external IdP id (becomes the
244
+ * session's `participantId`); Ablo does not model your users, so it's an
245
+ * honest string at the trust boundary. */
246
+ export interface CreateUserSessionParams {
247
+ /** Your end user. `id` becomes the token's `participantId`. */
248
+ user: {
249
+ id: string;
250
+ };
251
+ /** Mint the session into THIS organization instead of the key's own org — the
252
+ * Stripe Connect `Stripe-Account` pattern, for a platform serving many tenants
253
+ * from one backend. Requires the `sk_` to carry the `ephemeral:mint-any-org`
254
+ * scope; omit for the normal single-tenant case. */
255
+ organizationId?: string;
256
+ /** Sync groups this session may subscribe to — typed (`'default'` or
257
+ * `<namespace>:<id>`; build with `syncGroup(kind, id)` from
258
+ * `@abloatai/ablo/schema`). Omit for the server default:
259
+ * `[org:<your org>, user:<user.id>]`. */
260
+ syncGroups?: readonly SyncGroupInput[];
261
+ /** Token lifetime in seconds. Defaults to 900 (15m, the Stripe ephemeral default). */
262
+ ttlSeconds?: number;
263
+ /** Opaque identity blob echoed back to the client as `ablo.user`. */
264
+ userMeta?: Record<string, unknown>;
265
+ agent?: never;
266
+ can?: never;
267
+ }
268
+ /** Mint params for a scoped **agent** session — mints a restricted `rk_` token
269
+ * gated to exactly the operations named in `can`. `can` is typed off your
270
+ * schema (no magic `'task.update'` strings): `{ Task: ['update'], Deck: ['read'] }`
271
+ * — the SDK serializes each entry to the wire allowlist (`task.update`). */
272
+ export interface CreateAgentSessionParams<S extends SchemaRecord> {
273
+ /** Your agent. `id` becomes the token's `participantId`. */
274
+ agent: {
275
+ id: string;
276
+ };
277
+ /** Per-model operation allowlist, typed against the schema's model names. */
278
+ can: Partial<Record<keyof S & string, readonly SessionOperation[]>>;
279
+ /** Sync groups this session may subscribe to — typed (`'default'` or
280
+ * `<namespace>:<id>`; build with `syncGroup(kind, id)` from
281
+ * `@abloatai/ablo/schema`). Omit for the server default: the org
282
+ * anchor (`org:<your org>`) + the agent's own anchor. */
283
+ syncGroups?: readonly SyncGroupInput[];
284
+ /** Token lifetime in seconds. Defaults to 900 (15m, the Stripe ephemeral default). */
285
+ ttlSeconds?: number;
286
+ /** Opaque identity blob echoed back to the client as `ablo.agent`. */
287
+ userMeta?: Record<string, unknown>;
288
+ user?: never;
289
+ }
290
+ /** Params for {@link Ablo.sessions}.create — a discriminated union: pass
291
+ * `{ user }` for a full-authority end-user session (`ek_`) or `{ agent, can }`
292
+ * for a scoped agent session (`rk_`). */
293
+ export type CreateSessionParams<S extends SchemaRecord> = CreateUserSessionParams | CreateAgentSessionParams<S>;
294
+ /** Params for {@link Ablo.agents}.create — a flattened agent descriptor (no
295
+ * `{ agent }` discriminator: `agents.create` only ever mints an agent). Unlike
296
+ * {@link CreateSessionParams} it resolves to a connected, scoped {@link Ablo}
297
+ * client rather than a raw token. */
298
+ export interface CreateAgentClientParams<S extends SchemaRecord> {
299
+ /** Wire participant identity (`agent:<id>`) — what claim exclusion and the
300
+ * FIFO queue gate on. OMIT to get a fresh `crypto.randomUUID()`: a distinct,
301
+ * independent participant (the default, and what you want for concurrent
302
+ * agents). Pass a STABLE string only when one logical agent must re-attach
303
+ * to its own held claims across reconnects/restarts. */
304
+ id?: string;
305
+ /** Human-readable label for logs / attribution (carried in `userMeta.name`).
306
+ * INDEPENDENT of `id`: two agents that share a `name` still receive distinct
307
+ * ids and coordinate as SEPARATE participants — `name` never derives or
308
+ * collapses identity. */
309
+ name?: string;
310
+ /** Per-model operation allowlist, typed against the schema's model names. */
311
+ can: Partial<Record<keyof S & string, readonly SessionOperation[]>>;
312
+ /** Sync groups this agent may subscribe to — typed (`'default'` or
313
+ * `<namespace>:<id>`). Omit for the server default (org anchor + the
314
+ * agent's own anchor). */
315
+ syncGroups?: readonly SyncGroupInput[];
316
+ /** Token lifetime in seconds. Defaults to 900 (15m); the returned client
317
+ * auto-re-mints before expiry, so a long-running agent never handles
318
+ * rotation itself. */
319
+ ttlSeconds?: number;
320
+ /** Extra opaque identity blob echoed on the session scope. Merged with
321
+ * `name` (the `name` param wins if you also set `userMeta.name`). */
322
+ userMeta?: Record<string, unknown>;
323
+ }
324
+ /** A minted session token — the Stripe ephemeral-key / Supabase session
325
+ * resource. `token` is the secret the holder presents as its bearer. */
326
+ export interface AbloSession {
327
+ object: 'session';
328
+ /** Stable id of the minted credential (for revocation). */
329
+ id: string;
330
+ /** The short-lived session token — `ek_` for a `{ user }` session, `rk_`
331
+ * for an `{ agent }` session. Hand this to the participant's runtime. */
332
+ token: string;
333
+ /** ISO-8601 expiry. */
334
+ expiresAt: string;
335
+ organizationId: string;
336
+ scope: {
337
+ organizationId: string;
338
+ syncGroups: readonly string[];
339
+ operations: readonly string[];
340
+ participantKind: 'user' | 'agent' | 'system';
341
+ participantId: string;
342
+ };
343
+ userMeta: Record<string, unknown>;
344
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The shared resource-type surface of the client — `ModelRead`, `ModelClient`,
3
+ * the `Commit*` / `Claim*` shapes, the session-mint params/resource, and the
4
+ * `HttpClaimApi` derivation.
5
+ *
6
+ * Extracted from `Ablo.ts` so `ApiClient.ts` / `httpClient.ts` /
7
+ * `sessionMint.ts` can take the wire-facing types WITHOUT importing the
8
+ * factory module that runtime-imports them back (the 4-cycle cluster madge
9
+ * flagged). This module is type-only — ZERO runtime imports — so importing it
10
+ * can never create a cycle. `Ablo.ts` re-exports everything here, so existing
11
+ * import paths keep resolving.
12
+ */
13
+ export {};
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Schema-derived engine config — `computeFKDepthPriority` (the Tarjan-SCC
3
+ * create-order derivation) and `deriveConfigFromSchema` (the `SyncEngineConfig`
4
+ * the factory seeds the DI context with).
5
+ *
6
+ * Extracted from `Ablo.ts` as a pure leaf: both functions are deterministic
7
+ * schema → value derivations with no engine state.
8
+ */
9
+ import type { Schema } from '../schema/schema.js';
10
+ import type { SyncEngineConfig } from '../interfaces/index.js';
11
+ /**
12
+ * Compute a create-priority map from schema `belongsTo` relations using
13
+ * Tarjan's strongly-connected-components algorithm.
14
+ *
15
+ * The FK graph has an edge `child → parent` for every `belongsTo`. Tarjan
16
+ * runs a single linear DFS that simultaneously (a) detects cycles by
17
+ * grouping mutually-reachable nodes into SCCs and (b) emits those SCCs
18
+ * in reverse topological order of the condensation graph. In this edge
19
+ * convention a "sink" SCC has no outgoing edges — i.e. no parents — so
20
+ * it is an *FK root* (`organizations`, `themes`, etc.). Tarjan emits
21
+ * roots first and leaves last, exactly the order in which rows must be
22
+ * inserted to satisfy FK constraints.
23
+ *
24
+ * Priorities are assigned by emit order: SCC #0 → 10, SCC #1 → 20, …
25
+ * Members of the same SCC share a priority, so insertion order wins the
26
+ * tiebreak inside a cycle (this matters for cyclic schemas like
27
+ * `slideDecks ↔ layouts`, where one direction is the user's chosen
28
+ * "soft" edge — only the consumer's mutator sequence knows which one).
29
+ *
30
+ * This algorithm is iteration-order-independent: starting the DFS from
31
+ * any node yields the same SCC partitioning, and SCCs always come out
32
+ * in valid topological order. The previous DFS-with-memoization
33
+ * heuristic broke under cycles by treating the back-edge as depth 0,
34
+ * which made priorities depend on which node the walk happened to
35
+ * enter the cycle at.
36
+ *
37
+ * Schema authors can mark one side of a cycle with
38
+ * `belongsTo(target, fk, { defer: true })`. Those edges are excluded
39
+ * from the dependency graph entirely, which deterministically breaks
40
+ * the cycle and turns the SCC into a chain — the marked child gets a
41
+ * strictly higher priority than its parent instead of being tied with
42
+ * it. Pair with a Postgres `DEFERRABLE INITIALLY DEFERRED` constraint
43
+ * if you want the database side of the cycle to also relax. See
44
+ * {@link BelongsToOptions.defer}.
45
+ *
46
+ * The returned map is keyed by {@link ModelDef.typename} (falling back
47
+ * to the schema key), because that is what `Model.getModelName()`
48
+ * returns at transaction time — keying by schema key would silently
49
+ * miss the lookup and every model would fall through to
50
+ * `defaultCreatePriority`.
51
+ *
52
+ * Reference: Tarjan, R. (1972), "Depth-first search and linear graph
53
+ * algorithms." Linear in V + E.
54
+ */
55
+ export declare function computeFKDepthPriority(schema: Schema): ReadonlyMap<string, number>;
56
+ export declare function deriveConfigFromSchema(schema: Schema): SyncEngineConfig;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Schema-derived engine config — `computeFKDepthPriority` (the Tarjan-SCC
3
+ * create-order derivation) and `deriveConfigFromSchema` (the `SyncEngineConfig`
4
+ * the factory seeds the DI context with).
5
+ *
6
+ * Extracted from `Ablo.ts` as a pure leaf: both functions are deterministic
7
+ * schema → value derivations with no engine state.
8
+ */
9
+ import { schemaHash } from '../schema/serialize.js';
10
+ // ── Config derivation from schema ─────────────────────────────────────────
11
+ /**
12
+ * Compute a create-priority map from schema `belongsTo` relations using
13
+ * Tarjan's strongly-connected-components algorithm.
14
+ *
15
+ * The FK graph has an edge `child → parent` for every `belongsTo`. Tarjan
16
+ * runs a single linear DFS that simultaneously (a) detects cycles by
17
+ * grouping mutually-reachable nodes into SCCs and (b) emits those SCCs
18
+ * in reverse topological order of the condensation graph. In this edge
19
+ * convention a "sink" SCC has no outgoing edges — i.e. no parents — so
20
+ * it is an *FK root* (`organizations`, `themes`, etc.). Tarjan emits
21
+ * roots first and leaves last, exactly the order in which rows must be
22
+ * inserted to satisfy FK constraints.
23
+ *
24
+ * Priorities are assigned by emit order: SCC #0 → 10, SCC #1 → 20, …
25
+ * Members of the same SCC share a priority, so insertion order wins the
26
+ * tiebreak inside a cycle (this matters for cyclic schemas like
27
+ * `slideDecks ↔ layouts`, where one direction is the user's chosen
28
+ * "soft" edge — only the consumer's mutator sequence knows which one).
29
+ *
30
+ * This algorithm is iteration-order-independent: starting the DFS from
31
+ * any node yields the same SCC partitioning, and SCCs always come out
32
+ * in valid topological order. The previous DFS-with-memoization
33
+ * heuristic broke under cycles by treating the back-edge as depth 0,
34
+ * which made priorities depend on which node the walk happened to
35
+ * enter the cycle at.
36
+ *
37
+ * Schema authors can mark one side of a cycle with
38
+ * `belongsTo(target, fk, { defer: true })`. Those edges are excluded
39
+ * from the dependency graph entirely, which deterministically breaks
40
+ * the cycle and turns the SCC into a chain — the marked child gets a
41
+ * strictly higher priority than its parent instead of being tied with
42
+ * it. Pair with a Postgres `DEFERRABLE INITIALLY DEFERRED` constraint
43
+ * if you want the database side of the cycle to also relax. See
44
+ * {@link BelongsToOptions.defer}.
45
+ *
46
+ * The returned map is keyed by {@link ModelDef.typename} (falling back
47
+ * to the schema key), because that is what `Model.getModelName()`
48
+ * returns at transaction time — keying by schema key would silently
49
+ * miss the lookup and every model would fall through to
50
+ * `defaultCreatePriority`.
51
+ *
52
+ * Reference: Tarjan, R. (1972), "Depth-first search and linear graph
53
+ * algorithms." Linear in V + E.
54
+ */
55
+ export function computeFKDepthPriority(schema) {
56
+ // schemaKey → typename (wire name used at transaction time)
57
+ const keyToTypename = new Map();
58
+ for (const [key, def] of Object.entries(schema.models)) {
59
+ keyToTypename.set(key, def.typename ?? key);
60
+ }
61
+ // Adjacency: schemaKey → parent schema keys pulled from `belongsTo`.
62
+ // Parents not in the schema (e.g. external types) are dropped so the
63
+ // graph stays closed. Edges marked `{ defer: true }` are also
64
+ // dropped — the schema author has declared this side of a cycle to
65
+ // be the "soft" one (insert with null FK, patch later), so the
66
+ // dependency-graph walker treats it as if the edge weren't there.
67
+ // That breaks the cycle deterministically and lets the other side
68
+ // become a strict topological predecessor.
69
+ const parentsOf = new Map();
70
+ for (const [key, def] of Object.entries(schema.models)) {
71
+ const out = [];
72
+ for (const rel of Object.values(def.relations)) {
73
+ if (rel.type !== 'belongsTo')
74
+ continue;
75
+ if (!keyToTypename.has(rel.target))
76
+ continue;
77
+ if (rel.options?.defer === true)
78
+ continue;
79
+ out.push(rel.target);
80
+ }
81
+ parentsOf.set(key, out);
82
+ }
83
+ // Tarjan SCC bookkeeping
84
+ const dfsIndex = new Map();
85
+ const lowlink = new Map();
86
+ const onStack = new Set();
87
+ const stack = [];
88
+ const sccs = [];
89
+ let counter = 0;
90
+ function strongconnect(v) {
91
+ dfsIndex.set(v, counter);
92
+ lowlink.set(v, counter);
93
+ counter++;
94
+ stack.push(v);
95
+ onStack.add(v);
96
+ for (const w of parentsOf.get(v) ?? []) {
97
+ if (!dfsIndex.has(w)) {
98
+ strongconnect(w);
99
+ lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
100
+ }
101
+ else if (onStack.has(w)) {
102
+ // Back-edge into the active DFS path — w is in the same SCC as v.
103
+ lowlink.set(v, Math.min(lowlink.get(v), dfsIndex.get(w)));
104
+ }
105
+ }
106
+ // v is the root of an SCC: pop everything down to v inclusive.
107
+ if (lowlink.get(v) === dfsIndex.get(v)) {
108
+ const component = [];
109
+ let w;
110
+ do {
111
+ w = stack.pop();
112
+ onStack.delete(w);
113
+ component.push(w);
114
+ } while (w !== v);
115
+ sccs.push(component);
116
+ }
117
+ }
118
+ for (const key of keyToTypename.keys()) {
119
+ if (!dfsIndex.has(key))
120
+ strongconnect(key);
121
+ }
122
+ // Tarjan emits SCCs in reverse topological order of the condensation.
123
+ // In our edge convention (child→parent), reverse-topo of the
124
+ // condensation means root-SCCs (no outgoing edges = no parents)
125
+ // first, leaf-SCCs (deepest descendants) last. We could just use
126
+ // emit-order as the priority — but that gives independent sibling
127
+ // SCCs different priorities, which is semantically wrong: siblings
128
+ // don't depend on each other and shouldn't be ordered relative to
129
+ // each other.
130
+ //
131
+ // Instead, do one more pass to compute *longest-path depth* on the
132
+ // condensation DAG: depth(SCC) = max(depth(parent SCC)) + 1, or 0
133
+ // for SCCs with no in-schema parents. SCCs at the same depth get
134
+ // the same priority — siblings stay tied, insertion order in the
135
+ // queue breaks the tie. Priority = (depth + 1) * 10.
136
+ //
137
+ // We can compute this in a single pass over the SCCs because
138
+ // Tarjan's emit-order *is* a valid topological order of the
139
+ // condensation: when we process sccs[i], every parent SCC has
140
+ // already been assigned a depth.
141
+ const nodeToSccIdx = new Map();
142
+ sccs.forEach((scc, i) => {
143
+ for (const node of scc)
144
+ nodeToSccIdx.set(node, i);
145
+ });
146
+ const sccDepth = new Map();
147
+ sccs.forEach((scc, i) => {
148
+ let maxParentDepth = -1;
149
+ for (const node of scc) {
150
+ for (const parent of parentsOf.get(node) ?? []) {
151
+ const parentSccIdx = nodeToSccIdx.get(parent);
152
+ if (parentSccIdx === undefined)
153
+ continue;
154
+ if (parentSccIdx === i)
155
+ continue; // intra-SCC edge — not a dep
156
+ const d = sccDepth.get(parentSccIdx);
157
+ if (d !== undefined && d > maxParentDepth)
158
+ maxParentDepth = d;
159
+ }
160
+ }
161
+ sccDepth.set(i, maxParentDepth + 1);
162
+ });
163
+ const out = new Map();
164
+ sccs.forEach((scc, i) => {
165
+ const priority = (sccDepth.get(i) + 1) * 10;
166
+ for (const key of scc) {
167
+ out.set(keyToTypename.get(key), priority);
168
+ }
169
+ });
170
+ return out;
171
+ }
172
+ export function deriveConfigFromSchema(schema) {
173
+ // Commit payload projection is done directly inside `TransactionQueue`
174
+ // — see `projectCommitPayload` there. Each model's field metadata
175
+ // rides on `ModelRegistry` (populated by `registerModelsFromSchema`),
176
+ // so there's no config-layer shim: the queue asks the registry for
177
+ // the declared fields and serializes accordingly.
178
+ return {
179
+ modelCreatePriority: computeFKDepthPriority(schema),
180
+ defaultCreatePriority: 40,
181
+ defaultNonCreatePriority: 50,
182
+ essentialFields: {},
183
+ classNameFallbackMap: {},
184
+ // Hash this client's schema once so bootstrap can detect drift against the
185
+ // server's active hash (same `schemaHash` the CLI push + server compute).
186
+ expectedSchemaHash: schemaHash(schema),
187
+ };
188
+ }
@@ -1,5 +1,5 @@
1
1
  import type { SchemaRecord } from '../schema/schema.js';
2
- import type { AbloSession, CreateSessionParams } from './Ablo.js';
2
+ import type { AbloSession, CreateSessionParams } from './resourceTypes.js';
3
3
  /** The resolved control-plane context a mint needs. `fetch` is optional — the
4
4
  * auth helpers fall back to the runtime global when omitted. */
5
5
  export interface MintSessionContext {
@@ -10,7 +10,7 @@
10
10
  * because the error messages reference URLs and would mislead if a
11
11
  * URL was actually present.
12
12
  */
13
- import { AbloValidationError } from '../errors.js';
13
+ import { AbloError, AbloValidationError } from '../errors.js';
14
14
  export function validateAbloOptions(input) {
15
15
  const { options, url, configuredApiKey, configuredAuthToken } = input;
16
16
  const kind = options.kind ?? 'user';
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The default `MutationExecutor` — sends `{ type: 'commit', ... }` frames over
3
+ * the engine's own WebSocket, resolved lazily at commit time (the WS doesn't
4
+ * exist when the executor is constructed).
5
+ *
6
+ * Extracted from `Ablo.ts`; the factory wires it up with a `getWs` closure
7
+ * over the store holder unless the caller supplies its own executor.
8
+ */
9
+ import type { StaleNotification, ReadDependency } from '../coordination/schema.js';
10
+ import type { MutationExecutor, MutationOperation } from '../interfaces/index.js';
11
+ /**
12
+ * Default mutation executor: sends `{ type: 'commit', payload: ... }` over
13
+ * the sync engine's own WebSocket.
14
+ *
15
+ * Transport ownership follows the Zero / Liveblocks pattern — the engine
16
+ * owns its socket end-to-end and the executor is internal. Apps pass URLs
17
+ * and auth; they do NOT inject transport callbacks. That's why this
18
+ * factory takes a `getWs` closure instead of a full SyncWebSocket: the WS
19
+ * doesn't exist when the executor is constructed (it's created later in
20
+ * `Ablo` during `BaseSyncedStore` init), so we resolve it
21
+ * lazily at commit time. Same trick Zero uses internally — see
22
+ * `packages/zero-client/src/client/zero.ts` where `Pusher`/`Puller` are
23
+ * constructed before the socket then wired up at connect time.
24
+ *
25
+ * `options.idempotencyKey` becomes the wire-level `clientTxId` when set,
26
+ * matching Stripe-style retry semantics. Otherwise the SDK generates one.
27
+ */
28
+ export declare function createDefaultMutationExecutor(getWs: () => {
29
+ sendCommit?: (operations: readonly MutationOperation[], clientTxId: string, timeoutMs?: number, causedByTaskId?: string | null, reads?: readonly ReadDependency[] | null) => Promise<{
30
+ lastSyncId: number;
31
+ notifications?: StaleNotification[];
32
+ }>;
33
+ } | null): MutationExecutor;