@abloatai/ablo 0.29.2 → 0.30.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/CHANGELOG.md +27 -0
- package/README.md +3 -2
- package/dist/BaseSyncedStore.js +3 -1
- package/dist/Database.js +29 -2
- package/dist/SyncClient.d.ts +5 -3
- package/dist/SyncClient.js +8 -4
- package/dist/ai-sdk/coordinatedTool.d.ts +0 -1
- package/dist/ai-sdk/coordinatedTool.js +0 -1
- package/dist/ai-sdk/coordinationContext.js +1 -5
- package/dist/cli.cjs +561 -90
- package/dist/client/Ablo.d.ts +1 -1
- package/dist/client/Ablo.js +24 -36
- package/dist/client/auth.d.ts +8 -18
- package/dist/client/auth.js +16 -96
- package/dist/client/createModelProxy.d.ts +16 -11
- package/dist/client/createModelProxy.js +162 -20
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/httpTransport.js +263 -89
- package/dist/client/options.d.ts +0 -15
- package/dist/client/resourceTypes.d.ts +13 -26
- package/dist/client/schemaConfig.js +5 -0
- package/dist/client/writeOptionsSchema.d.ts +1 -0
- package/dist/client/writeOptionsSchema.js +11 -2
- package/dist/client/wsMutationExecutor.d.ts +3 -5
- package/dist/coordination/schema.d.ts +17 -13
- package/dist/coordination/schema.js +36 -10
- package/dist/errorCodes.d.ts +9 -4
- package/dist/errorCodes.js +10 -5
- package/dist/errors.js +4 -9
- package/dist/index.d.ts +4 -0
- package/dist/interfaces/index.d.ts +31 -19
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/schema.d.ts +13 -0
- package/dist/schema/select.js +7 -0
- package/dist/schema/syncDeltaRow.d.ts +31 -13
- package/dist/schema/syncDeltaRow.js +33 -15
- package/dist/schema/tenancy.d.ts +52 -16
- package/dist/schema/tenancy.js +86 -25
- package/dist/server/commit.d.ts +8 -24
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/readConfig.d.ts +10 -0
- package/dist/source/adapter.d.ts +26 -15
- package/dist/source/adapter.js +5 -3
- package/dist/source/adapters/drizzle.js +35 -9
- package/dist/source/adapters/kysely.d.ts +33 -61
- package/dist/source/adapters/kysely.js +170 -177
- package/dist/source/adapters/kyselyMutationCore.d.ts +76 -0
- package/dist/source/adapters/kyselyMutationCore.js +125 -0
- package/dist/source/adapters/memory.d.ts +1 -1
- package/dist/source/adapters/memory.js +24 -10
- package/dist/source/adapters/prisma.js +35 -8
- package/dist/source/conformance.d.ts +26 -28
- package/dist/source/conformance.js +142 -69
- package/dist/source/contract.d.ts +46 -4
- package/dist/source/contract.js +66 -5
- package/dist/source/factory.d.ts +3 -3
- package/dist/source/factory.js +24 -3
- package/dist/source/idempotency.d.ts +46 -0
- package/dist/source/idempotency.js +129 -0
- package/dist/source/index.d.ts +6 -4
- package/dist/source/index.js +5 -3
- package/dist/source/migrations.d.ts +9 -5
- package/dist/source/migrations.js +39 -6
- package/dist/source/types.d.ts +88 -14
- package/dist/source/types.js +16 -0
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +0 -1
- package/dist/sync/BootstrapFetcher.js +7 -0
- package/dist/sync/SyncWebSocket.d.ts +5 -27
- package/dist/sync/awaitClaimGrant.d.ts +6 -0
- package/dist/sync/awaitClaimGrant.js +12 -2
- package/dist/sync/commitFrames.d.ts +2 -4
- package/dist/sync/commitFrames.js +1 -0
- package/dist/sync/createClaimStream.d.ts +1 -1
- package/dist/sync/createClaimStream.js +13 -15
- package/dist/sync/deltaPipeline.d.ts +1 -1
- package/dist/sync/deltaPipeline.js +1 -1
- package/dist/sync/participants.d.ts +2 -3
- package/dist/sync/participants.js +2 -2
- package/dist/sync/wsFrameHandlers.js +35 -15
- package/dist/testing/mocks/MockMutationExecutor.d.ts +21 -0
- package/dist/testing/mocks/MockMutationExecutor.js +53 -1
- package/dist/transactions/TransactionQueue.d.ts +61 -9
- package/dist/transactions/TransactionQueue.js +502 -114
- package/dist/transactions/commitEnvelope.d.ts +4 -0
- package/dist/transactions/commitEnvelope.js +25 -0
- package/dist/transactions/commitPayload.d.ts +23 -5
- package/dist/transactions/commitPayload.js +3 -0
- package/dist/transactions/deltaConfirmation.d.ts +2 -1
- package/dist/transactions/deltaConfirmation.js +22 -4
- package/dist/transactions/durableWriteStore.d.ts +8 -1
- package/dist/transactions/httpCommitEnvelope.d.ts +3 -1
- package/dist/transactions/httpCommitEnvelope.js +30 -0
- package/dist/transactions/replayValidation.d.ts +4 -0
- package/dist/transactions/replayValidation.js +6 -0
- package/dist/types/streams.d.ts +29 -17
- package/dist/wire/commit.d.ts +671 -0
- package/dist/wire/commit.js +300 -0
- package/dist/wire/delta.d.ts +4 -0
- package/dist/wire/delta.js +9 -0
- package/dist/wire/frames.d.ts +17 -45
- package/dist/wire/index.d.ts +2 -0
- package/dist/wire/index.js +4 -0
- package/docs/agent-messaging.md +4 -5
- package/docs/agents.md +1 -2
- package/docs/api-keys.md +1 -2
- package/docs/client-behavior.md +4 -6
- package/docs/coordination.md +64 -9
- package/docs/data-sources.md +4 -7
- package/docs/integration-guide.md +6 -8
- package/docs/migration.md +0 -3
- package/docs/quickstart.md +7 -8
- package/docs/schema-contract.md +7 -8
- package/package.json +4 -4
- package/dist/client/registerDataSource.d.ts +0 -19
- package/dist/client/registerDataSource.js +0 -66
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.30.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **Removed the deprecated `databaseUrl` client option — your database connects to Ablo out of band, not through the client.** Ablo is Stripe-shaped: the client only ever talks to the engine. You construct it with `Ablo({ schema, apiKey })` (add `transport: 'http'` for a stateless server worker), and it reaches Ablo over WebSocket or HTTP — it never opens a Postgres connection itself. How your data connects is a separate step, set up once, in one of two shapes:
|
|
8
|
+
|
|
9
|
+
- **Logical replication — the default.** `ablo connect` registers your database and Ablo tails its write-ahead log. Your rows stay in your database; Ablo reads the change stream and keeps its own transaction log. Nothing in the client holds a connection string.
|
|
10
|
+
- **Signed data-source endpoint — the fallback.** For a database that cannot grant a `REPLICATION` role, your app exposes a signed HTTP endpoint that Ablo calls. Same out-of-band principle: the credentials live with your app, not in the Ablo client.
|
|
11
|
+
|
|
12
|
+
Because the client no longer carries a connection string, `ablo init` no longer scaffolds the `--storage direct` connector — the last surface that emitted `databaseUrl: process.env.DATABASE_URL` into a generated client. Run `ablo init --storage replication` (the default) or `--storage endpoint` instead.
|
|
13
|
+
|
|
14
|
+
- **The claim work label is `description`, not `reason`.** A claim is an advisory coordination lock: you take it on a row before editing so other participants — agents or people — see the row is held and back off. The free-text label that says _what_ you are doing is now `description`, the text a peer reads when it hits a row you hold: `ablo.<model>.claim({ id, description: 'Renaming the task to match the brief' })`. The former `reason` option is removed with no alias, so a call still passing `reason` no longer type-checks — rename it to `description`. This is a different field from the `reason` on a claim _rejection_ or _lost_ event (`'conflict'`, `'expired'`, `'preempted'`), which says _why_ coordination denied or ended a claim and is unchanged.
|
|
15
|
+
|
|
16
|
+
- **Claim a key Ablo hasn't synced: `ablo.<model>.claim(id)`.** Claims now come in two shapes that mirror whether Ablo holds the row. The object form `claim({ id })` loads the row from Ablo's synced pool and returns a `HeldClaim<T>` carrying `.data`. The new string overload `claim(id)` locks a row that lives _only_ in your own database — Ablo has never synced it — so it skips the pool load and the `entity_not_found` throw and returns a `HeldLease`: a `HeldClaim` without `.data`. This is Rung 0 of the connect ladder — advisory coordination with no replication grant, no schema change, and no `organization_id` column, so you can coordinate agents against your database before wiring any sync. `await using` auto-releases either shape at end of scope, and the object form is unchanged, so no existing caller shifts.
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- **`ablo push` defers provisioning instead of failing when the plane's role cannot run DDL.** When the engine role bound to a plane lacks the privilege to create or alter tables, `push` no longer aborts the whole command. It records the schema and defers the provisioning step, so your models still register and the table changes are applied later by a role that can run them — rather than blocking the push on a privilege the plane may never grant the engine directly.
|
|
21
|
+
|
|
22
|
+
- **Schema-drift no longer false-alarms for `selectModels` / `omitModels` projection clients.** A projected client deliberately carries a subset of the full schema, so its schema hash legitimately differs from the unprojected one. The drift check now compares against the projection's own source hash, so a correctly-projected app no longer sees a spurious drift warning at bootstrap.
|
|
23
|
+
|
|
24
|
+
## 0.29.3
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- **`ablo connect` validates from Ablo's network when your machine can't reach the database.** Replication runs from Ablo's infrastructure, not from your laptop, so a database that is IPv6-only (Supabase direct hosts), IP-allowlisted, or behind a VPN can be perfectly replicable even when every local dial fails. When `--check` can't connect at all, it now asks the engine to run the same readiness checklist from its own network instead of reporting a false failure; `--register` no longer blocks on local unreachability and lets the registration preflight decide. A host that _was_ reached and rejected the connection — bad credentials, a TLS error, a Postgres error — still fails locally, because the engine would see exactly the same thing.
|
|
29
|
+
|
|
3
30
|
## 0.29.2
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -250,7 +250,9 @@ await ablo.weatherReports.update({
|
|
|
250
250
|
If someone else holds the row, `claim()` waits in a fair queue, then re-reads —
|
|
251
251
|
so `report` is the current row, never a stale snapshot. Reads stay open by
|
|
252
252
|
default; only acting on the row serializes. The claim releases when the `await
|
|
253
|
-
using` scope exits.
|
|
253
|
+
using` scope exits — on return, and on a throw. That "on throw" is why `await
|
|
254
|
+
using` earns its keep: if the agent call fails before the write, the row frees
|
|
255
|
+
for the next in line and stays exactly as it was, with no cleanup of your own.
|
|
254
256
|
|
|
255
257
|
See who's mid-edit before you act — decide to wait, or skip:
|
|
256
258
|
|
|
@@ -482,7 +484,6 @@ Every other option has correct defaults:
|
|
|
482
484
|
| --- | --- | --- | --- |
|
|
483
485
|
| `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
|
|
484
486
|
| `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
|
|
485
|
-
| `databaseUrl` | `string \| null` | `—` | Deprecated. Connect via `npx ablo connect` instead — see [Connect Your Database](./docs/data-sources.md). |
|
|
486
487
|
|
|
487
488
|
Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
|
|
488
489
|
authenticates with the signed-in user's session; the raw-key path is gated
|
package/dist/BaseSyncedStore.js
CHANGED
|
@@ -1405,7 +1405,9 @@ export class BaseSyncedStore {
|
|
|
1405
1405
|
get highestProcessedSyncId() { return store.highestProcessedSyncId; },
|
|
1406
1406
|
get lastAckedId() { return store.lastAckedId; },
|
|
1407
1407
|
// SyncClient position/transaction bookkeeping.
|
|
1408
|
-
onDeltaReceived: (syncId) => {
|
|
1408
|
+
onDeltaReceived: (syncId, transactionId, correlationId) => {
|
|
1409
|
+
this.syncClient.onDeltaReceived(syncId, transactionId, correlationId);
|
|
1410
|
+
},
|
|
1409
1411
|
advanceApplied: (syncId) => { this.syncClient.position.advanceApplied(syncId); },
|
|
1410
1412
|
advancePersisted: (syncId) => { this.syncClient.position.advancePersisted(syncId); },
|
|
1411
1413
|
// Persistence + pool writes.
|
package/dist/Database.js
CHANGED
|
@@ -34,6 +34,11 @@ function isSameOutboxRecord(existing, candidate) {
|
|
|
34
34
|
request: record.request,
|
|
35
35
|
scopeNamespace: record.scopeNamespace,
|
|
36
36
|
});
|
|
37
|
+
if (existing.correlationId !== undefined &&
|
|
38
|
+
candidate.correlationId !== undefined &&
|
|
39
|
+
existing.correlationId !== candidate.correlationId) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
37
42
|
return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
|
|
38
43
|
}
|
|
39
44
|
if (existing.type === 'commit_envelope' &&
|
|
@@ -49,10 +54,23 @@ function isSameOutboxRecord(existing, candidate) {
|
|
|
49
54
|
commitOptions: record.commitOptions,
|
|
50
55
|
scope: record.scope,
|
|
51
56
|
});
|
|
57
|
+
if (existing.correlationId !== undefined &&
|
|
58
|
+
candidate.correlationId !== undefined &&
|
|
59
|
+
existing.correlationId !== candidate.correlationId) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
52
62
|
return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
|
|
53
63
|
}
|
|
54
64
|
return JSON.stringify(existing) === JSON.stringify(candidate);
|
|
55
65
|
}
|
|
66
|
+
function isAcceptedOutboxPromotion(existing, candidate) {
|
|
67
|
+
return (existing !== undefined &&
|
|
68
|
+
(existing.type === 'commit_envelope' ||
|
|
69
|
+
existing.type === 'http_commit_envelope') &&
|
|
70
|
+
existing.type === candidate.type &&
|
|
71
|
+
existing.acceptedAt === undefined &&
|
|
72
|
+
candidate.acceptedAt !== undefined);
|
|
73
|
+
}
|
|
56
74
|
export class Database {
|
|
57
75
|
// Core database components
|
|
58
76
|
databaseManager;
|
|
@@ -1266,6 +1284,9 @@ export class Database {
|
|
|
1266
1284
|
code: 'idempotency_conflict',
|
|
1267
1285
|
});
|
|
1268
1286
|
}
|
|
1287
|
+
if (isAcceptedOutboxPromotion(existing, record)) {
|
|
1288
|
+
await store.put(record);
|
|
1289
|
+
}
|
|
1269
1290
|
if (!existing) {
|
|
1270
1291
|
const sources = await Promise.all(consumedRecordIds.map((id) => store.get(id)));
|
|
1271
1292
|
if (sources.some((source) => source === undefined)) {
|
|
@@ -1321,8 +1342,12 @@ export class Database {
|
|
|
1321
1342
|
tx.abort();
|
|
1322
1343
|
return;
|
|
1323
1344
|
}
|
|
1324
|
-
if (!existing)
|
|
1345
|
+
if (!existing) {
|
|
1325
1346
|
store.add(record);
|
|
1347
|
+
}
|
|
1348
|
+
else if (isAcceptedOutboxPromotion(existing, record)) {
|
|
1349
|
+
store.put(record);
|
|
1350
|
+
}
|
|
1326
1351
|
for (const id of sourceIds)
|
|
1327
1352
|
store.delete(id);
|
|
1328
1353
|
};
|
|
@@ -1370,7 +1395,9 @@ export class Database {
|
|
|
1370
1395
|
// idempotency retention may already have elapsed, so silently deleting or
|
|
1371
1396
|
// blindly replaying an old envelope would both be unsafe. Restoration
|
|
1372
1397
|
// owns quarantine/reconciliation for these records.
|
|
1373
|
-
if (tx.type === 'commit_envelope' ||
|
|
1398
|
+
if (tx.type === 'commit_envelope' ||
|
|
1399
|
+
tx.type === 'http_commit_envelope' ||
|
|
1400
|
+
tx.type === 'pending_mutation') {
|
|
1374
1401
|
continue;
|
|
1375
1402
|
}
|
|
1376
1403
|
if (typeof tx.timestamp === 'number' && tx.timestamp < cutoff) {
|
package/dist/SyncClient.d.ts
CHANGED
|
@@ -348,11 +348,13 @@ export declare class SyncClient extends EventEmitter {
|
|
|
348
348
|
dispose(): void;
|
|
349
349
|
/**
|
|
350
350
|
* Notify the {@link TransactionQueue} of an incoming delta so it can confirm
|
|
351
|
-
*
|
|
352
|
-
*
|
|
351
|
+
* hosted writes by sync-id threshold and queued forwards by their echoed
|
|
352
|
+
* source-batch correlation id.
|
|
353
353
|
* @param syncId - The sync id of the received delta.
|
|
354
|
+
* @param transactionId - Optional server echo of the originating local write.
|
|
355
|
+
* @param correlationId - Opaque batch identity decoded from a source WAL echo.
|
|
354
356
|
*/
|
|
355
|
-
onDeltaReceived(syncId: number): void;
|
|
357
|
+
onDeltaReceived(syncId: number, transactionId?: string, correlationId?: string): void;
|
|
356
358
|
/**
|
|
357
359
|
* Cancel pending transactions for child entities orphaned by a parent's
|
|
358
360
|
* deletion. The store calls this when a delete delta arrives for a parent,
|
package/dist/SyncClient.js
CHANGED
|
@@ -1664,17 +1664,21 @@ export class SyncClient extends EventEmitter {
|
|
|
1664
1664
|
}
|
|
1665
1665
|
/**
|
|
1666
1666
|
* Notify the {@link TransactionQueue} of an incoming delta so it can confirm
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1667
|
+
* hosted writes by sync-id threshold and queued forwards by their echoed
|
|
1668
|
+
* source-batch correlation id.
|
|
1669
1669
|
* @param syncId - The sync id of the received delta.
|
|
1670
|
+
* @param transactionId - Optional server echo of the originating local write.
|
|
1671
|
+
* @param correlationId - Opaque batch identity decoded from a source WAL echo.
|
|
1670
1672
|
*/
|
|
1671
|
-
onDeltaReceived(syncId) {
|
|
1673
|
+
onDeltaReceived(syncId, transactionId, correlationId) {
|
|
1672
1674
|
try {
|
|
1673
|
-
this.transactionQueue.onDeltaReceived(syncId);
|
|
1675
|
+
this.transactionQueue.onDeltaReceived(syncId, transactionId, correlationId);
|
|
1674
1676
|
}
|
|
1675
1677
|
catch (e) {
|
|
1676
1678
|
getContext().observability.breadcrumb('Failed to notify delta received', 'sync.transaction', 'warning', {
|
|
1677
1679
|
syncId,
|
|
1680
|
+
transactionId,
|
|
1681
|
+
correlationId,
|
|
1678
1682
|
});
|
|
1679
1683
|
}
|
|
1680
1684
|
}
|
|
@@ -96,7 +96,6 @@ export interface CoordinatedToolOptions<TInput, T> {
|
|
|
96
96
|
strategy?: CoordinationStrategy;
|
|
97
97
|
/** Human-readable coordination metadata attached to the claim, used by the `'claim'` and `'queue'` strategies. */
|
|
98
98
|
claim?: {
|
|
99
|
-
reason?: string;
|
|
100
99
|
description?: string;
|
|
101
100
|
};
|
|
102
101
|
/** How many reconcile rounds `'merge'` may take before it gives up with `AbloContentionError`. */
|
|
@@ -79,20 +79,16 @@ function formatCoordinationNote(claims, target) {
|
|
|
79
79
|
const entityLabel = target.type.toLowerCase();
|
|
80
80
|
const c = claims.length === 1 ? claims[0] : undefined;
|
|
81
81
|
if (c) {
|
|
82
|
-
const details = c.description ? `Declared work: ${c.description}. ` : '';
|
|
83
82
|
return (`<multiplayer_context>\n` +
|
|
84
83
|
`Another participant is currently editing this ${entityLabel}. ` +
|
|
85
|
-
`
|
|
86
|
-
details +
|
|
84
|
+
`Declared work: ${c.description}. ` +
|
|
87
85
|
`Defer to their concurrent changes when reasonable, or note your work as complementary to theirs. ` +
|
|
88
86
|
`Avoid stomping their in-flight edits.\n` +
|
|
89
87
|
`</multiplayer_context>`);
|
|
90
88
|
}
|
|
91
|
-
const actions = Array.from(new Set(claims.map((c) => c.reason))).join(', ');
|
|
92
89
|
const descriptions = Array.from(new Set(claims.map((c) => c.description).filter(Boolean))).join('; ');
|
|
93
90
|
return (`<multiplayer_context>\n` +
|
|
94
91
|
`${claims.length} other participants are currently editing this ${entityLabel}. ` +
|
|
95
|
-
`Active actions: ${actions}. ` +
|
|
96
92
|
(descriptions ? `Declared work: ${descriptions}. ` : '') +
|
|
97
93
|
`Coordinate with their in-flight work — defer where reasonable, ` +
|
|
98
94
|
`or describe your work as complementary.\n` +
|