@abloatai/ablo 0.29.3 → 0.30.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.
Files changed (118) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +3 -2
  3. package/dist/BaseSyncedStore.js +3 -1
  4. package/dist/Database.js +29 -2
  5. package/dist/SyncClient.d.ts +5 -3
  6. package/dist/SyncClient.js +8 -4
  7. package/dist/ai-sdk/coordinatedTool.d.ts +0 -1
  8. package/dist/ai-sdk/coordinatedTool.js +0 -1
  9. package/dist/ai-sdk/coordinationContext.js +1 -5
  10. package/dist/cli.cjs +377 -84
  11. package/dist/client/Ablo.d.ts +1 -1
  12. package/dist/client/Ablo.js +24 -36
  13. package/dist/client/auth.d.ts +8 -18
  14. package/dist/client/auth.js +16 -96
  15. package/dist/client/createModelProxy.d.ts +16 -11
  16. package/dist/client/createModelProxy.js +162 -20
  17. package/dist/client/httpClient.d.ts +1 -1
  18. package/dist/client/httpTransport.js +263 -89
  19. package/dist/client/options.d.ts +0 -15
  20. package/dist/client/resourceTypes.d.ts +13 -26
  21. package/dist/client/schemaConfig.js +5 -0
  22. package/dist/client/writeOptionsSchema.d.ts +1 -0
  23. package/dist/client/writeOptionsSchema.js +11 -2
  24. package/dist/client/wsMutationExecutor.d.ts +3 -5
  25. package/dist/coordination/schema.d.ts +17 -13
  26. package/dist/coordination/schema.js +36 -10
  27. package/dist/errorCodes.d.ts +9 -4
  28. package/dist/errorCodes.js +10 -5
  29. package/dist/errors.js +4 -9
  30. package/dist/index.d.ts +4 -0
  31. package/dist/interfaces/index.d.ts +31 -19
  32. package/dist/schema/index.d.ts +1 -1
  33. package/dist/schema/index.js +4 -1
  34. package/dist/schema/schema.d.ts +13 -0
  35. package/dist/schema/select.js +7 -0
  36. package/dist/schema/syncDeltaRow.d.ts +31 -13
  37. package/dist/schema/syncDeltaRow.js +33 -15
  38. package/dist/schema/tenancy.d.ts +52 -16
  39. package/dist/schema/tenancy.js +86 -25
  40. package/dist/server/commit.d.ts +8 -24
  41. package/dist/server/index.d.ts +1 -0
  42. package/dist/server/index.js +1 -0
  43. package/dist/server/readConfig.d.ts +10 -0
  44. package/dist/source/adapter.d.ts +26 -15
  45. package/dist/source/adapter.js +5 -3
  46. package/dist/source/adapters/drizzle.js +35 -9
  47. package/dist/source/adapters/kysely.d.ts +33 -61
  48. package/dist/source/adapters/kysely.js +170 -177
  49. package/dist/source/adapters/kyselyMutationCore.d.ts +76 -0
  50. package/dist/source/adapters/kyselyMutationCore.js +125 -0
  51. package/dist/source/adapters/memory.d.ts +1 -1
  52. package/dist/source/adapters/memory.js +24 -10
  53. package/dist/source/adapters/prisma.js +35 -8
  54. package/dist/source/conformance.d.ts +26 -28
  55. package/dist/source/conformance.js +142 -69
  56. package/dist/source/contract.d.ts +46 -4
  57. package/dist/source/contract.js +66 -5
  58. package/dist/source/factory.d.ts +3 -3
  59. package/dist/source/factory.js +24 -3
  60. package/dist/source/idempotency.d.ts +46 -0
  61. package/dist/source/idempotency.js +129 -0
  62. package/dist/source/index.d.ts +6 -4
  63. package/dist/source/index.js +5 -3
  64. package/dist/source/migrations.d.ts +9 -5
  65. package/dist/source/migrations.js +39 -6
  66. package/dist/source/types.d.ts +88 -14
  67. package/dist/source/types.js +16 -0
  68. package/dist/surface.d.ts +1 -1
  69. package/dist/surface.js +0 -1
  70. package/dist/sync/BootstrapFetcher.js +7 -0
  71. package/dist/sync/SyncWebSocket.d.ts +5 -27
  72. package/dist/sync/awaitClaimGrant.d.ts +6 -0
  73. package/dist/sync/awaitClaimGrant.js +12 -2
  74. package/dist/sync/commitFrames.d.ts +2 -4
  75. package/dist/sync/commitFrames.js +1 -0
  76. package/dist/sync/createClaimStream.d.ts +1 -1
  77. package/dist/sync/createClaimStream.js +13 -15
  78. package/dist/sync/deltaPipeline.d.ts +1 -1
  79. package/dist/sync/deltaPipeline.js +1 -1
  80. package/dist/sync/participants.d.ts +2 -3
  81. package/dist/sync/participants.js +2 -2
  82. package/dist/sync/wsFrameHandlers.js +35 -15
  83. package/dist/testing/mocks/MockMutationExecutor.d.ts +21 -0
  84. package/dist/testing/mocks/MockMutationExecutor.js +53 -1
  85. package/dist/transactions/TransactionQueue.d.ts +61 -9
  86. package/dist/transactions/TransactionQueue.js +502 -114
  87. package/dist/transactions/commitEnvelope.d.ts +4 -0
  88. package/dist/transactions/commitEnvelope.js +25 -0
  89. package/dist/transactions/commitPayload.d.ts +23 -5
  90. package/dist/transactions/commitPayload.js +3 -0
  91. package/dist/transactions/deltaConfirmation.d.ts +2 -1
  92. package/dist/transactions/deltaConfirmation.js +22 -4
  93. package/dist/transactions/durableWriteStore.d.ts +8 -1
  94. package/dist/transactions/httpCommitEnvelope.d.ts +3 -1
  95. package/dist/transactions/httpCommitEnvelope.js +30 -0
  96. package/dist/transactions/replayValidation.d.ts +4 -0
  97. package/dist/transactions/replayValidation.js +6 -0
  98. package/dist/types/streams.d.ts +29 -17
  99. package/dist/wire/commit.d.ts +671 -0
  100. package/dist/wire/commit.js +300 -0
  101. package/dist/wire/delta.d.ts +4 -0
  102. package/dist/wire/delta.js +9 -0
  103. package/dist/wire/frames.d.ts +17 -45
  104. package/dist/wire/index.d.ts +2 -0
  105. package/dist/wire/index.js +4 -0
  106. package/docs/agent-messaging.md +4 -5
  107. package/docs/agents.md +1 -2
  108. package/docs/api-keys.md +1 -2
  109. package/docs/client-behavior.md +4 -6
  110. package/docs/coordination.md +64 -9
  111. package/docs/data-sources.md +4 -7
  112. package/docs/integration-guide.md +6 -8
  113. package/docs/migration.md +0 -3
  114. package/docs/quickstart.md +7 -8
  115. package/docs/schema-contract.md +7 -8
  116. package/package.json +4 -4
  117. package/dist/client/registerDataSource.d.ts +0 -19
  118. package/dist/client/registerDataSource.js +0 -66
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.30.1
4
+
5
+ ### Patch Changes
6
+
7
+ - **Writes land in your own database on a connected source.** Once you register a database with `ablo connect`, the models you already read from its replication stream become writable: `ablo.<model>.create`, `.update`, and `.delete` route through Ablo's commit chokepoint and into your database, with no connection string in the client. Coordination travels with the write — a claim you hold is checked at the chokepoint before the change is applied, so agents and people serialize on a row whether they reach it by reading from Ablo or by writing back to your own database.
8
+
9
+ A write resolves as soon as it is durably accepted, which the receipt reports as `queued`. Ask for the authoritative outcome with `wait: 'confirmed'` — Ablo watches the change actually land in your database (the write-ahead-log echo for a replicated source, or the event feed for a signed data-source endpoint) and promotes the receipt to `confirmed` before the call returns. The client call is identical across both connection shapes; only the feed Ablo watches for the confirmation differs.
10
+
11
+ - **A clear readiness error when a model's table isn't provisioned yet.** Registering a schema with `ablo push` records your models, but a plane's physical tables are created separately, out of band — so a model can exist in the registered schema before its table does. Reading or writing such a model now returns a typed `model_not_provisioned` error (HTTP 409) that names the gap and points you at provisioning the plane's tables, rather than surfacing as an opaque failure.
12
+
13
+ ## 0.30.0
14
+
15
+ ### Minor Changes
16
+
17
+ - **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:
18
+
19
+ - **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.
20
+ - **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.
21
+
22
+ 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.
23
+
24
+ - **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.
25
+
26
+ - **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.
27
+
28
+ ### Patch Changes
29
+
30
+ - **`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.
31
+
32
+ - **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.
33
+
3
34
  ## 0.29.3
4
35
 
5
36
  ### 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
@@ -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) => { this.syncClient.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' || tx.type === 'pending_mutation') {
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) {
@@ -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
- * transactions by sync-id threshold. A transaction is confirmed once any
352
- * delta with an id at or beyond its `lastSyncId` threshold arrives.
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,
@@ -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
- * transactions by sync-id threshold. A transaction is confirmed once any
1668
- * delta with an id at or beyond its `lastSyncId` threshold arrives.
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`. */
@@ -84,7 +84,6 @@ export function coordinatedTool(model, options) {
84
84
  const claim = await model.claim({
85
85
  id,
86
86
  queue: false,
87
- reason: options.claim?.reason,
88
87
  description: options.claim?.description,
89
88
  });
90
89
  try {
@@ -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
- `Action declared: ${c.reason}. ` +
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` +