@abloatai/ablo 0.56.0 → 0.58.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.
- package/AGENTS.md +10 -4
- package/CHANGELOG.md +428 -10
- package/LICENSE +1 -1
- package/NOTICE +3 -3
- package/README.md +2 -1
- package/dist/ai-sdk.d.ts +1 -1
- package/dist/ai-sdk.d.ts.map +1 -1
- package/dist/context/evidence.d.ts +6 -8
- package/dist/context/evidence.d.ts.map +1 -1
- package/dist/context/evidence.js +6 -20
- package/dist/context/evidence.js.map +1 -1
- package/dist/context/index.d.ts +23 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +26 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/onChange.d.ts +9 -0
- package/dist/context/onChange.d.ts.map +1 -0
- package/dist/context/onChange.js +37 -0
- package/dist/context/onChange.js.map +1 -0
- package/dist/source-conformance.d.ts +1 -1
- package/dist/source-conformance.d.ts.map +1 -1
- package/dist/source-conformance.js +1 -1
- package/dist/source-conformance.js.map +1 -1
- package/dist/source-drizzle.d.ts +1 -1
- package/dist/source-drizzle.d.ts.map +1 -1
- package/dist/source-drizzle.js +1 -1
- package/dist/source-drizzle.js.map +1 -1
- package/dist/source-kysely.d.ts +1 -1
- package/dist/source-kysely.d.ts.map +1 -1
- package/dist/source-kysely.js +1 -1
- package/dist/source-kysely.js.map +1 -1
- package/dist/source-next.d.ts +1 -1
- package/dist/source-next.d.ts.map +1 -1
- package/dist/source-next.js +1 -1
- package/dist/source-next.js.map +1 -1
- package/docs/agent-integration-decision-guide.md +123 -0
- package/docs/agents.md +74 -13
- package/docs/api-keys.md +6 -6
- package/docs/api.md +117 -43
- package/docs/branch-development.md +23 -4
- package/docs/cli.md +16 -9
- package/docs/client-behavior.md +21 -15
- package/docs/concurrency-convention.md +67 -77
- package/docs/context.md +56 -31
- package/docs/coordination.md +115 -36
- package/docs/customer-organizations.md +49 -31
- package/docs/data-sources.md +12 -6
- package/docs/debugging.md +1 -1
- package/docs/examples/agent-human.md +6 -18
- package/docs/examples/coordination-conformance.md +69 -0
- package/docs/examples/existing-document-pipeline.md +488 -0
- package/docs/examples/existing-python-backend.md +10 -13
- package/docs/examples/nextjs.md +49 -6
- package/docs/examples/scoped-agent.md +18 -1
- package/docs/examples/server-agent.md +2 -2
- package/docs/groups.md +19 -139
- package/docs/guarantees.md +5 -6
- package/docs/identity.md +2 -1
- package/docs/index.md +5 -0
- package/docs/integration-guide.md +46 -19
- package/docs/integrations/sandbox-runtime.md +148 -0
- package/docs/integrations.md +9 -0
- package/docs/operating-on-your-database.md +7 -0
- package/docs/quickstart.md +19 -13
- package/docs/react.md +9 -9
- package/docs/schema-contract.md +14 -13
- package/docs/session-settings.md +9 -0
- package/docs/sessions.md +1 -1
- package/examples/README.md +2 -2
- package/examples/agent-turn.ts +1 -1
- package/examples/data-source/customer-server.ts +12 -5
- package/examples/expensive-agent-turn.ts +1 -1
- package/llms.txt +72 -10
- package/package.json +6 -6
- package/dist/context/sources.d.ts +0 -21
- package/dist/context/sources.d.ts.map +0 -1
- package/dist/context/sources.js +0 -36
- package/dist/context/sources.js.map +0 -1
- package/dist/context.d.ts +0 -22
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -33
- package/dist/context.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -6,6 +6,12 @@ Claims don't lock. If another writer holds the row, `claim` waits for them and r
|
|
|
6
6
|
|
|
7
7
|
## Start here — scaffold with `ablo init`
|
|
8
8
|
|
|
9
|
+
Before choosing among identifier claims, row claims, captured reads, atomic
|
|
10
|
+
commits, existing database writes, and Ablo-routed writes, use the
|
|
11
|
+
[Agent Integration Decision Guide](./docs/agent-integration-decision-guide.md).
|
|
12
|
+
It routes existing applications to the smallest relevant example and names the
|
|
13
|
+
test layer that proves each guarantee.
|
|
14
|
+
|
|
9
15
|
Don't hand-write the integration. Run the CLI; it generates the current-API schema, client, the database connection (logical replication by default, or a signed Data Source endpoint as the fallback), and (for Next.js) the browser provider + session route:
|
|
10
16
|
|
|
11
17
|
- **Read the docs for THIS version:** `npx ablo docs` lists every page, `npx ablo docs <page>` prints one. They ship inside the installed package, so they describe the code in `node_modules` and work with no network. Read them instead of a docs URL — a website describes the newest release, so against a pinned version it will hand you a call your package doesn't have (`retrieve`/`list` replaced `get`/`getAll`/`getCount` in 0.35.0).
|
|
@@ -21,7 +27,7 @@ Don't hand-write the integration. Run the CLI; it generates the current-API sche
|
|
|
21
27
|
|
|
22
28
|
When you use the signed-endpoint fallback, the generated `ablo/data-source.ts` is the whole endpoint and needs no hand-editing: `dataSourceNext({ schema, apiKey, adapter: prismaDataSource(prisma, schema) })` (or `drizzleDataSource(db, schema)`). The adapter owns commit / idempotency / outbox.
|
|
23
29
|
|
|
24
|
-
**Working on a real database?** Plain model writes are last-write-wins when no active claim applies. Use a functional update, a held claim, or `
|
|
30
|
+
**Working on a real database?** Plain model writes are last-write-wins when no active claim applies. Use a functional update, a held claim, or `read` plus `reads` when a result depends on an earlier value. Reads are safe to inspect; raw application DDL (`ALTER TABLE …`) and a `--yes` connection cutover belong to a human. When you're unsure whether a write fits, `npx ablo check` reports the live column-by-column fit read-only, before anything runs. Full sorting rule: [Operating on Your Database](./docs/operating-on-your-database.md).
|
|
25
31
|
|
|
26
32
|
## Rule
|
|
27
33
|
|
|
@@ -35,10 +41,10 @@ The schema is the integration contract — it drives typed model clients, React
|
|
|
35
41
|
|
|
36
42
|
Every model verb takes ONE options object. The common loop:
|
|
37
43
|
|
|
38
|
-
1. **
|
|
44
|
+
1. **Get or read** the row — `get({ id })` observes; `read({ id })` declares that a later mutation depends on this exact version. `list({ where })` is observational. In React render, use `local.get(id)`.
|
|
39
45
|
2. **See who's active** (optional) — `ablo.<model>.claim.state({ id })` (synchronous; never blocks).
|
|
40
46
|
3. **Claim** the row before changing it — `await using claim = await ablo.<model>.claim({ id, description?, 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`).
|
|
41
|
-
4. **Write** — `
|
|
47
|
+
4. **Write** — pass `reads: [row]` when the decision used a row returned by `read`, or write through the held claim. If the declared read moved, the mutation does not land.
|
|
42
48
|
|
|
43
49
|
Keep coding assistants on this schema-backed path.
|
|
44
50
|
|
|
@@ -58,7 +64,7 @@ const schema = defineSchema({
|
|
|
58
64
|
|
|
59
65
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
60
66
|
|
|
61
|
-
const report = await ablo.weatherReports.
|
|
67
|
+
const report = await ablo.weatherReports.read({ id: 'report_stockholm' });
|
|
62
68
|
if (!report) throw new Error('Report not found');
|
|
63
69
|
|
|
64
70
|
// If someone else holds the row, claim waits for them and re-reads the fresh
|
package/CHANGELOG.md
CHANGED
|
@@ -1,19 +1,437 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.58.0
|
|
4
|
+
|
|
5
|
+
### Reads now distinguish observation from decision input
|
|
6
|
+
|
|
7
|
+
`get({ id })` returns the current row for display or inspection. `read({ id })`
|
|
8
|
+
returns the same row while privately retaining its model, id, and watermark so a
|
|
9
|
+
later write can prove exactly which state it depended on:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const report = await ablo.reports.read({ id: reportId });
|
|
13
|
+
|
|
14
|
+
await ablo.reports.update({
|
|
15
|
+
id: report.id,
|
|
16
|
+
data: { summary },
|
|
17
|
+
reads: [report],
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If `report` changed in between, the update does not land and rejects with
|
|
22
|
+
`AbloStaleContextError`. `get()` and `list()` remain observational, while
|
|
23
|
+
`onChange()` remains the live notification surface.
|
|
24
|
+
|
|
25
|
+
This replaces the older `retrieve()` and durable `track()` model surfaces. It is
|
|
26
|
+
a public-surface break with no compatibility aliases:
|
|
27
|
+
|
|
28
|
+
- replace `retrieve({ id })` with `get({ id })` for observation, or `read({ id })`
|
|
29
|
+
when a later mutation depends on the row;
|
|
30
|
+
- replace `track(...)`, `CommitContext.track`, and mutation `track` / `onStale`
|
|
31
|
+
options with captured rows passed through the mutation's `reads` option; and
|
|
32
|
+
- replace AI SDK `ToolModel.get` with `ToolModel.read`.
|
|
33
|
+
|
|
34
|
+
Schema conflict-policy configuration and the `agents*`, `humans*`, and
|
|
35
|
+
`system*` policy constants are also removed, together with the supporting
|
|
36
|
+
conflict, stale-notification, persisted-read-set, and internal read-set exports.
|
|
37
|
+
Use an active claim when work must exclude another participant. Use `read()` and
|
|
38
|
+
`reads` when work may run concurrently but must not commit from a stale premise.
|
|
39
|
+
|
|
40
|
+
The removals announced by earlier releases now take effect too:
|
|
41
|
+
`SourceRequestContext.requiredSyncGroups` is gone in favor of `syncGroups`, and
|
|
42
|
+
`DeltaPosition`, `deltaPositionSchema`, `ReadSetWatermark`, and
|
|
43
|
+
`readSetWatermarkSchema` are gone in favor of `LogPosition` and
|
|
44
|
+
`logPositionSchema`.
|
|
45
|
+
|
|
46
|
+
### Context follows exact reads and can stop stale work early
|
|
47
|
+
|
|
48
|
+
`context()` now assembles ordinary application values and exact Ablo reads into
|
|
49
|
+
one result with `data`, `reads`, and `onChange`. It no longer publishes a
|
|
50
|
+
context-level cursor or source classification. `ContextResult.cursor`,
|
|
51
|
+
`ContextResult.sources`, `ContextChange`, `ContextSource`, and
|
|
52
|
+
`contextSourceSchema` leave with that older model.
|
|
53
|
+
|
|
54
|
+
The reactive client's cache-based `snapshot()` operation and `Snapshot` type are
|
|
55
|
+
removed. Call `read()` for every row an action depends on, assemble those values
|
|
56
|
+
with `context()`, and pass `ctx.reads` to the final write:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const ctx = await context({
|
|
60
|
+
ablo,
|
|
61
|
+
data: {
|
|
62
|
+
report: ablo.reports.read({ id: reportId }),
|
|
63
|
+
documents: searchDocuments(reportId),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const stop = ctx.onChange((error) => controller.abort(error));
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const summary = await generateSummary(ctx.data, controller.signal);
|
|
71
|
+
await ablo.reports.update({
|
|
72
|
+
id: reportId,
|
|
73
|
+
data: { summary },
|
|
74
|
+
reads: ctx.reads,
|
|
75
|
+
});
|
|
76
|
+
} finally {
|
|
77
|
+
stop();
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`onChange` calls its listeners once when any captured row moves, which lets an
|
|
82
|
+
expensive model call stop early. The final write must still receive
|
|
83
|
+
`reads: ctx.reads`; that server-side check stays authoritative if notification
|
|
84
|
+
races the write or the connection drops.
|
|
85
|
+
|
|
86
|
+
Atomic `commits.create()` batches accept the same captured rows on both the
|
|
87
|
+
stateless HTTP and reactive WebSocket clients. Typed model claim handles can be
|
|
88
|
+
passed directly as a batch's `claim`, without losing their row type. Disposing a
|
|
89
|
+
reactive client now also disposes its mutation queue and commit-lane timers.
|
|
90
|
+
|
|
91
|
+
### Claims are available from the shell
|
|
92
|
+
|
|
93
|
+
The CLI can now acquire, queue for, inspect, heartbeat, and release a row lease:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx ablo claims acquire reports report_123 --queue -- npm run reconcile
|
|
97
|
+
npx ablo claims list reports report_123
|
|
98
|
+
npx ablo claims release reports report_123
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The `-- <command>` form keeps the lease alive only while the child process runs
|
|
102
|
+
and releases it on success, failure, or interruption. This lets an operator or
|
|
103
|
+
coding agent participate in the same coordination boundary as SDK clients
|
|
104
|
+
without first adding application code.
|
|
105
|
+
|
|
106
|
+
### An agent run can carry the person who started it
|
|
107
|
+
|
|
108
|
+
An agent session can now name the person it acts for. Pass `onBehalfOf` when you
|
|
109
|
+
mint, and every write that agent makes records both principals: the agent as the
|
|
110
|
+
actor, that user as the delegator.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const { token } = await server.sessions.create({
|
|
114
|
+
agent: { id: agentId },
|
|
115
|
+
onBehalfOf: { user: { id: requestingUserId } },
|
|
116
|
+
can: { records: ['read', 'update'] },
|
|
117
|
+
syncGroups: [syncGroup('workspace', workspaceId)],
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Until now the delegation chain root came from whoever called the mint. A browser
|
|
122
|
+
session minting for its own user recorded that person; a backend minting with a
|
|
123
|
+
secret key recorded nobody. Background work is the second case, so a queued
|
|
124
|
+
job's writes arrived attributed to the agent alone, or to `system` where the
|
|
125
|
+
worker wrote around Ablo entirely. Ablo never sees your user directory, so it
|
|
126
|
+
cannot recover that identity afterwards.
|
|
127
|
+
|
|
128
|
+
The rule is about who may attest, not who may ask. A secret key already carries
|
|
129
|
+
organization authority, so it may name any user id, which is what lets a worker
|
|
130
|
+
resume a job somebody else started. A human session may name only its own user,
|
|
131
|
+
and naming another is refused. `onBehalfOf` on a capability that is not an agent
|
|
132
|
+
is refused as well.
|
|
133
|
+
|
|
134
|
+
Two habits make this hold in practice. Persist the user id on the job before you
|
|
135
|
+
enqueue it, because request context and in-memory arguments do not survive a
|
|
136
|
+
retry or a process boundary, and a durable job without its delegator can only
|
|
137
|
+
produce agent-only attribution. And do not quietly fall back to writing straight
|
|
138
|
+
to the database when minting is unavailable. Those writes are still observed,
|
|
139
|
+
but they carry no trusted correlation and are recorded as `system`.
|
|
140
|
+
|
|
141
|
+
Omit the field and sessions mint exactly as they did before.
|
|
142
|
+
|
|
143
|
+
### The CLI has one credential input and a clearer project boundary
|
|
144
|
+
|
|
145
|
+
`ABLO_API_KEY` is now the CLI's single explicit credential input. Management,
|
|
146
|
+
branch-runtime, and restricted-agent authority follow from the credential kind
|
|
147
|
+
and its server-side grant; `ABLO_MANAGEMENT_KEY` is no longer a separate input.
|
|
148
|
+
Keep management credentials at the control-plane boundary and pass only a
|
|
149
|
+
delegated, per-run `rk_` credential into a sandbox or agent runtime.
|
|
150
|
+
|
|
151
|
+
After browser approval, `ablo login` now lets an organization with multiple
|
|
152
|
+
projects choose the project in the terminal. `ablo login --project <slug>` skips
|
|
153
|
+
the picker. Credentials remain fixed to the project that minted them, and the
|
|
154
|
+
CLI refuses to silently use another project's stored key.
|
|
155
|
+
|
|
156
|
+
Database connection setup now handles more provider-specific PostgreSQL role
|
|
157
|
+
constraints, repairs required role inheritance, registers local connectors
|
|
158
|
+
through the control-plane boundary, and reports a missing Data Source API key as
|
|
159
|
+
an authentication problem with a concrete fix. A branch still needs a database
|
|
160
|
+
connected before its schema can be pushed.
|
|
161
|
+
|
|
162
|
+
### Hosted test branches are explicit and temporary
|
|
163
|
+
|
|
164
|
+
Live integration fixtures can create an expiring `test` branch backed by Ablo's
|
|
165
|
+
hosted log storage. Hosted storage must be requested explicitly and the branch
|
|
166
|
+
must expire within 24 hours. Ordinary customer branches remain unbound until
|
|
167
|
+
their own database is connected, so a test convenience cannot silently choose
|
|
168
|
+
where customer data lives.
|
|
169
|
+
|
|
170
|
+
The documentation now includes a coordination-conformance fixture, an
|
|
171
|
+
existing-document evidence pipeline, an existing Python backend path, a
|
|
172
|
+
GraphQL.js approach, and a sandbox-runtime integration guide. These examples
|
|
173
|
+
separate the guarantees Ablo enforces from application, provider, database, and
|
|
174
|
+
deployment responsibilities.
|
|
175
|
+
|
|
176
|
+
### Pricing and plan limits moved
|
|
177
|
+
|
|
178
|
+
The rate card, the included allowances, and the plan ceilings all changed in
|
|
179
|
+
this release. The canonical page is published at
|
|
180
|
+
https://docs.abloatai.com/pricing and needs no sign-in.
|
|
181
|
+
|
|
182
|
+
## 0.57.0
|
|
183
|
+
|
|
184
|
+
### The endpoint outbox upgrades without a drain
|
|
185
|
+
|
|
186
|
+
Endpoint events now have an explicit envelope version. Existing rows and writes
|
|
187
|
+
from an older endpoint are version 1 and pass through the preserved pre-subject
|
|
188
|
+
routing decoder. New adapters write version 2, capturing `sync_groups` in the
|
|
189
|
+
same transaction as the row change. The database constraint requires every
|
|
190
|
+
version-2 event to carry those immutable routes.
|
|
191
|
+
|
|
192
|
+
Old and new endpoint versions may run together during rollout without stalling
|
|
193
|
+
the feed. A page served by an old reader necessarily uses version-1 semantics,
|
|
194
|
+
even when a new writer created the row, because that reader does not select the
|
|
195
|
+
new routing columns. Version-2 routing is therefore universal once every
|
|
196
|
+
endpoint reader has upgraded. There is still no pre-upgrade drain, write pause,
|
|
197
|
+
cursor inspection, or manual deletion.
|
|
198
|
+
|
|
199
|
+
Ablo sends `cursor` as the read position and `acknowledgedThrough` separately
|
|
200
|
+
after the event and consumer position are durable. Built-in adapters use that
|
|
201
|
+
explicit acknowledgement for bounded cleanup; custom event handlers may do the
|
|
202
|
+
same.
|
|
203
|
+
|
|
204
|
+
Version 1 remains a deliberately named compatibility decoder, not a
|
|
205
|
+
NULL value silently interpreted as an empty audience. Replication connections
|
|
206
|
+
have no endpoint outbox and require no action.
|
|
207
|
+
|
|
208
|
+
### A row is authorized by the subject its schema declares
|
|
209
|
+
|
|
210
|
+
A model may now declare which field decides who a row belongs to, and that rule
|
|
211
|
+
is enforced on every path: reads, writes, claims, presence, and every storage
|
|
212
|
+
adapter, including the endpoint ones. A row is authorized exactly when the
|
|
213
|
+
request carries the sync group `${group}:${row[field]}`.
|
|
214
|
+
|
|
215
|
+
This is what 0.56.0's boundary change was heading towards. Sync groups routed
|
|
216
|
+
delivery and did not decide authorization, so a model that used them as though
|
|
217
|
+
they did was relying on something the guide told you not to rely on. A declared
|
|
218
|
+
subject is that rule made real, checked in one place and failing closed.
|
|
219
|
+
|
|
220
|
+
Two consequences worth knowing. A subject-scoped model stamps exactly one group
|
|
221
|
+
on a change, because delivery matching is OR-based and a second group would
|
|
222
|
+
widen the audience rather than narrow it. And a tombstone now reaches only the
|
|
223
|
+
row's authorized subject group, where before a delete could be announced more
|
|
224
|
+
widely than the row ever was.
|
|
225
|
+
|
|
226
|
+
A schema that routes by sync group without a matching row-access policy is
|
|
227
|
+
flagged. If the routing really is only routing, acknowledge it explicitly and
|
|
228
|
+
the flag goes quiet.
|
|
229
|
+
|
|
230
|
+
### Creating many rows is one commit
|
|
231
|
+
|
|
232
|
+
`create` takes a list as well as a single row, under the same verb:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const rows = await ablo.weatherReports.create({
|
|
236
|
+
data: [
|
|
237
|
+
{ location: 'Stockholm', summary: 'Clear' },
|
|
238
|
+
{ location: 'Oslo', summary: 'Rain' },
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
They are written as one atomic commit rather than one request each, so either
|
|
244
|
+
every row lands or none does. The result comes back in the order you gave it,
|
|
245
|
+
not the order the batch settled, and carries whatever defaults the server
|
|
246
|
+
stamped. An empty list writes nothing rather than opening an empty commit.
|
|
247
|
+
|
|
248
|
+
### Reading a whole collection, in as many words
|
|
249
|
+
|
|
250
|
+
`listAll({ where, maxPages, signal })` reads a complete collection by walking the
|
|
251
|
+
same cursor `list` returns, so the common case stops being a hand-rolled loop:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const open = await ablo.weatherReports.listAll({
|
|
255
|
+
where: { status: ['draft', 'review'] },
|
|
256
|
+
maxPages: 20,
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
It is bounded on purpose. `maxPages` is how you say how much you are willing to
|
|
261
|
+
read, and `signal` cancels a walk that is taking longer than the work is worth.
|
|
262
|
+
A complete read that cannot say when it will stop is how a page turns into an
|
|
263
|
+
outage.
|
|
264
|
+
|
|
265
|
+
### A claimed write carries its stale guard again
|
|
266
|
+
|
|
267
|
+
Holding a claim and then writing gave mutual exclusion but not lost-update
|
|
268
|
+
detection, on the stateless transport agents run. The claim handle carries the
|
|
269
|
+
position the row was read at, and the write defaults to rejecting on a change
|
|
270
|
+
since then. Unwrapping the handle cleared the claim before that default was
|
|
271
|
+
read, so the guard was unreachable and every model write through the public
|
|
272
|
+
surface lost it.
|
|
273
|
+
|
|
274
|
+
It read as though both protections were present: claim, read, decide, write. The
|
|
275
|
+
watermark now travels with the handle, and your own `readAt` or `onStale` still
|
|
276
|
+
win where you set them.
|
|
277
|
+
|
|
278
|
+
### A create on an id that already exists is refused
|
|
279
|
+
|
|
280
|
+
It reported success and returned a row. A caller-selected id is a claim about
|
|
281
|
+
which row this is, so a create that finds one already there is a conflict rather
|
|
282
|
+
than an update, and it now says so.
|
|
283
|
+
|
|
284
|
+
### CLI: a session route that revalidates before it mints
|
|
285
|
+
|
|
286
|
+
`ablo init` scaffolds a Next.js session route that re-checks membership at mint
|
|
287
|
+
time rather than trusting the caller, and puts secret clients behind the
|
|
288
|
+
framework's `server-only` boundary so a key cannot be imported into a component
|
|
289
|
+
that ships to a browser.
|
|
290
|
+
|
|
291
|
+
### Filtering a server read by a reference field
|
|
292
|
+
|
|
293
|
+
`list({ where: { issueId } })` matched nothing on a replicated plane. It raised
|
|
294
|
+
no error and returned a well-formed empty array, so the read looked like a
|
|
295
|
+
question with no answers rather than a filter that never ran. Filtering on
|
|
296
|
+
`id`, `title` or `body` worked, which made the failure look like a property of
|
|
297
|
+
the data instead of a property of the field name.
|
|
298
|
+
|
|
299
|
+
The cause was a key space. A row served from the log is a snapshot in the wire
|
|
300
|
+
shape, so its fields are spelled the way your schema spells them, while the
|
|
301
|
+
filter looked them up by their database column. Those two agree exactly when a
|
|
302
|
+
column is a single word, and part ways on every `issueId`, `teamId` or
|
|
303
|
+
`assigneeId`. Ordering, relation expansion and any field declared with
|
|
304
|
+
`.from()` were reading the same wrong spelling: a `related` list came back
|
|
305
|
+
empty, and a `.from()` field was simply absent from the row.
|
|
306
|
+
|
|
307
|
+
If you page a collection and filter it in your own code to work around this,
|
|
308
|
+
that code can go.
|
|
309
|
+
|
|
310
|
+
A filter naming a field the model does not declare is now refused, with the
|
|
311
|
+
same error the direct-database plane already gave it. It used to return
|
|
312
|
+
nothing, which reads as an answer.
|
|
313
|
+
|
|
314
|
+
### A list read walks its own pages
|
|
315
|
+
|
|
316
|
+
`list` returns a page, and a page of 20 looks exactly like a complete answer of 20. Every caller either checked `hasMore` or, more often, reasoned about a
|
|
317
|
+
truncated collection without knowing there was more.
|
|
318
|
+
|
|
319
|
+
Iterate the result for the page. Walk it for the collection:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
for await (const issue of await ablo.issues.list({ where: { teamId } })) {
|
|
323
|
+
…
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`hasMore` and `nextCursor` are unchanged, and taking the cursor yourself is
|
|
328
|
+
still the right thing when the pages go somewhere other than a loop.
|
|
329
|
+
|
|
330
|
+
### Clearing a field
|
|
331
|
+
|
|
332
|
+
`null` clears a field, and the types now say so. They used to accept only the
|
|
333
|
+
field's own type or `undefined`, and `undefined` means "leave this alone": it
|
|
334
|
+
is dropped from the payload, so an unassign written that way kept the old
|
|
335
|
+
assignee and reported success. The only spelling that both compiled and worked
|
|
336
|
+
was one that cast the payload, which turned off type checking for the whole
|
|
337
|
+
write.
|
|
338
|
+
|
|
339
|
+
Only a field your schema declares optional accepts `null`. A required field has
|
|
340
|
+
no empty value to move to, and the type says that too.
|
|
341
|
+
|
|
342
|
+
### A write that does not name its row is refused
|
|
343
|
+
|
|
344
|
+
`delete({ where: { id } })` reads like it should work, and `where` is what the
|
|
345
|
+
commit protocol takes one layer down. It used to spell the missing id into the
|
|
346
|
+
request as the literal text `undefined`, match no row, and return an ordinary
|
|
347
|
+
receipt. It now fails at the call, naming the model, the action, and `{ id }`.
|
|
348
|
+
|
|
349
|
+
The same guard covers `update`.
|
|
350
|
+
|
|
351
|
+
### A create honours the id you gave it
|
|
352
|
+
|
|
353
|
+
An id passed inside `data`, which the create input has always allowed, was
|
|
354
|
+
never read: the row was written under a generated id and you were handed back
|
|
355
|
+
one you had not named. Both spellings now work, and the standalone `id` wins if
|
|
356
|
+
they disagree.
|
|
357
|
+
|
|
358
|
+
### Every response says what your allowance is
|
|
359
|
+
|
|
360
|
+
The limiter knew the allowance and the refill and told you neither, so the only
|
|
361
|
+
strategy available was to retry and find the wall again.
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
RateLimit-Policy: "secret";q=600;w=12
|
|
365
|
+
RateLimit: "secret";r=573;t=8
|
|
366
|
+
Retry-After: 3
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`RateLimit-Policy` is the standing allowance and is always present.
|
|
370
|
+
`RateLimit` reports what is left and when it refills, once a request is
|
|
371
|
+
attributed to a key. A 429 adds `Retry-After` in whole seconds. Pace against
|
|
372
|
+
these rather than retrying blind.
|
|
373
|
+
|
|
374
|
+
### A route says when it is going away
|
|
375
|
+
|
|
376
|
+
Every response carries `Ablo-Version`, a date stamp for the contract being
|
|
377
|
+
served, so a caller can notice the contract moved under it.
|
|
378
|
+
|
|
379
|
+
A route being withdrawn now says so on itself for at least 180 days first.
|
|
380
|
+
`Deprecation` (RFC 9745) carries when the deprecation took effect, and the route
|
|
381
|
+
keeps answering; `Sunset` (RFC 8594) carries when it stops. The same operations
|
|
382
|
+
are marked `deprecated: true` in the OpenAPI document, so a generated client
|
|
383
|
+
sees it too.
|
|
384
|
+
|
|
385
|
+
Breaking changes still arrive as a new path segment beside `/v1`, never as a
|
|
386
|
+
change to it. Additive ones land in `/v1`, so ignore what you do not recognise.
|
|
387
|
+
|
|
388
|
+
### The documentation answers a reader that is not a browser
|
|
389
|
+
|
|
390
|
+
The surfaces `llms.txt` names are routes now rather than a promise:
|
|
391
|
+
`/llms-full.txt` for the whole corpus in one fetch, `/openapi.json` for the REST
|
|
392
|
+
contract, `/developers` naming every developer surface on one page,
|
|
393
|
+
`/.well-known/mcp.json` for the MCP manifest.
|
|
394
|
+
|
|
395
|
+
Every page also answers from its own URL in Markdown. Send
|
|
396
|
+
`Accept: text/markdown`, or append `.md` where a client cannot set headers.
|
|
397
|
+
Responses carry `Vary: Accept`, a client that will take neither type gets a 406
|
|
398
|
+
listing what is available, and a path that does not exist answers a real 404
|
|
399
|
+
rather than a 200 carrying a sign-in page.
|
|
400
|
+
|
|
401
|
+
### Renamed and removed
|
|
402
|
+
|
|
403
|
+
`SourceRequestContext.requiredSyncGroups` is now `syncGroups`. Ablo populates
|
|
404
|
+
both spellings this release, so a source adapter still reading the old name gets
|
|
405
|
+
the groups rather than `undefined`, which on a routing field would read as "no
|
|
406
|
+
groups" rather than as a field that moved. The old spelling is removed in
|
|
407
|
+
0.58.0.
|
|
408
|
+
|
|
409
|
+
`DeltaPosition`, `deltaPositionSchema`, `ReadSetWatermark`, and
|
|
410
|
+
`readSetWatermarkSchema` are removed, as 0.56.0 announced. Use `LogPosition` and
|
|
411
|
+
`logPositionSchema`, which they have resolved to since then.
|
|
412
|
+
|
|
3
413
|
## 0.56.0
|
|
4
414
|
|
|
5
|
-
###
|
|
415
|
+
### Coordination reads are scoped to the customer, not the organization
|
|
416
|
+
|
|
417
|
+
Coordination has always been scoped to the organization, and through 0.51.0 that
|
|
418
|
+
was the whole boundary: a platform gave each customer its own organization, and
|
|
419
|
+
the sessions guide was explicit that sync groups decide which changes travel
|
|
420
|
+
rather than what a session may read.
|
|
421
|
+
|
|
422
|
+
This release moves that line. A platform's customers are rows in its own schema,
|
|
423
|
+
reached by the sync groups on the session, so many customers share one
|
|
424
|
+
organization and the organization is no longer the finest boundary. The delivery
|
|
425
|
+
path already applied the finer cut. The claim listing and the presence read did
|
|
426
|
+
not, so under that newer arrangement one customer could see which rows another
|
|
427
|
+
had claimed, who held them, what the work was called, and who was online. Row
|
|
428
|
+
contents were never exposed; everything around them was.
|
|
6
429
|
|
|
7
|
-
|
|
8
|
-
organization and one plane. The claim listing and the presence read were scoped
|
|
9
|
-
to exactly that pair and nothing finer. One customer could therefore see which
|
|
10
|
-
rows another had claimed, who held them, what the work was called, and who was
|
|
11
|
-
online. Row contents were never exposed; everything around them was.
|
|
430
|
+
Both reads now take the same cut, from the groups each side already carries.
|
|
12
431
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
organization, this closes the gap without any change on your side.
|
|
432
|
+
If you give each customer its own organization, nothing changes for you and
|
|
433
|
+
nothing was reachable across customers. If you serve many customers from one
|
|
434
|
+
organization, this closes the gap with no change on your side.
|
|
17
435
|
|
|
18
436
|
### A client converges on the head it was measured against
|
|
19
437
|
|
package/LICENSE
CHANGED
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright 2025-2026
|
|
189
|
+
Copyright 2025-2026 Ablo Inc.
|
|
190
190
|
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
192
|
you may not use this file except in compliance with the License.
|
package/NOTICE
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
@ablo/ablo
|
|
2
|
-
Copyright 2025-2026
|
|
2
|
+
Copyright 2025-2026 Ablo Inc.
|
|
3
3
|
|
|
4
|
-
This product includes software developed by
|
|
4
|
+
This product includes software developed by Ablo Inc.
|
|
5
5
|
(https://abloatai.com).
|
|
6
6
|
|
|
7
|
-
"Ablo" is a trademark of
|
|
7
|
+
"Ablo" is a trademark of Ablo Inc. This license does not grant
|
|
8
8
|
permission to use the Ablo name, logo, or trademarks. Third parties
|
|
9
9
|
may describe their use of or compatibility with Ablo factually (e.g.,
|
|
10
10
|
"built with @ablo/ablo") but may not use the Ablo name in a way
|
package/README.md
CHANGED
|
@@ -56,12 +56,13 @@ for changes.
|
|
|
56
56
|
Read and write through one typed API:
|
|
57
57
|
|
|
58
58
|
```ts
|
|
59
|
-
const order = await ablo.orders.
|
|
59
|
+
const order = await ablo.orders.read({ id: orderId });
|
|
60
60
|
|
|
61
61
|
if (!order) throw new Error('Order not found');
|
|
62
62
|
await ablo.orders.update({
|
|
63
63
|
id: order.id,
|
|
64
64
|
data: { status: 'approved' },
|
|
65
|
+
reads: [order],
|
|
65
66
|
});
|
|
66
67
|
```
|
|
67
68
|
|
package/dist/ai-sdk.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from '@abloatai/transaction/ai-sdk';
|
|
2
|
-
import type { ContextResult } from './context.js';
|
|
2
|
+
import type { ContextResult } from './context/index.js';
|
|
3
3
|
export interface ContextMessageOptions<TData extends Readonly<Record<string, unknown>>> {
|
|
4
4
|
/** Top-level context keys to render. The default is every selected key. */
|
|
5
5
|
readonly include?: readonly (keyof TData & string)[];
|
package/dist/ai-sdk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-sdk.d.ts","sourceRoot":"","sources":["../src/ai-sdk.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAG7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"ai-sdk.d.ts","sourceRoot":"","sources":["../src/ai-sdk.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAG7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,MAAM,WAAW,qBAAqB,CAAC,KAAK,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpF,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC;CACtD;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,KAAK,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC5E,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,EAC3B,OAAO,GAAE,qBAAqB,CAAC,KAAK,CAAM,GACzC,cAAc,CAehB"}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { type CapturedReadEvidence } from '@abloatai/transaction/internal/read-set';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import type { ReadDependency } from '@abloatai/transaction/coordination';
|
|
3
|
+
import type { AbloStaleContextError } from '@abloatai/transaction';
|
|
4
|
+
export interface BoundContextEvidence {
|
|
5
|
+
readonly collect: (data: unknown) => readonly CapturedReadEvidence[];
|
|
6
|
+
readonly onChange?: (reads: readonly ReadDependency[], listener: (error: AbloStaleContextError) => void) => () => void;
|
|
5
7
|
}
|
|
6
|
-
export
|
|
7
|
-
readonly all: readonly CapturedReadEvidence[];
|
|
8
|
-
readonly inspect: (value: unknown) => ContextEvidenceSlice;
|
|
9
|
-
}
|
|
10
|
-
export declare function bindContextEvidence(client: object): (data: unknown) => ContextEvidence;
|
|
8
|
+
export declare function bindContextEvidence(client: object): BoundContextEvidence;
|
|
11
9
|
//# sourceMappingURL=evidence.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../../src/context/evidence.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,oBAAoB,EAE1B,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../../src/context/evidence.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,oBAAoB,EAE1B,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAiCnE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,SAAS,oBAAoB,EAAE,CAAC;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAClB,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,KAC7C,MAAM,IAAI,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,CAQxE"}
|
package/dist/context/evidence.js
CHANGED
|
@@ -8,12 +8,9 @@ function isTraversable(value) {
|
|
|
8
8
|
function inspectValue(binding, value) {
|
|
9
9
|
const found = [];
|
|
10
10
|
const seen = new WeakSet();
|
|
11
|
-
let includesInformational = false;
|
|
12
11
|
const visit = (current) => {
|
|
13
|
-
if (typeof current !== 'object' || current === null)
|
|
14
|
-
includesInformational = true;
|
|
12
|
+
if (typeof current !== 'object' || current === null)
|
|
15
13
|
return;
|
|
16
|
-
}
|
|
17
14
|
if (seen.has(current))
|
|
18
15
|
return;
|
|
19
16
|
seen.add(current);
|
|
@@ -23,31 +20,20 @@ function inspectValue(binding, value) {
|
|
|
23
20
|
return;
|
|
24
21
|
}
|
|
25
22
|
if (isTraversable(current)) {
|
|
26
|
-
const
|
|
27
|
-
if (children.length === 0)
|
|
28
|
-
includesInformational = true;
|
|
29
|
-
for (const child of children)
|
|
23
|
+
for (const child of Object.values(current))
|
|
30
24
|
visit(child);
|
|
31
|
-
return;
|
|
32
25
|
}
|
|
33
|
-
includesInformational = true;
|
|
34
26
|
};
|
|
35
27
|
visit(value);
|
|
36
|
-
return
|
|
37
|
-
reads: [...new Map(found.map((item) => [item.row, item])).values()],
|
|
38
|
-
includesInformational,
|
|
39
|
-
};
|
|
28
|
+
return [...new Map(found.map((item) => [item.row, item])).values()];
|
|
40
29
|
}
|
|
41
30
|
export function bindContextEvidence(client) {
|
|
42
31
|
const binding = readEvidenceBinding(client);
|
|
43
32
|
if (!binding)
|
|
44
33
|
throw new TypeError('context() requires an Ablo client in `ablo`.');
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
all: inspect(data).reads,
|
|
49
|
-
inspect,
|
|
50
|
-
};
|
|
34
|
+
return {
|
|
35
|
+
collect: (data) => inspectValue(binding, data),
|
|
36
|
+
...(binding.onChange ? { onChange: binding.onChange } : {}),
|
|
51
37
|
};
|
|
52
38
|
}
|
|
53
39
|
//# sourceMappingURL=evidence.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evidence.js","sourceRoot":"","sources":["../../src/context/evidence.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,mBAAmB,GAGpB,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"evidence.js","sourceRoot":"","sources":["../../src/context/evidence.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,mBAAmB,GAGpB,MAAM,yCAAyC,CAAC;AAIjD,SAAS,aAAa,CAAC,KAAa;IAClC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC;AAC9D,CAAC;AAED,SAAS,YAAY,CACnB,OAA4B,EAC5B,KAAc;IAEd,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,OAAO,EAAU,CAAC;IAEnC,MAAM,KAAK,GAAG,CAAC,OAAgB,EAAQ,EAAE;QACvC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO;QAC5D,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO;QAC9B,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAClB,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAClD,IAAI,QAAQ,EAAE,CAAC;YACb,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QACD,IAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;gBAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,CAAC,KAAK,CAAC,CAAC;IACb,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AACtE,CAAC;AAUD,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,MAAM,OAAO,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,SAAS,CAAC,8CAA8C,CAAC,CAAC;IAElF,OAAO;QACL,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC;QAC9C,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5D,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CapturedRow } from '@abloatai/transaction';
|
|
2
|
+
import { type AwaitedDeep } from './await.js';
|
|
3
|
+
import { type ContextOnChange } from './onChange.js';
|
|
4
|
+
export type { AwaitedDeep } from './await.js';
|
|
5
|
+
export type { ContextChangeListener, ContextOnChange } from './onChange.js';
|
|
6
|
+
export interface ContextOptions<TData extends Readonly<Record<string, unknown>>> {
|
|
7
|
+
/** The client whose read evidence may guard a later create, update, or delete. */
|
|
8
|
+
readonly ablo: object;
|
|
9
|
+
/** Values selected by the application. Nested promises are accepted. */
|
|
10
|
+
readonly data: TData;
|
|
11
|
+
}
|
|
12
|
+
export interface ContextResult<TData extends Readonly<Record<string, unknown>>> {
|
|
13
|
+
readonly data: AwaitedDeep<TData>;
|
|
14
|
+
/**
|
|
15
|
+
* Exact returned Ablo rows, ready to pass to create, update, or delete
|
|
16
|
+
* through `reads`.
|
|
17
|
+
*/
|
|
18
|
+
readonly reads: readonly CapturedRow[];
|
|
19
|
+
/** Called once when any exact read in this context becomes stale. */
|
|
20
|
+
readonly onChange: ContextOnChange;
|
|
21
|
+
}
|
|
22
|
+
export declare function context<const TData extends Readonly<Record<string, unknown>>>(options: ContextOptions<TData>): Promise<ContextResult<TData>>;
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|