@abloatai/ablo 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +220 -26
  3. package/NOTICE +1 -1
  4. package/docs/agents.md +28 -28
  5. package/docs/api-keys.md +30 -3
  6. package/docs/api.md +32 -3
  7. package/docs/cli.md +3 -3
  8. package/docs/client-behavior.md +1 -1
  9. package/docs/concurrency-convention.md +7 -7
  10. package/docs/context.md +11 -11
  11. package/docs/coordination.md +19 -19
  12. package/docs/customer-organizations.md +215 -0
  13. package/docs/data-sources.md +3 -3
  14. package/docs/debugging.md +8 -8
  15. package/docs/examples/agent-human.md +17 -17
  16. package/docs/examples/ai-sdk-tool.md +6 -6
  17. package/docs/examples/existing-python-backend.md +1 -1
  18. package/docs/examples/nextjs.md +15 -15
  19. package/docs/examples/scoped-agent.md +10 -10
  20. package/docs/examples/server-agent.md +14 -14
  21. package/docs/groups.md +12 -12
  22. package/docs/how-it-works.md +7 -7
  23. package/docs/idempotency.md +4 -4
  24. package/docs/identity.md +45 -38
  25. package/docs/index.md +2 -1
  26. package/docs/integration-guide.md +1 -1
  27. package/docs/integrations/inngest.md +2 -2
  28. package/docs/integrations/temporal.md +3 -3
  29. package/docs/integrations.md +3 -3
  30. package/docs/react.md +4 -4
  31. package/docs/sessions.md +21 -16
  32. package/docs/webhooks.md +2 -2
  33. package/examples/README.md +6 -6
  34. package/examples/agent-turn.ts +13 -13
  35. package/examples/data-source/README.md +3 -3
  36. package/examples/data-source/customer-server.ts +23 -23
  37. package/examples/data-source/run.ts +8 -8
  38. package/examples/data-source/schema.ts +2 -2
  39. package/examples/lease-outlives-the-machine.ts +66 -0
  40. package/examples/tsconfig.json +4 -10
  41. package/llms.txt +11 -0
  42. package/package.json +4 -4
package/docs/sessions.md CHANGED
@@ -13,13 +13,13 @@ One resource mints both:
13
13
  // A logged-in person's browser session — only the operations this UI needs.
14
14
  const userSession = await ablo.sessions.create({
15
15
  user: { id: currentUser.id },
16
- can: { tasks: ['read', 'update'], workspaces: ['read'] },
16
+ can: { records: ['read', 'update'], workspaces: ['read'] },
17
17
  });
18
18
 
19
19
  // Recommended agent path — returns a ready, scoped client.
20
20
  const agent = await ablo.agents.create({
21
- name: 'task-writer',
22
- can: { tasks: ['read', 'update'], workspaces: ['read'] },
21
+ name: 'record-writer',
22
+ can: { records: ['read', 'update'], workspaces: ['read'] },
23
23
  });
24
24
  ```
25
25
 
@@ -51,7 +51,7 @@ import { credentialEndpointSuccessSchema } from '@abloatai/ablo/auth';
51
51
 
52
52
  const { token, expiresAt } = await ablo.sessions.create({
53
53
  user: { id: currentUser.id }, // who the session acts as
54
- can: { tasks: ['read', 'update'] },
54
+ can: { records: ['read', 'update'] },
55
55
  // syncGroups: [...], // optional; defaults to the user's org + user
56
56
  });
57
57
  return Response.json(
@@ -104,18 +104,18 @@ error, not a silent over-grant:
104
104
 
105
105
  ```ts
106
106
  const agent = await ablo.agents.create({
107
- name: 'task-writer',
108
- can: { tasks: ['update'] }, // typed off the schema — no magic strings
107
+ name: 'record-writer',
108
+ can: { records: ['update'] }, // typed off the schema — no magic strings
109
109
  ttlSeconds: 600,
110
110
  });
111
111
 
112
- await agent.tasks.update({ id, data });
112
+ await agent.records.update({ id, data });
113
113
  await agent.dispose();
114
114
  ```
115
115
 
116
116
  The returned client refreshes its own short-lived credential. A write grant
117
117
  automatically includes the corresponding read, so
118
- `can: { tasks: ['update'] }` is enforced as `task.update` plus `task.read`.
118
+ `can: { records: ['update'] }` is enforced as `record.update` plus `record.read`.
119
119
  Operations are `'read' | 'create' | 'update' | 'delete'`.
120
120
 
121
121
  For a reusable grant, use TypeScript's `satisfies`. It checks the object against
@@ -126,14 +126,14 @@ second permission model:
126
126
  import type { CapabilityGrant } from '@abloatai/ablo/auth';
127
127
  import { schema } from './ablo.schema';
128
128
 
129
- const taskWriterCan = {
130
- tasks: ['update'],
129
+ const recordWriterCan = {
130
+ records: ['update'],
131
131
  } satisfies CapabilityGrant<typeof schema>;
132
132
 
133
- const agent = await ablo.agents.create({ can: taskWriterCan });
133
+ const agent = await ablo.agents.create({ can: recordWriterCan });
134
134
  ```
135
135
 
136
- `documents` instead of `tasks`, or `'write'` instead of `'update'`, is a compile
136
+ `documents` instead of `records`, or `'write'` instead of `'update'`, is a compile
137
137
  error. At runtime the SDK parses the same grant with the schema-bound Zod
138
138
  contract before minting, and the server validates it again against the active
139
139
  pushed schema.
@@ -154,6 +154,8 @@ for the actor.
154
154
  |---|---|---|
155
155
  | `user` / `agent` | both | The actor. `id` becomes the token's `participantId`. Pass exactly one. |
156
156
  | `can` | both | Required non-empty per-model operation allowlist, typed off the schema. |
157
+ | `organizationId` | user | Mint into a customer organization instead of the key's own. Requires `ephemeral:mint-any-org`. |
158
+ | `schemaProject` | user | Override the schema project for a cross-org mint. Usually omitted because the owning key's project is the default. |
157
159
  | `syncGroups` | both | Narrow the session below its default scope. Omit to inherit. |
158
160
  | `ttlSeconds` | both | Lifetime in seconds. Defaults to `900` (15m). |
159
161
  | `userMeta` | both | Opaque identity blob echoed back to the client. |
@@ -217,7 +219,7 @@ errors.
217
219
 
218
220
  A user session carries the user's **base** sync-groups (`org:`/`user:`/`team:`),
219
221
  derived from the identity you minted it for. **Dynamic, relation-driven
220
- membership** (e.g. a `dataroom:<id>` the user was just added to) is resolved
222
+ membership** (e.g. a `archive:<id>` the user was just added to) is resolved
221
223
  **server-side at connect** and unioned on top — so scope stays live, not frozen
222
224
  at mint time. Pass `syncGroups` only when you want to *narrow* below the default.
223
225
 
@@ -255,6 +257,9 @@ deny reads. Do not use scope roots as a tenant security boundary unless every
255
257
  model declares the matching policy. If that invariant is difficult to audit,
256
258
  use one organization per customer.
257
259
 
260
+ For the complete key, backend-route, browser, lifecycle, and troubleshooting
261
+ flow, see [Customer Organizations](./customer-organizations.md).
262
+
258
263
  The problem that creates: if each customer is a separate org, a naïve setup would
259
264
  make you re-push your schema into every new customer's org. You don't have to.
260
265
  Keep **one** project as the home of your schema. When its key mints into another
@@ -266,7 +271,7 @@ const ablo = Ablo({ schema, apiKey: process.env.ABLO_PLATFORM_KEY });
266
271
  const { token } = await ablo.sessions.create({
267
272
  user: { id: userId },
268
273
  organizationId, // DATA → this customer's isolated org
269
- can: { tasks: ['read', 'update'] },
274
+ can: { records: ['read', 'update'] },
270
275
  ttlSeconds: 3600,
271
276
  });
272
277
  ```
@@ -281,8 +286,8 @@ plane (connection + row-level isolation) stays the customer's. A shared schema
281
286
  can't leak data across orgs.
282
287
 
283
288
  <Note>
284
- This requires a platform `sk_` carrying the `ephemeral:mint-any-org` scope —
285
- only a trusted platform key can mint a session into another org. Omit
289
+ This requires a dedicated `sk_` carrying the `ephemeral:mint-any-org` scope —
290
+ only a trusted cross-organization key can mint a session into another org. Omit
286
291
  `organizationId` and you get the default above: one project, one schema, all
287
292
  your users in the key's own organization.
288
293
  </Note>
package/docs/webhooks.md CHANGED
@@ -39,7 +39,7 @@ Every delivery is a batch of events. Each event:
39
39
 
40
40
  | field | meaning |
41
41
  |---|---|
42
- | `type` | `"<model>.<verb>"` with the model name lowercased, e.g. `task.updated` |
42
+ | `type` | `"<model>.<verb>"` with the model name lowercased, e.g. `record.updated` |
43
43
  | `model` | the model name exactly as declared in your schema: the table to write |
44
44
  | `objectId` | the changed row's id |
45
45
  | `data` | the post-change row, or `null` on delete |
@@ -127,7 +127,7 @@ Scope which models fire and label the endpoint at creation with `--events` and
127
127
 
128
128
  ```bash
129
129
  npx ablo webhooks create https://yourapp.com/api/ablo/[...all] \
130
- --events task,project --description "prod mirror"
130
+ --events record,project --description "prod mirror"
131
131
  ```
132
132
 
133
133
  Manage and inspect endpoints:
@@ -30,13 +30,13 @@ For read-reason-write work, pass the exact returned rows that informed the
30
30
  decision. Their watermarks stay opaque:
31
31
 
32
32
  ```ts
33
- const task = await ablo.tasks.get({ id: taskId });
33
+ const record = await ablo.records.get({ id: recordId });
34
34
  const policy = await ablo.policies.get({ id: policyId });
35
- const result = await model({ task, policy });
36
- await ablo.tasks.update({
37
- id: task.id,
35
+ const result = await model({ record, policy });
36
+ await ablo.records.update({
37
+ id: record.id,
38
38
  data: result,
39
- reads: [task, policy],
39
+ reads: [record, policy],
40
40
  });
41
41
  ```
42
42
 
@@ -61,7 +61,7 @@ root and a bare `quickstart.ts` won't be found.
61
61
  ```bash
62
62
  cd packages/ablo
63
63
  ABLO_API_KEY=sk_... npx tsx examples/quickstart.ts
64
- ABLO_API_KEY=sk_... TASK_ID=task_... npx tsx examples/agent-turn.ts
64
+ ABLO_API_KEY=sk_... RECORD_ID=record_... npx tsx examples/agent-turn.ts
65
65
  ABLO_API_KEY=sk_... JOB_ID=job_... npx tsx examples/expensive-agent-turn.ts
66
66
  ```
67
67
 
@@ -2,36 +2,36 @@
2
2
  * Canonical cheap turn: explicitly attach the exact rows used to decide the
3
3
  * write. Ablo keeps their watermarks opaque and checks them at commit time.
4
4
  *
5
- * Run: ABLO_API_KEY=sk_... TASK_ID=task_... npx tsx examples/agent-turn.ts
5
+ * Run: ABLO_API_KEY=sk_... RECORD_ID=record_... npx tsx examples/agent-turn.ts
6
6
  */
7
7
  import { Ablo } from '@abloatai/ablo';
8
8
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
9
9
 
10
10
  const schema = defineSchema({
11
- tasks: model({
11
+ records: model({
12
12
  title: z.string(),
13
13
  status: z.enum(['pending', 'done']),
14
14
  result: z.string().optional(),
15
15
  }),
16
16
  });
17
17
 
18
- const taskId = process.env.TASK_ID;
19
- if (!taskId) throw new Error('TASK_ID is required');
18
+ const recordId = process.env.RECORD_ID;
19
+ if (!recordId) throw new Error('RECORD_ID is required');
20
20
 
21
21
  const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
22
22
  try {
23
23
  await ablo.ready();
24
- const task = await ablo.tasks.get({ id: taskId });
25
- if (!task) throw new Error(`Task ${taskId} was not found`);
26
- const commitId = `task:${taskId}:cheap`;
27
- await ablo.tasks.update({
28
- id: task.id,
29
- data: { status: 'done', result: `Completed: ${task.title}` },
30
- reads: [task],
24
+ const record = await ablo.records.get({ id: recordId });
25
+ if (!record) throw new Error(`Record ${recordId} was not found`);
26
+ const commitId = `record:${recordId}:cheap`;
27
+ await ablo.records.update({
28
+ id: record.id,
29
+ data: { status: 'done', result: `Completed: ${record.title}` },
30
+ reads: [record],
31
31
  idempotencyKey: commitId,
32
32
  });
33
- const record = await ablo.commits.get({ id: commitId });
34
- console.log({ identity: ablo.identity, commit: record });
33
+ const commit = await ablo.commits.get({ id: commitId });
34
+ console.log({ identity: ablo.identity, commit });
35
35
  } finally {
36
36
  await ablo.dispose();
37
37
  }
@@ -93,9 +93,9 @@ createServer(async (req, res) => {
93
93
  Replace the `Map`-based store in `customer-server.ts` with your real
94
94
  data layer. The handler shape stays the same:
95
95
 
96
- - `tasks.load({ id })` -> `db.task.findUnique({ where: { id } })`
97
- - `tasks.list({ query })` -> `db.task.findMany({ take, cursor })`
98
- - `tasks.commit({ operations, clientTxId })` -> `db.$transaction` that
96
+ - `records.load({ id })` -> `db.record.findUnique({ where: { id } })`
97
+ - `records.list({ query })` -> `db.record.findMany({ take, cursor })`
98
+ - `records.commit({ operations, clientTxId })` -> `db.$transaction` that
99
99
  applies each `op` and writes an outbox marker with `clientTxId` before commit
100
100
  - `events({ cursor, limit })` -> read from your outbox table, return
101
101
  rows with their `clientTxId` (Ablo dedupes its own commits) and the
@@ -23,7 +23,7 @@ import {
23
23
  } from '@abloatai/ablo/source';
24
24
  import { schema } from './schema';
25
25
 
26
- type TaskRow = {
26
+ type RecordRow = {
27
27
  id: string;
28
28
  title: string;
29
29
  status: 'todo' | 'doing' | 'done';
@@ -31,9 +31,9 @@ type TaskRow = {
31
31
  };
32
32
 
33
33
  // Stand-in for the customer's real database. Map keyed by row id.
34
- const taskStore = new Map<string, TaskRow>();
34
+ const recordStore = new Map<string, RecordRow>();
35
35
 
36
- // Outbox table. In production this is a `tasks_outbox` Postgres table
36
+ // Outbox table. In production this is a `records_outbox` Postgres table
37
37
  // populated in the same transaction as the app-row write. Ablo polls `events`
38
38
  // to fan out changes that bypassed Ablo, and to repair SDK-origin writes if
39
39
  // Ablo's immediate post-commit append failed.
@@ -41,8 +41,8 @@ const outbox: SourceEvent[] = [];
41
41
  let outboxSequence = 0;
42
42
 
43
43
  // Seed one row so the example's first `load` returns something.
44
- taskStore.set('task_seed', {
45
- id: 'task_seed',
44
+ recordStore.set('record_seed', {
45
+ id: 'record_seed',
46
46
  title: 'Seeded by customer database',
47
47
  status: 'todo',
48
48
  });
@@ -91,13 +91,13 @@ export const handleAbloSource = dataSource({
91
91
  return {};
92
92
  },
93
93
 
94
- tasks: {
94
+ records: {
95
95
  load({ id }) {
96
- return taskStore.get(id) ?? null;
96
+ return recordStore.get(id) ?? null;
97
97
  },
98
98
 
99
99
  list({ query }) {
100
- const all = Array.from(taskStore.values());
100
+ const all = Array.from(recordStore.values());
101
101
  const start = query.cursor ? Number(query.cursor) : 0;
102
102
  const limit = query.limit ?? 50;
103
103
  const page = all.slice(start, start + limit);
@@ -115,7 +115,7 @@ export const handleAbloSource = dataSource({
115
115
  // update; the surrounding `apply` helper shows where you would
116
116
  // open `db.transaction(async (tx) => { ... })`.
117
117
  commit({ operations, clientTxId }) {
118
- const rows: TaskRow[] = [];
118
+ const rows: RecordRow[] = [];
119
119
  for (const op of operations) {
120
120
  const row = applyOperation(op, clientTxId);
121
121
  if (row) rows.push(row);
@@ -145,38 +145,38 @@ export const handleAbloSource = dataSource({
145
145
  function applyOperation(
146
146
  op: SourceOperation,
147
147
  clientTxId: string | undefined,
148
- ): TaskRow | null {
149
- if (op.model !== 'tasks') return null;
150
- const id = op.id ?? `task_${Math.random().toString(36).slice(2, 10)}`;
148
+ ): RecordRow | null {
149
+ if (op.model !== 'records') return null;
150
+ const id = op.id ?? `record_${Math.random().toString(36).slice(2, 10)}`;
151
151
 
152
152
  if (op.type === 'CREATE') {
153
- const row: TaskRow = {
153
+ const row: RecordRow = {
154
154
  id,
155
155
  title: String(op.input?.title ?? ''),
156
156
  status:
157
- (op.input?.status as TaskRow['status'] | undefined) ?? 'todo',
157
+ (op.input?.status as RecordRow['status'] | undefined) ?? 'todo',
158
158
  ...(op.input?.assignee
159
159
  ? { assignee: String(op.input.assignee) }
160
160
  : {}),
161
161
  };
162
- taskStore.set(id, row);
162
+ recordStore.set(id, row);
163
163
  appendOutbox({ operation: op, entityId: id, data: row, clientTxId });
164
164
  return row;
165
165
  }
166
166
 
167
167
  if (op.type === 'UPDATE') {
168
- const existing = taskStore.get(id);
168
+ const existing = recordStore.get(id);
169
169
  if (!existing) return null;
170
- const next: TaskRow = { ...existing, ...(op.input as Partial<TaskRow>) };
171
- taskStore.set(id, next);
170
+ const next: RecordRow = { ...existing, ...(op.input as Partial<RecordRow>) };
171
+ recordStore.set(id, next);
172
172
  appendOutbox({ operation: op, entityId: id, data: next, clientTxId });
173
173
  return next;
174
174
  }
175
175
 
176
176
  if (op.type === 'DELETE') {
177
- const existing = taskStore.get(id);
177
+ const existing = recordStore.get(id);
178
178
  if (!existing) return null;
179
- taskStore.delete(id);
179
+ recordStore.delete(id);
180
180
  appendOutbox({ operation: op, entityId: id, data: null, clientTxId });
181
181
  return existing;
182
182
  }
@@ -187,7 +187,7 @@ function applyOperation(
187
187
  function appendOutbox(input: {
188
188
  operation: SourceOperation;
189
189
  entityId: string;
190
- data: TaskRow | null;
190
+ data: RecordRow | null;
191
191
  clientTxId: string | undefined;
192
192
  }): void {
193
193
  outboxSequence += 1;
@@ -205,11 +205,11 @@ function appendOutbox(input: {
205
205
  // Exposed for the orchestrator's `run.ts`. A real customer doesn't
206
206
  // need this — it's a back door for the demo to verify state.
207
207
  export function _inspectStore(): {
208
- rows: TaskRow[];
208
+ rows: RecordRow[];
209
209
  outboxSize: number;
210
210
  } {
211
211
  return {
212
- rows: Array.from(taskStore.values()),
212
+ rows: Array.from(recordStore.values()),
213
213
  outboxSize: outbox.length,
214
214
  };
215
215
  }
@@ -36,7 +36,7 @@ async function main() {
36
36
  });
37
37
 
38
38
  log('--- 1. load (existing seeded row) ---');
39
- const seeded = await driver.load('tasks', 'task_seed');
39
+ const seeded = await driver.load('records', 'record_seed');
40
40
  log('loaded:', seeded);
41
41
 
42
42
  log('\n--- 2. commit (CREATE + UPDATE in one batch) ---');
@@ -44,14 +44,14 @@ async function main() {
44
44
  [
45
45
  {
46
46
  type: 'CREATE',
47
- model: 'tasks',
48
- id: 'task_new',
47
+ model: 'records',
48
+ id: 'record_new',
49
49
  input: { title: 'Wire the data source', status: 'todo' },
50
50
  },
51
51
  {
52
52
  type: 'UPDATE',
53
- model: 'tasks',
54
- id: 'task_seed',
53
+ model: 'records',
54
+ id: 'record_seed',
55
55
  input: { status: 'doing', assignee: 'alice' },
56
56
  },
57
57
  ],
@@ -59,8 +59,8 @@ async function main() {
59
59
  );
60
60
  log('committed rows:', committed);
61
61
 
62
- log('\n--- 3. list (all tasks after commit) ---');
63
- const listed = await driver.list('tasks');
62
+ log('\n--- 3. list (all records after commit) ---');
63
+ const listed = await driver.list('records');
64
64
  log('listed:', listed);
65
65
 
66
66
  log('\n--- 4. events (outbox feed for cross-channel writes) ---');
@@ -73,7 +73,7 @@ async function main() {
73
73
  apiKey: 'sk_wrong_example_key',
74
74
  });
75
75
  try {
76
- await badDriver.load('tasks', 'task_seed');
76
+ await badDriver.load('records', 'record_seed');
77
77
  throw new Error('expected signature failure');
78
78
  } catch (err) {
79
79
  log('rejected as expected:', (err as Error).message);
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The schema is the contract between three sides:
5
5
  *
6
- * 1. The application UI — `ablo.tasks.update(...)`.
6
+ * 1. The application UI — `ablo.records.update(...)`.
7
7
  * 2. The Ablo Cloud — translates writes into signed POSTs.
8
8
  * 3. The customer's Data Source endpoint — applies them to its own
9
9
  * database.
@@ -15,7 +15,7 @@
15
15
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
16
16
 
17
17
  export const schema = defineSchema({
18
- tasks: model({
18
+ records: model({
19
19
  title: z.string(),
20
20
  status: z.enum(['todo', 'doing', 'done']),
21
21
  assignee: z.string().optional(),
@@ -0,0 +1,66 @@
1
+ /**
2
+ * A lease outlives the process that took it.
3
+ *
4
+ * Run in two terminals against your own project. The `holder` takes a claim
5
+ * and is killed without releasing it, exactly as a sandbox that is torn down
6
+ * mid-turn would be. The `successor`, already queued, is granted the claim when
7
+ * the lease lapses and reads the row as it stands then.
8
+ *
9
+ * Terminal 1: ABLO_API_KEY=sk_... JOB_ID=job_... npx tsx examples/lease-outlives-the-machine.ts holder
10
+ * Terminal 2: ABLO_API_KEY=sk_... JOB_ID=job_... npx tsx examples/lease-outlives-the-machine.ts successor
11
+ *
12
+ * Start the successor first, then the holder, so the queue is populated before
13
+ * the lease lapses.
14
+ */
15
+ import { Ablo } from '@abloatai/ablo';
16
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
17
+
18
+ const schema = defineSchema({
19
+ jobs: model({
20
+ prompt: z.string(),
21
+ status: z.enum(['pending', 'complete']),
22
+ answer: z.string().optional(),
23
+ }),
24
+ });
25
+
26
+ const role = process.argv[2];
27
+ if (role !== 'holder' && role !== 'successor') {
28
+ throw new Error('Pass "holder" or "successor" as the first argument');
29
+ }
30
+
31
+ const jobId = process.env.JOB_ID;
32
+ if (!jobId) throw new Error('JOB_ID is required');
33
+
34
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
35
+ await ablo.ready();
36
+
37
+ if (role === 'holder') {
38
+ // A short TTL and no heartbeat: this process takes the lease and then stops
39
+ // proving it is alive, which is what a machine that disappears looks like
40
+ // from the server's side.
41
+ const claim = await ablo.jobs.claim({
42
+ id: jobId,
43
+ description: 'drafting the summary',
44
+ ttl: '10s',
45
+ });
46
+ console.log('holder: lease taken, status is', claim.data.status);
47
+ console.log('holder: exiting without releasing it');
48
+ // Deliberately skip release and skip dispose. `process.exit` runs no
49
+ // cleanup, so the server never hears from this participant again.
50
+ process.exit(0);
51
+ }
52
+
53
+ console.log('successor: queueing behind whoever holds the lease');
54
+ const started = Date.now();
55
+ await using claim = await ablo.jobs.claim({
56
+ id: jobId,
57
+ description: 'taking over the draft',
58
+ ttl: '30s',
59
+ heartbeat: { every: '10s' },
60
+ });
61
+ console.log(`successor: granted after ${Math.round((Date.now() - started) / 1000)}s`);
62
+ console.log('successor: read the row as it stands now —', {
63
+ status: claim.data.status,
64
+ answer: claim.data.answer,
65
+ });
66
+ await ablo.dispose();
@@ -1,16 +1,10 @@
1
1
  {
2
+ "extends": "../tsconfig.json",
2
3
  "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["ES2022", "DOM"],
7
- "strict": true,
8
4
  "noEmit": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "types": ["node"]
5
+ "rootDir": "..",
6
+ "lib": ["ES2022", "ESNext.Disposable"]
13
7
  },
14
8
  "include": ["**/*.ts"],
15
- "exclude": ["node_modules"]
9
+ "exclude": ["node_modules", "dist"]
16
10
  }
package/llms.txt CHANGED
@@ -85,6 +85,17 @@ second verb to learn — `local.` is the only difference. The query reads accept
85
85
  and `state`; state defaults to `'live'`, with `'archived'` and `'all'` to include
86
86
  retired rows.
87
87
 
88
+ `where` takes operators, not only equality: an array value is an `IN`
89
+ (`{ status: ['draft', 'review'] }`), and tuple form spells the rest out
90
+ (`[['title', 'ILIKE', '%storm%'], ['createdAt', '>=', cutoff]]`). Clauses combine
91
+ with AND; for OR, run two reads and union them.
92
+
93
+ `list` returns a page, not always the whole collection. The result is an array,
94
+ so it maps and iterates as usual, and it carries `hasMore` and `nextCursor`
95
+ beside the rows. Pass `nextCursor` back as `cursor`, keeping `where` and
96
+ `orderBy` the same, to walk the rest. Check `hasMore` before treating a result
97
+ as complete.
98
+
88
99
  Workers import the same app schema and select `transport: 'http'`. The transport
89
100
  changes; the typed `ablo.<model>` contract does not. There is no public
90
101
  schema-less or string-keyed model client.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "description": "The public Ablo SDK for coordinated reads, commits, claims, observation, and reactive applications.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -112,7 +112,7 @@
112
112
  "prepack": "npm run build && node scripts/strip-source-condition.mjs",
113
113
  "postpack": "node scripts/restore-source-condition.mjs",
114
114
  "pack:check": "node scripts/pack-check.mjs",
115
- "typecheck": "tsc --noEmit && tsc -p typetests/tsconfig.json",
115
+ "typecheck": "tsc --noEmit && tsc -p typetests/tsconfig.json && tsc -p examples/tsconfig.json",
116
116
  "test": "vitest run",
117
117
  "generate:errors": "tsx scripts/generate-error-docs.mts",
118
118
  "lint:errors": "tsx scripts/check-error-docs.mts",
@@ -137,8 +137,8 @@
137
137
  "directory": "packages/ablo"
138
138
  },
139
139
  "dependencies": {
140
- "@abloatai/humans": "^0.51.0",
141
- "@abloatai/transaction": "^0.51.0",
140
+ "@abloatai/humans": "^0.53.0",
141
+ "@abloatai/transaction": "^0.53.0",
142
142
  "zod": "^4.4.3"
143
143
  },
144
144
  "peerDependencies": {