@abloatai/ablo 0.3.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 (278) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +12 -0
  4. package/README.md +230 -0
  5. package/dist/BaseSyncedStore.d.ts +709 -0
  6. package/dist/BaseSyncedStore.js +1843 -0
  7. package/dist/Database.d.ts +344 -0
  8. package/dist/Database.js +1259 -0
  9. package/dist/LazyReferenceCollection.d.ts +181 -0
  10. package/dist/LazyReferenceCollection.js +460 -0
  11. package/dist/Model.d.ts +339 -0
  12. package/dist/Model.js +715 -0
  13. package/dist/ModelRegistry.d.ts +200 -0
  14. package/dist/ModelRegistry.js +535 -0
  15. package/dist/NetworkMonitor.d.ts +27 -0
  16. package/dist/NetworkMonitor.js +73 -0
  17. package/dist/ObjectPool.d.ts +202 -0
  18. package/dist/ObjectPool.js +1106 -0
  19. package/dist/SyncClient.d.ts +489 -0
  20. package/dist/SyncClient.js +1555 -0
  21. package/dist/SyncEngineContext.d.ts +46 -0
  22. package/dist/SyncEngineContext.js +74 -0
  23. package/dist/adapters/alwaysOnline.d.ts +16 -0
  24. package/dist/adapters/alwaysOnline.js +19 -0
  25. package/dist/adapters/inMemoryStorage.d.ts +30 -0
  26. package/dist/adapters/inMemoryStorage.js +94 -0
  27. package/dist/agent/Agent.d.ts +358 -0
  28. package/dist/agent/Agent.js +500 -0
  29. package/dist/agent/index.d.ts +115 -0
  30. package/dist/agent/index.js +128 -0
  31. package/dist/agent/session.d.ts +90 -0
  32. package/dist/agent/session.js +156 -0
  33. package/dist/agent/types.d.ts +73 -0
  34. package/dist/agent/types.js +10 -0
  35. package/dist/ai-sdk/coordination-context.d.ts +51 -0
  36. package/dist/ai-sdk/coordination-context.js +107 -0
  37. package/dist/ai-sdk/index.d.ts +68 -0
  38. package/dist/ai-sdk/index.js +68 -0
  39. package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
  40. package/dist/ai-sdk/intent-broadcast.js +72 -0
  41. package/dist/ai-sdk/wrap.d.ts +67 -0
  42. package/dist/ai-sdk/wrap.js +45 -0
  43. package/dist/api/index.d.ts +10 -0
  44. package/dist/api/index.js +9 -0
  45. package/dist/auth/index.d.ts +137 -0
  46. package/dist/auth/index.js +246 -0
  47. package/dist/client/Ablo.d.ts +835 -0
  48. package/dist/client/Ablo.js +1440 -0
  49. package/dist/client/ApiClient.d.ts +200 -0
  50. package/dist/client/ApiClient.js +659 -0
  51. package/dist/client/auth.d.ts +79 -0
  52. package/dist/client/auth.js +81 -0
  53. package/dist/client/createInternalComponents.d.ts +44 -0
  54. package/dist/client/createInternalComponents.js +88 -0
  55. package/dist/client/createModelProxy.d.ts +152 -0
  56. package/dist/client/createModelProxy.js +199 -0
  57. package/dist/client/identity.d.ts +63 -0
  58. package/dist/client/identity.js +156 -0
  59. package/dist/client/index.d.ts +36 -0
  60. package/dist/client/index.js +33 -0
  61. package/dist/client/persistence.d.ts +7 -0
  62. package/dist/client/persistence.js +11 -0
  63. package/dist/client/validateAbloOptions.d.ts +42 -0
  64. package/dist/client/validateAbloOptions.js +43 -0
  65. package/dist/config/index.d.ts +10 -0
  66. package/dist/config/index.js +12 -0
  67. package/dist/context.d.ts +27 -0
  68. package/dist/context.js +58 -0
  69. package/dist/core/DatabaseManager.d.ts +108 -0
  70. package/dist/core/DatabaseManager.js +361 -0
  71. package/dist/core/QueryProcessor.d.ts +77 -0
  72. package/dist/core/QueryProcessor.js +262 -0
  73. package/dist/core/QueryView.d.ts +64 -0
  74. package/dist/core/QueryView.js +219 -0
  75. package/dist/core/StoreManager.d.ts +131 -0
  76. package/dist/core/StoreManager.js +334 -0
  77. package/dist/core/ViewRegistry.d.ts +20 -0
  78. package/dist/core/ViewRegistry.js +55 -0
  79. package/dist/core/index.d.ts +34 -0
  80. package/dist/core/index.js +59 -0
  81. package/dist/core/openIDBWithTimeout.d.ts +27 -0
  82. package/dist/core/openIDBWithTimeout.js +63 -0
  83. package/dist/core/query-utils.d.ts +37 -0
  84. package/dist/core/query-utils.js +60 -0
  85. package/dist/errors.d.ts +235 -0
  86. package/dist/errors.js +243 -0
  87. package/dist/index.d.ts +41 -0
  88. package/dist/index.js +82 -0
  89. package/dist/interfaces/headless.d.ts +95 -0
  90. package/dist/interfaces/headless.js +41 -0
  91. package/dist/interfaces/index.d.ts +321 -0
  92. package/dist/interfaces/index.js +8 -0
  93. package/dist/mutators/RecordingTransaction.d.ts +36 -0
  94. package/dist/mutators/RecordingTransaction.js +216 -0
  95. package/dist/mutators/Transaction.d.ts +48 -0
  96. package/dist/mutators/Transaction.js +64 -0
  97. package/dist/mutators/UndoManager.d.ts +114 -0
  98. package/dist/mutators/UndoManager.js +143 -0
  99. package/dist/mutators/defineMutators.d.ts +55 -0
  100. package/dist/mutators/defineMutators.js +28 -0
  101. package/dist/policy/index.d.ts +19 -0
  102. package/dist/policy/index.js +18 -0
  103. package/dist/policy/types.d.ts +74 -0
  104. package/dist/policy/types.js +17 -0
  105. package/dist/principal.d.ts +44 -0
  106. package/dist/principal.js +49 -0
  107. package/dist/query/client.d.ts +43 -0
  108. package/dist/query/client.js +84 -0
  109. package/dist/query/index.d.ts +6 -0
  110. package/dist/query/index.js +5 -0
  111. package/dist/query/types.d.ts +143 -0
  112. package/dist/query/types.js +36 -0
  113. package/dist/react/AbloProvider.d.ts +205 -0
  114. package/dist/react/AbloProvider.js +398 -0
  115. package/dist/react/ClientSideSuspense.d.ts +36 -0
  116. package/dist/react/ClientSideSuspense.js +17 -0
  117. package/dist/react/DefaultFallback.d.ts +24 -0
  118. package/dist/react/DefaultFallback.js +43 -0
  119. package/dist/react/SyncGroupProvider.d.ts +19 -0
  120. package/dist/react/SyncGroupProvider.js +44 -0
  121. package/dist/react/context.d.ts +161 -0
  122. package/dist/react/context.js +35 -0
  123. package/dist/react/index.d.ts +64 -0
  124. package/dist/react/index.js +73 -0
  125. package/dist/react/internalContext.d.ts +35 -0
  126. package/dist/react/internalContext.js +3 -0
  127. package/dist/react/useAblo.d.ts +72 -0
  128. package/dist/react/useAblo.js +63 -0
  129. package/dist/react/useCurrentUserId.d.ts +21 -0
  130. package/dist/react/useCurrentUserId.js +33 -0
  131. package/dist/react/useErrorListener.d.ts +20 -0
  132. package/dist/react/useErrorListener.js +39 -0
  133. package/dist/react/useIntent.d.ts +29 -0
  134. package/dist/react/useIntent.js +42 -0
  135. package/dist/react/useMutate.d.ts +83 -0
  136. package/dist/react/useMutate.js +122 -0
  137. package/dist/react/useMutationFailureListener.d.ts +26 -0
  138. package/dist/react/useMutationFailureListener.js +38 -0
  139. package/dist/react/useMutators.d.ts +56 -0
  140. package/dist/react/useMutators.js +66 -0
  141. package/dist/react/usePresence.d.ts +32 -0
  142. package/dist/react/usePresence.js +41 -0
  143. package/dist/react/useQuery.d.ts +123 -0
  144. package/dist/react/useQuery.js +145 -0
  145. package/dist/react/useReactive.d.ts +35 -0
  146. package/dist/react/useReactive.js +111 -0
  147. package/dist/react/useReader.d.ts +69 -0
  148. package/dist/react/useReader.js +73 -0
  149. package/dist/react/useSyncStatus.d.ts +61 -0
  150. package/dist/react/useSyncStatus.js +76 -0
  151. package/dist/react/useUndoScope.d.ts +36 -0
  152. package/dist/react/useUndoScope.js +73 -0
  153. package/dist/realtime/index.d.ts +10 -0
  154. package/dist/realtime/index.js +9 -0
  155. package/dist/schema/field.d.ts +134 -0
  156. package/dist/schema/field.js +264 -0
  157. package/dist/schema/index.d.ts +29 -0
  158. package/dist/schema/index.js +38 -0
  159. package/dist/schema/model.d.ts +326 -0
  160. package/dist/schema/model.js +89 -0
  161. package/dist/schema/queries.d.ts +203 -0
  162. package/dist/schema/queries.js +145 -0
  163. package/dist/schema/relation.d.ts +172 -0
  164. package/dist/schema/relation.js +104 -0
  165. package/dist/schema/schema.d.ts +259 -0
  166. package/dist/schema/schema.js +188 -0
  167. package/dist/schema/sugar.d.ts +129 -0
  168. package/dist/schema/sugar.js +94 -0
  169. package/dist/source/index.d.ts +423 -0
  170. package/dist/source/index.js +320 -0
  171. package/dist/source/pushQueue.d.ts +112 -0
  172. package/dist/source/pushQueue.js +249 -0
  173. package/dist/stores/ObjectStore.d.ts +103 -0
  174. package/dist/stores/ObjectStore.js +371 -0
  175. package/dist/stores/ObjectStoreContract.d.ts +39 -0
  176. package/dist/stores/ObjectStoreContract.js +1 -0
  177. package/dist/stores/SyncActionStore.d.ts +101 -0
  178. package/dist/stores/SyncActionStore.js +481 -0
  179. package/dist/sync/BootstrapHelper.d.ts +127 -0
  180. package/dist/sync/BootstrapHelper.js +434 -0
  181. package/dist/sync/ConnectionManager.d.ts +136 -0
  182. package/dist/sync/ConnectionManager.js +465 -0
  183. package/dist/sync/HydrationCoordinator.d.ts +137 -0
  184. package/dist/sync/HydrationCoordinator.js +468 -0
  185. package/dist/sync/NetworkProbe.d.ts +43 -0
  186. package/dist/sync/NetworkProbe.js +113 -0
  187. package/dist/sync/OfflineFlush.d.ts +9 -0
  188. package/dist/sync/OfflineFlush.js +22 -0
  189. package/dist/sync/OfflineTransactionStore.d.ts +37 -0
  190. package/dist/sync/OfflineTransactionStore.js +263 -0
  191. package/dist/sync/SyncWebSocket.d.ts +663 -0
  192. package/dist/sync/SyncWebSocket.js +1336 -0
  193. package/dist/sync/createIntentStream.d.ts +33 -0
  194. package/dist/sync/createIntentStream.js +243 -0
  195. package/dist/sync/createPresenceStream.d.ts +46 -0
  196. package/dist/sync/createPresenceStream.js +192 -0
  197. package/dist/sync/createSnapshot.d.ts +33 -0
  198. package/dist/sync/createSnapshot.js +124 -0
  199. package/dist/sync/participants.d.ts +114 -0
  200. package/dist/sync/participants.js +336 -0
  201. package/dist/sync/schemas.d.ts +79 -0
  202. package/dist/sync/schemas.js +78 -0
  203. package/dist/testing/fixtures/bootstrap.d.ts +45 -0
  204. package/dist/testing/fixtures/bootstrap.js +53 -0
  205. package/dist/testing/fixtures/deltas.d.ts +86 -0
  206. package/dist/testing/fixtures/deltas.js +139 -0
  207. package/dist/testing/fixtures/models.d.ts +82 -0
  208. package/dist/testing/fixtures/models.js +270 -0
  209. package/dist/testing/helpers/react-wrapper.d.ts +66 -0
  210. package/dist/testing/helpers/react-wrapper.js +64 -0
  211. package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
  212. package/dist/testing/helpers/sync-engine-harness.js +70 -0
  213. package/dist/testing/helpers/wait.d.ts +25 -0
  214. package/dist/testing/helpers/wait.js +44 -0
  215. package/dist/testing/index.d.ts +21 -0
  216. package/dist/testing/index.js +32 -0
  217. package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
  218. package/dist/testing/mocks/MockMutationExecutor.js +139 -0
  219. package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
  220. package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
  221. package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
  222. package/dist/testing/mocks/MockSyncContext.js +100 -0
  223. package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
  224. package/dist/testing/mocks/MockSyncStore.js +171 -0
  225. package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
  226. package/dist/testing/mocks/MockWebSocket.js +117 -0
  227. package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
  228. package/dist/transactions/OptimisticEchoTracker.js +104 -0
  229. package/dist/transactions/TransactionQueue.d.ts +499 -0
  230. package/dist/transactions/TransactionQueue.js +1895 -0
  231. package/dist/transactions/index.d.ts +16 -0
  232. package/dist/transactions/index.js +7 -0
  233. package/dist/transactions/mutation-error-handler.d.ts +5 -0
  234. package/dist/transactions/mutation-error-handler.js +39 -0
  235. package/dist/types/global.d.ts +107 -0
  236. package/dist/types/global.js +38 -0
  237. package/dist/types/index.d.ts +241 -0
  238. package/dist/types/index.js +70 -0
  239. package/dist/types/streams.d.ts +495 -0
  240. package/dist/types/streams.js +11 -0
  241. package/dist/utils/asyncIterator.d.ts +41 -0
  242. package/dist/utils/asyncIterator.js +142 -0
  243. package/dist/utils/duration.d.ts +28 -0
  244. package/dist/utils/duration.js +47 -0
  245. package/dist/utils/mobx-setup.d.ts +42 -0
  246. package/dist/utils/mobx-setup.js +381 -0
  247. package/docs/api-keys.md +24 -0
  248. package/docs/api.md +230 -0
  249. package/docs/audit.md +81 -0
  250. package/docs/capabilities.md +163 -0
  251. package/docs/client-behavior.md +202 -0
  252. package/docs/data-sources.md +214 -0
  253. package/docs/examples/agent-human.md +84 -0
  254. package/docs/examples/ai-sdk-tool.md +92 -0
  255. package/docs/examples/existing-python-backend.md +249 -0
  256. package/docs/examples/nextjs.md +88 -0
  257. package/docs/examples/server-agent.md +86 -0
  258. package/docs/guarantees.md +148 -0
  259. package/docs/index.md +97 -0
  260. package/docs/integration-guide.md +493 -0
  261. package/docs/interaction-model.md +140 -0
  262. package/docs/mcp/claude-code.md +43 -0
  263. package/docs/mcp/cursor.md +53 -0
  264. package/docs/mcp/windsurf.md +46 -0
  265. package/docs/mcp.md +59 -0
  266. package/docs/quickstart.md +152 -0
  267. package/docs/react.md +115 -0
  268. package/docs/roadmap.md +45 -0
  269. package/examples/README.md +54 -0
  270. package/examples/data-source/README.md +102 -0
  271. package/examples/data-source/ablo-driver.ts +89 -0
  272. package/examples/data-source/customer-server.ts +208 -0
  273. package/examples/data-source/run.ts +101 -0
  274. package/examples/data-source/schema.ts +25 -0
  275. package/examples/quickstart.ts +54 -0
  276. package/examples/tsconfig.json +16 -0
  277. package/llms.txt +143 -0
  278. package/package.json +147 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * RecordingTransaction — wraps a base `Transaction` and captures inverse ops
3
+ * for the undo system. Each write is observed BEFORE it runs (to snapshot
4
+ * pre-state) and AFTER (to capture the forward op for redo).
5
+ *
6
+ * The wrapped mutator sees the exact same `Transaction<S>` shape; recording
7
+ * is invisible. When the mutator returns, the caller reads `getEntry()` and
8
+ * pushes it into the active `UndoScope`.
9
+ *
10
+ * Why snapshots live here (not in the UndoScope):
11
+ * - Update inverse requires `prev` field values — must be captured before
12
+ * the write lands in the pool.
13
+ * - Delete inverse requires the full model data — same reason.
14
+ * - Create inverse is simpler (delete by id) but the id must be known
15
+ * post-creation (schema generates UUIDs if caller omitted one).
16
+ */
17
+ import { createTransaction } from './Transaction.js';
18
+ /**
19
+ * Build a transaction that records inverses + forwards as it runs.
20
+ * Consumers use this only when they want the invocation to be undoable;
21
+ * read-only or side-effect-only mutators should use `createTransaction`
22
+ * directly to avoid the bookkeeping overhead.
23
+ */
24
+ export function createRecordingTransaction(schema, store, organizationId) {
25
+ const inverses = [];
26
+ const forwards = [];
27
+ const inner = createTransaction(schema, store, organizationId);
28
+ // Wrap mutations with a Proxy that intercepts each model key's
29
+ // methods. We keep `inner.read` as-is — reads don't need recording.
30
+ const mutateProxy = new Proxy({}, {
31
+ get(_target, prop) {
32
+ if (typeof prop !== 'string')
33
+ return undefined;
34
+ const innerMutate = inner.mutations[prop];
35
+ if (!innerMutate)
36
+ return innerMutate;
37
+ return wrapMutateForKey(prop, innerMutate, store, inverses, forwards);
38
+ },
39
+ });
40
+ return {
41
+ tx: { mutations: mutateProxy, read: inner.read },
42
+ getEntry: (label) => {
43
+ if (inverses.length === 0)
44
+ return null;
45
+ // Undo applies inverses in REVERSE order of how the forward writes ran.
46
+ // Redo applies forwards in the ORIGINAL order.
47
+ return { label, inverses: [...inverses].reverse(), forwards: [...forwards] };
48
+ },
49
+ };
50
+ }
51
+ // ── Per-key wrapper ────────────────────────────────────────────────────────
52
+ function wrapMutateForKey(modelKey, mutate, store, inverses, forwards) {
53
+ const snapshot = (id) => {
54
+ const model = store.pool.get(id);
55
+ if (!model)
56
+ return null;
57
+ // Model.toJSON produces a plain object suitable for re-create. We need
58
+ // ALL fields when generating a delete→create inverse, so toJSON's
59
+ // wider shape is exactly right.
60
+ return model.toJSON();
61
+ };
62
+ const snapshotFields = (id, fieldNames) => {
63
+ const model = store.pool.get(id);
64
+ if (!model)
65
+ return null;
66
+ const out = {};
67
+ // `modifiedProperties` is populated by M1's `observe()` listener the
68
+ // moment the caller mutates an observable field directly. Thanks to
69
+ // `Model.propertyChanged`'s first-old-wins policy, `.old` holds the TRUE
70
+ // pre-session baseline even after many in-place mutations (e.g. a drag
71
+ // frame loop). That makes it the authoritative source for the undo
72
+ // inverse when the caller pre-mutates before invoking the mutator.
73
+ //
74
+ // Fallback chain for models/fields that weren't pre-mutated (so no
75
+ // `modifiedProperties` entry exists yet): `getOriginalSnapshot()`
76
+ // (populated on load/`markAsPersisted`/sync-ack), then the live
77
+ // observable. The live read is correct only when the caller didn't
78
+ // touch the field first.
79
+ const original = model.getOriginalSnapshot();
80
+ for (const f of fieldNames) {
81
+ if (f === 'id')
82
+ continue;
83
+ const mod = model.modifiedProperties.get(f);
84
+ if (mod) {
85
+ out[f] = mod.old;
86
+ }
87
+ else if (original && f in original) {
88
+ out[f] = original[f];
89
+ }
90
+ else {
91
+ out[f] = Reflect.get(model, f);
92
+ }
93
+ }
94
+ return out;
95
+ };
96
+ // After a mutator's `base.update` succeeds, drop the `modifiedProperties`
97
+ // entries we snapshotted from. The next mutator call should see THIS
98
+ // update's result as its baseline, not the pre-session old value. The
99
+ // transaction queue already captured its frozen copy synchronously inside
100
+ // `store.save` (via `captureModelChanges`/`extractPreviousData`), so this
101
+ // clear is safe for server rollback.
102
+ const consumeModifiedFields = (id, fieldNames) => {
103
+ const model = store.pool.get(id);
104
+ if (!model)
105
+ return;
106
+ for (const f of fieldNames) {
107
+ if (f === 'id')
108
+ continue;
109
+ model.modifiedProperties.delete(f);
110
+ }
111
+ };
112
+ return {
113
+ // Overloaded — single row or array. The recorder dispatches the
114
+ // matching forward/inverse op shape (`create`/`createMany`,
115
+ // `update`/`updateMany`, `delete`/`deleteMany`) so the persisted
116
+ // undo entry is symmetric with what was originally invoked.
117
+ create: (async (data) => {
118
+ if (Array.isArray(data)) {
119
+ const created = await mutate.create(data);
120
+ const withIds = created.map((m, i) => ({
121
+ ...data[i],
122
+ id: m.id,
123
+ }));
124
+ const ids = created.map((m) => m.id);
125
+ forwards.push({ kind: 'createMany', modelKey, data: withIds });
126
+ inverses.push({ kind: 'deleteMany', modelKey, ids });
127
+ return created;
128
+ }
129
+ const created = await mutate.create(data);
130
+ const id = created.id;
131
+ forwards.push({
132
+ kind: 'create',
133
+ modelKey,
134
+ data: { ...data, id },
135
+ });
136
+ inverses.push({ kind: 'delete', modelKey, id });
137
+ return created;
138
+ }),
139
+ update: (async (patch) => {
140
+ if (Array.isArray(patch)) {
141
+ // Snapshot all previous values BEFORE applying — later patches
142
+ // in the same list would corrupt the inverse state of earlier
143
+ // ones if we snapshotted lazily.
144
+ const prevPatches = [];
145
+ for (const p of patch) {
146
+ const fields = Object.keys(p).filter((k) => k !== 'id');
147
+ const prev = snapshotFields(p.id, fields);
148
+ if (prev)
149
+ prevPatches.push({ id: p.id, ...prev });
150
+ }
151
+ const updated = await mutate.update(patch);
152
+ const forwardPatches = patch.map((p) => ({ ...p }));
153
+ for (const p of forwardPatches) {
154
+ consumeModifiedFields(p.id, Object.keys(p).filter((k) => k !== 'id'));
155
+ }
156
+ forwards.push({ kind: 'updateMany', modelKey, patches: forwardPatches });
157
+ if (prevPatches.length > 0) {
158
+ inverses.push({ kind: 'updateMany', modelKey, patches: prevPatches });
159
+ }
160
+ return updated;
161
+ }
162
+ const id = patch.id;
163
+ const fields = Object.keys(patch).filter((k) => k !== 'id');
164
+ const prev = snapshotFields(id, fields);
165
+ const updated = await mutate.update(patch);
166
+ const patchCopy = {
167
+ id,
168
+ ...patch,
169
+ };
170
+ consumeModifiedFields(id, fields);
171
+ forwards.push({ kind: 'update', modelKey, patch: patchCopy });
172
+ if (prev) {
173
+ inverses.push({ kind: 'update', modelKey, patch: { id, ...prev } });
174
+ }
175
+ return updated;
176
+ }),
177
+ delete: (async (idOrIds) => {
178
+ if (Array.isArray(idOrIds)) {
179
+ const prevs = idOrIds
180
+ .map((id) => snapshot(id))
181
+ .filter((d) => d !== null);
182
+ await mutate.delete(idOrIds);
183
+ forwards.push({ kind: 'deleteMany', modelKey, ids: [...idOrIds] });
184
+ if (prevs.length > 0) {
185
+ inverses.push({ kind: 'createMany', modelKey, data: prevs });
186
+ }
187
+ return;
188
+ }
189
+ const prev = snapshot(idOrIds);
190
+ await mutate.delete(idOrIds);
191
+ forwards.push({ kind: 'delete', modelKey, id: idOrIds });
192
+ if (prev) {
193
+ inverses.push({ kind: 'create', modelKey, data: prev });
194
+ }
195
+ }),
196
+ archive: async (id) => {
197
+ await mutate.archive(id);
198
+ forwards.push({
199
+ kind: 'update',
200
+ modelKey,
201
+ patch: { id, archivedAt: new Date() },
202
+ });
203
+ // Inverse of archive is unarchive, modeled here as a "restore" update.
204
+ inverses.push({ kind: 'update', modelKey, patch: { id, archivedAt: null } });
205
+ },
206
+ unarchive: async (id) => {
207
+ await mutate.unarchive(id);
208
+ forwards.push({ kind: 'update', modelKey, patch: { id, archivedAt: null } });
209
+ inverses.push({
210
+ kind: 'update',
211
+ modelKey,
212
+ patch: { id, archivedAt: new Date() },
213
+ });
214
+ },
215
+ };
216
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Transaction — Zero-style typed transaction object exposed to custom mutators.
3
+ *
4
+ * A mutator function receives `{ tx, args }`. Through `tx.mutations.<modelKey>.*`
5
+ * it performs writes; through `tx.read.<modelKey>.*` it takes imperative
6
+ * snapshots of the ObjectPool.
7
+ *
8
+ * Semantics:
9
+ * - Writes dispatch eagerly via the existing `createMutateActions` / store
10
+ * primitives (no buffering, no rollback). Partial state is possible if
11
+ * a mutator throws midway. Atomic rollback is a follow-up.
12
+ * - Reads are synchronous snapshots via `createReaderActions`. They use the
13
+ * FK index fast path where available (O(1) on registered FK fields).
14
+ *
15
+ * The mutate surface is intentionally one-row-at-a-time
16
+ * (`create`/`update`/`delete`). For batches, mutator authors compose
17
+ * `Promise.all(rows.map((r) => tx.mutations.x.create(r)))` — every push
18
+ * stages in the same synchronous tick, the await happens once, and the
19
+ * microtask coalescer in `TransactionQueue` collapses N pushes into one
20
+ * wire commit. Same shape Zero uses: no `insertMany`, just an array map.
21
+ */
22
+ import type { Schema } from '../schema/schema.js';
23
+ import type { SyncStoreContract } from '../react/context.js';
24
+ import { type MutateActions } from '../react/useMutate.js';
25
+ import { type ReaderActions, type ReaderFindOptions } from '../react/useReader.js';
26
+ /**
27
+ * The full transaction surface. `tx.mutations.<key>.*` for writes,
28
+ * `tx.read.<key>.*` for imperative reads. Re-exports the base read options
29
+ * type so mutator authors can type `where` payloads without reaching into
30
+ * the React barrel.
31
+ *
32
+ * The name `mutations` (not `mutate`) matches the React hook naming.
33
+ */
34
+ export interface Transaction<S extends Schema> {
35
+ mutations: {
36
+ [K in keyof S['models'] & string]: MutateActions<S, K>;
37
+ };
38
+ read: {
39
+ [K in keyof S['models'] & string]: ReaderActions<S, K>;
40
+ };
41
+ }
42
+ export type { ReaderFindOptions };
43
+ /**
44
+ * Build a Transaction for a single mutator invocation. The returned object
45
+ * lazily instantiates per-model actions on first access so we don't pay for
46
+ * models the mutator never touches.
47
+ */
48
+ export declare function createTransaction<S extends Schema>(schema: S, store: SyncStoreContract, organizationId: string): Transaction<S>;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Transaction — Zero-style typed transaction object exposed to custom mutators.
3
+ *
4
+ * A mutator function receives `{ tx, args }`. Through `tx.mutations.<modelKey>.*`
5
+ * it performs writes; through `tx.read.<modelKey>.*` it takes imperative
6
+ * snapshots of the ObjectPool.
7
+ *
8
+ * Semantics:
9
+ * - Writes dispatch eagerly via the existing `createMutateActions` / store
10
+ * primitives (no buffering, no rollback). Partial state is possible if
11
+ * a mutator throws midway. Atomic rollback is a follow-up.
12
+ * - Reads are synchronous snapshots via `createReaderActions`. They use the
13
+ * FK index fast path where available (O(1) on registered FK fields).
14
+ *
15
+ * The mutate surface is intentionally one-row-at-a-time
16
+ * (`create`/`update`/`delete`). For batches, mutator authors compose
17
+ * `Promise.all(rows.map((r) => tx.mutations.x.create(r)))` — every push
18
+ * stages in the same synchronous tick, the await happens once, and the
19
+ * microtask coalescer in `TransactionQueue` collapses N pushes into one
20
+ * wire commit. Same shape Zero uses: no `insertMany`, just an array map.
21
+ */
22
+ import { createMutateActions } from '../react/useMutate.js';
23
+ import { createReaderActions } from '../react/useReader.js';
24
+ import { AbloValidationError } from '../errors.js';
25
+ /**
26
+ * Build a Transaction for a single mutator invocation. The returned object
27
+ * lazily instantiates per-model actions on first access so we don't pay for
28
+ * models the mutator never touches.
29
+ */
30
+ export function createTransaction(schema, store, organizationId) {
31
+ const mutateCache = new Map();
32
+ const readCache = new Map();
33
+ const mutations = new Proxy({}, {
34
+ get(_target, prop) {
35
+ if (typeof prop !== 'string')
36
+ return undefined;
37
+ const cached = mutateCache.get(prop);
38
+ if (cached)
39
+ return cached;
40
+ if (!(prop in schema.models)) {
41
+ throw new AbloValidationError(`Transaction.mutations: unknown model key "${prop}". Known keys: ${Object.keys(schema.models).join(', ')}`, { code: 'transaction_mutate_unknown_model' });
42
+ }
43
+ const actions = createMutateActions(schema, prop, store, organizationId);
44
+ mutateCache.set(prop, actions);
45
+ return actions;
46
+ },
47
+ });
48
+ const read = new Proxy({}, {
49
+ get(_target, prop) {
50
+ if (typeof prop !== 'string')
51
+ return undefined;
52
+ const cached = readCache.get(prop);
53
+ if (cached)
54
+ return cached;
55
+ if (!(prop in schema.models)) {
56
+ throw new AbloValidationError(`Transaction.read: unknown model key "${prop}". Known keys: ${Object.keys(schema.models).join(', ')}`, { code: 'transaction_read_unknown_model' });
57
+ }
58
+ const actions = createReaderActions(schema, prop, store);
59
+ readCache.set(prop, actions);
60
+ return actions;
61
+ },
62
+ });
63
+ return { mutations, read };
64
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * UndoManager — per-scope history of reversible mutations.
3
+ *
4
+ * Each mutator invocation records an ordered list of inverse operations.
5
+ * On `undo()` we pop the last group and apply the inverses as a non-recorded
6
+ * transaction (so the inverse itself doesn't push to the redo stack; we do
7
+ * that explicitly below).
8
+ *
9
+ * Scopes: every consumer (deck editor, spreadsheet, etc.) gets a named scope
10
+ * via `getScope(name)`. Cmd+Z in one surface never affects another.
11
+ *
12
+ * V1 limitations:
13
+ * - No persistence across sessions (in-memory stack).
14
+ * - No collaborative awareness — undoing after a teammate edited the same
15
+ * row produces a "last writer wins" outcome, not a true merge.
16
+ * - Server-side mutation rejection after optimistic apply does NOT
17
+ * automatically invalidate the undo stack. Consumers should `clear()`
18
+ * the scope on sync error if they want strict correctness.
19
+ */
20
+ import type { Schema } from '../schema/schema.js';
21
+ import type { SyncStoreContract } from '../react/context.js';
22
+ /**
23
+ * A single reversible operation. The runtime captures these during a
24
+ * recorded transaction and replays them (in reverse order) on undo.
25
+ * Model keys and data shapes are stored as strings/records so the manager
26
+ * is schema-agnostic — the transaction it replays through is schema-typed.
27
+ */
28
+ export type InverseOp = {
29
+ kind: 'create';
30
+ modelKey: string;
31
+ data: Record<string, unknown>;
32
+ } | {
33
+ kind: 'update';
34
+ modelKey: string;
35
+ patch: {
36
+ id: string;
37
+ } & Record<string, unknown>;
38
+ } | {
39
+ kind: 'delete';
40
+ modelKey: string;
41
+ id: string;
42
+ } | {
43
+ kind: 'createMany';
44
+ modelKey: string;
45
+ data: Record<string, unknown>[];
46
+ } | {
47
+ kind: 'updateMany';
48
+ modelKey: string;
49
+ patches: Array<{
50
+ id: string;
51
+ } & Record<string, unknown>>;
52
+ } | {
53
+ kind: 'deleteMany';
54
+ modelKey: string;
55
+ ids: string[];
56
+ };
57
+ /** One undo entry = one mutator invocation's set of inverses, in reverse order. */
58
+ export interface UndoEntry {
59
+ /** Optional label for diagnostics / UI ("Move layer", "Delete slide", etc). */
60
+ label?: string;
61
+ inverses: InverseOp[];
62
+ /**
63
+ * Paired forward ops, captured at record time so redo can replay them
64
+ * without re-running the user's mutator (which may have non-idempotent
65
+ * side effects like generating new IDs).
66
+ */
67
+ forwards: InverseOp[];
68
+ }
69
+ export interface UndoScopeOptions {
70
+ /** Max number of undo entries. Older entries drop off the bottom. Default: 100. */
71
+ maxHistory?: number;
72
+ }
73
+ /**
74
+ * A single undo stack for one surface. Access via `UndoManager.getScope(name)`.
75
+ * Consumers call `record(entry)` after each mutator; `undo()` / `redo()` to
76
+ * traverse the stacks.
77
+ */
78
+ export declare class UndoScope<S extends Schema> {
79
+ private readonly schema;
80
+ private readonly store;
81
+ private readonly organizationId;
82
+ private undoStack;
83
+ private redoStack;
84
+ private readonly maxHistory;
85
+ constructor(schema: S, store: SyncStoreContract, organizationId: string, options?: UndoScopeOptions);
86
+ /** Internal: record a mutator's inverses. Clears the redo stack. */
87
+ record(entry: UndoEntry): void;
88
+ canUndo(): boolean;
89
+ canRedo(): boolean;
90
+ /** Pop the last mutator and apply its inverses. Pushes to redo. */
91
+ undo(): Promise<void>;
92
+ /** Pop the last undone entry and re-apply the forward ops. Pushes to undo. */
93
+ redo(): Promise<void>;
94
+ /** Drop all history. Use after bootstrap / sync group change / sync error. */
95
+ clear(): void;
96
+ /** Introspection — for debug panels / e2e tests. */
97
+ size(): {
98
+ undo: number;
99
+ redo: number;
100
+ };
101
+ }
102
+ /**
103
+ * Central registry of named undo scopes. One per-app instance, created once
104
+ * during engine setup. Mutator invocations find their scope by name.
105
+ */
106
+ export declare class UndoManager<S extends Schema> {
107
+ private readonly schema;
108
+ private readonly store;
109
+ private readonly organizationId;
110
+ private readonly scopes;
111
+ constructor(schema: S, store: SyncStoreContract, organizationId: string);
112
+ getScope(name: string, options?: UndoScopeOptions): UndoScope<S>;
113
+ clearAll(): void;
114
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * UndoManager — per-scope history of reversible mutations.
3
+ *
4
+ * Each mutator invocation records an ordered list of inverse operations.
5
+ * On `undo()` we pop the last group and apply the inverses as a non-recorded
6
+ * transaction (so the inverse itself doesn't push to the redo stack; we do
7
+ * that explicitly below).
8
+ *
9
+ * Scopes: every consumer (deck editor, spreadsheet, etc.) gets a named scope
10
+ * via `getScope(name)`. Cmd+Z in one surface never affects another.
11
+ *
12
+ * V1 limitations:
13
+ * - No persistence across sessions (in-memory stack).
14
+ * - No collaborative awareness — undoing after a teammate edited the same
15
+ * row produces a "last writer wins" outcome, not a true merge.
16
+ * - Server-side mutation rejection after optimistic apply does NOT
17
+ * automatically invalidate the undo stack. Consumers should `clear()`
18
+ * the scope on sync error if they want strict correctness.
19
+ */
20
+ import { createTransaction } from './Transaction.js';
21
+ /**
22
+ * A single undo stack for one surface. Access via `UndoManager.getScope(name)`.
23
+ * Consumers call `record(entry)` after each mutator; `undo()` / `redo()` to
24
+ * traverse the stacks.
25
+ */
26
+ export class UndoScope {
27
+ schema;
28
+ store;
29
+ organizationId;
30
+ undoStack = [];
31
+ redoStack = [];
32
+ maxHistory;
33
+ constructor(schema, store, organizationId, options = {}) {
34
+ this.schema = schema;
35
+ this.store = store;
36
+ this.organizationId = organizationId;
37
+ this.maxHistory = options.maxHistory ?? 100;
38
+ }
39
+ /** Internal: record a mutator's inverses. Clears the redo stack. */
40
+ record(entry) {
41
+ this.undoStack.push(entry);
42
+ if (this.undoStack.length > this.maxHistory)
43
+ this.undoStack.shift();
44
+ this.redoStack = [];
45
+ }
46
+ canUndo() {
47
+ return this.undoStack.length > 0;
48
+ }
49
+ canRedo() {
50
+ return this.redoStack.length > 0;
51
+ }
52
+ /** Pop the last mutator and apply its inverses. Pushes to redo. */
53
+ async undo() {
54
+ const entry = this.undoStack.pop();
55
+ if (!entry)
56
+ return;
57
+ const tx = createTransaction(this.schema, this.store, this.organizationId);
58
+ await applyOps(tx, entry.inverses);
59
+ this.redoStack.push(entry);
60
+ if (this.redoStack.length > this.maxHistory)
61
+ this.redoStack.shift();
62
+ }
63
+ /** Pop the last undone entry and re-apply the forward ops. Pushes to undo. */
64
+ async redo() {
65
+ const entry = this.redoStack.pop();
66
+ if (!entry)
67
+ return;
68
+ const tx = createTransaction(this.schema, this.store, this.organizationId);
69
+ await applyOps(tx, entry.forwards);
70
+ this.undoStack.push(entry);
71
+ if (this.undoStack.length > this.maxHistory)
72
+ this.undoStack.shift();
73
+ }
74
+ /** Drop all history. Use after bootstrap / sync group change / sync error. */
75
+ clear() {
76
+ this.undoStack = [];
77
+ this.redoStack = [];
78
+ }
79
+ /** Introspection — for debug panels / e2e tests. */
80
+ size() {
81
+ return { undo: this.undoStack.length, redo: this.redoStack.length };
82
+ }
83
+ }
84
+ // ── Manager ────────────────────────────────────────────────────────────────
85
+ /**
86
+ * Central registry of named undo scopes. One per-app instance, created once
87
+ * during engine setup. Mutator invocations find their scope by name.
88
+ */
89
+ export class UndoManager {
90
+ schema;
91
+ store;
92
+ organizationId;
93
+ scopes = new Map();
94
+ constructor(schema, store, organizationId) {
95
+ this.schema = schema;
96
+ this.store = store;
97
+ this.organizationId = organizationId;
98
+ }
99
+ getScope(name, options) {
100
+ let scope = this.scopes.get(name);
101
+ if (!scope) {
102
+ scope = new UndoScope(this.schema, this.store, this.organizationId, options);
103
+ this.scopes.set(name, scope);
104
+ }
105
+ return scope;
106
+ }
107
+ clearAll() {
108
+ for (const scope of this.scopes.values())
109
+ scope.clear();
110
+ }
111
+ }
112
+ // ── Internal helpers ───────────────────────────────────────────────────────
113
+ /**
114
+ * Replay a list of InverseOps through a Transaction. Used by both undo
115
+ * (replaying captured inverses) and redo (replaying the captured forwards).
116
+ * Every op is awaited sequentially to preserve ordering guarantees.
117
+ */
118
+ async function applyOps(tx, ops) {
119
+ const mutateAny = tx.mutations;
120
+ for (const op of ops) {
121
+ const m = mutateAny[op.modelKey];
122
+ switch (op.kind) {
123
+ case 'create':
124
+ await m.create(op.data);
125
+ break;
126
+ case 'update':
127
+ await m.update(op.patch);
128
+ break;
129
+ case 'delete':
130
+ await m.delete(op.id);
131
+ break;
132
+ case 'createMany':
133
+ await m.create(op.data);
134
+ break;
135
+ case 'updateMany':
136
+ await m.update(op.patches);
137
+ break;
138
+ case 'deleteMany':
139
+ await m.delete(op.ids);
140
+ break;
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * defineMutators — Zero-style custom mutator declaration.
3
+ *
4
+ * Consumers declare a tree of named mutators grouped by model key. Each
5
+ * mutator is a plain async function that receives `{ tx, args }`. The body
6
+ * composes any number of `tx.mutate.*` / `tx.read.*` calls to implement a
7
+ * named operation (e.g. `slides.createWithLayers`).
8
+ *
9
+ * This file is pure type scaffolding + a pass-through factory. The runtime
10
+ * dispatcher lives in `./Transaction` (the `tx` object) and
11
+ * `../react/useMutators` (the React-side invoker builder). Keeping those
12
+ * concerns separate makes the types trivially inferable at the call site:
13
+ * `defineMutators(schema, { ... })` returns the literal object the consumer
14
+ * wrote, so `typeof mutators` carries every mutator's exact `args`/result
15
+ * signature into `useMutators`.
16
+ */
17
+ import type { Schema } from '../schema/schema.js';
18
+ import type { Transaction } from './Transaction.js';
19
+ /**
20
+ * Signature of a single custom mutator. The host injects `tx`; the consumer
21
+ * controls `args` (whatever shape they want) and the resolved return value.
22
+ *
23
+ * We bound `TArgs`/`TResult` with `unknown` rather than `any` so consumers
24
+ * opt into the inference they need — the `MutatorDefs` record relaxes to
25
+ * `unknown` to let heterogeneous mutator trees unify without `any`.
26
+ */
27
+ export type MutatorFn<S extends Schema, TArgs, TResult = void> = (options: {
28
+ tx: Transaction<S>;
29
+ args: TArgs;
30
+ }) => Promise<TResult>;
31
+ /**
32
+ * The shape `defineMutators` accepts: an optional record per model key whose
33
+ * values are named mutator functions.
34
+ *
35
+ * We intentionally use `unknown` in the bounds rather than `any` to preserve
36
+ * type-safety at the public API boundary. When a consumer writes their
37
+ * mutators inline, TypeScript infers the concrete `TArgs`/`TResult` for each
38
+ * function — the `unknown` here is just a ceiling, not what the consumer
39
+ * ends up seeing.
40
+ */
41
+ export type MutatorDefs<S extends Schema> = {
42
+ [K in keyof S['models']]?: {
43
+ [mutatorName: string]: MutatorFn<S, never, unknown>;
44
+ };
45
+ };
46
+ /**
47
+ * Identity function that forwards the mutators object while constraining its
48
+ * shape against the schema. The `S` generic pins model keys; the `M` generic
49
+ * is `const`-inferred so each mutator's literal signature survives.
50
+ *
51
+ * Pattern mirrors Zero's own `defineMutators` / `createBuilder` — there is
52
+ * no runtime work to do here, it's purely a location for type inference to
53
+ * anchor.
54
+ */
55
+ export declare function defineMutators<S extends Schema, const M extends MutatorDefs<S>>(_schema: S, mutators: M): M;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * defineMutators — Zero-style custom mutator declaration.
3
+ *
4
+ * Consumers declare a tree of named mutators grouped by model key. Each
5
+ * mutator is a plain async function that receives `{ tx, args }`. The body
6
+ * composes any number of `tx.mutate.*` / `tx.read.*` calls to implement a
7
+ * named operation (e.g. `slides.createWithLayers`).
8
+ *
9
+ * This file is pure type scaffolding + a pass-through factory. The runtime
10
+ * dispatcher lives in `./Transaction` (the `tx` object) and
11
+ * `../react/useMutators` (the React-side invoker builder). Keeping those
12
+ * concerns separate makes the types trivially inferable at the call site:
13
+ * `defineMutators(schema, { ... })` returns the literal object the consumer
14
+ * wrote, so `typeof mutators` carries every mutator's exact `args`/result
15
+ * signature into `useMutators`.
16
+ */
17
+ /**
18
+ * Identity function that forwards the mutators object while constraining its
19
+ * shape against the schema. The `S` generic pins model keys; the `M` generic
20
+ * is `const`-inferred so each mutator's literal signature survives.
21
+ *
22
+ * Pattern mirrors Zero's own `defineMutators` / `createBuilder` — there is
23
+ * no runtime work to do here, it's purely a location for type inference to
24
+ * anchor.
25
+ */
26
+ export function defineMutators(_schema, mutators) {
27
+ return mutators;
28
+ }