@abloatai/ablo 0.9.1 → 0.9.3
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.
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +40 -0
- package/README.md +53 -27
- package/dist/BaseSyncedStore.d.ts +2 -36
- package/dist/BaseSyncedStore.js +11 -55
- package/dist/NetworkMonitor.js +4 -1
- package/dist/SyncClient.d.ts +22 -5
- package/dist/SyncClient.js +77 -0
- package/dist/SyncEngineContext.js +5 -1
- package/dist/agent/index.js +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/auth/index.js +3 -1
- package/dist/cli.cjs +302645 -0
- package/dist/client/Ablo.d.ts +19 -52
- package/dist/client/Ablo.js +30 -106
- package/dist/client/ApiClient.d.ts +1 -113
- package/dist/client/ApiClient.js +39 -238
- package/dist/client/auth.js +32 -2
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/createModelProxy.d.ts +9 -0
- package/dist/client/createModelProxy.js +34 -10
- package/dist/client/httpClient.d.ts +5 -6
- package/dist/client/httpClient.js +2 -3
- package/dist/client/index.d.ts +1 -1
- package/dist/client/persistence.d.ts +6 -1
- package/dist/client/persistence.js +1 -1
- package/dist/client/registerDataSource.d.ts +4 -4
- package/dist/client/registerDataSource.js +39 -31
- package/dist/client/writeOptionsSchema.d.ts +50 -0
- package/dist/client/writeOptionsSchema.js +57 -0
- package/dist/core/index.d.ts +18 -26
- package/dist/core/index.js +22 -46
- package/dist/errorCodes.d.ts +13 -0
- package/dist/errorCodes.js +19 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -1
- package/dist/interfaces/index.d.ts +14 -4
- package/dist/mutators/UndoManager.d.ts +48 -5
- package/dist/mutators/UndoManager.js +166 -1
- package/dist/react/AbloProvider.d.ts +18 -8
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/useUndoScope.js +7 -0
- package/dist/schema/ddl.js +2 -1
- package/dist/schema/field.js +2 -1
- package/dist/schema/serialize.js +2 -1
- package/dist/server/commit.d.ts +4 -5
- package/dist/server/storage-mode.d.ts +7 -0
- package/dist/server/storage-mode.js +6 -0
- package/dist/source/adapters/drizzle.js +3 -2
- package/dist/source/adapters/kysely.d.ts +68 -0
- package/dist/source/adapters/kysely.js +210 -0
- package/dist/source/adapters/memory.js +2 -1
- package/dist/source/adapters/prisma.js +3 -2
- package/dist/source/index.js +2 -1
- package/dist/transactions/TransactionQueue.d.ts +6 -7
- package/dist/transactions/TransactionQueue.js +33 -9
- package/dist/types/streams.d.ts +2 -1
- package/dist/utils/duration.js +3 -2
- package/dist/wire/frames.d.ts +6 -8
- package/docs/api.md +1 -1
- package/docs/cli.md +17 -4
- package/docs/client-behavior.md +1 -1
- package/docs/data-sources.md +129 -125
- package/docs/examples/ai-sdk-tool.md +11 -5
- package/docs/examples/existing-python-backend.md +26 -4
- package/docs/examples/nextjs.md +3 -2
- package/docs/examples/scoped-agent.md +38 -11
- package/docs/guarantees.md +2 -2
- package/docs/identity.md +86 -59
- package/docs/index.md +2 -2
- package/docs/integration-guide.md +89 -61
- package/docs/mcp.md +1 -1
- package/docs/quickstart.md +84 -37
- package/docs/react.md +39 -28
- package/docs/schema-contract.md +2 -4
- package/llms-full.txt +360 -0
- package/llms.txt +30 -18
- package/package.json +23 -3
package/AGENTS.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Ablo lets AI agents and humans safely edit the same typed data without clobbering each other. When two of them touch the same row, a "claim" makes one wait for the other instead of overwriting it. This file shows a coding assistant the one safe pattern: read a row, claim it, then write.
|
|
4
|
+
|
|
5
|
+
Claims don't lock. If another writer holds the row, `claim` waits for them and re-reads the fresh row before handing it to you — so two writers serialize instead of clobbering.
|
|
6
|
+
|
|
7
|
+
## Start here — scaffold with `ablo init`
|
|
8
|
+
|
|
9
|
+
Don't hand-write the integration. Run the CLI; it generates the current-API schema, client, Data Source endpoint, and (for Next.js) the browser provider + session route:
|
|
10
|
+
|
|
11
|
+
- **Scaffold:** `npx ablo init --yes` — flag-driven, never prompts. Override defaults with `--framework <nextjs|vite|remix|vanilla>`, `--auth <apikey|…>`, `--no-agent`, `--no-pull`, `--no-install`, `--no-login`. (Plain `ablo init` needs a TTY and will **HANG** in an agent/CI run — always pass `--yes`.)
|
|
12
|
+
- **Auth:** set `ABLO_API_KEY` in the environment. Do **NOT** run `ablo login` — it opens a browser device flow and blocks an agent.
|
|
13
|
+
- **Provision your DB (Data Source mode):** `npx ablo migrate` creates the tables for your Ablo models plus the adapter's bookkeeping tables (`ablo_outbox`, `ablo_idempotency`). It does **not** touch your other tables — keep your own migrations (drizzle-kit / prisma migrate) for auth and anything not in the Ablo schema.
|
|
14
|
+
- **Adopt an existing DB schema:** `npx ablo pull prisma [path]` / `pull drizzle <module>` (lossless) or `pull` (live DB, lossy). Writes `ablo/schema.ts`.
|
|
15
|
+
- **Push your schema — REQUIRED before any write works.** The server keeps its OWN copy of the schema. After you create or edit `ablo/schema.ts`, run `npx ablo push` (one-shot) — or `npx ablo dev --no-watch`. **Skip this and every write to a new or changed model fails with `server_execute_unknown_model`.** (Plain `ablo dev` watches forever — never run it bare in an agent.)
|
|
16
|
+
- **Other long-running:** `npx ablo logs --no-follow` (default tails forever). `npx ablo mode test|live` ALWAYS pass the argument. `status`, `push`, `pull`, `check`, `generate` are one-shot — safe as-is.
|
|
17
|
+
|
|
18
|
+
The generated `ablo/data-source.ts` is the whole Data Source endpoint and needs no hand-editing: `dataSourceNext({ schema, apiKey, adapter: prismaDataSource(prisma, schema) })` (or `drizzleDataSource(db, schema)`). The adapter owns commit / idempotency / outbox.
|
|
19
|
+
|
|
20
|
+
## Rule
|
|
21
|
+
|
|
22
|
+
Edit the generated files; teach this API only:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The schema is the integration contract — it drives typed model clients, React selectors, server and agent writes, the Data Source shape, and schema push. Ablo owns only the models you declare; your auth and other non-synced tables stay in your own ORM schema, side by side in the same database. Don't create a parallel string-keyed write path for rows that belong to a schema model.
|
|
29
|
+
|
|
30
|
+
Every model verb takes ONE options object. The common loop:
|
|
31
|
+
|
|
32
|
+
1. **Read** the row — `await ablo.<model>.retrieve({ id })` (async; from the server) or `await ablo.<model>.list({ where })` for many. In React render, read synchronously with `useAblo((a) => a.<model>.get(id))`.
|
|
33
|
+
2. **See who's active** (optional) — `ablo.<model>.claim.state({ id })` (synchronous; never blocks).
|
|
34
|
+
3. **Claim** the row before changing it — `await using claim = await ablo.<model>.claim({ id, action?, ttl? })`. If someone else holds it, this waits for them, then gives you the fresh row on `claim.data`. The claim auto-releases when it goes out of scope (`await using`).
|
|
35
|
+
4. **Write** — `await ablo.<model>.update({ id: claim.data.id, data })`. Because you hold the claim, the write is rejected if the row changed underneath you.
|
|
36
|
+
|
|
37
|
+
Keep coding assistants on this schema-backed path.
|
|
38
|
+
|
|
39
|
+
## Minimal example
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import Ablo from '@abloatai/ablo';
|
|
43
|
+
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
44
|
+
|
|
45
|
+
const schema = defineSchema({
|
|
46
|
+
weatherReports: model({
|
|
47
|
+
location: z.string(),
|
|
48
|
+
status: z.enum(['pending', 'ready']),
|
|
49
|
+
forecast: z.string().optional(),
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
54
|
+
|
|
55
|
+
const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
|
|
56
|
+
if (!report) throw new Error('Report not found');
|
|
57
|
+
|
|
58
|
+
// If someone else holds the row, claim waits for them and re-reads the fresh
|
|
59
|
+
// row before resolving. Auto-released at the end of this scope (`await using`).
|
|
60
|
+
await using claim = await ablo.weatherReports.claim({
|
|
61
|
+
id: 'report_stockholm',
|
|
62
|
+
action: 'forecasting',
|
|
63
|
+
ttl: '2m',
|
|
64
|
+
});
|
|
65
|
+
const claimed = claim.data;
|
|
66
|
+
|
|
67
|
+
// Because we hold the claim, update is rejected if the row changed underneath us.
|
|
68
|
+
await ablo.weatherReports.update({
|
|
69
|
+
id: claimed.id,
|
|
70
|
+
data: { status: 'ready', forecast: await getForecast(claimed.location) },
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Coordination surface
|
|
75
|
+
|
|
76
|
+
Claims live on a callable namespace beside `create` / `update` / `retrieve`. Every member takes an options object:
|
|
77
|
+
|
|
78
|
+
- `await using claim = await ablo.<model>.claim({ id })` — acquire the row (waits if held); read it via `claim.data`; auto-releases on scope exit (or call `claim.release()`).
|
|
79
|
+
- `ablo.<model>.claim.state({ id })` — who is currently working on the row (synchronous; never blocks).
|
|
80
|
+
- `ablo.<model>.claim.queue({ id })` — who is waiting behind the current holder.
|
|
81
|
+
- `ablo.<model>.claim.release({ id })` — release a claim early.
|
|
82
|
+
- `ablo.<model>.claim.reorder({ id, order })` — reorder the waiting queue.
|
|
83
|
+
|
|
84
|
+
Most users declare a schema and write through `ablo.<model>.update({ id, data })`.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Onboarding: quickstart leads with your-own-database (Drizzle Data Source), drop Ablo-managed mode, add `ablo push` step; context7 library-claim config.
|
|
8
|
+
|
|
9
|
+
## 0.9.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Developer-onboarding overhaul so an LLM or a person gets a working integration on the first try.
|
|
14
|
+
- **`ablo init` scaffolds a project that builds and is current-API.** The Next.js scaffold now ships `app/providers.tsx` + an `app/api/ablo-session` route, uses `useAblo` (the removed `withSync` is gone), object-param verbs, and never bundles your `sk_` key into the browser. The webhook receiver moved off the `[...all]` catch-all.
|
|
15
|
+
- **Agent docs are accurate and ship.** `AGENTS.md`, `llms.txt`, and `llms-full.txt` are on the 0.9.x API (object-param `create`/`update`/`delete`/`retrieve`, disposable `await using claim`, `AbloProvider client` prop), lead with `ablo init`, and `AGENTS.md` now ships in the package.
|
|
16
|
+
- **`ablo push` is self-documenting.** Writing to a model the server hasn't seen now fails with an error that tells you to run `ablo push` (the `server_execute_unknown_model` / `unknown_model` messages), instead of a cryptic "unknown model."
|
|
17
|
+
- **`intents` is deprecated in favor of `claim`** everywhere the docs and the MCP scaffold/prompts teach or generate coordination; the public `ablo.intents` accessor is marked `@internal`.
|
|
18
|
+
- Docs say Node 24+, and the `drizzle-orm` peer floor is `>=0.44`.
|
|
19
|
+
|
|
20
|
+
- a88747a: Remove the `turn` primitive and the agent-work `tasks` resource from the client surface — the SDK is now purely `ablo.<model>` + `claim`.
|
|
21
|
+
|
|
22
|
+
**Breaking**
|
|
23
|
+
- `engine.beginTurn()`, the `Turn` handle interface, and the `Ablo.Turn` type are removed. `AbloApi.beginTurn` and the HTTP client's `beginTurn` are gone too.
|
|
24
|
+
- `CommitCreateOptions.causedByTaskId` is removed. (Lineage is no longer stamped from the client.)
|
|
25
|
+
- The engine no longer exposes a `protocol` accessor or a public `tasks` work-unit resource. `ablo.tasks` is, and always was, the schema `tasks` model proxy.
|
|
26
|
+
- The **`agent().run()` helper and the low-level agent/task type family are removed**: `AbloApi.agent(id, options)` and `AbloApi.tasks` (the `TaskResource`), plus the exported types `Agent`, `AgentOptions`, `AgentRunOptions`, `AgentRunResult`/`Done`/`Failed`/`Cancelled`, `AgentRunStatus`, `AgentRunContext`, `AgentModelClient`, `AgentModelReadOptions`, `AgentModelMutationOptions`, `AgentIntentOptions`, `AgentIntentInput`, `Task`, `TaskResource`, `TaskCreateOptions`, `TaskCloseOptions`, `TaskCloseResult` (and the `Ablo.*` namespace aliases for all of them). The `Ablo.Auth.Agent` principal constructor and the schema-backed `tasks` model are unaffected.
|
|
27
|
+
|
|
28
|
+
**Why**
|
|
29
|
+
|
|
30
|
+
`turn`/`agent_tasks` was a second coordination-and-attribution mechanism living alongside `claim`. It is redundant on the client:
|
|
31
|
+
- `claim` already serializes writers **and** carries the causal link — its `intent` id rides on every guarded write.
|
|
32
|
+
- The server stamps `actor` / `onBehalfOf` / `capabilityId` onto each delta from the auth context.
|
|
33
|
+
- Per-run token/cost is recorded in Langfuse, not the `agent_tasks` table.
|
|
34
|
+
|
|
35
|
+
So the only thing the client lost is the audit pane's "show everything this exact prompt produced" filter, which keyed off `caused_by_task_id`; new writes leave that column null.
|
|
36
|
+
|
|
37
|
+
**Migration**
|
|
38
|
+
|
|
39
|
+
Agents stop opening/closing tasks — just issue `ablo.<model>` writes (schema-backed) or `ablo.commits.create(...)` (schema-less) under a `claim`. Replace `Ablo({ apiKey }).agent(id, opts).run(prompt, handler)` with: mint a scoped credential via `sessions.create({ agent })`, then `claim` the row and `update` / `commits.create`.
|
|
40
|
+
|
|
41
|
+
The **server** `agent_tasks` table, the `caused_by_task_id` delta column, the `/api/sync/commit` wire field, and the `agent_actions_log` compliance hash-chain remain in place but **dormant** (client writes leave the field null) — they are load-bearing for the tamper-evident audit chain and historical-row audit JOINs, so they are intentionally NOT dropped. The dead `/v1/tasks` + `/api/agent/turn` route handlers ARE removed (zero live callers).
|
|
42
|
+
|
|
3
43
|
## 0.9.1
|
|
4
44
|
|
|
5
45
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@abloatai/ablo)
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
[](#)
|
|
6
|
-
[](#keys--runtime)
|
|
7
7
|
|
|
8
8
|
**Let people and AI agents work on the same data without overwriting each other.**
|
|
9
9
|
|
|
@@ -33,27 +33,43 @@ claims are visible while the work is still in progress.
|
|
|
33
33
|
|
|
34
34
|
[Get started ↓](#quick-start) · point your coding agent at the shipped `llms.txt`
|
|
35
35
|
|
|
36
|
-
It works with the auth and database you already have
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
It works with the auth and database you already have. **Your database is the
|
|
37
|
+
system of record — Ablo never hosts your data.** Ablo is the transaction layer
|
|
38
|
+
on top of it: realtime data is scoped to *sync groups* from your own identity,
|
|
39
|
+
and every committed row lives in your Postgres.
|
|
39
40
|
|
|
40
41
|
**Built for** collaborative editors, AI agent workflows, and internal tools —
|
|
41
42
|
anywhere people and agents change shared state and everyone has to see it live.
|
|
42
43
|
|
|
43
44
|
## Set up
|
|
44
45
|
|
|
46
|
+
The CLI takes you from nothing to a synced schema — it handles the account,
|
|
47
|
+
the key, and the env file. You bring one thing: a Postgres `DATABASE_URL`
|
|
48
|
+
(local, Neon, RDS — any will do; **your database is the system of record,
|
|
49
|
+
Ablo never hosts your data**).
|
|
50
|
+
|
|
45
51
|
```bash
|
|
46
52
|
npm install @abloatai/ablo
|
|
53
|
+
npx ablo login # opens the browser: sign in (or sign up) → a sk_test_ key is saved locally
|
|
54
|
+
npx ablo init # scaffolds ablo/schema.ts (offers to log in if you skipped it)
|
|
55
|
+
npx ablo migrate # creates the synced tables in YOUR Postgres (reads DATABASE_URL)
|
|
56
|
+
npx ablo dev # pushes your schema (test mode), writes ABLO_API_KEY to .env.local, watches for changes
|
|
47
57
|
```
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
After `ablo dev`, the [Quick Start](#quick-start) below runs as-is —
|
|
60
|
+
`ABLO_API_KEY` is already in `.env.local` (frameworks load it automatically;
|
|
61
|
+
plain Node: `node --env-file=.env.local app.ts`). `npx ablo status` shows
|
|
62
|
+
what's configured at any time.
|
|
63
|
+
|
|
64
|
+
**Keys & runtime.** Ablo needs Node 24+ and TypeScript 5+. Keys come in two of
|
|
65
|
+
*your* environments — `sk_test_` and `sk_live_`, like Stripe — and `ablo login`
|
|
66
|
+
mints both. Keep the key and the database URL in trusted server runtimes only.
|
|
52
67
|
In the browser, `<AbloProvider>` authenticates with the signed-in user's
|
|
53
|
-
session — never the raw key.
|
|
68
|
+
session — never the raw key, never the database URL. Prefer the connection
|
|
69
|
+
string never leaving your infrastructure? Expose a signed
|
|
70
|
+
[Data Source endpoint](./docs/data-sources.md) instead and omit `databaseUrl`.
|
|
54
71
|
|
|
55
|
-
|
|
56
|
-
copy. For production (React, an existing backend, Data Source, agents), the
|
|
72
|
+
For production (React, an existing backend, Data Source, agents), the
|
|
57
73
|
[Integration Guide](./docs/integration-guide.md) is the deeper map.
|
|
58
74
|
|
|
59
75
|
**Prefer to let an agent wire it?** The package ships an `llms.txt` — a precise
|
|
@@ -78,7 +94,8 @@ const schema = defineSchema({
|
|
|
78
94
|
|
|
79
95
|
const ablo = Ablo({
|
|
80
96
|
schema,
|
|
81
|
-
apiKey: process.env.ABLO_API_KEY,
|
|
97
|
+
apiKey: process.env.ABLO_API_KEY, // written to .env.local by `npx ablo dev`
|
|
98
|
+
databaseUrl: process.env.DATABASE_URL, // your Postgres — rows live here, never with Ablo
|
|
82
99
|
});
|
|
83
100
|
|
|
84
101
|
await ablo.ready();
|
|
@@ -99,7 +116,7 @@ const forecast = await fetchForecast(report.location); // slow: API or LLM call
|
|
|
99
116
|
await ablo.weatherReports.update({ id: report.id, data: { status: 'ready', forecast } });
|
|
100
117
|
|
|
101
118
|
const ready = ablo.weatherReports.get(created.id);
|
|
102
|
-
console.log({ id: ready
|
|
119
|
+
console.log({ id: ready?.id, status: ready?.status });
|
|
103
120
|
|
|
104
121
|
await ablo.dispose();
|
|
105
122
|
```
|
|
@@ -218,12 +235,16 @@ provider and read with hooks, from `@abloatai/ablo/react`. Wrap your tree once;
|
|
|
218
235
|
everything inside is live.
|
|
219
236
|
|
|
220
237
|
```tsx
|
|
238
|
+
import Ablo from '@abloatai/ablo';
|
|
221
239
|
import { AbloProvider, useAblo } from '@abloatai/ablo/react';
|
|
222
240
|
import { schema } from './ablo/schema';
|
|
223
241
|
|
|
242
|
+
// Build the client once — it authenticates via your session route, no key in the browser.
|
|
243
|
+
const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
|
|
244
|
+
|
|
224
245
|
function App() {
|
|
225
246
|
return (
|
|
226
|
-
<AbloProvider
|
|
247
|
+
<AbloProvider client={ablo}>
|
|
227
248
|
<Report id="report_stockholm" />
|
|
228
249
|
</AbloProvider>
|
|
229
250
|
);
|
|
@@ -250,8 +271,9 @@ method as the server example above.
|
|
|
250
271
|
`<AbloProvider>` owns the connection — no API key in the browser. That's the
|
|
251
272
|
whole loop: read with `useAblo(selector)`, write with `ablo.<model>`, and every
|
|
252
273
|
other client (human or agent) on that row sees it in real time. See
|
|
253
|
-
[React](./docs/react.md) for the
|
|
254
|
-
`
|
|
274
|
+
[React](./docs/react.md) for the `<AbloProvider>` prop surface (`client`,
|
|
275
|
+
`userId`, `fallback`, `onError`) — schema, scope, and team membership live on the
|
|
276
|
+
`Ablo({ … })` client you pass it — plus status hooks.
|
|
255
277
|
|
|
256
278
|
## Identity & Sync Groups
|
|
257
279
|
|
|
@@ -270,7 +292,10 @@ to sync-group strings.
|
|
|
270
292
|
`userId` / `teamIds` come from your auth, resolved server-side:
|
|
271
293
|
|
|
272
294
|
```tsx
|
|
273
|
-
|
|
295
|
+
// team membership is asserted server-side when the session route mints the token.
|
|
296
|
+
const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
|
|
297
|
+
|
|
298
|
+
<AbloProvider client={ablo} userId={user.id}>
|
|
274
299
|
<App />
|
|
275
300
|
</AbloProvider>
|
|
276
301
|
```
|
|
@@ -316,18 +341,18 @@ curl https://api.abloatai.com/v1/commits \
|
|
|
316
341
|
{ "object": "commit_receipt", "status": "confirmed", "serverTxId": "tx_…", "lastSyncId": 1042, "ops": 1 }
|
|
317
342
|
```
|
|
318
343
|
|
|
319
|
-
##
|
|
344
|
+
## Your Database
|
|
320
345
|
|
|
321
|
-
Every schema model
|
|
322
|
-
|
|
323
|
-
write to Ablo-managed state.
|
|
346
|
+
Every schema model is backed by **your own database** — Ablo is the transaction
|
|
347
|
+
layer on top of it, never the home for your rows. Two ways to connect it:
|
|
324
348
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
349
|
+
| | How Ablo reaches your Postgres | Use when |
|
|
350
|
+
| --- | --- | --- |
|
|
351
|
+
| **Connection string** (default) | `databaseUrl` at init. Ablo registers the connection once (sent over TLS, stored sealed, never echoed back) and commits each write directly — through a non-superuser role, behind row-level security. | You can hand over a scoped connection string. |
|
|
352
|
+
| **Signed endpoint** | Your app exposes one route built from an ORM adapter (`prismaDataSource` / `drizzleDataSource`); Ablo sends signed commit requests and your app writes its own database. | Database credentials must never leave your infrastructure. |
|
|
329
353
|
|
|
330
|
-
|
|
354
|
+
Same product, same truth either way: your database is the system of record. See
|
|
355
|
+
[Connect Your Database](./docs/data-sources.md) for both shapes.
|
|
331
356
|
|
|
332
357
|
## Configuration
|
|
333
358
|
|
|
@@ -337,6 +362,7 @@ See [Connect Your Database](./docs/data-sources.md) for the integration shape.
|
|
|
337
362
|
| --- | --- | --- | --- |
|
|
338
363
|
| `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
|
|
339
364
|
| `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
|
|
365
|
+
| `databaseUrl` | `string \| null` | `process.env.DATABASE_URL` | Your Postgres, registered as the data plane. Server runtimes only — the SDK throws if it sees this in a browser. Omit it when your app exposes a signed [Data Source endpoint](./docs/data-sources.md) instead. |
|
|
340
366
|
| `baseURL` | `string` | `wss://api.abloatai.com` | Point at a self-hosted or private API |
|
|
341
367
|
|
|
342
368
|
Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
|
|
@@ -387,11 +413,11 @@ contract; there are no retry or timeout knobs to tune.
|
|
|
387
413
|
- [Identity & Sync Groups](./docs/identity.md) — use your own authentication; tell Ablo who's connecting and how org / team / user map to sync-group scope.
|
|
388
414
|
- [Schema Contract](./docs/schema-contract.md) — one schema becomes typed model clients, React reads, agent writes, Data Source shape, and schema push.
|
|
389
415
|
- [Guarantees](./docs/guarantees.md) — confirmed writes, stale-write protection, claim coordination, and agent lifecycle.
|
|
390
|
-
- [Integration Guide](./docs/integration-guide.md) —
|
|
416
|
+
- [Integration Guide](./docs/integration-guide.md) — integrate React, your database, multiplayer, and agents.
|
|
391
417
|
- [React](./docs/react.md) — `<AbloProvider>`, `useAblo`, presence, status, and bootstrap gating.
|
|
392
418
|
- [Coordination](./docs/coordination.md) — `claim` / `claim.state` / `claim.queue` / `claim.release` reference: hold a row across slow agent work, and observe the line waiting behind it.
|
|
393
419
|
- [Client Behavior](./docs/client-behavior.md) — options, errors, retries, timeouts, and public imports.
|
|
394
|
-
- [Connect Your Database](./docs/data-sources.md) —
|
|
420
|
+
- [Connect Your Database](./docs/data-sources.md) — connect your Postgres by connection string (`databaseUrl`) or signed endpoint; your database is the system of record either way.
|
|
395
421
|
- [Existing Python Backend](./docs/examples/existing-python-backend.md) — migrate existing Python endpoints to multiplayer and agent-safe writes gradually.
|
|
396
422
|
- [AI SDK Tool](./docs/examples/ai-sdk-tool.md) — use Ablo inside an AI SDK tool call.
|
|
397
423
|
- [Server Agent](./docs/examples/server-agent.md) — schema-backed worker.
|
|
@@ -21,7 +21,6 @@ import { QueryProcessor } from './core/QueryProcessor.js';
|
|
|
21
21
|
import { Model } from './Model.js';
|
|
22
22
|
import { ModelScope } from './ObjectPool.js';
|
|
23
23
|
import type { Schema } from './schema/schema.js';
|
|
24
|
-
import { type ReaderActions } from './mutators/readerActions.js';
|
|
25
24
|
import type { LocalMutation } from './react/context.js';
|
|
26
25
|
import type { AuthCredentialSource } from './auth/credentialSource.js';
|
|
27
26
|
/** Constructor type for Model subclasses (accepts abstract classes) */
|
|
@@ -224,18 +223,6 @@ export declare function deriveSyncPlanFromSchema(schema: Schema): {
|
|
|
224
223
|
enrichmentPlan: EnrichmentPlanEntry[];
|
|
225
224
|
foreignKeyIndexes: ForeignKeyIndexSpec[];
|
|
226
225
|
};
|
|
227
|
-
/**
|
|
228
|
-
* Schema-derived accessor namespace exposed on `store.query`. Each key is
|
|
229
|
-
* a model name from the schema and resolves to a `ReaderActions<S, K>`
|
|
230
|
-
* with `findById` / `findMany` / `findFirst` / `count`. Return types are
|
|
231
|
-
* inferred from the schema (`InferModel<S, K>`) so callers don't need to
|
|
232
|
-
* cast or pass class constructors.
|
|
233
|
-
*
|
|
234
|
-
* Prisma / Drizzle / Zero all use this shape: `store.query.<modelKey>.*`.
|
|
235
|
-
*/
|
|
236
|
-
export type QueryNamespace<S extends Schema> = {
|
|
237
|
-
readonly [K in keyof S['models'] & string]: ReaderActions<S, K>;
|
|
238
|
-
};
|
|
239
226
|
export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaboration> = DefaultCollaborationEvents, TSchema extends Schema = Schema> {
|
|
240
227
|
syncStatus: SyncStatus;
|
|
241
228
|
protected readonly syncClient: SyncClient;
|
|
@@ -244,13 +231,10 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
244
231
|
protected readonly modelRegistry: ModelRegistry;
|
|
245
232
|
protected readonly auth?: AuthCredentialSource;
|
|
246
233
|
/**
|
|
247
|
-
* Schema the store was constructed with.
|
|
248
|
-
*
|
|
249
|
-
* without callers having to pass the schema at every lookup site.
|
|
234
|
+
* Schema the store was constructed with. Used by the schema-typed
|
|
235
|
+
* `create(key, data)` factory and model self-healing.
|
|
250
236
|
*/
|
|
251
237
|
protected readonly schema?: TSchema;
|
|
252
|
-
/** Lazily-built `query.<modelKey>.*` accessor namespace. */
|
|
253
|
-
private _queryProxy?;
|
|
254
238
|
protected syncWebSocket: SyncWebSocket<TCollaboration> | null;
|
|
255
239
|
private _syncServerUrl?;
|
|
256
240
|
/**
|
|
@@ -694,24 +678,6 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
694
678
|
id: string;
|
|
695
679
|
archivedAt?: Date | null;
|
|
696
680
|
}>(entity: T): Promise<void>;
|
|
697
|
-
/**
|
|
698
|
-
* Schema-keyed accessor namespace — the primary type-safe lookup surface.
|
|
699
|
-
*
|
|
700
|
-
* ```ts
|
|
701
|
-
* const chat = store.query.chats.retrieve(chatId); // Chat | undefined
|
|
702
|
-
* const slides = store.query.slides.findMany({ where: { deckId } }); // Slide[]
|
|
703
|
-
* ```
|
|
704
|
-
*
|
|
705
|
-
* Each `query.<modelKey>` is a `ReaderActions<Schema, K>` built lazily on
|
|
706
|
-
* first access via `createReaderActions`. The returned types are inferred
|
|
707
|
-
* from the schema (`InferModel<S, K>`), including `InferRelations` — so
|
|
708
|
-
* `chat.messages`, `slide.layers`, etc. are typed without a cast.
|
|
709
|
-
*
|
|
710
|
-
* Throws if the store was constructed without a schema (class-based
|
|
711
|
-
* subclasses that wire models via `modelRegistry.registerModel` directly
|
|
712
|
-
* don't have access to schema-derived inference).
|
|
713
|
-
*/
|
|
714
|
-
get query(): QueryNamespace<TSchema>;
|
|
715
681
|
/** Retrieve a single entity by id. Synchronous pool read. */
|
|
716
682
|
retrieve(_modelClass: ModelConstructor<Model>, id: string): Model | undefined;
|
|
717
683
|
/** Find any entity by ID regardless of type */
|
package/dist/BaseSyncedStore.js
CHANGED
|
@@ -22,7 +22,6 @@ import { getContext } from './context.js';
|
|
|
22
22
|
import { SyncSessionError } from './errors.js';
|
|
23
23
|
import { ModelScope } from './ObjectPool.js';
|
|
24
24
|
import { LazyReferenceCollection } from './LazyReferenceCollection.js';
|
|
25
|
-
import { createReaderActions } from './mutators/readerActions.js';
|
|
26
25
|
/** Bootstrap timeout configuration */
|
|
27
26
|
export const BOOTSTRAP_CONFIG = {
|
|
28
27
|
OVERALL_TIMEOUT_MS: 15_000,
|
|
@@ -126,13 +125,10 @@ export class BaseSyncedStore {
|
|
|
126
125
|
modelRegistry;
|
|
127
126
|
auth;
|
|
128
127
|
/**
|
|
129
|
-
* Schema the store was constructed with.
|
|
130
|
-
*
|
|
131
|
-
* without callers having to pass the schema at every lookup site.
|
|
128
|
+
* Schema the store was constructed with. Used by the schema-typed
|
|
129
|
+
* `create(key, data)` factory and model self-healing.
|
|
132
130
|
*/
|
|
133
131
|
schema;
|
|
134
|
-
/** Lazily-built `query.<modelKey>.*` accessor namespace. */
|
|
135
|
-
_queryProxy;
|
|
136
132
|
// ── Real-time sync ──
|
|
137
133
|
syncWebSocket = null;
|
|
138
134
|
_syncServerUrl;
|
|
@@ -421,8 +417,12 @@ export class BaseSyncedStore {
|
|
|
421
417
|
* emit here, so undo is naturally local-only (you can't undo a teammate).
|
|
422
418
|
*/
|
|
423
419
|
subscribeLocalMutations(handler) {
|
|
424
|
-
|
|
425
|
-
|
|
420
|
+
// Tap the TransactionQueue directly via `onLocalTransaction`. The previous
|
|
421
|
+
// `syncClient.subscribe('transaction:created', …)` route registered the
|
|
422
|
+
// handler on SyncClient's OWN emitter, which never fires that event (only
|
|
423
|
+
// the queue's emitter does) — so undo recorded nothing. See
|
|
424
|
+
// `SyncClient.onLocalTransaction` for the full rationale.
|
|
425
|
+
return this.syncClient.onLocalTransaction((tx) => {
|
|
426
426
|
if (!tx || !tx.type || !tx.modelName || !tx.modelId)
|
|
427
427
|
return;
|
|
428
428
|
handler({
|
|
@@ -1785,53 +1785,9 @@ export class BaseSyncedStore {
|
|
|
1785
1785
|
this.syncClient.update(model);
|
|
1786
1786
|
}
|
|
1787
1787
|
// ── Query API ────────────────────────────────────────────────────────────
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
* ```ts
|
|
1792
|
-
* const chat = store.query.chats.retrieve(chatId); // Chat | undefined
|
|
1793
|
-
* const slides = store.query.slides.findMany({ where: { deckId } }); // Slide[]
|
|
1794
|
-
* ```
|
|
1795
|
-
*
|
|
1796
|
-
* Each `query.<modelKey>` is a `ReaderActions<Schema, K>` built lazily on
|
|
1797
|
-
* first access via `createReaderActions`. The returned types are inferred
|
|
1798
|
-
* from the schema (`InferModel<S, K>`), including `InferRelations` — so
|
|
1799
|
-
* `chat.messages`, `slide.layers`, etc. are typed without a cast.
|
|
1800
|
-
*
|
|
1801
|
-
* Throws if the store was constructed without a schema (class-based
|
|
1802
|
-
* subclasses that wire models via `modelRegistry.registerModel` directly
|
|
1803
|
-
* don't have access to schema-derived inference).
|
|
1804
|
-
*/
|
|
1805
|
-
get query() {
|
|
1806
|
-
if (!this.schema) {
|
|
1807
|
-
throw new AbloValidationError('store.query requires a schema to be passed to the BaseSyncedStore constructor. ' +
|
|
1808
|
-
'Pass `{ schema }` in the dependencies argument.', { code: 'store_query_schema_missing' });
|
|
1809
|
-
}
|
|
1810
|
-
if (!this._queryProxy) {
|
|
1811
|
-
const schema = this.schema;
|
|
1812
|
-
// BaseSyncedStore satisfies SyncStoreContract structurally via
|
|
1813
|
-
// `findById` / `queryByClass` / `save` / `delete`. Pass `this`
|
|
1814
|
-
// directly — `createReaderActions` accepts the contract shape.
|
|
1815
|
-
const store = this;
|
|
1816
|
-
const cache = new Map();
|
|
1817
|
-
this._queryProxy = new Proxy({}, {
|
|
1818
|
-
get: (_target, prop) => {
|
|
1819
|
-
if (typeof prop !== 'string')
|
|
1820
|
-
return undefined;
|
|
1821
|
-
const cached = cache.get(prop);
|
|
1822
|
-
if (cached)
|
|
1823
|
-
return cached;
|
|
1824
|
-
if (!(prop in schema.models)) {
|
|
1825
|
-
throw new AbloValidationError(`store.query: unknown model key "${prop}". Known keys: ${Object.keys(schema.models).join(', ')}`, { code: 'store_query_unknown_model' });
|
|
1826
|
-
}
|
|
1827
|
-
const actions = createReaderActions(schema, prop, store);
|
|
1828
|
-
cache.set(prop, actions);
|
|
1829
|
-
return actions;
|
|
1830
|
-
},
|
|
1831
|
-
});
|
|
1832
|
-
}
|
|
1833
|
-
return this._queryProxy;
|
|
1834
|
-
}
|
|
1788
|
+
// `store.query.<model>.*` was DELETED — `ablo.<model>.get/getAll` is the
|
|
1789
|
+
// one read surface. Custom mutators still read transactionally through
|
|
1790
|
+
// `tx.<model>` (mutators/Transaction.ts), which owns `createReaderActions`.
|
|
1835
1791
|
/** Retrieve a single entity by id. Synchronous pool read. */
|
|
1836
1792
|
retrieve(_modelClass, id) {
|
|
1837
1793
|
return this.objectPool.get(id);
|
package/dist/NetworkMonitor.js
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
import { EventEmitter } from 'events';
|
|
10
10
|
import { getContext } from './context.js';
|
|
11
11
|
export class NetworkMonitor extends EventEmitter {
|
|
12
|
-
|
|
12
|
+
// Only `navigator.onLine === false` means offline. Node 18+ exposes a global
|
|
13
|
+
// `navigator` with `onLine === undefined`, so the naive `navigator.onLine`
|
|
14
|
+
// would seed `false` (offline) on every server client — start optimistic.
|
|
15
|
+
isOnline = !(typeof navigator !== 'undefined' && navigator.onLine === false);
|
|
13
16
|
lastOnlineCheck = new Date();
|
|
14
17
|
constructor() {
|
|
15
18
|
super();
|
package/dist/SyncClient.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { EventEmitter } from 'events';
|
|
|
13
13
|
import { TransactionQueue } from './transactions/TransactionQueue.js';
|
|
14
14
|
import { type OptimisticEchoMetrics } from './transactions/OptimisticEchoTracker.js';
|
|
15
15
|
import type { Database } from './Database.js';
|
|
16
|
-
import type {
|
|
16
|
+
import type { WriteOptions } from './interfaces/index.js';
|
|
17
17
|
interface SyncObserver {
|
|
18
18
|
onSync?: (event: SyncEvent) => void;
|
|
19
19
|
}
|
|
@@ -38,7 +38,6 @@ export interface RehydrationStats {
|
|
|
38
38
|
healed: number;
|
|
39
39
|
elapsedMs: number;
|
|
40
40
|
}
|
|
41
|
-
type MutationWriteOptions = Pick<MutationOptions, 'readAt' | 'onStale'>;
|
|
42
41
|
export declare class SyncClient extends EventEmitter {
|
|
43
42
|
private objectPool;
|
|
44
43
|
private database;
|
|
@@ -114,6 +113,12 @@ export declare class SyncClient extends EventEmitter {
|
|
|
114
113
|
* Initialize sync client with authentication
|
|
115
114
|
*/
|
|
116
115
|
initialize(userId: string, organizationId: string): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* The organization this client writes under (set by `initialize`).
|
|
118
|
+
* Read by the model proxy so `create()` defaults `organizationId` the
|
|
119
|
+
* same way the mutator path does — `null` until identity is wired.
|
|
120
|
+
*/
|
|
121
|
+
getOrganizationId(): string | null;
|
|
117
122
|
/**
|
|
118
123
|
* Self-healing helper for individual model records.
|
|
119
124
|
*
|
|
@@ -172,9 +177,9 @@ export declare class SyncClient extends EventEmitter {
|
|
|
172
177
|
*/
|
|
173
178
|
private captureModelChanges;
|
|
174
179
|
/** Add new model (CREATE) - works offline */
|
|
175
|
-
add(model: Model, options?:
|
|
180
|
+
add(model: Model, options?: WriteOptions): void;
|
|
176
181
|
/** Update existing model (UPDATE) - works offline */
|
|
177
|
-
update(model: Model, options?:
|
|
182
|
+
update(model: Model, options?: WriteOptions): void;
|
|
178
183
|
/**
|
|
179
184
|
* Update existing model with pre-computed changes.
|
|
180
185
|
* Used by saveManyOptimized when incoming models have empty change-tracking
|
|
@@ -186,7 +191,7 @@ export declare class SyncClient extends EventEmitter {
|
|
|
186
191
|
* but still need optimistic pool updates at the sync layer. */
|
|
187
192
|
get gql(): import("./interfaces/index.js").MutationExecutor;
|
|
188
193
|
/** Delete model (DELETE) - works offline */
|
|
189
|
-
delete(model: Model, options?:
|
|
194
|
+
delete(model: Model, options?: WriteOptions): void;
|
|
190
195
|
/**
|
|
191
196
|
* Clear all pending mutations for a specific model
|
|
192
197
|
* Called before deletion to prevent "layer not found" errors on the server
|
|
@@ -383,6 +388,18 @@ export declare class SyncClient extends EventEmitter {
|
|
|
383
388
|
error: Error;
|
|
384
389
|
permanent?: boolean;
|
|
385
390
|
}) => void): () => void;
|
|
391
|
+
/**
|
|
392
|
+
* Subscribe to LOCAL transaction creation with the full {@link Transaction}
|
|
393
|
+
* payload (`type`, `modelName`, `modelId`, `data`, `previousData`). This is
|
|
394
|
+
* the feed `BaseSyncedStore.subscribeLocalMutations` taps for undo recording.
|
|
395
|
+
*
|
|
396
|
+
* MUST subscribe to the TransactionQueue's emitter directly — that is the
|
|
397
|
+
* ONLY emitter that fires `transaction:created`. SyncClient's own emitter
|
|
398
|
+
* (reached via `subscribe()`) never re-broadcasts it, so routing undo through
|
|
399
|
+
* `subscribe('transaction:created')` silently records nothing. Mirrors
|
|
400
|
+
* `onMutationFailure`, which taps the queue for the same reason.
|
|
401
|
+
*/
|
|
402
|
+
onLocalTransaction(listener: (tx: import('./transactions/TransactionQueue.js').Transaction) => void): () => void;
|
|
386
403
|
/**
|
|
387
404
|
* Wait for the latest in-flight transaction for (modelName, modelId)
|
|
388
405
|
* to be confirmed by the server, or reject if it's rolled back.
|
package/dist/SyncClient.js
CHANGED
|
@@ -313,6 +313,14 @@ export class SyncClient extends EventEmitter {
|
|
|
313
313
|
this.emit('sync:offline');
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* The organization this client writes under (set by `initialize`).
|
|
318
|
+
* Read by the model proxy so `create()` defaults `organizationId` the
|
|
319
|
+
* same way the mutator path does — `null` until identity is wired.
|
|
320
|
+
*/
|
|
321
|
+
getOrganizationId() {
|
|
322
|
+
return this.organizationId;
|
|
323
|
+
}
|
|
316
324
|
/**
|
|
317
325
|
* Self-healing helper for individual model records.
|
|
318
326
|
*
|
|
@@ -1298,6 +1306,75 @@ export class SyncClient extends EventEmitter {
|
|
|
1298
1306
|
this.transactionQueue.on('transaction:failed', listener);
|
|
1299
1307
|
return () => this.transactionQueue.off('transaction:failed', listener);
|
|
1300
1308
|
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Subscribe to LOCAL transaction creation with the full {@link Transaction}
|
|
1311
|
+
* payload (`type`, `modelName`, `modelId`, `data`, `previousData`). This is
|
|
1312
|
+
* the feed `BaseSyncedStore.subscribeLocalMutations` taps for undo recording.
|
|
1313
|
+
*
|
|
1314
|
+
* MUST subscribe to the TransactionQueue's emitter directly — that is the
|
|
1315
|
+
* ONLY emitter that fires `transaction:created`. SyncClient's own emitter
|
|
1316
|
+
* (reached via `subscribe()`) never re-broadcasts it, so routing undo through
|
|
1317
|
+
* `subscribe('transaction:created')` silently records nothing. Mirrors
|
|
1318
|
+
* `onMutationFailure`, which taps the queue for the same reason.
|
|
1319
|
+
*/
|
|
1320
|
+
onLocalTransaction(listener) {
|
|
1321
|
+
this.transactionQueue.on('transaction:created', listener);
|
|
1322
|
+
// Commit-lane writes (`ablo.commits.create` — the agent/atomic door) ride
|
|
1323
|
+
// their own `commit:created` event: they have no optimistic pool apply,
|
|
1324
|
+
// so they must not feed the echo tracker's `transaction:created` path.
|
|
1325
|
+
// Enrich each operation with previous state captured from the pool HERE
|
|
1326
|
+
// (the queue is pool-free) and hand the synthesized transaction to the
|
|
1327
|
+
// same listener, so undo observes every write door — one stream.
|
|
1328
|
+
const onCommitCreated = (payload) => {
|
|
1329
|
+
const TYPE_BY_WIRE = {
|
|
1330
|
+
CREATE: 'create',
|
|
1331
|
+
UPDATE: 'update',
|
|
1332
|
+
DELETE: 'delete',
|
|
1333
|
+
ARCHIVE: 'archive',
|
|
1334
|
+
UNARCHIVE: 'unarchive',
|
|
1335
|
+
};
|
|
1336
|
+
payload.operations.forEach((op, index) => {
|
|
1337
|
+
const type = TYPE_BY_WIRE[op.type];
|
|
1338
|
+
if (!type || !op.id)
|
|
1339
|
+
return;
|
|
1340
|
+
const resident = this.objectPool.get(op.id);
|
|
1341
|
+
const snapshot = type === 'create' ? undefined : resident?.toJSON();
|
|
1342
|
+
// A DELETE of a row the local graph never saw is not invertible —
|
|
1343
|
+
// recording it would make undo "restore" an empty husk. Skip it.
|
|
1344
|
+
if (type === 'delete' && !snapshot)
|
|
1345
|
+
return;
|
|
1346
|
+
// UPDATE inverse must only revert the fields this op actually wrote;
|
|
1347
|
+
// handing undo the FULL row would clobber concurrent edits to
|
|
1348
|
+
// unrelated fields on revert.
|
|
1349
|
+
const previousData = type === 'update' && snapshot && op.input
|
|
1350
|
+
? Object.fromEntries(Object.keys(op.input).map((key) => [key, snapshot[key]]))
|
|
1351
|
+
: snapshot ?? null;
|
|
1352
|
+
listener({
|
|
1353
|
+
id: `${payload.clientTxId}_op${index}`,
|
|
1354
|
+
type,
|
|
1355
|
+
modelName: op.model,
|
|
1356
|
+
modelId: op.id,
|
|
1357
|
+
modelKey: op.model,
|
|
1358
|
+
data: op.input ?? undefined,
|
|
1359
|
+
previousData,
|
|
1360
|
+
context: {
|
|
1361
|
+
userId: this.userId ?? '',
|
|
1362
|
+
organizationId: this.organizationId ?? '',
|
|
1363
|
+
},
|
|
1364
|
+
status: 'pending',
|
|
1365
|
+
createdAt: Date.now(),
|
|
1366
|
+
attempts: 0,
|
|
1367
|
+
priority: 'normal',
|
|
1368
|
+
priorityScore: 0,
|
|
1369
|
+
});
|
|
1370
|
+
});
|
|
1371
|
+
};
|
|
1372
|
+
this.transactionQueue.on('commit:created', onCommitCreated);
|
|
1373
|
+
return () => {
|
|
1374
|
+
this.transactionQueue.off('transaction:created', listener);
|
|
1375
|
+
this.transactionQueue.off('commit:created', onCommitCreated);
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1301
1378
|
/**
|
|
1302
1379
|
* Wait for the latest in-flight transaction for (modelName, modelId)
|
|
1303
1380
|
* to be confirmed by the server, or reject if it's rolled back.
|