@abloatai/ablo 0.9.3 → 0.9.5
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 +12 -0
- package/README.md +8 -6
- package/dist/BaseSyncedStore.d.ts +5 -2
- package/dist/BaseSyncedStore.js +15 -14
- package/dist/Database.js +7 -1
- package/dist/SyncClient.d.ts +8 -0
- package/dist/SyncClient.js +9 -0
- package/dist/cli.cjs +9474 -29723
- package/dist/client/Ablo.js +8 -1
- package/dist/client/auth.d.ts +1 -1
- package/dist/client/auth.js +14 -5
- package/dist/errorCodes.d.ts +1 -0
- package/dist/errorCodes.js +1 -0
- package/dist/schema/ddl.js +10 -2
- package/dist/schema/model.d.ts +9 -7
- package/dist/schema/model.js +1 -1
- package/dist/schema/schema.js +7 -1
- package/dist/sync/syncPosition.d.ts +78 -0
- package/dist/sync/syncPosition.js +111 -0
- package/dist/transactions/TransactionQueue.d.ts +16 -1
- package/dist/transactions/TransactionQueue.js +43 -25
- package/docs/api-keys.md +4 -4
- package/docs/cli.md +6 -6
- package/docs/quickstart.md +21 -2
- package/llms-full.txt +19 -5
- package/llms.txt +5 -3
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Scoped-role automation + tenant-routing fix. `ablo migrate` now auto-creates the RLS-gated scoped role (zero SQL) with a log-safe SCRAM-SHA-256 password verifier, plus a Neon/Supabase scoped-role `databaseUrl` recipe. Fix a jsonb double-encode that corrupted per-tenant routing and silently fell back to the shared pool.
|
|
8
|
+
|
|
9
|
+
## 0.9.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Sync-position correctness + CLI hardening. Consolidate five scattered sync cursors into one typed `syncPosition` (persisted/applied/acked with a derived `readFloor`), fixing a claim taken right after an ack-confirmed write reading stale against that write's own delta. Add transaction ack-confirmation, schema DDL-first-push, and a reworked CLI (config/dev/login/mode/drizzle-pull).
|
|
14
|
+
|
|
3
15
|
## 0.9.3
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ npm install @abloatai/ablo
|
|
|
53
53
|
npx ablo login # opens the browser: sign in (or sign up) → a sk_test_ key is saved locally
|
|
54
54
|
npx ablo init # scaffolds ablo/schema.ts (offers to log in if you skipped it)
|
|
55
55
|
npx ablo migrate # creates the synced tables in YOUR Postgres (reads DATABASE_URL)
|
|
56
|
-
npx ablo dev # pushes your schema (
|
|
56
|
+
npx ablo dev # pushes your schema (sandbox), writes ABLO_API_KEY to .env.local, watches for changes
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
After `ablo dev`, the [Quick Start](#quick-start) below runs as-is —
|
|
@@ -356,20 +356,22 @@ Same product, same truth either way: your database is the system of record. See
|
|
|
356
356
|
|
|
357
357
|
## Configuration
|
|
358
358
|
|
|
359
|
-
`Ablo({ ... })` takes
|
|
359
|
+
`Ablo({ ... })` takes three things: your schema, your key, and your database —
|
|
360
|
+
the last either as `databaseUrl` here or as a signed
|
|
361
|
+
[Data Source endpoint](./docs/data-sources.md) in your app. Every other option
|
|
362
|
+
has correct defaults:
|
|
360
363
|
|
|
361
364
|
| Option | Type | Default | Purpose |
|
|
362
365
|
| --- | --- | --- | --- |
|
|
363
366
|
| `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
|
|
364
367
|
| `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
|
|
365
368
|
| `databaseUrl` | `string \| null` | `process.env.DATABASE_URL` | Your Postgres, registered as the data plane. Server runtimes only — the SDK throws if it sees this in a browser. Omit it when your app exposes a signed [Data Source endpoint](./docs/data-sources.md) instead. |
|
|
366
|
-
| `baseURL` | `string` | `wss://api.abloatai.com` | Point at a self-hosted or private API |
|
|
367
369
|
|
|
368
370
|
Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
|
|
369
371
|
authenticates with the signed-in user's session; the raw-key path is gated
|
|
370
|
-
behind `dangerouslyAllowBrowser` for server-proxy setups only.
|
|
371
|
-
|
|
372
|
-
|
|
372
|
+
behind `dangerouslyAllowBrowser` for server-proxy setups only. Advanced hooks
|
|
373
|
+
(custom `fetch`, logging, observability, transport overrides) live in
|
|
374
|
+
[Client Behavior](./docs/client-behavior.md).
|
|
373
375
|
|
|
374
376
|
## Errors
|
|
375
377
|
|
|
@@ -272,8 +272,11 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
272
272
|
protected pendingDeltas: SyncDelta[];
|
|
273
273
|
protected batchTimer: ReturnType<typeof setTimeout> | null;
|
|
274
274
|
protected syncPromise: Promise<void> | null;
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
/** Resume/ack cursor — delegates to the shared SyncPosition (see
|
|
276
|
+
* sync/syncPosition.ts). Advances only after IDB persistence. */
|
|
277
|
+
protected get lastAckedId(): number;
|
|
278
|
+
/** Pool-applied cursor — delegates to the shared SyncPosition. */
|
|
279
|
+
protected get highestProcessedSyncId(): number;
|
|
277
280
|
protected bootstrapDeltaQueue: SyncDelta[] | null;
|
|
278
281
|
protected activeBootstrapCount: number;
|
|
279
282
|
protected pendingDeletes: Set<string>;
|
package/dist/BaseSyncedStore.js
CHANGED
|
@@ -181,8 +181,15 @@ export class BaseSyncedStore {
|
|
|
181
181
|
pendingDeltas = [];
|
|
182
182
|
batchTimer = null;
|
|
183
183
|
syncPromise = null;
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
/** Resume/ack cursor — delegates to the shared SyncPosition (see
|
|
185
|
+
* sync/syncPosition.ts). Advances only after IDB persistence. */
|
|
186
|
+
get lastAckedId() {
|
|
187
|
+
return this.syncClient.position.persisted;
|
|
188
|
+
}
|
|
189
|
+
/** Pool-applied cursor — delegates to the shared SyncPosition. */
|
|
190
|
+
get highestProcessedSyncId() {
|
|
191
|
+
return this.syncClient.position.applied;
|
|
192
|
+
}
|
|
186
193
|
// ── Delta queuing during bootstrap ──
|
|
187
194
|
bootstrapDeltaQueue = null;
|
|
188
195
|
activeBootstrapCount = 0;
|
|
@@ -957,8 +964,7 @@ export class BaseSyncedStore {
|
|
|
957
964
|
}
|
|
958
965
|
// Get sync baseline for WebSocket
|
|
959
966
|
const lastSyncId = (yield this.database.getLastSyncId());
|
|
960
|
-
this.
|
|
961
|
-
this.highestProcessedSyncId = this.lastAckedId;
|
|
967
|
+
this.syncClient.position.advancePersisted(lastSyncId || 0);
|
|
962
968
|
try {
|
|
963
969
|
const versions = (yield this.database.getVersionVector());
|
|
964
970
|
if (versions && typeof versions === 'object')
|
|
@@ -1557,9 +1563,7 @@ export class BaseSyncedStore {
|
|
|
1557
1563
|
return;
|
|
1558
1564
|
}
|
|
1559
1565
|
// Advance watermark
|
|
1560
|
-
|
|
1561
|
-
this.highestProcessedSyncId = delta.id;
|
|
1562
|
-
}
|
|
1566
|
+
this.syncClient.position.advanceApplied(delta.id);
|
|
1563
1567
|
// Sync group added — handle immediately. Supports both legacy
|
|
1564
1568
|
// (addedGroups/removedGroups) and incremental (group/userId) payloads.
|
|
1565
1569
|
if (delta.actionType === 'G') {
|
|
@@ -1699,8 +1703,7 @@ export class BaseSyncedStore {
|
|
|
1699
1703
|
const persistedSyncId = batch.persistedSyncId;
|
|
1700
1704
|
if (persistedSyncId > this.lastAckedId) {
|
|
1701
1705
|
this.syncWebSocket?.acknowledge?.(persistedSyncId);
|
|
1702
|
-
this.
|
|
1703
|
-
this.highestProcessedSyncId = Math.max(this.highestProcessedSyncId, persistedSyncId);
|
|
1706
|
+
this.syncClient.position.advancePersisted(persistedSyncId);
|
|
1704
1707
|
}
|
|
1705
1708
|
// Cache invalidation is automatic via SyncClient 'models:changed' event
|
|
1706
1709
|
this.pendingDeltas = [];
|
|
@@ -1905,11 +1908,9 @@ export class BaseSyncedStore {
|
|
|
1905
1908
|
}
|
|
1906
1909
|
// Delegate pool writes to SyncClient (auto-invalidates cache via 'models:changed' event)
|
|
1907
1910
|
this.syncClient.applyDeltaBatchToPool([dbResult], (name, data) => this.enrichRelations(name, data));
|
|
1908
|
-
//
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
this.highestProcessedSyncId = Math.max(this.highestProcessedSyncId, delta.id);
|
|
1912
|
-
}
|
|
1911
|
+
// This path runs after the delta was written to IDB — advance both
|
|
1912
|
+
// cursors through the shared position.
|
|
1913
|
+
this.syncClient.position.advancePersisted(delta.id);
|
|
1913
1914
|
}
|
|
1914
1915
|
/** Handle bootstrap_required event */
|
|
1915
1916
|
handleBootstrapRequired(_hint) {
|
package/dist/Database.js
CHANGED
|
@@ -8,6 +8,7 @@ import { LoadStrategy } from './types/index.js';
|
|
|
8
8
|
import { getContext } from './context.js';
|
|
9
9
|
import { AbloConnectionError, AbloValidationError } from './errors.js';
|
|
10
10
|
import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
|
|
11
|
+
import { syncPositionSchema } from './sync/syncPosition.js';
|
|
11
12
|
export class Database {
|
|
12
13
|
// Core database components
|
|
13
14
|
databaseManager;
|
|
@@ -252,7 +253,12 @@ export class Database {
|
|
|
252
253
|
const instantModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.instant);
|
|
253
254
|
const lazyModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.lazy);
|
|
254
255
|
const modelsToLoad = [...instantModels, ...lazyModels];
|
|
255
|
-
|
|
256
|
+
// Gate the PERSISTED cursor through the sync-position schema field —
|
|
257
|
+
// the one trust boundary for resume state. IDB can hand back anything
|
|
258
|
+
// (a corrupted negative/float cursor would previously pass `|| 0`,
|
|
259
|
+
// which only catches falsy, and get sent to the server as the resume
|
|
260
|
+
// point). Invalid → 0 → full bootstrap, the safe degradation.
|
|
261
|
+
const metadataLastSyncId = syncPositionSchema.shape.persisted.safeParse(metadata?.lastSyncId).data ?? 0;
|
|
256
262
|
const dataAge = metadata?.updatedAt ? Date.now() - metadata.updatedAt.getTime() : Infinity;
|
|
257
263
|
// ── Zero-style cache-validity check ──────────────────────────
|
|
258
264
|
//
|
package/dist/SyncClient.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { TransactionQueue } from './transactions/TransactionQueue.js';
|
|
|
14
14
|
import { type OptimisticEchoMetrics } from './transactions/OptimisticEchoTracker.js';
|
|
15
15
|
import type { Database } from './Database.js';
|
|
16
16
|
import type { WriteOptions } from './interfaces/index.js';
|
|
17
|
+
import { SyncPosition } from './sync/syncPosition.js';
|
|
17
18
|
interface SyncObserver {
|
|
18
19
|
onSync?: (event: SyncEvent) => void;
|
|
19
20
|
}
|
|
@@ -68,6 +69,13 @@ export declare class SyncClient extends EventEmitter {
|
|
|
68
69
|
private offlineSince?;
|
|
69
70
|
private maxRetries;
|
|
70
71
|
private isDisposed;
|
|
72
|
+
/**
|
|
73
|
+
* THE client's place in the global delta order — the one canonical
|
|
74
|
+
* instance (see `sync/syncPosition.ts`). The store advances
|
|
75
|
+
* `applied`/`persisted` as deltas land; the queue advances `acked` on
|
|
76
|
+
* commit responses; snapshots/claims read `readFloor`.
|
|
77
|
+
*/
|
|
78
|
+
readonly position: SyncPosition;
|
|
71
79
|
constructor(objectPool: ObjectPool, database: Database);
|
|
72
80
|
/**
|
|
73
81
|
* Setup network monitoring handlers
|
package/dist/SyncClient.js
CHANGED
|
@@ -16,6 +16,7 @@ import { EventEmitter } from 'events';
|
|
|
16
16
|
import { NetworkMonitor } from './NetworkMonitor.js';
|
|
17
17
|
import { TransactionQueue } from './transactions/TransactionQueue.js';
|
|
18
18
|
import { OptimisticEchoTracker, } from './transactions/OptimisticEchoTracker.js';
|
|
19
|
+
import { SyncPosition } from './sync/syncPosition.js';
|
|
19
20
|
export class SyncClient extends EventEmitter {
|
|
20
21
|
objectPool;
|
|
21
22
|
database;
|
|
@@ -50,6 +51,13 @@ export class SyncClient extends EventEmitter {
|
|
|
50
51
|
// Configuration
|
|
51
52
|
maxRetries = 3;
|
|
52
53
|
isDisposed = false;
|
|
54
|
+
/**
|
|
55
|
+
* THE client's place in the global delta order — the one canonical
|
|
56
|
+
* instance (see `sync/syncPosition.ts`). The store advances
|
|
57
|
+
* `applied`/`persisted` as deltas land; the queue advances `acked` on
|
|
58
|
+
* commit responses; snapshots/claims read `readFloor`.
|
|
59
|
+
*/
|
|
60
|
+
position = new SyncPosition();
|
|
53
61
|
constructor(objectPool, database) {
|
|
54
62
|
super();
|
|
55
63
|
this.objectPool = objectPool;
|
|
@@ -57,6 +65,7 @@ export class SyncClient extends EventEmitter {
|
|
|
57
65
|
this.networkMonitor = new NetworkMonitor();
|
|
58
66
|
// Initialize TransactionQueue with proper configuration
|
|
59
67
|
this.transactionQueue = new TransactionQueue({
|
|
68
|
+
position: this.position,
|
|
60
69
|
maxBatchSize: 50, // Increased from 10 to reduce batch count for large operations
|
|
61
70
|
// Lower delay for snappier dev UX; batching still happens via coalescing
|
|
62
71
|
batchDelay: 150,
|