@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,76 @@
1
+ 'use client';
2
+ import { useCallback } from 'react';
3
+ import { useSyncContext } from './context.js';
4
+ import { useReactive } from './useReactive.js';
5
+ /**
6
+ * Reactive sync-status hook. Bridges MobX `store.syncStatus` +
7
+ * `store.isReady` into React via `useReactive` — concurrent-render
8
+ * safe and immune to the React #185 "getSnapshot should be cached"
9
+ * infinite-loop class of bugs.
10
+ *
11
+ * @example
12
+ * function StatusPill() {
13
+ * const status = useSyncStatus();
14
+ * switch (status.name) {
15
+ * case 'initial':
16
+ * case 'connecting': return <Pill progress={status.name === 'connecting' ? status.progress : 0}>Loading…</Pill>;
17
+ * case 'connected': return status.hasUnsyncedChanges ? <Pill>Saving…</Pill> : null;
18
+ * case 'reconnecting': return <Pill title={status.reason}>Reconnecting…</Pill>;
19
+ * case 'disconnected': return <Pill title={status.reason}>Offline</Pill>;
20
+ * case 'needs-auth': return null;
21
+ * }
22
+ * }
23
+ */
24
+ export function useSyncStatus() {
25
+ const { store } = useSyncContext();
26
+ // `useReactive` tracks the MobX observables read inside deriveStatus
27
+ // (syncStatus.state/progress/pendingChanges/isSessionError/error +
28
+ // the computed isReady), caches the result by shape, and only
29
+ // notifies React when a variant transition actually occurs.
30
+ //
31
+ // Stabilize the closure on `store` identity so useReactive's
32
+ // swap-detection doesn't see a "swap" every render and unnecessarily
33
+ // re-subscribe its MobX reaction.
34
+ const compute = useCallback(() => deriveStatus(store), [store]);
35
+ return useReactive(compute, sameSnapshot);
36
+ }
37
+ /** Map the current store state into the discriminated union. */
38
+ function deriveStatus(store) {
39
+ const { state, progress, pendingChanges, isSessionError, error } = store.syncStatus;
40
+ if (isSessionError) {
41
+ return { name: 'needs-auth' };
42
+ }
43
+ if (state === 'reconnecting') {
44
+ return { name: 'reconnecting', reason: error?.message };
45
+ }
46
+ if (state === 'offline') {
47
+ return { name: 'disconnected', reason: 'offline' };
48
+ }
49
+ if (state === 'error') {
50
+ return { name: 'disconnected', reason: error?.message };
51
+ }
52
+ if (store.isReady) {
53
+ return { name: 'connected', hasUnsyncedChanges: pendingChanges > 0 };
54
+ }
55
+ // state is 'idle' or 'syncing' and not yet ready — bootstrap underway.
56
+ if (state === 'idle' || state === 'syncing') {
57
+ return { name: 'connecting', progress };
58
+ }
59
+ return { name: 'initial' };
60
+ }
61
+ function sameSnapshot(a, b) {
62
+ if (a.name !== b.name)
63
+ return false;
64
+ switch (a.name) {
65
+ case 'initial':
66
+ case 'needs-auth':
67
+ return true;
68
+ case 'connecting':
69
+ return a.progress === b.progress;
70
+ case 'connected':
71
+ return a.hasUnsyncedChanges === b.hasUnsyncedChanges;
72
+ case 'reconnecting':
73
+ case 'disconnected':
74
+ return a.reason === b.reason;
75
+ }
76
+ }
@@ -0,0 +1,36 @@
1
+ import type { Schema } from '../schema/schema.js';
2
+ import type { UndoScope, UndoScopeOptions } from '../mutators/UndoManager.js';
3
+ import type { ResolveSchema } from '../types/global.js';
4
+ /**
5
+ * useUndoScope — per-surface undo/redo for mutator invocations.
6
+ *
7
+ * Zero deliberately does NOT ship a built-in undo API; consumers build one
8
+ * on top of mutation tracking. This is ours.
9
+ *
10
+ * Each named scope owns an independent undo/redo stack. Wire the returned
11
+ * `scope` into `useMutators(schema, mutators, { undoScope: scope })` and the
12
+ * invocations become recorded. `undo()` / `redo()` replay the inverses /
13
+ * forwards as new transactions that do NOT re-record (the manager pushes
14
+ * them between the two stacks explicitly).
15
+ *
16
+ * @example
17
+ * const { undo, redo, canUndo, canRedo, scope } = useUndoScope('deck-editor');
18
+ * const mutate = useMutators(schema, deckMutators, { undoScope: scope });
19
+ *
20
+ * // Cmd+Z handler
21
+ * useHotkey('mod+z', () => { if (canUndo) void undo(); });
22
+ */
23
+ export interface UseUndoScopeResult<S extends Schema> {
24
+ /** Pass to `useMutators(..., { undoScope })` to enable recording. */
25
+ scope: UndoScope<S>;
26
+ undo: () => Promise<void>;
27
+ redo: () => Promise<void>;
28
+ canUndo: boolean;
29
+ canRedo: boolean;
30
+ /** Drop history. Use after sync errors / auth context changes. */
31
+ clear: () => void;
32
+ }
33
+ /** Per-surface undo/redo (explicit schema arg). */
34
+ export declare function useUndoScope<S extends Schema>(schema: S, name: string, options?: UndoScopeOptions): UseUndoScopeResult<S>;
35
+ /** Per-surface undo/redo via the `AbloSync` global augmentation. */
36
+ export declare function useUndoScope(name: string, options?: UndoScopeOptions): UseUndoScopeResult<ResolveSchema extends Schema ? ResolveSchema : Schema>;
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { UndoManager } from '../mutators/UndoManager.js';
4
+ import { useSyncContext } from './context.js';
5
+ import { AbloValidationError } from '../errors.js';
6
+ // Module-level weak registry: `SyncStoreContract` → `UndoManager`.
7
+ // A single app wiring through one SyncProvider shares one manager across
8
+ // every useUndoScope call, so scopes with the same name are identity-equal.
9
+ // Storage erases the generic — `UndoManager<S>` is invariant in S, so
10
+ // the WeakMap can't hold the precise type alongside the typed factory
11
+ // signature. We re-assert at retrieval; the contract is "scope keys
12
+ // are unique per-app and the schema is the same across every call
13
+ // for that key."
14
+ const managers = new WeakMap();
15
+ function getManager(key, factory) {
16
+ let m = managers.get(key);
17
+ if (!m) {
18
+ // Generic-erasure boundary at storage. Concentrating the cast
19
+ // here so the retrieval path doesn't have to repeat it.
20
+ const created = factory();
21
+ managers.set(key, created);
22
+ m = created;
23
+ }
24
+ // Single typed cast at retrieval — `as UndoManager<S>` would be
25
+ // rejected (TS sees `UndoManager<Schema>` and `UndoManager<S>` as
26
+ // unrelated), but we're at the runtime/static schema-identity
27
+ // boundary the contract above pins.
28
+ return m;
29
+ }
30
+ export function useUndoScope(schemaOrName, nameOrOptions, maybeOptions) {
31
+ const { store, organizationId, schema: ctxSchema } = useSyncContext();
32
+ const isExplicit = typeof schemaOrName !== 'string';
33
+ const schema = isExplicit ? schemaOrName : ctxSchema;
34
+ const name = isExplicit ? nameOrOptions : schemaOrName;
35
+ const options = (isExplicit ? maybeOptions : nameOrOptions);
36
+ if (!schema) {
37
+ throw new AbloValidationError('useUndoScope: no schema available. Pass the schema as the first arg ' +
38
+ 'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'undo_scope_schema_missing' });
39
+ }
40
+ const scope = useMemo(() => {
41
+ // Store is the identity for the manager — one per SyncProvider.
42
+ const manager = getManager(store, () => new UndoManager(schema, store, organizationId));
43
+ return manager.getScope(name, options);
44
+ // eslint-disable-next-line react-hooks/exhaustive-deps
45
+ }, [store, organizationId, name]);
46
+ // Local tick forces re-render after undo/redo/clear so canUndo/canRedo
47
+ // reflect the new stack sizes. The scope itself doesn't emit React-
48
+ // friendly notifications; callers that want cross-component reactivity
49
+ // can wire a mobx observable or custom event bus on top.
50
+ const [, setTick] = useState(0);
51
+ // Reset tick when scope identity changes (new store / new orgId).
52
+ useEffect(() => {
53
+ setTick(0);
54
+ }, [scope]);
55
+ const size = scope.size();
56
+ return {
57
+ scope,
58
+ undo: async () => {
59
+ await scope.undo();
60
+ setTick((t) => t + 1);
61
+ },
62
+ redo: async () => {
63
+ await scope.redo();
64
+ setTick((t) => t + 1);
65
+ },
66
+ canUndo: size.undo > 0,
67
+ canRedo: size.redo > 0,
68
+ clear: () => {
69
+ scope.clear();
70
+ setTick((t) => t + 1);
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Internal compatibility entrypoint for the schema-powered realtime client.
3
+ *
4
+ * Use this build for applications that need typed model proxies,
5
+ * subscriptions, presence, offline queueing, and a long-lived WebSocket.
6
+ */
7
+ export { Ablo, computeFKDepthPriority } from '../client/Ablo.js';
8
+ export type { AbloOptions, InternalAbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ModelOperations, } from '../client/Ablo.js';
9
+ import { Ablo } from '../client/Ablo.js';
10
+ export default Ablo;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Internal compatibility entrypoint for the schema-powered realtime client.
3
+ *
4
+ * Use this build for applications that need typed model proxies,
5
+ * subscriptions, presence, offline queueing, and a long-lived WebSocket.
6
+ */
7
+ export { Ablo, computeFKDepthPriority } from '../client/Ablo.js';
8
+ import { Ablo } from '../client/Ablo.js';
9
+ export default Ablo;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Schema Field Helpers
3
+ *
4
+ * Thin wrappers around Zod that add sync-engine metadata (type tag, indexed).
5
+ * Metadata is stored in `z.describe()` as a JSON-encoded string so it
6
+ * survives `.optional()`, `.nullable()`, and `.default()` chain calls.
7
+ *
8
+ * Usage:
9
+ * import { field } from '@ablo/sync-engine/schema';
10
+ *
11
+ * const tasks = model({
12
+ * title: field.string(),
13
+ * projectId: field.string().indexed(), // fluent chain
14
+ * priority: field.number().optional(),
15
+ * status: field.enum(['todo', 'doing', 'done']),
16
+ * });
17
+ *
18
+ * Or use Zod directly (no metadata, but still works):
19
+ * import { z } from 'zod';
20
+ *
21
+ * const tasks = model({
22
+ * title: z.string(),
23
+ * });
24
+ */
25
+ import { z } from 'zod';
26
+ /** Runtime metadata for a schema field, readable via `ModelDef.fields`. */
27
+ export interface FieldMeta {
28
+ /** Sync-engine type tag (maps to storage/serialization hints). */
29
+ type: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'json';
30
+ /** Whether the field was marked optional via `.optional()` or `.nullable()`. */
31
+ isOptional: boolean;
32
+ /** Whether the field was marked indexed via `.indexed()`. */
33
+ isIndexed: boolean;
34
+ /** For enums: the allowed values. */
35
+ enumValues?: readonly string[];
36
+ }
37
+ /**
38
+ * Extract FieldMeta from a Zod schema. Returns null if no sync-engine
39
+ * metadata is attached (e.g., raw `z.string()` usage).
40
+ *
41
+ * Walks through `.optional()` and `.nullable()` wrappers to find the
42
+ * underlying description.
43
+ */
44
+ export declare function getFieldMeta(schema: z.ZodType): FieldMeta | null;
45
+ /**
46
+ * Fallback: infer FieldMeta directly from a raw Zod schema when no
47
+ * `field.*()` metadata was attached.
48
+ *
49
+ * Walks through `.optional()` / `.nullable()` / `.default()` wrappers
50
+ * to find the inner primitive, then maps Zod's `_def.typeName` to
51
+ * the sync-engine type tag. Used by `resolveFieldMeta` and by
52
+ * `model()` / `query()` at definition time.
53
+ *
54
+ * Kept as an internal helper rather than exported directly — the
55
+ * public API is `resolveFieldMeta`, which combines this fallback
56
+ * with the `getFieldMeta` fast path.
57
+ */
58
+ export declare function inferFieldMetaFromZod(schema: z.ZodType): FieldMeta;
59
+ /**
60
+ * Resolve FieldMeta for any Zod schema — whether it was built with
61
+ * `field.*()` (which attaches sync-engine metadata) or with raw Zod
62
+ * (which requires fallback inference from `_def.typeName`).
63
+ *
64
+ * This is the single public entry point for "given a Zod field, tell
65
+ * me its sync-engine type tag and optionality." Both `model()` and
66
+ * `query()` use it to populate their `fields` / `inputFields` maps at
67
+ * definition time, and the schema serializer reads those maps at
68
+ * serialization time.
69
+ *
70
+ * Contract: always returns a value. Never returns null. Unknown Zod
71
+ * types fall through to `'string'` — this is intentional and matches
72
+ * the existing behavior that was previously duplicated in
73
+ * `model.ts:inferMetaFromZod`.
74
+ */
75
+ export declare function resolveFieldMeta(schema: z.ZodType): FieldMeta;
76
+ export declare const field: {
77
+ /** String field */
78
+ readonly string: () => z.ZodString & {
79
+ indexed(): z.ZodString;
80
+ };
81
+ /** Number field */
82
+ readonly number: () => z.ZodNumber & {
83
+ indexed(): z.ZodNumber;
84
+ };
85
+ /** Boolean field */
86
+ readonly boolean: () => z.ZodBoolean & {
87
+ indexed(): z.ZodBoolean;
88
+ };
89
+ /** Date field */
90
+ readonly date: () => z.ZodDate & {
91
+ indexed(): z.ZodDate;
92
+ };
93
+ /** Enum field with constrained string values */
94
+ readonly enum: <const T extends readonly [string, ...string[]]>(values: T) => z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never> & {
95
+ indexed(): z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_2 ? { [k in keyof T_2]: T_2[k]; } : never>;
96
+ };
97
+ /**
98
+ * JSON field. Three call shapes:
99
+ *
100
+ * ```ts
101
+ * field.json() // unknown JSON blob
102
+ * field.json(z.array(z.string())) // typed JSON with Zod schema
103
+ * field.json({ icon: z.string().default('default') }) // typed sub-properties with defaults
104
+ * ```
105
+ *
106
+ * The third form is the key DX feature for metadata fields. It wraps the
107
+ * plain object in `z.object()` automatically, and the model runtime generates
108
+ * a `${field}Json` getter that parses the JSON string on read, applies Zod
109
+ * defaults, and caches the result.
110
+ *
111
+ * Example:
112
+ * ```ts
113
+ * const slideDecks = model({
114
+ * metadata: field.json({
115
+ * icon: z.string().default('presentation'),
116
+ * color: z.string().default('#F59E0B'),
117
+ * summary: z.string().optional(),
118
+ * }),
119
+ * });
120
+ *
121
+ * // At runtime:
122
+ * deck.metadata // raw JSON string (unchanged)
123
+ * deck.metadataJson // { icon: 'presentation', color: '#F59E0B', summary: undefined }
124
+ * deck.metadataJson.icon // 'presentation' (typed, with default)
125
+ * ```
126
+ */
127
+ readonly json: <T extends z.ZodType = z.ZodUnknown>(schemaOrShape?: T | z.ZodRawShape) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> & {
128
+ indexed(): z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
129
+ };
130
+ /** Indexed string field (shorthand for `field.string().indexed()`). */
131
+ readonly id: () => z.ZodString;
132
+ };
133
+ /** Mark a Zod schema as indexed for fast lookups (function form). */
134
+ export declare function indexed<T extends z.ZodType>(schema: T): T;
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Schema Field Helpers
3
+ *
4
+ * Thin wrappers around Zod that add sync-engine metadata (type tag, indexed).
5
+ * Metadata is stored in `z.describe()` as a JSON-encoded string so it
6
+ * survives `.optional()`, `.nullable()`, and `.default()` chain calls.
7
+ *
8
+ * Usage:
9
+ * import { field } from '@ablo/sync-engine/schema';
10
+ *
11
+ * const tasks = model({
12
+ * title: field.string(),
13
+ * projectId: field.string().indexed(), // fluent chain
14
+ * priority: field.number().optional(),
15
+ * status: field.enum(['todo', 'doing', 'done']),
16
+ * });
17
+ *
18
+ * Or use Zod directly (no metadata, but still works):
19
+ * import { z } from 'zod';
20
+ *
21
+ * const tasks = model({
22
+ * title: z.string(),
23
+ * });
24
+ */
25
+ import { z } from 'zod';
26
+ // ── Helpers ───────────────────────────────────────────────────────────────
27
+ /** Distinguish a Zod schema from a plain object shape (ZodRawShape). */
28
+ function isZodSchema(value) {
29
+ return (typeof value === 'object' &&
30
+ value !== null &&
31
+ '_def' in value &&
32
+ typeof value._def === 'object');
33
+ }
34
+ // ── Metadata encoding ─────────────────────────────────────────────────────
35
+ //
36
+ // We stash metadata in `.describe('__sync:{json}')` so it rides along with
37
+ // the Zod schema through `.optional()`, `.nullable()`, etc. At schema-build
38
+ // time we parse it back out into structured FieldMeta.
39
+ const META_PREFIX = '__sync:';
40
+ function encodeMeta(meta) {
41
+ return META_PREFIX + JSON.stringify(meta);
42
+ }
43
+ function decodeMeta(description) {
44
+ if (!description || !description.startsWith(META_PREFIX))
45
+ return null;
46
+ try {
47
+ return JSON.parse(description.slice(META_PREFIX.length));
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /**
54
+ * Extract FieldMeta from a Zod schema. Returns null if no sync-engine
55
+ * metadata is attached (e.g., raw `z.string()` usage).
56
+ *
57
+ * Walks through `.optional()` and `.nullable()` wrappers to find the
58
+ * underlying description.
59
+ */
60
+ export function getFieldMeta(schema) {
61
+ let current = schema;
62
+ let isOptional = false;
63
+ // Unwrap optional / nullable / default to reach the inner type
64
+ // (these are the wrappers that preserve .describe() but may hide it).
65
+ // `instanceof` keeps the narrowing typed; no `_def` digging.
66
+ const MAX_UNWRAP = 5;
67
+ for (let i = 0; i < MAX_UNWRAP; i++) {
68
+ if (current instanceof z.ZodOptional) {
69
+ isOptional = true;
70
+ current = current.unwrap();
71
+ continue;
72
+ }
73
+ if (current instanceof z.ZodNullable) {
74
+ isOptional = true;
75
+ current = current.unwrap();
76
+ continue;
77
+ }
78
+ if (current instanceof z.ZodDefault) {
79
+ // .removeDefault() — v4 deprecates in favor of .unwrap() but
80
+ // the installed @types only expose removeDefault on ZodDefault.
81
+ current = current.unwrap();
82
+ continue;
83
+ }
84
+ break;
85
+ }
86
+ // The description lives on the innermost schema we reached.
87
+ const description = current.description ?? schema.description;
88
+ const base = decodeMeta(description);
89
+ if (!base)
90
+ return null;
91
+ return { ...base, isOptional };
92
+ }
93
+ /**
94
+ * Fallback: infer FieldMeta directly from a raw Zod schema when no
95
+ * `field.*()` metadata was attached.
96
+ *
97
+ * Walks through `.optional()` / `.nullable()` / `.default()` wrappers
98
+ * to find the inner primitive, then maps Zod's `_def.typeName` to
99
+ * the sync-engine type tag. Used by `resolveFieldMeta` and by
100
+ * `model()` / `query()` at definition time.
101
+ *
102
+ * Kept as an internal helper rather than exported directly — the
103
+ * public API is `resolveFieldMeta`, which combines this fallback
104
+ * with the `getFieldMeta` fast path.
105
+ */
106
+ export function inferFieldMetaFromZod(schema) {
107
+ let current = schema;
108
+ let isOptional = false;
109
+ const MAX_UNWRAP = 5;
110
+ for (let i = 0; i < MAX_UNWRAP; i++) {
111
+ if (current instanceof z.ZodOptional || current instanceof z.ZodNullable) {
112
+ isOptional = true;
113
+ current = current.unwrap();
114
+ continue;
115
+ }
116
+ if (current instanceof z.ZodDefault) {
117
+ current = current.unwrap();
118
+ continue;
119
+ }
120
+ break;
121
+ }
122
+ let type = 'string';
123
+ let enumValues;
124
+ if (current instanceof z.ZodString) {
125
+ type = 'string';
126
+ }
127
+ else if (current instanceof z.ZodNumber) {
128
+ type = 'number';
129
+ }
130
+ else if (current instanceof z.ZodBoolean) {
131
+ type = 'boolean';
132
+ }
133
+ else if (current instanceof z.ZodDate) {
134
+ type = 'date';
135
+ }
136
+ else if (current instanceof z.ZodEnum) {
137
+ type = 'enum';
138
+ // ZodEnum.options is the public v4 accessor for enum values.
139
+ enumValues = current.options;
140
+ }
141
+ else if (current instanceof z.ZodObject ||
142
+ current instanceof z.ZodArray ||
143
+ current instanceof z.ZodRecord ||
144
+ current instanceof z.ZodUnion ||
145
+ current instanceof z.ZodUnknown) {
146
+ type = 'json';
147
+ }
148
+ return { type, isOptional, isIndexed: false, enumValues };
149
+ }
150
+ /**
151
+ * Resolve FieldMeta for any Zod schema — whether it was built with
152
+ * `field.*()` (which attaches sync-engine metadata) or with raw Zod
153
+ * (which requires fallback inference from `_def.typeName`).
154
+ *
155
+ * This is the single public entry point for "given a Zod field, tell
156
+ * me its sync-engine type tag and optionality." Both `model()` and
157
+ * `query()` use it to populate their `fields` / `inputFields` maps at
158
+ * definition time, and the schema serializer reads those maps at
159
+ * serialization time.
160
+ *
161
+ * Contract: always returns a value. Never returns null. Unknown Zod
162
+ * types fall through to `'string'` — this is intentional and matches
163
+ * the existing behavior that was previously duplicated in
164
+ * `model.ts:inferMetaFromZod`.
165
+ */
166
+ export function resolveFieldMeta(schema) {
167
+ const attached = getFieldMeta(schema);
168
+ if (attached)
169
+ return attached;
170
+ return inferFieldMetaFromZod(schema);
171
+ }
172
+ // ── Chainable field builders ──────────────────────────────────────────────
173
+ //
174
+ // Each builder returns the underlying Zod schema (so `z.object(shape)` still
175
+ // works) with `.indexed()` added as a chainable method. `.optional()` and
176
+ // `.nullable()` still come from Zod itself and preserve the description.
177
+ /** Add `.indexed()` to a Zod schema without disturbing its type. */
178
+ function withIndexed(schema, baseMeta) {
179
+ const described = schema.describe(encodeMeta({ ...baseMeta, isIndexed: false }));
180
+ described.indexed = () => {
181
+ return schema.describe(encodeMeta({ ...baseMeta, isIndexed: true }));
182
+ };
183
+ return described;
184
+ }
185
+ export const field = {
186
+ /** String field */
187
+ string() {
188
+ return withIndexed(z.string(), { type: 'string' });
189
+ },
190
+ /** Number field */
191
+ number() {
192
+ return withIndexed(z.number(), { type: 'number' });
193
+ },
194
+ /** Boolean field */
195
+ boolean() {
196
+ return withIndexed(z.boolean(), { type: 'boolean' });
197
+ },
198
+ /** Date field */
199
+ date() {
200
+ return withIndexed(z.date(), { type: 'date' });
201
+ },
202
+ /** Enum field with constrained string values */
203
+ enum(values) {
204
+ return withIndexed(z.enum(values), { type: 'enum', enumValues: values });
205
+ },
206
+ /**
207
+ * JSON field. Three call shapes:
208
+ *
209
+ * ```ts
210
+ * field.json() // unknown JSON blob
211
+ * field.json(z.array(z.string())) // typed JSON with Zod schema
212
+ * field.json({ icon: z.string().default('default') }) // typed sub-properties with defaults
213
+ * ```
214
+ *
215
+ * The third form is the key DX feature for metadata fields. It wraps the
216
+ * plain object in `z.object()` automatically, and the model runtime generates
217
+ * a `${field}Json` getter that parses the JSON string on read, applies Zod
218
+ * defaults, and caches the result.
219
+ *
220
+ * Example:
221
+ * ```ts
222
+ * const slideDecks = model({
223
+ * metadata: field.json({
224
+ * icon: z.string().default('presentation'),
225
+ * color: z.string().default('#F59E0B'),
226
+ * summary: z.string().optional(),
227
+ * }),
228
+ * });
229
+ *
230
+ * // At runtime:
231
+ * deck.metadata // raw JSON string (unchanged)
232
+ * deck.metadataJson // { icon: 'presentation', color: '#F59E0B', summary: undefined }
233
+ * deck.metadataJson.icon // 'presentation' (typed, with default)
234
+ * ```
235
+ */
236
+ json(schemaOrShape) {
237
+ let inner;
238
+ if (!schemaOrShape) {
239
+ inner = z.unknown();
240
+ }
241
+ else if (isZodSchema(schemaOrShape)) {
242
+ inner = schemaOrShape;
243
+ }
244
+ else {
245
+ // Plain object shape → wrap in z.object() for the sub-property pattern
246
+ inner = z.object(schemaOrShape);
247
+ }
248
+ return withIndexed(inner, { type: 'json' });
249
+ },
250
+ /** Indexed string field (shorthand for `field.string().indexed()`). */
251
+ id() {
252
+ return field.string().indexed();
253
+ },
254
+ };
255
+ // ── Legacy function form (kept for backward compat) ──────────────────────
256
+ /** Mark a Zod schema as indexed for fast lookups (function form). */
257
+ export function indexed(schema) {
258
+ // Try to preserve existing metadata type tag if present.
259
+ const meta = decodeMeta(schema.description);
260
+ const newMeta = meta
261
+ ? { ...meta, isIndexed: true }
262
+ : { type: 'string', isIndexed: true };
263
+ return schema.describe(encodeMeta(newMeta));
264
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @ablo/sync-engine/schema — Schema Definition DSL
3
+ *
4
+ * Define your data models with Zod. Types are inferred automatically.
5
+ *
6
+ * ```ts
7
+ * import { z } from 'zod';
8
+ * import { defineSchema, model, relation } from '@ablo/sync-engine/schema';
9
+ *
10
+ * export const schema = defineSchema({
11
+ * tasks: model({
12
+ * title: z.string(),
13
+ * status: z.enum(['todo', 'doing', 'done']).default('todo'),
14
+ * projectId: z.string().optional(),
15
+ * }, {
16
+ * project: relation.belongsTo('projects', 'projectId'),
17
+ * }),
18
+ * });
19
+ *
20
+ * type Task = InferModel<typeof schema, 'tasks'>;
21
+ * ```
22
+ */
23
+ export { z } from 'zod';
24
+ export { field, indexed, getFieldMeta, type FieldMeta } from './field.js';
25
+ export { relation, type RelationDef, type RelationType } from './relation.js';
26
+ export { model, type ModelDef, type ModelOptions, type LoadStrategy, type PersistOptions, type RelationRecord, } from './model.js';
27
+ export { mutable, readOnly, type SugarOptions } from './sugar.js';
28
+ export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, type IdentityRole, type IdentityContext, } from './schema.js';
29
+ export { query, defineQueries, type QueryDef, type QueryRecord, type Queries, type InferQueryInput, type InferQueryResult, } from './queries.js';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @ablo/sync-engine/schema — Schema Definition DSL
3
+ *
4
+ * Define your data models with Zod. Types are inferred automatically.
5
+ *
6
+ * ```ts
7
+ * import { z } from 'zod';
8
+ * import { defineSchema, model, relation } from '@ablo/sync-engine/schema';
9
+ *
10
+ * export const schema = defineSchema({
11
+ * tasks: model({
12
+ * title: z.string(),
13
+ * status: z.enum(['todo', 'doing', 'done']).default('todo'),
14
+ * projectId: z.string().optional(),
15
+ * }, {
16
+ * project: relation.belongsTo('projects', 'projectId'),
17
+ * }),
18
+ * });
19
+ *
20
+ * type Task = InferModel<typeof schema, 'tasks'>;
21
+ * ```
22
+ */
23
+ // Re-export Zod for convenience (consumers can also import directly)
24
+ export { z } from 'zod';
25
+ // Field helpers (optional convenience wrappers around Zod)
26
+ export { field, indexed, getFieldMeta } from './field.js';
27
+ // Relation builders
28
+ export { relation } from './relation.js';
29
+ // Model builder
30
+ export { model, } from './model.js';
31
+ // Intent-first shorthand: `mutable.lazy({...})` and friends. Read the
32
+ // safety posture and load shape off the verb tokens; everything else
33
+ // falls back to sensible defaults. See sugar.ts for the full pattern.
34
+ export { mutable, readOnly } from './sugar.js';
35
+ // Schema definition + type inference
36
+ export { defineSchema, composeIdentitySyncGroups, } from './schema.js';
37
+ // Query definition DSL + type inference
38
+ export { query, defineQueries, } from './queries.js';