@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,89 @@
1
+ /**
2
+ * Ablo-Cloud-side request driver.
3
+ *
4
+ * This file is what Ablo Cloud runs in production. Customers DON'T
5
+ * write it — they only see signed POSTs hit their endpoint. We
6
+ * publish it here so the example can show the full round-trip
7
+ * locally without standing up the cloud.
8
+ *
9
+ * In production:
10
+ * - Ablo Cloud holds the signing secret in its config
11
+ * - It signs each outbound POST with `signAbloSourceRequest`
12
+ * - The customer's `dataSource(...)` handler verifies the signature
13
+ * - The response feeds back into Ablo Cloud's hosted realtime layer
14
+ *
15
+ * Here we wire signer -> in-process handler with no network hop, so
16
+ * `npx tsx run.ts` works without ports, env, or cloud credentials.
17
+ */
18
+
19
+ import {
20
+ signAbloSourceRequest,
21
+ type Ablo,
22
+ } from '@ablo/sync-engine';
23
+
24
+ export interface AbloDriverOptions {
25
+ /**
26
+ * In-process target. Production calls a URL; the example calls
27
+ * the handler directly so there's no http port to manage.
28
+ */
29
+ readonly handler: (request: Request) => Promise<Response>;
30
+ /** Same secret the customer's `dataSource(...)` is configured with. */
31
+ readonly signingSecret: string;
32
+ }
33
+
34
+ export class AbloDriver {
35
+ private messageCounter = 0;
36
+
37
+ constructor(private readonly options: AbloDriverOptions) {}
38
+
39
+ async load(model: string, id: string): Promise<unknown> {
40
+ return this.send({ type: 'load', model, id });
41
+ }
42
+
43
+ async list(model: string, query?: Ablo.Source.Operation['input']) {
44
+ return this.send({ type: 'list', model, query: query ?? {} });
45
+ }
46
+
47
+ async commit(operations: readonly Ablo.Source.Operation[], clientTxId?: string) {
48
+ return this.send({
49
+ type: 'commit',
50
+ operations,
51
+ ...(clientTxId ? { clientTxId } : {}),
52
+ });
53
+ }
54
+
55
+ async events(cursor?: string, limit?: number) {
56
+ return this.send({
57
+ type: 'events',
58
+ ...(cursor !== undefined ? { cursor } : {}),
59
+ ...(limit !== undefined ? { limit } : {}),
60
+ });
61
+ }
62
+
63
+ // Builds the exact signed Request shape the production Ablo Cloud
64
+ // would send. The customer's handler can't tell this apart from a
65
+ // real cloud-originated request.
66
+ private async send(payload: Record<string, unknown>): Promise<unknown> {
67
+ this.messageCounter += 1;
68
+ const body = JSON.stringify(payload);
69
+ const messageId = `msg_${Date.now()}_${this.messageCounter}`;
70
+ const signed = await signAbloSourceRequest({
71
+ secret: this.options.signingSecret,
72
+ body,
73
+ messageId,
74
+ });
75
+ const request = new Request('http://example.test/api/ablo/source', {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json', ...signed.headers },
78
+ body,
79
+ });
80
+ const response = await this.options.handler(request);
81
+ if (!response.ok) {
82
+ const text = await response.text();
83
+ throw new Error(
84
+ `Data Source POST ${payload.type} failed: ${response.status} ${text}`,
85
+ );
86
+ }
87
+ return response.json();
88
+ }
89
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Customer-side Data Source endpoint.
3
+ *
4
+ * This file is what a customer running their own backend writes. It
5
+ * holds the canonical data — in production it's their Postgres,
6
+ * Mongo, or whatever — and exposes one handler that Ablo Cloud calls
7
+ * over HTTP for `load`, `list`, `commit`, and `events`.
8
+ *
9
+ * `dataSource(...)` returns `(req: Request) => Promise<Response>` —
10
+ * a Fetch-API handler. Drop it into Next.js (`export const POST`),
11
+ * Hono, Cloudflare Workers, or a thin `http.createServer` wrapper.
12
+ *
13
+ * This example uses an in-memory `Map` as the "database" so it runs
14
+ * with zero setup. A real customer swaps the Map calls for ORM calls
15
+ * inside a transaction. The shape of the handlers stays identical.
16
+ */
17
+
18
+ import Ablo, { dataSource } from '@ablo/sync-engine';
19
+ import { schema } from './schema';
20
+
21
+ type TaskRow = {
22
+ id: string;
23
+ title: string;
24
+ status: 'todo' | 'doing' | 'done';
25
+ assignee?: string;
26
+ };
27
+
28
+ // Stand-in for the customer's real database. Map keyed by row id.
29
+ const taskStore = new Map<string, TaskRow>();
30
+
31
+ // Outbox table. In production this is a `tasks_outbox` Postgres table
32
+ // populated by triggers or service code. Ablo polls `events` to fan
33
+ // out changes that didn't originate from an Ablo commit.
34
+ type OutboxRow = {
35
+ id: string;
36
+ entityId: string;
37
+ type: Ablo.Source.Operation['type'];
38
+ data: TaskRow | null;
39
+ clientTxId?: string;
40
+ };
41
+ const outbox: OutboxRow[] = [];
42
+ let outboxSequence = 0;
43
+
44
+ // Seed one row so the example's first `load` returns something.
45
+ taskStore.set('task_seed', {
46
+ id: 'task_seed',
47
+ title: 'Seeded by customer database',
48
+ status: 'todo',
49
+ });
50
+
51
+ /**
52
+ * The full Data Source handler. One symbol exposes load/list/commit/
53
+ * events for every model the schema declares.
54
+ *
55
+ * In Next.js:
56
+ *
57
+ * ```ts
58
+ * // app/api/ablo/source/route.ts
59
+ * export const POST = handleAbloSource;
60
+ * ```
61
+ *
62
+ * In Hono / Cloudflare Workers:
63
+ *
64
+ * ```ts
65
+ * app.post('/api/ablo/source', (c) => handleAbloSource(c.req.raw));
66
+ * ```
67
+ */
68
+ export const handleAbloSource = dataSource({
69
+ schema,
70
+
71
+ // The signing secret pairs with what Ablo Cloud is configured with.
72
+ // Wrong secret -> 401 with `source_signature_invalid`. Passing a
73
+ // function (instead of the env value directly) re-reads the secret
74
+ // on every request — convenient for rotation, and required by the
75
+ // example because `run.ts` configures the env after this module is
76
+ // imported.
77
+ signingSecret: () => {
78
+ const secret = process.env.ABLO_DATA_SOURCE_SIGNING_SECRET;
79
+ if (!secret) {
80
+ throw new Error(
81
+ 'ABLO_DATA_SOURCE_SIGNING_SECRET is not set — refusing to accept unsigned requests',
82
+ );
83
+ }
84
+ return secret;
85
+ },
86
+
87
+ // `authorize` runs before any handler. Use it to map the signed
88
+ // request to your tenant/user context. The returned value lands on
89
+ // `context.auth` inside every model handler. This example just
90
+ // returns `{}` since the in-memory store is single-tenant.
91
+ authorize() {
92
+ return {};
93
+ },
94
+
95
+ tasks: {
96
+ load({ id }) {
97
+ return taskStore.get(id) ?? null;
98
+ },
99
+
100
+ list({ query }) {
101
+ const all = Array.from(taskStore.values());
102
+ const start = query.cursor ? Number(query.cursor) : 0;
103
+ const limit = query.limit ?? 50;
104
+ const page = all.slice(start, start + limit);
105
+ return {
106
+ rows: page,
107
+ nextCursor:
108
+ start + page.length < all.length
109
+ ? String(start + page.length)
110
+ : undefined,
111
+ };
112
+ },
113
+
114
+ // The commit handler applies every operation in the customer's
115
+ // own transaction. The example uses a synchronous in-memory
116
+ // update; the surrounding `apply` helper shows where you would
117
+ // open `db.transaction(async (tx) => { ... })`.
118
+ commit({ operations, clientTxId }) {
119
+ const rows: TaskRow[] = [];
120
+ for (const op of operations) {
121
+ const row = applyOperation(op, clientTxId);
122
+ if (row) rows.push(row);
123
+ }
124
+ return { rows };
125
+ },
126
+ },
127
+
128
+ // `events` lets Ablo learn about writes that bypassed Ablo —
129
+ // cron jobs, dashboards, batch imports. Each call drains a batch
130
+ // from the outbox and reports the cursor to resume from.
131
+ events({ cursor, limit }) {
132
+ const start = cursor ? Number(cursor) : 0;
133
+ const cap = limit ?? 100;
134
+ const slice = outbox.slice(start, start + cap);
135
+ const events = slice.map((row) => ({
136
+ id: row.id,
137
+ model: 'tasks',
138
+ entityId: row.entityId,
139
+ type: row.type,
140
+ data: row.data,
141
+ ...(row.clientTxId ? { clientTxId: row.clientTxId } : {}),
142
+ }));
143
+ const nextCursor =
144
+ start + slice.length < outbox.length
145
+ ? String(start + slice.length)
146
+ : undefined;
147
+ return { events, ...(nextCursor !== undefined ? { nextCursor } : {}) };
148
+ },
149
+ });
150
+
151
+ function applyOperation(
152
+ op: Ablo.Source.Operation,
153
+ clientTxId: string | undefined,
154
+ ): TaskRow | null {
155
+ if (op.model !== 'tasks') return null;
156
+ const id = op.id ?? `task_${Math.random().toString(36).slice(2, 10)}`;
157
+
158
+ if (op.type === 'CREATE') {
159
+ const row: TaskRow = {
160
+ id,
161
+ title: String(op.input?.title ?? ''),
162
+ status:
163
+ (op.input?.status as TaskRow['status'] | undefined) ?? 'todo',
164
+ ...(op.input?.assignee
165
+ ? { assignee: String(op.input.assignee) }
166
+ : {}),
167
+ };
168
+ taskStore.set(id, row);
169
+ appendOutbox({ entityId: id, type: 'CREATE', data: row, clientTxId });
170
+ return row;
171
+ }
172
+
173
+ if (op.type === 'UPDATE') {
174
+ const existing = taskStore.get(id);
175
+ if (!existing) return null;
176
+ const next: TaskRow = { ...existing, ...(op.input as Partial<TaskRow>) };
177
+ taskStore.set(id, next);
178
+ appendOutbox({ entityId: id, type: 'UPDATE', data: next, clientTxId });
179
+ return next;
180
+ }
181
+
182
+ if (op.type === 'DELETE') {
183
+ const existing = taskStore.get(id);
184
+ if (!existing) return null;
185
+ taskStore.delete(id);
186
+ appendOutbox({ entityId: id, type: 'DELETE', data: null, clientTxId });
187
+ return existing;
188
+ }
189
+
190
+ return null;
191
+ }
192
+
193
+ function appendOutbox(input: Omit<OutboxRow, 'id'>): void {
194
+ outboxSequence += 1;
195
+ outbox.push({ id: `evt_${outboxSequence}`, ...input });
196
+ }
197
+
198
+ // Exposed for the orchestrator's `run.ts`. A real customer doesn't
199
+ // need this — it's a back door for the demo to verify state.
200
+ export function _inspectStore(): {
201
+ rows: TaskRow[];
202
+ outboxSize: number;
203
+ } {
204
+ return {
205
+ rows: Array.from(taskStore.values()),
206
+ outboxSize: outbox.length,
207
+ };
208
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * End-to-end Data Source demo.
3
+ *
4
+ * Run:
5
+ *
6
+ * cd packages/sync-engine/examples
7
+ * npx tsx data-source/run.ts
8
+ *
9
+ * What this proves:
10
+ *
11
+ * 1. Ablo Cloud's signer + the customer's verifier interop. A wrong
12
+ * secret produces `source_signature_invalid`.
13
+ * 2. `load`, `list`, `commit`, and `events` all flow through the
14
+ * same Fetch-API handler.
15
+ * 3. The customer's "database" (here a Map) holds canonical rows.
16
+ * Ablo never sees them directly — only via the contract.
17
+ * 4. Writes that touch the customer DB show up on the next `events`
18
+ * poll so Ablo Cloud can fan them out to other clients.
19
+ */
20
+
21
+ import { handleAbloSource, _inspectStore } from './customer-server';
22
+ import { AbloDriver } from './ablo-driver';
23
+
24
+ const SIGNING_SECRET =
25
+ process.env.ABLO_DATA_SOURCE_SIGNING_SECRET ?? 'whsec_example_secret_do_not_use_in_prod';
26
+
27
+ // `dataSource()` reads `options.signingSecret` at construction; we
28
+ // re-export the same value to the driver so signer and verifier agree.
29
+ // In production this is one secret shared between Ablo Cloud config
30
+ // and the customer's environment.
31
+ process.env.ABLO_DATA_SOURCE_SIGNING_SECRET = SIGNING_SECRET;
32
+
33
+ async function main() {
34
+ const driver = new AbloDriver({
35
+ handler: handleAbloSource,
36
+ signingSecret: SIGNING_SECRET,
37
+ });
38
+
39
+ log('--- 1. load (existing seeded row) ---');
40
+ const seeded = await driver.load('tasks', 'task_seed');
41
+ log('loaded:', seeded);
42
+
43
+ log('\n--- 2. commit (CREATE + UPDATE in one batch) ---');
44
+ const committed = await driver.commit(
45
+ [
46
+ {
47
+ type: 'CREATE',
48
+ model: 'tasks',
49
+ id: 'task_new',
50
+ input: { title: 'Wire the data source', status: 'todo' },
51
+ },
52
+ {
53
+ type: 'UPDATE',
54
+ model: 'tasks',
55
+ id: 'task_seed',
56
+ input: { status: 'doing', assignee: 'alice' },
57
+ },
58
+ ],
59
+ 'cltx_demo_1',
60
+ );
61
+ log('committed rows:', committed);
62
+
63
+ log('\n--- 3. list (all tasks after commit) ---');
64
+ const listed = await driver.list('tasks');
65
+ log('listed:', listed);
66
+
67
+ log('\n--- 4. events (outbox feed for cross-channel writes) ---');
68
+ const events = await driver.events();
69
+ log('events:', events);
70
+
71
+ log('\n--- 5. signature failure (wrong secret) ---');
72
+ const badDriver = new AbloDriver({
73
+ handler: handleAbloSource,
74
+ signingSecret: 'whsec_wrong_secret',
75
+ });
76
+ try {
77
+ await badDriver.load('tasks', 'task_seed');
78
+ throw new Error('expected signature failure');
79
+ } catch (err) {
80
+ log('rejected as expected:', (err as Error).message);
81
+ }
82
+
83
+ log('\n--- final customer DB state ---');
84
+ log(_inspectStore());
85
+ }
86
+
87
+ function log(...args: unknown[]) {
88
+ // Pretty-print objects so the demo output reads cleanly.
89
+ console.log(
90
+ ...args.map((arg) =>
91
+ typeof arg === 'object' && arg !== null
92
+ ? JSON.stringify(arg, null, 2)
93
+ : arg,
94
+ ),
95
+ );
96
+ }
97
+
98
+ main().catch((err) => {
99
+ console.error('data-source example failed:', err);
100
+ process.exit(1);
101
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared schema for the Data Source example.
3
+ *
4
+ * The schema is the contract between three sides:
5
+ *
6
+ * 1. The application UI — `ablo.tasks.update(...)`.
7
+ * 2. The Ablo Cloud — translates writes into signed POSTs.
8
+ * 3. The customer's Data Source endpoint — applies them to its own
9
+ * database.
10
+ *
11
+ * All three import the SAME schema file. Adding a column on either
12
+ * side without the other is a compile error.
13
+ */
14
+
15
+ import { defineSchema, model, z } from '@ablo/sync-engine/schema';
16
+
17
+ export const schema = defineSchema({
18
+ tasks: model({
19
+ title: z.string(),
20
+ status: z.enum(['todo', 'doing', 'done']),
21
+ assignee: z.string().optional(),
22
+ }),
23
+ });
24
+
25
+ export type Schema = typeof schema;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Quickstart — schema-backed state coordination.
3
+ *
4
+ * Run:
5
+ *
6
+ * ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
7
+ */
8
+
9
+ import Ablo from '@ablo/sync-engine';
10
+ import { defineSchema, model, z } from '@ablo/sync-engine/schema';
11
+
12
+ const schema = defineSchema({
13
+ weatherReports: model({
14
+ location: z.string(),
15
+ status: z.enum(['pending', 'ready']),
16
+ forecast: z.string().optional(),
17
+ }),
18
+ });
19
+
20
+ async function getWeather(location: string): Promise<string> {
21
+ return `Light rain in ${location}, 13C`;
22
+ }
23
+
24
+ async function main() {
25
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
26
+ const location = process.env.WEATHER_LOCATION ?? 'Stockholm';
27
+
28
+ try {
29
+ await ablo.ready();
30
+
31
+ const created = await ablo.weatherReports.create({
32
+ location,
33
+ status: 'pending',
34
+ });
35
+
36
+ const updated = await ablo.weatherReports.update(created.id, {
37
+ status: 'ready',
38
+ forecast: await getWeather(created.location),
39
+ });
40
+
41
+ console.log('updated', {
42
+ id: updated.id,
43
+ status: updated.status,
44
+ forecast: updated.forecast,
45
+ });
46
+ } finally {
47
+ await ablo.dispose();
48
+ }
49
+ }
50
+
51
+ main().catch((err) => {
52
+ console.error('quickstart failed:', err);
53
+ process.exit(1);
54
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["**/*.ts"],
15
+ "exclude": ["node_modules"]
16
+ }
package/llms.txt ADDED
@@ -0,0 +1,143 @@
1
+ # Ablo
2
+
3
+ Ablo is the state coordination layer for apps where humans and agents edit the same data.
4
+
5
+ Use AI SDK for the agent loop. Use Ablo when agent reads and writes must persist, coordinate with concurrent work, and leave an audit trail.
6
+
7
+ ## Use this API
8
+
9
+ ```ts
10
+ import Ablo from '@ablo/sync-engine';
11
+ import { defineSchema, model, z } from '@ablo/sync-engine/schema';
12
+
13
+ const schema = defineSchema({
14
+ tasks: model({
15
+ id: z.string(),
16
+ title: z.string(),
17
+ status: z.enum(['todo', 'doing', 'done']),
18
+ summary: z.string().optional(),
19
+ }),
20
+ });
21
+
22
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
23
+
24
+ const [task] = await ablo.tasks.load({ where: { id: 'task_123' } });
25
+ if (!task) throw new Error('Task not found');
26
+
27
+ const intents = ablo.intents.list({ resource: 'tasks', id: 'task_123' });
28
+ if (intents.length > 0) {
29
+ await ablo.intents.waitFor({ resource: 'tasks', id: 'task_123' }, { timeout: 30_000 });
30
+ }
31
+
32
+ const snap = ablo.snapshot({ tasks: 'task_123' });
33
+ const updated = await ablo.tasks.update(
34
+ 'task_123',
35
+ { status: 'done', summary: await summarize(task) },
36
+ {
37
+ readAt: snap.stamp,
38
+ onStale: 'reject',
39
+ wait: 'confirmed',
40
+ },
41
+ );
42
+ ```
43
+
44
+ That is the normal app path: declare models in a schema, then use `ablo.<model>.load(...)`, `ablo.<model>.retrieve(...)`, `ablo.<model>.create(...)`, `ablo.<model>.update(...)`, and `ablo.<model>.delete(...)`.
45
+
46
+ For full integrations, use `integration-guide` as the canonical doc. It covers
47
+ the same model API across Ablo-managed state, Data Source-backed app databases,
48
+ React selectors, multiplayer, and future agent workers.
49
+
50
+ `ablo.<model>.list(...)` and `count(...)` are synchronous local reads. They
51
+ accept `where`, `filter`, `orderBy`, `limit`, `offset`, and `scope`; scope
52
+ defaults to `'live'`, with `'archived'` and `'all'` for lifecycle-aware reads.
53
+
54
+ Advanced schema-less agents exist for workers that cannot import the app schema,
55
+ but do not teach that path first.
56
+
57
+ React reads should use selector `useAblo`: `useAblo((ablo) => ablo.tasks.retrieve(id))`.
58
+ Use zero-argument `useAblo()` only when a component needs the client for an
59
+ event handler or effect. Treat `useQuery`, `useOne`, `useReader`, and
60
+ `useMutate` as compatibility hooks for older string-keyed integrations, not the
61
+ first integration path.
62
+
63
+ ## Multiplayer
64
+
65
+ Multiplayer is not a separate mode. When human UI, server actions, and agents use
66
+ the same schema client and write through `ablo.<model>`, Ablo coordinates the
67
+ shared resource stream: confirmed deltas fan out to subscribers, active intents
68
+ are visible, and stale writes can be rejected with `readAt`.
69
+
70
+ If an app writes directly to its own database outside Ablo, that write bypasses
71
+ coordination until the app reports it through Data Source events.
72
+
73
+ ## Nouns
74
+
75
+ - `Model resource` is the typed `ablo.<model>` object generated from schema.
76
+ - `Intent` declares active work on a resource so humans and agents can coordinate.
77
+ - `Commit` is the durable protocol write behind `ablo.<model>.update(...)`.
78
+ - `Receipt` confirms the commit.
79
+ - `Capability` scopes what an agent may do. `agent.run(...)` handles the common case.
80
+ - `Task` is one agent run, with audit and cost attribution.
81
+
82
+ ## Busy Behavior
83
+
84
+ Reads never silently block. Pass `ifBusy: 'return'` to receive active intents, `ifBusy: 'fail'` to throw `AbloBusyError`, or `ifBusy: 'wait'` to wait until the active intent clears.
85
+
86
+ Schema clients wait from the realtime intent stream. Schema-less HTTP callers must provide an explicit `busyPollInterval` when using `ifBusy: 'wait'`; Ablo does not hide a hard-coded polling loop.
87
+
88
+ Use `busyTimeout` only as a maximum wait, not as the coordination mechanism.
89
+
90
+ ## Guarantees
91
+
92
+ `wait: 'confirmed'` means the server accepted the write. Schema model writes are optimistic by default; server rejection rolls back local state. Use `snapshot(...)` plus `readAt` and `onStale: 'reject'` to prevent lost updates.
93
+
94
+ Intents are coordination signals, not database locks. Capabilities and tasks are real protocol primitives, but most users let the SDK manage them through schema-backed writes or advanced `agent.run(...)`.
95
+
96
+ All SDK errors extend `AbloError`. Important classes: `AbloBusyError`, `AbloStaleContextError`, `AbloAuthenticationError`, `AbloPermissionError`, `AbloRateLimitError`, `AbloIdempotencyError`, `AbloConnectionError`, `AbloValidationError`, and `AbloServerError`.
97
+
98
+ ## Schema Scope
99
+
100
+ Teach schema as model fields and relations first. Advanced schema helpers such as `mutable`, `readOnly`, `field`, `indexed`, queries, and load strategies exist for offline/cache/indexing-heavy apps, but they should not be the first concept a new integration sees.
101
+
102
+ ## Storage Boundary
103
+
104
+ Do not add `databaseURL` to `Ablo(...)`. Application and agent code use `ABLO_API_KEY`.
105
+
106
+ Every schema model has a backing store. By default, Ablo stores rows for declared models, so `ablo.<model>.create/update/delete` write to Ablo-managed state. If the customer database is canonical, add a Data Source URL in Ablo, store `ABLO_DATA_SOURCE_SIGNING_SECRET` in the app, then expose a signed `dataSource({ schema, signingSecret, load, list, commit, events })` route from the customer app. Customer-owned app database credentials stay private.
107
+
108
+ Use `dataSource` from the root import:
109
+
110
+ ```ts
111
+ import { dataSource } from '@ablo/sync-engine';
112
+ ```
113
+
114
+ ## Sandboxes
115
+
116
+ Public `/sandbox` is a deterministic visual demo. It should teach shared state,
117
+ intents, stale-write rejection, receipts, and deltas, but it does not use a real
118
+ API key. It also exposes a Claude Code / Codex handoff prompt. Prefer that shape
119
+ when an agent is asked to "make Ablo work" in an existing app.
120
+
121
+ Authenticated org sandboxes are real test environments. Treat the default
122
+ sandbox like Stripe test mode: it has an isolated sync group prefix and mints
123
+ `sk_test_*` keys. Extra sandboxes can start blank or copy live configuration.
124
+ Resetting a sandbox creates a clean future stream without touching live data.
125
+ Use `sk_live_*` only for production.
126
+
127
+ For coding agents, the sandbox success path is: pick one shared resource,
128
+ declare schema, create the Ablo client, replace one direct mutation with a typed
129
+ `ablo.<model>.update(...)`, use selector `useAblo` for live reads, and add a
130
+ two-writer stale/intent smoke test.
131
+
132
+ ## Public Surface
133
+
134
+ Import from these public paths only:
135
+
136
+ - `@ablo/sync-engine` — `Ablo`, errors, typed model resources, intents, `dataSource`, and advanced protocol resources.
137
+ - `@ablo/sync-engine/schema` — schema DSL.
138
+ - `@ablo/sync-engine/react` — React provider and hooks.
139
+ - `@ablo/sync-engine/testing` — test harnesses and mocks.
140
+
141
+ Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, `/source`, or internal subpaths.
142
+
143
+ Canonical docs to read before integrating: `quickstart`, `integration-guide`, `guarantees`, `client-behavior`, `data-sources`, `examples/existing-python-backend`, `api`, `examples/ai-sdk-tool`, and `examples/server-agent`.