@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,33 @@
1
+ 'use client';
2
+ import { useContext } from 'react';
3
+ import { AbloInternalContext } from './internalContext.js';
4
+ import { AbloValidationError } from '../errors.js';
5
+ /**
6
+ * Returns the app user ID passed to the nearest `<AbloProvider>`, when
7
+ * the app chose to provide one.
8
+ *
9
+ * Hosted Ablo identity is resolved server-side from the API key, session,
10
+ * or capability token. This hook is only for app-owned fields like
11
+ * `assigneeId`; it is not required for Ablo sync to connect.
12
+ *
13
+ * Use this in leaf components that need the current user ID for
14
+ * mutation payloads, presence labels, permission checks, etc.
15
+ * @example
16
+ * function TaskRow({ id }) {
17
+ * const userId = useCurrentUserId();
18
+ * const ablo = useAblo();
19
+ * if (!userId) return null;
20
+ * return <button onClick={() => ablo?.tasks.update(id, { assigneeId: userId })}>
21
+ * Assign to me
22
+ * </button>;
23
+ * }
24
+ */
25
+ export function useCurrentUserId() {
26
+ const ctx = useContext(AbloInternalContext);
27
+ if (!ctx) {
28
+ throw new AbloValidationError('useCurrentUserId: no <AbloProvider> mounted above this component. ' +
29
+ 'Wrap your tree with <AbloProvider ...> from ' +
30
+ '@ablo/sync-engine/react.', { code: 'no_ablo_provider' });
31
+ }
32
+ return ctx.currentUserId;
33
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Register an imperative callback that fires whenever the provider
3
+ * surfaces an error. Covers engine errors (bootstrap failures,
4
+ * mutation rejections), WebSocket errors, and uncaught exceptions
5
+ * inside `postBootstrap` hooks.
6
+ *
7
+ * Use this for telemetry (Sentry, Datadog), user-facing toasts, or
8
+ * any side effect that should NOT trigger a re-render. The listener
9
+ * is stored in a ref, so re-renders don't thrash the subscription.
10
+ *
11
+ * @example
12
+ * function ErrorToaster() {
13
+ * useErrorListener((err) => {
14
+ * toast.error(err.message);
15
+ * Sentry.captureException(err);
16
+ * });
17
+ * return null;
18
+ * }
19
+ */
20
+ export declare function useErrorListener(listener: (error: Error) => void): void;
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+ import { useContext, useEffect, useRef } from 'react';
3
+ import { AbloInternalContext } from './internalContext.js';
4
+ import { AbloValidationError } from '../errors.js';
5
+ /**
6
+ * Register an imperative callback that fires whenever the provider
7
+ * surfaces an error. Covers engine errors (bootstrap failures,
8
+ * mutation rejections), WebSocket errors, and uncaught exceptions
9
+ * inside `postBootstrap` hooks.
10
+ *
11
+ * Use this for telemetry (Sentry, Datadog), user-facing toasts, or
12
+ * any side effect that should NOT trigger a re-render. The listener
13
+ * is stored in a ref, so re-renders don't thrash the subscription.
14
+ *
15
+ * @example
16
+ * function ErrorToaster() {
17
+ * useErrorListener((err) => {
18
+ * toast.error(err.message);
19
+ * Sentry.captureException(err);
20
+ * });
21
+ * return null;
22
+ * }
23
+ */
24
+ export function useErrorListener(listener) {
25
+ const ctx = useContext(AbloInternalContext);
26
+ if (!ctx) {
27
+ throw new AbloValidationError('useErrorListener: no <AbloProvider> mounted above this component. ' +
28
+ 'Wrap your tree with <AbloProvider ...> from @ablo/sync-engine/react.', { code: 'no_ablo_provider' });
29
+ }
30
+ // Stash the latest callback in a ref so the effect subscription
31
+ // stays stable across renders. Matches the `useEventCallback`
32
+ // pattern: late-bind the listener so callers can pass inline
33
+ // arrows without thrashing the subscription.
34
+ const ref = useRef(listener);
35
+ ref.current = listener;
36
+ useEffect(() => {
37
+ return ctx.subscribeError((err) => ref.current(err));
38
+ }, [ctx]);
39
+ }
@@ -0,0 +1,29 @@
1
+ import type { ResolveIntents } from '../types/global.js';
2
+ /**
3
+ * Named-intent invoker, typed via `ResolveIntents[IntentName]`.
4
+ *
5
+ * The consumer declares their intent vocabulary in the global:
6
+ *
7
+ * ```ts
8
+ * declare global {
9
+ * interface AbloSync {
10
+ * Intents: {
11
+ * editLayer: { slideId: string; layerId: string };
12
+ * generateWithAI: { entityId: string; tool: string };
13
+ * };
14
+ * }
15
+ * }
16
+ * ```
17
+ *
18
+ * Then `useIntent('editLayer')` returns a function whose sole argument
19
+ * is the `editLayer` claim shape — no runtime checks, purely compile-
20
+ * time narrowing.
21
+ *
22
+ * The SDK doesn't own what happens next: the `beginIntent` function on
23
+ * the React context (supplied via `SyncProvider`) is where the intent
24
+ * claim turns into a network effect. A Node-backed consumer wires it
25
+ * through `SyncAgent.beginIntent`; a browser-backed consumer may
26
+ * broadcast it through their own WebSocket. This hook is pure sugar
27
+ * that adds the typed name + claim narrowing.
28
+ */
29
+ export declare function useIntent<Name extends keyof ResolveIntents & string>(intentName: Name): (claim: ResolveIntents[Name]) => unknown;
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+ import { useCallback } from 'react';
3
+ import { useSyncContext } from './context.js';
4
+ import { AbloValidationError } from '../errors.js';
5
+ /**
6
+ * Named-intent invoker, typed via `ResolveIntents[IntentName]`.
7
+ *
8
+ * The consumer declares their intent vocabulary in the global:
9
+ *
10
+ * ```ts
11
+ * declare global {
12
+ * interface AbloSync {
13
+ * Intents: {
14
+ * editLayer: { slideId: string; layerId: string };
15
+ * generateWithAI: { entityId: string; tool: string };
16
+ * };
17
+ * }
18
+ * }
19
+ * ```
20
+ *
21
+ * Then `useIntent('editLayer')` returns a function whose sole argument
22
+ * is the `editLayer` claim shape — no runtime checks, purely compile-
23
+ * time narrowing.
24
+ *
25
+ * The SDK doesn't own what happens next: the `beginIntent` function on
26
+ * the React context (supplied via `SyncProvider`) is where the intent
27
+ * claim turns into a network effect. A Node-backed consumer wires it
28
+ * through `SyncAgent.beginIntent`; a browser-backed consumer may
29
+ * broadcast it through their own WebSocket. This hook is pure sugar
30
+ * that adds the typed name + claim narrowing.
31
+ */
32
+ export function useIntent(intentName) {
33
+ const { beginIntent } = useSyncContext();
34
+ return useCallback((claim) => {
35
+ if (!beginIntent) {
36
+ throw new AbloValidationError(`useIntent: no \`beginIntent\` wired into SyncProvider. Pass ` +
37
+ `a \`beginIntent\` prop (typically bound to your transport) ` +
38
+ `to enable intent invocations.`, { code: 'intent_not_wired' });
39
+ }
40
+ return beginIntent(intentName, claim);
41
+ }, [beginIntent, intentName]);
42
+ }
@@ -0,0 +1,83 @@
1
+ import type { Schema, InferModel, InferCreate } from '../schema/schema.js';
2
+ import type { ResolveSchema } from '../types/global.js';
3
+ import type { SyncStoreContract } from './context.js';
4
+ type GlobalMutateKey = ResolveSchema extends {
5
+ models: infer M;
6
+ } ? keyof M & string : string;
7
+ type GlobalMutateActions<K extends string> = ResolveSchema extends Schema ? K extends keyof ResolveSchema['models'] & string ? MutateActions<ResolveSchema, K> : MutateActions<Schema, string> : MutateActions<Schema, string>;
8
+ /**
9
+ * Compatibility mutation hook. Returns CRUD methods for a single model type.
10
+ *
11
+ * Prefer `useAblo()` and call `ablo.<model>.create/update/delete` inside
12
+ * callbacks for new integrations. This hook remains for older string-keyed code.
13
+ *
14
+ * @example
15
+ * import { schema } from '@ablo/schema';
16
+ * import { useMutate } from '@ablo/sync-engine/react';
17
+ *
18
+ * const tasks = useMutate(schema, 'tasks');
19
+ *
20
+ * // Create — fields are type-checked against the schema's Zod shape
21
+ * await tasks.create({ title: 'Fix bug', status: 'todo', projectId });
22
+ *
23
+ * // Update — id + partial changes, no need to hold a model instance
24
+ * await tasks.update({ id: task.id, status: 'done', completedAt: new Date() });
25
+ *
26
+ * // Delete / archive / unarchive — by id
27
+ * await tasks.delete(task.id);
28
+ * await tasks.archive(task.id);
29
+ *
30
+ * Mirrors the Zero pattern: `zero.mutate.task.update({ id, status: 'done' })`.
31
+ */
32
+ /**
33
+ * `create` / `update` / `delete` are overloaded: pass one row or an
34
+ * array. Drizzle and Prisma use the same shape (`db.insert(table).values(rowOrRows)`).
35
+ * Avoids the `*Many` suffix while keeping the semantics: every entry in
36
+ * an array call lands in the same synchronous tick (Promise.all under
37
+ * the hood), so the microtask coalescer in `TransactionQueue` collapses
38
+ * N pushes into one wire commit with one `batchIndex` — structurally
39
+ * identical to Zero's mutator-boundary commit.
40
+ */
41
+ type UpdatePatch<S extends Schema, K extends keyof S['models'] & string> = {
42
+ id: string;
43
+ } & Partial<InferModel<S, K>>;
44
+ export interface MutateActions<S extends Schema, K extends keyof S['models'] & string> {
45
+ /**
46
+ * Create one entity, or an array of entities in a single tick. ID,
47
+ * createdAt, updatedAt, organizationId default automatically per row.
48
+ */
49
+ create(data: InferCreate<S, K>): Promise<InferModel<S, K>>;
50
+ create(data: InferCreate<S, K>[]): Promise<InferModel<S, K>[]>;
51
+ /**
52
+ * Update one row, or an array of rows in a single tick. Each patch is
53
+ * `{ id, ...changes }` — missing ids throw. Schema-generated models
54
+ * are MobX-observable, so direct assignment fires reactivity.
55
+ */
56
+ update(patch: UpdatePatch<S, K>): Promise<InferModel<S, K>>;
57
+ update(patches: UpdatePatch<S, K>[]): Promise<InferModel<S, K>[]>;
58
+ /**
59
+ * Delete one row by id, or an array of ids in a single tick. Missing
60
+ * ids are silently ignored.
61
+ */
62
+ delete(id: string): Promise<void>;
63
+ delete(ids: string[]): Promise<void>;
64
+ /** Soft-archive by ID. */
65
+ archive: (id: string) => Promise<void>;
66
+ /** Restore an archived entity by ID. */
67
+ unarchive: (id: string) => Promise<void>;
68
+ }
69
+ /**
70
+ * Pure factory — testable without React. The hook just wraps this in
71
+ * useMemo with the React context.
72
+ */
73
+ export declare function createMutateActions<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, store: SyncStoreContract, organizationId: string): MutateActions<S, K>;
74
+ /** @deprecated Prefer `useAblo()` plus `ablo.<model>.create/update/delete`. */
75
+ export declare function useMutate<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K): MutateActions<S, K>;
76
+ /** Typed CRUD via the `AbloSync` global augmentation. The schema is
77
+ * resolved from the `SyncProvider`'s context — consumer doesn't pass it
78
+ * at the call site.
79
+ *
80
+ * @deprecated Prefer `useAblo()` plus `ablo.<model>.create/update/delete`.
81
+ */
82
+ export declare function useMutate<K extends GlobalMutateKey>(modelKey: K): GlobalMutateActions<K>;
83
+ export {};
@@ -0,0 +1,122 @@
1
+ 'use client';
2
+ import { useMemo } from 'react';
3
+ import { Model, modelAsRow } from '../Model.js';
4
+ import { AbloValidationError } from '../errors.js';
5
+ import { useSyncContext } from './context.js';
6
+ /**
7
+ * Pure factory — testable without React. The hook just wraps this in
8
+ * useMemo with the React context.
9
+ */
10
+ export function createMutateActions(schema, modelKey, store, organizationId) {
11
+ const modelDef = schema.models[modelKey];
12
+ const typename = modelDef?.typename ?? modelKey;
13
+ // Materialise one input row into a Model and stage a save. The default
14
+ // fields land here once so create-of-one and create-of-array share the
15
+ // same defaulting logic.
16
+ const buildModelForCreate = (data, now) => {
17
+ const record = data;
18
+ const fullData = {
19
+ ...record,
20
+ __typename: typename,
21
+ id: record.id ?? Model.generateId(),
22
+ organizationId: record.organizationId ?? organizationId,
23
+ createdAt: record.createdAt ?? now,
24
+ updatedAt: record.updatedAt ?? now,
25
+ };
26
+ const model = store.pool.createFromData(fullData);
27
+ if (!model) {
28
+ throw new AbloValidationError(`useMutate: failed to create ${typename} — no constructor in registry`, { code: 'mutate_create_unknown_model' });
29
+ }
30
+ return model;
31
+ };
32
+ // Apply a patch onto an existing pool model. Returns the model.
33
+ const applyPatch = (patch, now) => {
34
+ const { id, ...changes } = patch;
35
+ const model = store.pool.get(id);
36
+ if (!model) {
37
+ throw new AbloValidationError(`useMutate: ${typename} with id "${id}" not found in pool`, { code: 'mutate_update_entity_not_found' });
38
+ }
39
+ // Schema-derived patch keys are validated at the call-site type
40
+ // signature (`UpdatePatch<S, K>`); writes here are dynamic-class
41
+ // field assignments. `Reflect.set` is the typed bridge — Model
42
+ // doesn't carry an index signature for arbitrary string keys, but
43
+ // the dynamic field installation in `createDynamicModelClass`
44
+ // guarantees these keys resolve at runtime.
45
+ for (const [fieldName, value] of Object.entries(changes)) {
46
+ Reflect.set(model, fieldName, value);
47
+ }
48
+ Reflect.set(model, 'updatedAt', now);
49
+ return model;
50
+ };
51
+ return {
52
+ // Overloaded — runtime check on `Array.isArray` decides shape. Both
53
+ // branches stage via `Promise.all` so the microtask coalescer in
54
+ // `TransactionQueue` collapses N pushes into one wire commit.
55
+ create: (async (data) => {
56
+ const now = new Date();
57
+ if (Array.isArray(data)) {
58
+ if (data.length === 0)
59
+ return [];
60
+ const models = data.map((d) => buildModelForCreate(d, now));
61
+ await Promise.all(models.map((m) => store.save(m)));
62
+ return models.map((m) => modelAsRow(m));
63
+ }
64
+ const model = buildModelForCreate(data, now);
65
+ await store.save(model);
66
+ return modelAsRow(model);
67
+ }),
68
+ update: (async (patch) => {
69
+ const now = new Date();
70
+ if (Array.isArray(patch)) {
71
+ if (patch.length === 0)
72
+ return [];
73
+ const models = patch.map((p) => applyPatch(p, now));
74
+ await Promise.all(models.map((m) => store.save(m)));
75
+ return models.map((m) => modelAsRow(m));
76
+ }
77
+ const model = applyPatch(patch, now);
78
+ await store.save(model);
79
+ return modelAsRow(model);
80
+ }),
81
+ delete: (async (idOrIds) => {
82
+ if (Array.isArray(idOrIds)) {
83
+ if (idOrIds.length === 0)
84
+ return;
85
+ const models = [];
86
+ for (const id of idOrIds) {
87
+ const m = store.pool.get(id);
88
+ if (m)
89
+ models.push(m);
90
+ }
91
+ await Promise.all(models.map((m) => store.delete(m)));
92
+ return;
93
+ }
94
+ const model = store.pool.get(idOrIds);
95
+ if (!model)
96
+ return;
97
+ await store.delete(model);
98
+ }),
99
+ archive: async (id) => {
100
+ const model = store.pool.get(id);
101
+ if (!model)
102
+ return;
103
+ await store.archive(model);
104
+ },
105
+ unarchive: async (id) => {
106
+ const model = store.pool.get(id);
107
+ if (!model)
108
+ return;
109
+ await store.unarchive(model);
110
+ },
111
+ };
112
+ }
113
+ export function useMutate(schemaOrKey, maybeKey) {
114
+ const { store, organizationId, schema: ctxSchema } = useSyncContext();
115
+ const resolvedSchema = typeof schemaOrKey === 'string' ? ctxSchema : schemaOrKey;
116
+ const resolvedKey = typeof schemaOrKey === 'string' ? schemaOrKey : maybeKey;
117
+ if (!resolvedSchema) {
118
+ throw new AbloValidationError('useMutate: no schema available. Pass the schema as the first arg ' +
119
+ 'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'mutate_schema_missing' });
120
+ }
121
+ return useMemo(() => createMutateActions(resolvedSchema, resolvedKey, store, organizationId), [store, organizationId, resolvedSchema, resolvedKey]);
122
+ }
@@ -0,0 +1,26 @@
1
+ import type { Transaction } from '../transactions/TransactionQueue.js';
2
+ export interface MutationFailurePayload {
3
+ transaction: Transaction;
4
+ error: Error;
5
+ permanent?: boolean;
6
+ }
7
+ /**
8
+ * Register a side-effect listener for mutation failures. Fires whenever
9
+ * the underlying transaction queue rolls back an optimistic write —
10
+ * permanent rejections (validation, FK, auth) and exhausted-retry
11
+ * rollbacks (connection lost mid-burst).
12
+ *
13
+ * Use this to mount a single `<MutationFailureBoundary>` near the app
14
+ * shell that turns silent pool rollbacks into toasts / banners. The
15
+ * listener is stored in a ref so re-renders don't thrash the
16
+ * subscription — matches `useErrorListener`.
17
+ *
18
+ * @example
19
+ * function MutationFailureBoundary() {
20
+ * useMutationFailureListener(({ transaction, error }) => {
21
+ * toast.error(`Couldn't save ${transaction.modelName}: ${error.message}`);
22
+ * });
23
+ * return null;
24
+ * }
25
+ */
26
+ export declare function useMutationFailureListener(listener: (payload: MutationFailurePayload) => void): void;
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+ import { useContext, useEffect, useRef } from 'react';
3
+ import { AbloInternalContext } from './internalContext.js';
4
+ import { AbloValidationError } from '../errors.js';
5
+ /**
6
+ * Register a side-effect listener for mutation failures. Fires whenever
7
+ * the underlying transaction queue rolls back an optimistic write —
8
+ * permanent rejections (validation, FK, auth) and exhausted-retry
9
+ * rollbacks (connection lost mid-burst).
10
+ *
11
+ * Use this to mount a single `<MutationFailureBoundary>` near the app
12
+ * shell that turns silent pool rollbacks into toasts / banners. The
13
+ * listener is stored in a ref so re-renders don't thrash the
14
+ * subscription — matches `useErrorListener`.
15
+ *
16
+ * @example
17
+ * function MutationFailureBoundary() {
18
+ * useMutationFailureListener(({ transaction, error }) => {
19
+ * toast.error(`Couldn't save ${transaction.modelName}: ${error.message}`);
20
+ * });
21
+ * return null;
22
+ * }
23
+ */
24
+ export function useMutationFailureListener(listener) {
25
+ const ctx = useContext(AbloInternalContext);
26
+ if (!ctx) {
27
+ throw new AbloValidationError('useMutationFailureListener: no <AbloProvider> mounted above this component. ' +
28
+ 'Wrap your tree with <AbloProvider ...> from @ablo/sync-engine/react.', { code: 'no_ablo_provider' });
29
+ }
30
+ const ref = useRef(listener);
31
+ ref.current = listener;
32
+ useEffect(() => {
33
+ const engine = ctx.engine;
34
+ if (!engine)
35
+ return;
36
+ return engine.onMutationFailure((payload) => ref.current(payload));
37
+ }, [ctx, ctx.engine]);
38
+ }
@@ -0,0 +1,56 @@
1
+ import type { Schema } from '../schema/schema.js';
2
+ import type { MutatorDefs } from '../mutators/defineMutators.js';
3
+ import type { UndoScope } from '../mutators/UndoManager.js';
4
+ import type { ResolveSchema } from '../types/global.js';
5
+ /**
6
+ * useMutators — turn a `defineMutators` tree into callable invokers.
7
+ *
8
+ * The returned object mirrors the mutator tree one-to-one, but each leaf is
9
+ * now a `(args) => Promise<TResult>` function. Internally each invocation:
10
+ * 1. Builds a fresh `Transaction` bound to the current store/org context.
11
+ * 2. Calls the user's mutator with `{ tx, args }`.
12
+ * 3. Returns the mutator's resolved value.
13
+ *
14
+ * V1 error handling: if the mutator throws, we `console.error` + rethrow.
15
+ * Any writes that already dispatched stay in place (no rollback). That
16
+ * matches the existing behaviour of batch helpers like `saveManyOptimized`
17
+ * and keeps the contract honest — consumers can layer their own try/catch
18
+ * + compensating writes until V2 adds atomicity.
19
+ */
20
+ /**
21
+ * Map a `MutatorFn` onto its invoker form — strip `tx`, keep `args`/return.
22
+ *
23
+ * Uses nested `infer O` so the `args`/`result` types are extracted from the
24
+ * function signature without binding the `tx` parameter to a specific
25
+ * `Transaction<S>` variance. Function parameters are contravariant, so a
26
+ * match against `MutatorFn<Schema>` would reject mutators declared against
27
+ * a narrower schema (e.g. `Transaction<typeof appSchema>`). The two-step
28
+ * inference sidesteps that without resorting to `any`/`unknown` placeholders.
29
+ */
30
+ export type InvokerFor<F> = F extends (options: infer O) => Promise<infer R> ? O extends {
31
+ args: infer A;
32
+ } ? (args: A) => Promise<R> : never : never;
33
+ /**
34
+ * The hook's return shape: same tree as the input `MutatorDefs`, every leaf
35
+ * rewritten to its invoker form.
36
+ */
37
+ export type MutatorInvokers<M> = {
38
+ [K in keyof M]: {
39
+ [N in keyof M[K]]: InvokerFor<M[K][N]>;
40
+ };
41
+ };
42
+ /**
43
+ * Options passed to `useMutators`. When `undoScope` is set, every mutator
44
+ * invocation is wrapped in a `RecordingTransaction` and its inverses are
45
+ * pushed to the scope as one undo entry.
46
+ */
47
+ export interface UseMutatorsOptions<S extends Schema> {
48
+ /** Target undo scope for recording inverses. Omit to disable recording. */
49
+ undoScope?: UndoScope<S>;
50
+ }
51
+ /** Mutator invokers (explicit schema arg). */
52
+ export declare function useMutators<S extends Schema, M extends MutatorDefs<S>>(schema: S, mutators: M, options?: UseMutatorsOptions<S>): MutatorInvokers<M>;
53
+ /** Mutator invokers via the `AbloSync` global augmentation. Schema comes
54
+ * from the `SyncProvider`'s context; the mutator tree is typed against
55
+ * `ResolveSchema` at the call site. */
56
+ export declare function useMutators<M extends ResolveSchema extends Schema ? MutatorDefs<ResolveSchema> : MutatorDefs<Schema>>(mutators: M, options?: UseMutatorsOptions<ResolveSchema extends Schema ? ResolveSchema : Schema>): MutatorInvokers<M>;
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+ import { useMemo } from 'react';
3
+ import { createTransaction } from '../mutators/Transaction.js';
4
+ import { createRecordingTransaction } from '../mutators/RecordingTransaction.js';
5
+ import { useSyncContext } from './context.js';
6
+ import { AbloValidationError } from '../errors.js';
7
+ import { getContext } from '../context.js';
8
+ export function useMutators(schemaOrMutators, mutatorsOrOptions, maybeOptions) {
9
+ const { store, organizationId, schema: ctxSchema } = useSyncContext();
10
+ // Disambiguate: explicit-schema path has the schema object in first slot;
11
+ // the global-resolved path has the mutator tree there. A schema object
12
+ // has a `.models` property; a mutator tree doesn't.
13
+ const isExplicit = typeof schemaOrMutators === 'object' &&
14
+ schemaOrMutators !== null &&
15
+ 'models' in schemaOrMutators;
16
+ const schema = isExplicit ? schemaOrMutators : ctxSchema;
17
+ const mutators = (isExplicit ? mutatorsOrOptions : schemaOrMutators);
18
+ const options = (isExplicit ? maybeOptions : mutatorsOrOptions);
19
+ if (!schema) {
20
+ throw new AbloValidationError('useMutators: no schema available. Pass the schema as the first arg ' +
21
+ 'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'mutators_schema_missing' });
22
+ }
23
+ const { undoScope } = options ?? {};
24
+ return useMemo(() => {
25
+ const out = {};
26
+ for (const modelKey of Object.keys(mutators)) {
27
+ const group = mutators[modelKey];
28
+ if (!group)
29
+ continue;
30
+ const invokers = {};
31
+ for (const mutatorName of Object.keys(group)) {
32
+ const fn = group[mutatorName];
33
+ const label = `${String(modelKey)}.${mutatorName}`;
34
+ invokers[mutatorName] = async (args) => {
35
+ // Recording path: wrap the transaction so each write snapshots its
36
+ // inverse. On success, push the captured entry to the scope.
37
+ if (undoScope) {
38
+ const recording = createRecordingTransaction(schema, store, organizationId);
39
+ try {
40
+ const result = await fn({ tx: recording.tx, args });
41
+ const entry = recording.getEntry(label);
42
+ if (entry)
43
+ undoScope.record(entry);
44
+ return result;
45
+ }
46
+ catch (err) {
47
+ getContext().logger.error(`[useMutators] mutator "${label}" threw`, { error: err });
48
+ throw err;
49
+ }
50
+ }
51
+ // Non-recording path — plain transaction, identical to pre-undo V1.
52
+ const tx = createTransaction(schema, store, organizationId);
53
+ try {
54
+ return await fn({ tx, args });
55
+ }
56
+ catch (err) {
57
+ getContext().logger.error(`[useMutators] mutator "${label}" threw`, { error: err });
58
+ throw err;
59
+ }
60
+ };
61
+ }
62
+ out[modelKey] = invokers;
63
+ }
64
+ return out;
65
+ }, [schema, mutators, store, organizationId, undoScope]);
66
+ }
@@ -0,0 +1,32 @@
1
+ import type { ResolvePresence } from '../types/global.js';
2
+ /**
3
+ * Read the consumer-supplied presence state with `ResolvePresence`d
4
+ * typing — the shape the consumer declared in
5
+ * `declare global { interface AbloSync { Presence: ... } }`.
6
+ *
7
+ * The SDK doesn't own a presence wire format. Consumers plug whatever
8
+ * backs their cursors, status, or activity (a MobX store, a custom
9
+ * WebSocket channel, `SyncAgent` in Node, a Zustand slice) via the
10
+ * `presence` prop on `SyncProvider`. This hook returns it typed.
11
+ *
12
+ * ```ts
13
+ * // apps/your-app/src/ablo-sync.d.ts
14
+ * declare global {
15
+ * interface AbloSync {
16
+ * Presence: { cursor: { x: number; y: number } | null; status: 'away' | 'online' };
17
+ * }
18
+ * }
19
+ *
20
+ * // consumer's <SyncProvider> wiring
21
+ * <SyncProvider store={store} organizationId={orgId} presence={presenceStore}>
22
+ *
23
+ * // any component
24
+ * const presence = usePresence();
25
+ * presence?.cursor?.x; // fully typed
26
+ * ```
27
+ *
28
+ * Returns `undefined` when no provider-level presence source is wired —
29
+ * consumers can narrow with a guard or configure a default in their
30
+ * provider.
31
+ */
32
+ export declare function usePresence(): ResolvePresence | undefined;
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+ import { useSyncContext } from './context.js';
3
+ /**
4
+ * Read the consumer-supplied presence state with `ResolvePresence`d
5
+ * typing — the shape the consumer declared in
6
+ * `declare global { interface AbloSync { Presence: ... } }`.
7
+ *
8
+ * The SDK doesn't own a presence wire format. Consumers plug whatever
9
+ * backs their cursors, status, or activity (a MobX store, a custom
10
+ * WebSocket channel, `SyncAgent` in Node, a Zustand slice) via the
11
+ * `presence` prop on `SyncProvider`. This hook returns it typed.
12
+ *
13
+ * ```ts
14
+ * // apps/your-app/src/ablo-sync.d.ts
15
+ * declare global {
16
+ * interface AbloSync {
17
+ * Presence: { cursor: { x: number; y: number } | null; status: 'away' | 'online' };
18
+ * }
19
+ * }
20
+ *
21
+ * // consumer's <SyncProvider> wiring
22
+ * <SyncProvider store={store} organizationId={orgId} presence={presenceStore}>
23
+ *
24
+ * // any component
25
+ * const presence = usePresence();
26
+ * presence?.cursor?.x; // fully typed
27
+ * ```
28
+ *
29
+ * Returns `undefined` when no provider-level presence source is wired —
30
+ * consumers can narrow with a guard or configure a default in their
31
+ * provider.
32
+ */
33
+ export function usePresence() {
34
+ const ctx = useSyncContext();
35
+ // The runtime value is whatever the consumer passed to `SyncProvider`.
36
+ // The type assertion reflects the consumer's declared global, which
37
+ // the hook can't verify at runtime — but the consumer controls both
38
+ // ends (the global declaration and the provider prop) so this is a
39
+ // single-source-of-truth contract, not blind trust.
40
+ return ctx.presence;
41
+ }