@abloatai/ablo 0.8.0 → 0.9.1
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/CHANGELOG.md +46 -1
- package/README.md +33 -28
- package/dist/BaseSyncedStore.d.ts +83 -0
- package/dist/BaseSyncedStore.js +194 -2
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/agent/session.js +3 -3
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +4 -42
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +160 -42
- package/dist/client/Ablo.js +145 -75
- package/dist/client/ApiClient.d.ts +20 -4
- package/dist/client/ApiClient.js +166 -28
- package/dist/client/auth.d.ts +14 -5
- package/dist/client/auth.js +60 -7
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +130 -66
- package/dist/client/createModelProxy.js +152 -49
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +49 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +3 -3
- package/dist/client/registerDataSource.js +11 -9
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +70 -1
- package/dist/errorCodes.js +108 -9
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +72 -22
- package/dist/index.d.ts +17 -8
- package/dist/index.js +15 -6
- package/dist/keys/index.d.ts +16 -1
- package/dist/keys/index.js +26 -6
- package/dist/mutators/UndoManager.d.ts +158 -50
- package/dist/mutators/UndoManager.js +345 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +3 -6
- package/dist/react/AbloProvider.d.ts +23 -126
- package/dist/react/AbloProvider.js +62 -199
- package/dist/react/context.d.ts +31 -0
- package/dist/react/useAblo.d.ts +2 -2
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/schema/ddl.d.ts +34 -3
- package/dist/schema/ddl.js +162 -4
- package/dist/schema/index.d.ts +5 -1
- package/dist/schema/index.js +13 -1
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +4 -0
- package/dist/schema/serialize.js +4 -0
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +65 -0
- package/dist/source/adapter.js +20 -0
- package/dist/source/adapters/drizzle.d.ts +43 -0
- package/dist/source/adapters/drizzle.js +185 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +176 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +144 -0
- package/dist/source/contract.js +99 -0
- package/dist/source/index.d.ts +62 -10
- package/dist/source/index.js +99 -0
- package/dist/source/migrations.d.ts +14 -0
- package/dist/source/migrations.js +39 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +10 -15
- package/dist/sync/ConnectionManager.d.ts +55 -1
- package/dist/sync/ConnectionManager.js +155 -16
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +238 -39
- package/dist/sync/NetworkProbe.d.ts +58 -24
- package/dist/sync/NetworkProbe.js +118 -42
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +70 -36
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api.md +47 -44
- package/docs/cli.md +44 -44
- package/docs/client-behavior.md +30 -30
- package/docs/coordination.md +33 -36
- package/docs/data-sources.md +35 -15
- package/docs/examples/agent-human.md +45 -43
- package/docs/examples/ai-sdk-tool.md +20 -16
- package/docs/examples/existing-python-backend.md +16 -12
- package/docs/examples/nextjs.md +14 -12
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/examples/server-agent.md +24 -21
- package/docs/guarantees.md +15 -13
- package/docs/index.md +2 -2
- package/docs/integration-guide.md +30 -30
- package/docs/interaction-model.md +19 -23
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +2 -2
- package/docs/mcp.md +6 -6
- package/docs/quickstart.md +41 -31
- package/docs/react.md +13 -9
- package/docs/schema-contract.md +12 -10
- package/docs/the-loop.md +21 -0
- package/examples/data-source/README.md +4 -5
- package/examples/data-source/customer-server.ts +27 -25
- package/llms.txt +28 -5
- package/package.json +43 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 90b656c: `drizzleDataSource` now takes `(db, schema)` and derives snake_case columns from your schema, so it composes with `ablo migrate` with no parallel Drizzle table. Update calls from `drizzleDataSource(db, tables)` → `drizzleDataSource(db, schema)`. Also adds the `snakeToCamel` export and provisions the adapter's `ablo_outbox` / `ablo_idempotency` tables via `ablo migrate`.
|
|
8
|
+
|
|
9
|
+
## 0.9.0
|
|
10
|
+
|
|
11
|
+
A single options object for every model verb, and a disposable `claim` handle.
|
|
12
|
+
|
|
13
|
+
### Breaking Changes
|
|
14
|
+
|
|
15
|
+
- **One options object per verb.** `create`, `update`, `delete`, and the async
|
|
16
|
+
server `retrieve` each take a single options object instead of positional
|
|
17
|
+
arguments, so the id, the data, and every modifier live as named siblings:
|
|
18
|
+
`create({ data, id? })`, `update({ id, data, ...options })`,
|
|
19
|
+
`delete({ id, ...options })`, `retrieve({ id, ...options })`. Reactive local
|
|
20
|
+
reads stay on `get(id)` (synchronous) —
|
|
21
|
+
`useAblo((ablo) => ablo.tasks.get(id))`.
|
|
22
|
+
|
|
23
|
+
```diff
|
|
24
|
+
- await ablo.tasks.update(id, { status: 'done' }, { wait: 'confirmed' })
|
|
25
|
+
+ await ablo.tasks.update({ id, data: { status: 'done' }, wait: 'confirmed' })
|
|
26
|
+
|
|
27
|
+
- await ablo.tasks.retrieve(id)
|
|
28
|
+
+ await ablo.tasks.retrieve({ id })
|
|
29
|
+
|
|
30
|
+
- useAblo((ablo) => ablo.tasks.retrieve(id)) ?? serverTask
|
|
31
|
+
+ useAblo((ablo) => ablo.tasks.get(id)) ?? serverTask
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- **`claim` returns a disposable handle** instead of taking a callback. The
|
|
35
|
+
handle exposes the fresh row on `.data` and is released on scope exit
|
|
36
|
+
(`await using`) or explicitly via `.release()`. `claim.state`, `claim.queue`,
|
|
37
|
+
`claim.release`, and `claim.reorder` also take the options object.
|
|
38
|
+
|
|
39
|
+
```diff
|
|
40
|
+
- await ablo.tasks.claim(id, async (task) => {
|
|
41
|
+
- await ablo.tasks.update(task.id, { status: 'in_review' })
|
|
42
|
+
- })
|
|
43
|
+
+ await using claim = await ablo.tasks.claim({ id })
|
|
44
|
+
+ const task = claim.data
|
|
45
|
+
+ await ablo.tasks.update({ id: task.id, data: { status: 'in_review' } })
|
|
46
|
+
```
|
|
47
|
+
|
|
3
48
|
## 0.8.0
|
|
4
49
|
|
|
5
50
|
A callable `claim` coordination namespace and bring-your-own-database support
|
|
@@ -325,7 +370,7 @@ The SDK covers exactly three integration shapes. Each has a canonical example in
|
|
|
325
370
|
### Env / config
|
|
326
371
|
|
|
327
372
|
- `ABLO_API_KEY` — required for server-side use.
|
|
328
|
-
- `
|
|
373
|
+
- `baseURL` — optional override for private deployments / local-dev (defaults to `wss://api.abloatai.com`).
|
|
329
374
|
- `organizationId` — **no longer required** in `createMesh`. The API key or session binds the caller to one org; the capability mint response echoes it back.
|
|
330
375
|
- `createMeshFromEnv` — removed. `new Ablo({ schema })` auto-reads env.
|
|
331
376
|
|
package/README.md
CHANGED
|
@@ -84,17 +84,19 @@ const ablo = Ablo({
|
|
|
84
84
|
await ablo.ready();
|
|
85
85
|
|
|
86
86
|
const created = await ablo.weatherReports.create({
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
data: {
|
|
88
|
+
location: 'Stockholm',
|
|
89
|
+
status: 'pending',
|
|
90
|
+
},
|
|
89
91
|
});
|
|
90
92
|
|
|
91
93
|
// An agent claims the row, does its slow work, then writes back. While the
|
|
92
94
|
// claim is held nobody else can overwrite it; anyone else who tries waits in
|
|
93
95
|
// line and re-reads the result. This is the whole point of Ablo.
|
|
94
|
-
await ablo.weatherReports.claim(created.id
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
});
|
|
96
|
+
await using claim = await ablo.weatherReports.claim({ id: created.id });
|
|
97
|
+
const report = claim.data;
|
|
98
|
+
const forecast = await fetchForecast(report.location); // slow: API or LLM call
|
|
99
|
+
await ablo.weatherReports.update({ id: report.id, data: { status: 'ready', forecast } });
|
|
98
100
|
|
|
99
101
|
const ready = ablo.weatherReports.get(created.id);
|
|
100
102
|
console.log({ id: ready.id, status: ready.status });
|
|
@@ -145,7 +147,7 @@ matter day to day:
|
|
|
145
147
|
| `idempotencyKey` | `string` | Auto-generated per call. Override only when you own the retry boundary (e.g. a job id) so a re-run dedupes server-side. |
|
|
146
148
|
|
|
147
149
|
```ts
|
|
148
|
-
await ablo.weatherReports.update(id, { status: 'ready' },
|
|
150
|
+
await ablo.weatherReports.update({ id, data: { status: 'ready' }, wait: 'confirmed' });
|
|
149
151
|
```
|
|
150
152
|
|
|
151
153
|
To guard a write against a row that changed under you, pass `readAt` + `onStale`
|
|
@@ -157,30 +159,32 @@ An agent reads a row, thinks for 30s, writes back — and clobbers whatever chan
|
|
|
157
159
|
meanwhile, or worse, acts on stale state. `claim` holds the row across that gap:
|
|
158
160
|
|
|
159
161
|
```ts
|
|
160
|
-
await ablo.weatherReports.claim('report_stockholm'
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
});
|
|
162
|
+
await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
|
|
163
|
+
const report = claim.data;
|
|
164
|
+
const forecast = await weatherAgent.getWeather(report.location);
|
|
165
|
+
await ablo.weatherReports.update({ id: report.id, data: { forecast, status: 'ready' } });
|
|
164
166
|
```
|
|
165
167
|
|
|
166
168
|
If someone else holds the row, `claim()` waits in a fair queue, then re-reads —
|
|
167
169
|
so `report` is the current row, never a stale snapshot. Reads stay open by
|
|
168
|
-
default; only acting on the row serializes. The claim releases when the
|
|
169
|
-
|
|
170
|
+
default; only acting on the row serializes. The claim releases when the `await
|
|
171
|
+
using` scope exits.
|
|
170
172
|
|
|
171
173
|
See who's mid-edit before you act — decide to wait, or skip:
|
|
172
174
|
|
|
173
175
|
```ts
|
|
174
|
-
ablo.weatherReports.claim.state('report_stockholm');
|
|
175
|
-
ablo.weatherReports.claim.queue('report_stockholm');
|
|
176
|
+
ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
177
|
+
ablo.weatherReports.claim.queue({ id: 'report_stockholm' });
|
|
176
178
|
|
|
177
|
-
|
|
179
|
+
{
|
|
180
|
+
await using claim = await ablo.weatherReports.claim({ id, wait: false });
|
|
178
181
|
/* do the held work */
|
|
179
|
-
}
|
|
182
|
+
}
|
|
180
183
|
|
|
181
|
-
|
|
184
|
+
{
|
|
185
|
+
await using claim = await ablo.weatherReports.claim({ id, maxQueueDepth: 2 });
|
|
182
186
|
/* do the held work */
|
|
183
|
-
}
|
|
187
|
+
}
|
|
184
188
|
```
|
|
185
189
|
|
|
186
190
|
`claim.state` returns the holder (or `null`); `claim.queue` returns the line waiting
|
|
@@ -194,14 +198,15 @@ Even an unclaimed write can't land on stale reasoning — the commit is guarded:
|
|
|
194
198
|
|
|
195
199
|
```ts
|
|
196
200
|
try {
|
|
197
|
-
await ablo.weatherReports.update(id, { status: 'ready' },
|
|
201
|
+
await ablo.weatherReports.update({ id, data: { status: 'ready' }, readAt, onStale: 'reject' });
|
|
198
202
|
} catch (e) {
|
|
199
203
|
if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
|
|
200
204
|
}
|
|
201
205
|
```
|
|
202
206
|
|
|
203
|
-
>
|
|
204
|
-
>
|
|
207
|
+
> Use `await using` for ordinary held work — the claim releases when the scope
|
|
208
|
+
> exits. Call `claim.release({ id })` only to give a manually held claim back
|
|
209
|
+
> early.
|
|
205
210
|
|
|
206
211
|
See [Coordination](./docs/coordination.md) for the full `claim` / `claim.state` /
|
|
207
212
|
`claim.queue` / `claim.release` reference.
|
|
@@ -231,7 +236,7 @@ function Report({ id }: { id: string }) {
|
|
|
231
236
|
if (!report) return null;
|
|
232
237
|
|
|
233
238
|
return (
|
|
234
|
-
<button onClick={() => ablo?.weatherReports.update(id, { status: 'ready' })}>
|
|
239
|
+
<button onClick={() => ablo?.weatherReports.update({ id, data: { status: 'ready' } })}>
|
|
235
240
|
{report.status}
|
|
236
241
|
</button>
|
|
237
242
|
);
|
|
@@ -285,7 +290,7 @@ each other's changes in real time — that's the default, not a feature you turn
|
|
|
285
290
|
|
|
286
291
|
- `ablo.<model>.create/update/delete` fan out confirmed deltas to subscribers.
|
|
287
292
|
- `useAblo(...)` gives React clients the live row, kept current automatically.
|
|
288
|
-
- `ablo.<model>.claim(id)` / `claim.state(id)` / `queue(id)` let humans and agents coordinate (and observe) active work on a row — and the line waiting behind it — before a write lands.
|
|
293
|
+
- `ablo.<model>.claim({ id })` / `claim.state({ id })` / `claim.queue({ id })` let humans and agents coordinate (and observe) active work on a row — and the line waiting behind it — before a write lands.
|
|
289
294
|
|
|
290
295
|
Always write through Ablo — either the SDK model methods
|
|
291
296
|
(`ablo.<model>.create/update/delete`) or the HTTP write endpoint below. If you
|
|
@@ -314,7 +319,7 @@ curl https://api.abloatai.com/v1/commits \
|
|
|
314
319
|
## Connect Your Database
|
|
315
320
|
|
|
316
321
|
Every schema model has a backing store. By default, Ablo stores rows for the
|
|
317
|
-
models you declare, so `ablo.weatherReports.create(
|
|
322
|
+
models you declare, so `ablo.weatherReports.create({ data })` and `ablo.weatherReports.update({ id, data })`
|
|
318
323
|
write to Ablo-managed state.
|
|
319
324
|
|
|
320
325
|
If your existing database stays the source of truth, connect it as a Data
|
|
@@ -332,7 +337,7 @@ See [Connect Your Database](./docs/data-sources.md) for the integration shape.
|
|
|
332
337
|
| --- | --- | --- | --- |
|
|
333
338
|
| `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
|
|
334
339
|
| `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
|
|
335
|
-
| `baseURL` | `string` | `wss://
|
|
340
|
+
| `baseURL` | `string` | `wss://api.abloatai.com` | Point at a self-hosted or private API |
|
|
336
341
|
|
|
337
342
|
Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
|
|
338
343
|
authenticates with the signed-in user's session; the raw-key path is gated
|
|
@@ -348,7 +353,7 @@ survives worker / `postMessage` boundaries, where `instanceof` does not:
|
|
|
348
353
|
|
|
349
354
|
```ts
|
|
350
355
|
try {
|
|
351
|
-
await ablo.weatherReports.update(id, { status: 'ready' },
|
|
356
|
+
await ablo.weatherReports.update({ id, data: { status: 'ready' }, readAt, onStale: 'reject' });
|
|
352
357
|
} catch (e) {
|
|
353
358
|
if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
|
|
354
359
|
if ((e as AbloError).type === 'AbloClaimedError') { /* another participant holds it */ }
|
|
@@ -379,7 +384,7 @@ contract; there are no retry or timeout knobs to tune.
|
|
|
379
384
|
|
|
380
385
|
## Production Reference
|
|
381
386
|
|
|
382
|
-
- [Identity & Sync Groups](./docs/identity.md) —
|
|
387
|
+
- [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.
|
|
383
388
|
- [Schema Contract](./docs/schema-contract.md) — one schema becomes typed model clients, React reads, agent writes, Data Source shape, and schema push.
|
|
384
389
|
- [Guarantees](./docs/guarantees.md) — confirmed writes, stale-write protection, claim coordination, and agent lifecycle.
|
|
385
390
|
- [Integration Guide](./docs/integration-guide.md) — pick the backing mode and integrate React, Data Source, multiplayer, and agents.
|
|
@@ -22,6 +22,8 @@ import { Model } from './Model.js';
|
|
|
22
22
|
import { ModelScope } from './ObjectPool.js';
|
|
23
23
|
import type { Schema } from './schema/schema.js';
|
|
24
24
|
import { type ReaderActions } from './mutators/readerActions.js';
|
|
25
|
+
import type { LocalMutation } from './react/context.js';
|
|
26
|
+
import type { AuthCredentialSource } from './auth/credentialSource.js';
|
|
25
27
|
/** Constructor type for Model subclasses (accepts abstract classes) */
|
|
26
28
|
export type ModelConstructor<T extends Model> = abstract new (...args: never[]) => T;
|
|
27
29
|
/** Concrete constructor type for instantiation */
|
|
@@ -240,6 +242,7 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
240
242
|
protected readonly database: Database;
|
|
241
243
|
protected readonly objectPool: ObjectPool;
|
|
242
244
|
protected readonly modelRegistry: ModelRegistry;
|
|
245
|
+
protected readonly auth?: AuthCredentialSource;
|
|
243
246
|
/**
|
|
244
247
|
* Schema the store was constructed with. Persisted so the `query`
|
|
245
248
|
* accessor namespace can build typed per-model reader actions lazily
|
|
@@ -308,6 +311,8 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
308
311
|
schema?: TSchema;
|
|
309
312
|
/** Sync server URL for WebSocket connection. Converted to wss:// automatically. */
|
|
310
313
|
url?: string;
|
|
314
|
+
/** Shared bearer credential source for every auth-aware transport. */
|
|
315
|
+
auth?: AuthCredentialSource;
|
|
311
316
|
}, config?: SyncedStoreConfig);
|
|
312
317
|
/**
|
|
313
318
|
* Register foreign key indexes for O(1) lookups.
|
|
@@ -355,6 +360,24 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
355
360
|
* return null.
|
|
356
361
|
*/
|
|
357
362
|
protected connectionManager: import('./sync/ConnectionManager.js').ConnectionManager | null;
|
|
363
|
+
/**
|
|
364
|
+
* Re-mint hook for the short-lived access credential (the Stripe-style
|
|
365
|
+
* `ek_`/`rk_`). Wired by the React provider from its `getToken`/`authEndpoint`
|
|
366
|
+
* — the engine owns WHEN to refresh (a stale-credential probe / an external
|
|
367
|
+
* nudge), the integrator owns HOW to mint. Mirrors the `getToken` contract:
|
|
368
|
+
* resolves a token string on success, `null` when the long-lived login is
|
|
369
|
+
* gone (terminal), and THROWS on a transient/offline failure. Used by
|
|
370
|
+
* {@link performCredentialRefresh}. Absent ⇒ no silent re-mint (e.g. a static
|
|
371
|
+
* `apiKey` deployment whose credential source refreshes out-of-band).
|
|
372
|
+
*/
|
|
373
|
+
private credentialRefresher;
|
|
374
|
+
/** Single-flight guard so a wake nudge + an in-flight request + a probe don't
|
|
375
|
+
* all mint at once (the classic "token thrash → random logout" bug). */
|
|
376
|
+
private inFlightCredentialRefresh;
|
|
377
|
+
/** Teardown for the proactive credential lifecycle (refresh timer + wake/
|
|
378
|
+
* online/focus listeners) installed by {@link startCredentialLifecycle};
|
|
379
|
+
* cleared on {@link disconnect}. Null when no resolver is wired. */
|
|
380
|
+
private credentialLifecycleTeardown;
|
|
358
381
|
/**
|
|
359
382
|
* Listeners registered via `subscribeSessionError()`. Fired when the
|
|
360
383
|
* WebSocket closes with a session-invalid code (1008/4001/4003) or a
|
|
@@ -393,6 +416,15 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
393
416
|
* lookup contract; resolves immediately if nothing is in flight.
|
|
394
417
|
*/
|
|
395
418
|
waitForConfirmation(modelName: string, modelId: string): Promise<void>;
|
|
419
|
+
/**
|
|
420
|
+
* Observe the LOCAL mutation stream for undo recording (see
|
|
421
|
+
* {@link import('./react/context.js').LocalMutation}). Taps the
|
|
422
|
+
* TransactionQueue's `transaction:created` event — fired once per local
|
|
423
|
+
* create/update/delete/archive with `previousData` already captured.
|
|
424
|
+
* Remote/collaborator deltas apply via `applyDeltaBatchToPool` and never
|
|
425
|
+
* emit here, so undo is naturally local-only (you can't undo a teammate).
|
|
426
|
+
*/
|
|
427
|
+
subscribeLocalMutations(handler: (mutation: LocalMutation) => void): () => void;
|
|
396
428
|
/**
|
|
397
429
|
* Execute a bootstrap function with timeout protection and automatic retry.
|
|
398
430
|
* Prevents the common issue where bootstrap hangs on startup.
|
|
@@ -404,6 +436,57 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
404
436
|
protected resetBootstrapState(): void;
|
|
405
437
|
/** Perform reconnect: bootstrap + WS reconnect. Returns outcome for state machine. */
|
|
406
438
|
performReconnect(): Promise<'success' | 'session_error' | 'network_error'>;
|
|
439
|
+
/**
|
|
440
|
+
* Register the access-credential re-mint hook. Called by the React provider
|
|
441
|
+
* with a thunk that mints a fresh `ek_`/`rk_` (typically its `getToken`).
|
|
442
|
+
* See {@link credentialRefresher}.
|
|
443
|
+
*/
|
|
444
|
+
setCredentialRefresher(refresher: (() => Promise<string | null>) | null): void;
|
|
445
|
+
/**
|
|
446
|
+
* Re-mint the short-lived access credential and push it into the credential
|
|
447
|
+
* source, reporting a tri-state outcome the {@link ConnectionManager} maps to
|
|
448
|
+
* its FSM. The contract mirrors `getToken` (and PowerSync's `fetchCredentials`
|
|
449
|
+
* / Liveblocks' `authEndpoint`, but made explicit instead of overloading
|
|
450
|
+
* return/throw):
|
|
451
|
+
* - token string → `'refreshed'` (fresh key in place; re-probe & reconnect)
|
|
452
|
+
* - `null` → `'session_error'` (login itself is gone → terminal, sign out)
|
|
453
|
+
* - throw → `'network_error'` (couldn't reach the mint endpoint → transient)
|
|
454
|
+
*
|
|
455
|
+
* SINGLE-FLIGHT: concurrent callers (a wake nudge, an in-flight request, the
|
|
456
|
+
* probe) share one in-flight promise so we never double-mint — the canonical
|
|
457
|
+
* fix for the "every 401 mints a token → thrash → spurious logout" anti-pattern.
|
|
458
|
+
*
|
|
459
|
+
* No refresher wired ⇒ `'refreshed'` (a no-op re-probe): a static-`apiKey`
|
|
460
|
+
* deployment has no session to re-mint from; its credential source refreshes
|
|
461
|
+
* out-of-band, so we just re-probe with whatever it currently holds.
|
|
462
|
+
*/
|
|
463
|
+
performCredentialRefresh(): Promise<'refreshed' | 'session_error' | 'network_error'>;
|
|
464
|
+
/**
|
|
465
|
+
* Nudge the connection FSM to re-probe with the current credential. Idempotent
|
|
466
|
+
* and safe in any state (ignored while `connected`). Call after pushing a
|
|
467
|
+
* freshly-minted token via `setAuthToken`, or on an OS-wake signal, so a
|
|
468
|
+
* connection parked in `offline` / `backoff` / `auth_blocked` picks the new
|
|
469
|
+
* credential up immediately instead of waiting for the 30s watchdog.
|
|
470
|
+
*/
|
|
471
|
+
nudgeReconnect(): void;
|
|
472
|
+
/**
|
|
473
|
+
* Install the access-credential lifecycle the CLIENT owns (this used to live
|
|
474
|
+
* in the React provider — wrong layer). Two parts:
|
|
475
|
+
* 1. REACTIVE — register `getToken` as the re-mint hook the FSM calls when a
|
|
476
|
+
* probe finds the key stale (`credential_stale`) or on a nudge.
|
|
477
|
+
* 2. PROACTIVE — keep the short-lived key fresh ahead of trouble: a refresh
|
|
478
|
+
* timer inside the TTL, plus re-mint on OS wake / network-online / tab
|
|
479
|
+
* focus. Browser-only triggers are env-gated, so Node/agent hosts get
|
|
480
|
+
* only the timer (a no-op there — agents use a static `apiKey`, no
|
|
481
|
+
* resolver, so this is never called for them).
|
|
482
|
+
*
|
|
483
|
+
* Config-driven and invisible, like Supabase's `autoRefreshToken` — consumers
|
|
484
|
+
* never call a refresh method. Idempotent (a second call replaces the first);
|
|
485
|
+
* torn down on {@link disconnect}.
|
|
486
|
+
*/
|
|
487
|
+
startCredentialLifecycle(getToken: () => Promise<string | null>): void;
|
|
488
|
+
/** Tear down the proactive credential lifecycle (idempotent). */
|
|
489
|
+
private stopCredentialLifecycle;
|
|
407
490
|
/**
|
|
408
491
|
* Handle an actionType 'G' delta.
|
|
409
492
|
*
|
package/dist/BaseSyncedStore.js
CHANGED
|
@@ -124,6 +124,7 @@ export class BaseSyncedStore {
|
|
|
124
124
|
database;
|
|
125
125
|
objectPool;
|
|
126
126
|
modelRegistry;
|
|
127
|
+
auth;
|
|
127
128
|
/**
|
|
128
129
|
* Schema the store was constructed with. Persisted so the `query`
|
|
129
130
|
* accessor namespace can build typed per-model reader actions lazily
|
|
@@ -199,6 +200,7 @@ export class BaseSyncedStore {
|
|
|
199
200
|
this.database = dependencies.database;
|
|
200
201
|
this.objectPool = dependencies.objectPool;
|
|
201
202
|
this.modelRegistry = dependencies.modelRegistry;
|
|
203
|
+
this.auth = dependencies.auth;
|
|
202
204
|
this.schema = dependencies.schema;
|
|
203
205
|
this._syncServerUrl = dependencies.url;
|
|
204
206
|
// Set this store as the global Model store
|
|
@@ -351,6 +353,24 @@ export class BaseSyncedStore {
|
|
|
351
353
|
* return null.
|
|
352
354
|
*/
|
|
353
355
|
connectionManager = null;
|
|
356
|
+
/**
|
|
357
|
+
* Re-mint hook for the short-lived access credential (the Stripe-style
|
|
358
|
+
* `ek_`/`rk_`). Wired by the React provider from its `getToken`/`authEndpoint`
|
|
359
|
+
* — the engine owns WHEN to refresh (a stale-credential probe / an external
|
|
360
|
+
* nudge), the integrator owns HOW to mint. Mirrors the `getToken` contract:
|
|
361
|
+
* resolves a token string on success, `null` when the long-lived login is
|
|
362
|
+
* gone (terminal), and THROWS on a transient/offline failure. Used by
|
|
363
|
+
* {@link performCredentialRefresh}. Absent ⇒ no silent re-mint (e.g. a static
|
|
364
|
+
* `apiKey` deployment whose credential source refreshes out-of-band).
|
|
365
|
+
*/
|
|
366
|
+
credentialRefresher = null;
|
|
367
|
+
/** Single-flight guard so a wake nudge + an in-flight request + a probe don't
|
|
368
|
+
* all mint at once (the classic "token thrash → random logout" bug). */
|
|
369
|
+
inFlightCredentialRefresh = null;
|
|
370
|
+
/** Teardown for the proactive credential lifecycle (refresh timer + wake/
|
|
371
|
+
* online/focus listeners) installed by {@link startCredentialLifecycle};
|
|
372
|
+
* cleared on {@link disconnect}. Null when no resolver is wired. */
|
|
373
|
+
credentialLifecycleTeardown = null;
|
|
354
374
|
/**
|
|
355
375
|
* Listeners registered via `subscribeSessionError()`. Fired when the
|
|
356
376
|
* WebSocket closes with a session-invalid code (1008/4001/4003) or a
|
|
@@ -392,6 +412,28 @@ export class BaseSyncedStore {
|
|
|
392
412
|
waitForConfirmation(modelName, modelId) {
|
|
393
413
|
return this.syncClient.waitForConfirmation(modelName, modelId);
|
|
394
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Observe the LOCAL mutation stream for undo recording (see
|
|
417
|
+
* {@link import('./react/context.js').LocalMutation}). Taps the
|
|
418
|
+
* TransactionQueue's `transaction:created` event — fired once per local
|
|
419
|
+
* create/update/delete/archive with `previousData` already captured.
|
|
420
|
+
* Remote/collaborator deltas apply via `applyDeltaBatchToPool` and never
|
|
421
|
+
* emit here, so undo is naturally local-only (you can't undo a teammate).
|
|
422
|
+
*/
|
|
423
|
+
subscribeLocalMutations(handler) {
|
|
424
|
+
return this.syncClient.subscribe('transaction:created', (data) => {
|
|
425
|
+
const tx = data;
|
|
426
|
+
if (!tx || !tx.type || !tx.modelName || !tx.modelId)
|
|
427
|
+
return;
|
|
428
|
+
handler({
|
|
429
|
+
type: tx.type,
|
|
430
|
+
modelName: tx.modelName,
|
|
431
|
+
modelId: tx.modelId,
|
|
432
|
+
data: tx.data ?? null,
|
|
433
|
+
previousData: tx.previousData ?? null,
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
|
395
437
|
// ── Bootstrap + Retry ────────────────────────────────────────────────────
|
|
396
438
|
/**
|
|
397
439
|
* Execute a bootstrap function with timeout protection and automatic retry.
|
|
@@ -531,6 +573,147 @@ export class BaseSyncedStore {
|
|
|
531
573
|
return 'network_error';
|
|
532
574
|
}
|
|
533
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Register the access-credential re-mint hook. Called by the React provider
|
|
578
|
+
* with a thunk that mints a fresh `ek_`/`rk_` (typically its `getToken`).
|
|
579
|
+
* See {@link credentialRefresher}.
|
|
580
|
+
*/
|
|
581
|
+
setCredentialRefresher(refresher) {
|
|
582
|
+
this.credentialRefresher = refresher;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Re-mint the short-lived access credential and push it into the credential
|
|
586
|
+
* source, reporting a tri-state outcome the {@link ConnectionManager} maps to
|
|
587
|
+
* its FSM. The contract mirrors `getToken` (and PowerSync's `fetchCredentials`
|
|
588
|
+
* / Liveblocks' `authEndpoint`, but made explicit instead of overloading
|
|
589
|
+
* return/throw):
|
|
590
|
+
* - token string → `'refreshed'` (fresh key in place; re-probe & reconnect)
|
|
591
|
+
* - `null` → `'session_error'` (login itself is gone → terminal, sign out)
|
|
592
|
+
* - throw → `'network_error'` (couldn't reach the mint endpoint → transient)
|
|
593
|
+
*
|
|
594
|
+
* SINGLE-FLIGHT: concurrent callers (a wake nudge, an in-flight request, the
|
|
595
|
+
* probe) share one in-flight promise so we never double-mint — the canonical
|
|
596
|
+
* fix for the "every 401 mints a token → thrash → spurious logout" anti-pattern.
|
|
597
|
+
*
|
|
598
|
+
* No refresher wired ⇒ `'refreshed'` (a no-op re-probe): a static-`apiKey`
|
|
599
|
+
* deployment has no session to re-mint from; its credential source refreshes
|
|
600
|
+
* out-of-band, so we just re-probe with whatever it currently holds.
|
|
601
|
+
*/
|
|
602
|
+
async performCredentialRefresh() {
|
|
603
|
+
const refresher = this.credentialRefresher;
|
|
604
|
+
if (!refresher)
|
|
605
|
+
return 'refreshed';
|
|
606
|
+
if (this.inFlightCredentialRefresh)
|
|
607
|
+
return this.inFlightCredentialRefresh;
|
|
608
|
+
const run = (async () => {
|
|
609
|
+
try {
|
|
610
|
+
const token = await refresher();
|
|
611
|
+
if (!token) {
|
|
612
|
+
// null = the long-lived login is gone (mint endpoint answered 401/403).
|
|
613
|
+
// Terminal — the FSM routes this to sign-out.
|
|
614
|
+
return 'session_error';
|
|
615
|
+
}
|
|
616
|
+
this.auth?.setAuthToken(token);
|
|
617
|
+
return 'refreshed';
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
// A throw = transient (offline / mint endpoint unreachable / 5xx). The
|
|
621
|
+
// login may be perfectly valid; never sign out for this — back off and
|
|
622
|
+
// retry. Mirrors the `getToken` throw-vs-null contract end-to-end.
|
|
623
|
+
getContext().logger.warn('[BaseSyncedStore] Access-credential re-mint failed (transient)', {
|
|
624
|
+
error: error?.message,
|
|
625
|
+
});
|
|
626
|
+
return 'network_error';
|
|
627
|
+
}
|
|
628
|
+
})();
|
|
629
|
+
this.inFlightCredentialRefresh = run;
|
|
630
|
+
try {
|
|
631
|
+
return await run;
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
this.inFlightCredentialRefresh = null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Nudge the connection FSM to re-probe with the current credential. Idempotent
|
|
639
|
+
* and safe in any state (ignored while `connected`). Call after pushing a
|
|
640
|
+
* freshly-minted token via `setAuthToken`, or on an OS-wake signal, so a
|
|
641
|
+
* connection parked in `offline` / `backoff` / `auth_blocked` picks the new
|
|
642
|
+
* credential up immediately instead of waiting for the 30s watchdog.
|
|
643
|
+
*/
|
|
644
|
+
nudgeReconnect() {
|
|
645
|
+
this.connectionManager?.send({ type: 'CREDENTIAL_REFRESHED' });
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Install the access-credential lifecycle the CLIENT owns (this used to live
|
|
649
|
+
* in the React provider — wrong layer). Two parts:
|
|
650
|
+
* 1. REACTIVE — register `getToken` as the re-mint hook the FSM calls when a
|
|
651
|
+
* probe finds the key stale (`credential_stale`) or on a nudge.
|
|
652
|
+
* 2. PROACTIVE — keep the short-lived key fresh ahead of trouble: a refresh
|
|
653
|
+
* timer inside the TTL, plus re-mint on OS wake / network-online / tab
|
|
654
|
+
* focus. Browser-only triggers are env-gated, so Node/agent hosts get
|
|
655
|
+
* only the timer (a no-op there — agents use a static `apiKey`, no
|
|
656
|
+
* resolver, so this is never called for them).
|
|
657
|
+
*
|
|
658
|
+
* Config-driven and invisible, like Supabase's `autoRefreshToken` — consumers
|
|
659
|
+
* never call a refresh method. Idempotent (a second call replaces the first);
|
|
660
|
+
* torn down on {@link disconnect}.
|
|
661
|
+
*/
|
|
662
|
+
startCredentialLifecycle(getToken) {
|
|
663
|
+
this.stopCredentialLifecycle();
|
|
664
|
+
this.setCredentialRefresher(getToken);
|
|
665
|
+
// A transient failure is swallowed: the engine keeps its current token and
|
|
666
|
+
// the next trigger — or the reactive `credential_stale` path — retries. We
|
|
667
|
+
// never tear down or sign out on a failed proactive roll.
|
|
668
|
+
const refresh = async () => {
|
|
669
|
+
try {
|
|
670
|
+
const token = await getToken();
|
|
671
|
+
if (token) {
|
|
672
|
+
// Push into the shared credential source (read lazily by bootstrap
|
|
673
|
+
// HTTP, probes, and the WS reconnect URL), then nudge a parked
|
|
674
|
+
// connection to re-probe with the fresh key. Same two steps the
|
|
675
|
+
// engine's `setAuthToken` wrapper performs.
|
|
676
|
+
this.auth?.setAuthToken(token);
|
|
677
|
+
this.nudgeReconnect();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
// transient (offline / mint hiccup) — a later trigger retries.
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
// Comfortably inside the 15m `ek_` TTL; a missed (background-throttled) tick
|
|
685
|
+
// is recovered by the next, or by the reactive probe.
|
|
686
|
+
const REFRESH_INTERVAL_MS = 10 * 60 * 1000;
|
|
687
|
+
const timer = setInterval(() => void refresh(), REFRESH_INTERVAL_MS);
|
|
688
|
+
const teardowns = [() => clearInterval(timer)];
|
|
689
|
+
if (typeof window !== 'undefined') {
|
|
690
|
+
const onTrigger = () => void refresh();
|
|
691
|
+
window.addEventListener('online', onTrigger);
|
|
692
|
+
// OS-wake: the desktop shell bridges Electron `powerMonitor` 'resume' to
|
|
693
|
+
// this DOM event (visibilitychange does NOT fire on wake-from-sleep, so a
|
|
694
|
+
// nap longer than the TTL would otherwise leave a dead key untouched).
|
|
695
|
+
window.addEventListener('ablo:wake', onTrigger);
|
|
696
|
+
teardowns.push(() => window.removeEventListener('online', onTrigger));
|
|
697
|
+
teardowns.push(() => window.removeEventListener('ablo:wake', onTrigger));
|
|
698
|
+
}
|
|
699
|
+
if (typeof document !== 'undefined') {
|
|
700
|
+
const onVisible = () => {
|
|
701
|
+
if (document.visibilityState === 'visible')
|
|
702
|
+
void refresh();
|
|
703
|
+
};
|
|
704
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
705
|
+
teardowns.push(() => document.removeEventListener('visibilitychange', onVisible));
|
|
706
|
+
}
|
|
707
|
+
this.credentialLifecycleTeardown = () => {
|
|
708
|
+
for (const t of teardowns)
|
|
709
|
+
t();
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
/** Tear down the proactive credential lifecycle (idempotent). */
|
|
713
|
+
stopCredentialLifecycle() {
|
|
714
|
+
this.credentialLifecycleTeardown?.();
|
|
715
|
+
this.credentialLifecycleTeardown = null;
|
|
716
|
+
}
|
|
534
717
|
// ── Sync Group Management ────────────────────────────────────────────────
|
|
535
718
|
/**
|
|
536
719
|
* Handle an actionType 'G' delta.
|
|
@@ -970,10 +1153,14 @@ export class BaseSyncedStore {
|
|
|
970
1153
|
createConnectionManager(kind) {
|
|
971
1154
|
if (kind === 'agent')
|
|
972
1155
|
return null;
|
|
973
|
-
return new ConnectionManager({
|
|
1156
|
+
return new ConnectionManager({
|
|
1157
|
+
baseUrl: this._syncServerUrl,
|
|
1158
|
+
getAuthToken: () => this.auth?.getAuthToken() ?? this.syncWebSocket?.getAuthToken() ?? null,
|
|
1159
|
+
});
|
|
974
1160
|
}
|
|
975
1161
|
/** Disconnect and clean up all resources */
|
|
976
1162
|
async disconnect() {
|
|
1163
|
+
this.stopCredentialLifecycle();
|
|
977
1164
|
if (this.batchTimer) {
|
|
978
1165
|
clearTimeout(this.batchTimer);
|
|
979
1166
|
this.batchTimer = null;
|
|
@@ -1099,6 +1286,7 @@ export class BaseSyncedStore {
|
|
|
1099
1286
|
versions: this.versionVector,
|
|
1100
1287
|
kind: context.kind,
|
|
1101
1288
|
capabilityToken: context.capabilityToken,
|
|
1289
|
+
getAuthToken: this.auth?.getAuthToken,
|
|
1102
1290
|
capabilities: {
|
|
1103
1291
|
partialBootstrap: true,
|
|
1104
1292
|
compressedDeltas: true,
|
|
@@ -1217,6 +1405,7 @@ export class BaseSyncedStore {
|
|
|
1217
1405
|
};
|
|
1218
1406
|
manager.start({
|
|
1219
1407
|
onReconnect: () => this.performReconnect(),
|
|
1408
|
+
onRefreshCredential: () => this.performCredentialRefresh(),
|
|
1220
1409
|
onSessionExpired: () => {
|
|
1221
1410
|
const err = new SyncSessionError('Session expired');
|
|
1222
1411
|
for (const listener of this.sessionErrorListeners) {
|
|
@@ -1253,10 +1442,13 @@ export class BaseSyncedStore {
|
|
|
1253
1442
|
}
|
|
1254
1443
|
break;
|
|
1255
1444
|
case 'probing_network':
|
|
1445
|
+
case 'refreshing_credential':
|
|
1256
1446
|
case 'reconnecting':
|
|
1257
1447
|
case 'backoff':
|
|
1258
1448
|
// Active recovery — the UI should reflect that the FSM
|
|
1259
|
-
// is doing work, not that we've given up.
|
|
1449
|
+
// is doing work, not that we've given up. (Re-minting a stale
|
|
1450
|
+
// access key is just another recovery step, surfaced the same
|
|
1451
|
+
// way; the user never sees a credential-level distinction.)
|
|
1260
1452
|
if (this.syncStatus.state !== 'reconnecting') {
|
|
1261
1453
|
this.updateSyncStatus({ state: 'reconnecting' });
|
|
1262
1454
|
}
|
package/dist/Model.d.ts
CHANGED
|
@@ -212,10 +212,52 @@ export declare abstract class Model {
|
|
|
212
212
|
* Prepare unarchive operation
|
|
213
213
|
*/
|
|
214
214
|
prepareUnarchive(): ModelChanges;
|
|
215
|
+
/**
|
|
216
|
+
* Safely assign each field of `data` onto this instance, skipping `id`,
|
|
217
|
+
* unknown keys, MobX computed accessors, and getter-only (read-only)
|
|
218
|
+
* properties, and coercing date fields. Shared by `updateFromData`
|
|
219
|
+
* (hydration) and `applyChanges` (local user update).
|
|
220
|
+
*
|
|
221
|
+
* Change tracking is EXPLICIT, not magic: for every field actually
|
|
222
|
+
* written, `onWrite(key, oldValue, newValue)` is invoked with the value
|
|
223
|
+
* captured immediately before assignment. `applyChanges` passes a hook
|
|
224
|
+
* that records the change in `modifiedProperties`; `updateFromData`
|
|
225
|
+
* passes none (hydration must not generate outbound mutations). This
|
|
226
|
+
* is the single source of mutation tracking now that the `mobx-setup`
|
|
227
|
+
* `observe()` bridge has been removed (one write path: the SDK proxy).
|
|
228
|
+
*/
|
|
229
|
+
private assignFieldsFromData;
|
|
215
230
|
/**
|
|
216
231
|
* Update from raw data (hydration)
|
|
232
|
+
*
|
|
233
|
+
* Used for inbound server deltas and pool upserts. Change tracking is
|
|
234
|
+
* deliberately suppressed: hydration writes must NOT land in
|
|
235
|
+
* `modifiedProperties`, otherwise applying a server delta would queue a
|
|
236
|
+
* brand-new outbound mutation and the record would echo forever. For a
|
|
237
|
+
* LOCAL user edit, use `applyChanges` instead.
|
|
238
|
+
*
|
|
239
|
+
* Suppression is belt-and-suspenders: we pass no `onWrite` hook AND
|
|
240
|
+
* clear/restore `modifiedProperties` around the assignment, so any
|
|
241
|
+
* remaining `mobx-setup` `observe()` side-channel writes are discarded
|
|
242
|
+
* too. (The clear/restore is a harmless no-op once that bridge is gone.)
|
|
217
243
|
*/
|
|
218
244
|
updateFromData(data: ModelData): void;
|
|
245
|
+
/**
|
|
246
|
+
* Apply a LOCAL user-initiated update from a data object — the write
|
|
247
|
+
* path for `proxy.update({ id, data })`, which is the ONE AND ONLY way
|
|
248
|
+
* application code mutates synced fields.
|
|
249
|
+
*
|
|
250
|
+
* Unlike `updateFromData` (hydration, untracked), this records every
|
|
251
|
+
* written field in `modifiedProperties` via `propertyChanged`, so
|
|
252
|
+
* `getChanges()` / the transaction queue send the edited fields to the
|
|
253
|
+
* server and the undo system gets a correct pre-write baseline.
|
|
254
|
+
* Recording is EXPLICIT here (via the `onWrite` hook) — it does not rely
|
|
255
|
+
* on any MobX `observe()` side-channel.
|
|
256
|
+
*
|
|
257
|
+
* `_originalData` is intentionally NOT reset here: it stays as the
|
|
258
|
+
* last-persisted baseline until `clearChanges()` runs on sync-ack.
|
|
259
|
+
*/
|
|
260
|
+
applyChanges(data: ModelData): void;
|
|
219
261
|
/**
|
|
220
262
|
* Serialize to JSON
|
|
221
263
|
* This method should not trigger MobX reactions since it's used for serialization
|