@abloatai/ablo 0.16.3 → 0.18.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 +67 -0
- package/dist/BaseSyncedStore.d.ts +8 -5
- package/dist/BaseSyncedStore.js +65 -18
- package/dist/NetworkMonitor.js +3 -1
- package/dist/cli.cjs +572 -256
- package/dist/client/Ablo.d.ts +33 -1
- package/dist/client/Ablo.js +42 -12
- package/dist/client/auth.js +1 -1
- package/dist/core/StoreManager.js +3 -1
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/policy/types.d.ts +24 -9
- package/dist/policy/types.js +32 -11
- package/dist/schema/diff.d.ts +14 -0
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +2 -0
- package/dist/sync/SyncWebSocket.js +1 -1
- package/dist/sync/awaitClaimGrant.js +9 -2
- package/dist/sync/createClaimStream.js +32 -0
- package/dist/wire/errorEnvelope.d.ts +13 -0
- package/docs/data-sources.md +164 -287
- package/docs/quickstart.md +51 -63
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **Client observability — `debug` / `logLevel`, off by default.** The SDK used to
|
|
8
|
+
emit a `debug` line per model and per property during schema registration (a
|
|
9
|
+
firehose). It now defaults to a quiet `warn` threshold and exposes two new
|
|
10
|
+
`Ablo()` options to opt back in:
|
|
11
|
+
- `logLevel: 'debug' | 'info' | 'warn' | 'error' | 'silent'` — `'info'` surfaces
|
|
12
|
+
coordination and connection events without the per-model registration noise;
|
|
13
|
+
`'debug'` is everything. Precedence: explicit `logLevel` → `debug: true` →
|
|
14
|
+
`ABLO_LOG_LEVEL` env → default `warn`. Supplying your own `logger` bypasses both.
|
|
15
|
+
- `debug: boolean` — shorthand for `logLevel: 'debug'`.
|
|
16
|
+
|
|
17
|
+
Coordination is now traceable at `info`: claims that are **rejected** or **lost**
|
|
18
|
+
(preempted/expired), and your position **advancing in a claim queue**, each log
|
|
19
|
+
once per change with a readable target (`documents:abc.title`) — quiet lowercase
|
|
20
|
+
lines, no shouty tags.
|
|
21
|
+
|
|
22
|
+
**New: canonical wire-egress contract export.** `errorEnvelope`, `statusForType`,
|
|
23
|
+
and the `ErrorEnvelope` type are now exported from the package root. Server
|
|
24
|
+
consumers (e.g. a self-hosted sync server) can assert against the one source of
|
|
25
|
+
truth for the error-envelope shape and the `AbloError`-subclass→HTTP-status
|
|
26
|
+
table instead of keeping a copy that silently drifts.
|
|
27
|
+
|
|
28
|
+
**Structured CLI error rendering.** CLI failures render as a titled block with a
|
|
29
|
+
reason code and per-code remediation (`--verbose` for the stack) instead of a
|
|
30
|
+
console wall-of-text; `AbloError.toString()` produces a leak-proof one-liner.
|
|
31
|
+
|
|
32
|
+
**`ABLO_API_KEY` resolution + sandbox key scopes.** The key is now resolved from
|
|
33
|
+
`.env.local` / `.env` (not just the process env), and sandbox keys are granted
|
|
34
|
+
`schema:push` by default so `ablo push` works out of the box in a fresh sandbox.
|
|
35
|
+
|
|
36
|
+
## 0.17.0
|
|
37
|
+
|
|
38
|
+
### Minor Changes
|
|
39
|
+
|
|
40
|
+
- **Bring-your-own database is now one model.** Ablo connects to your Postgres and
|
|
41
|
+
never operates it. There used to be two confusing BYO paths, and the
|
|
42
|
+
connection-string one would create roles, force row-level security, transfer
|
|
43
|
+
table ownership, and push you to run `ablo migrate` before anything worked. That
|
|
44
|
+
cascade is gone. Ablo now follows the shape every serious "sync over your own
|
|
45
|
+
Postgres" engine uses (ElectricSQL, PowerSync, Zero): it reads your database via
|
|
46
|
+
Postgres logical replication and never runs DDL, creates roles, forces RLS, or
|
|
47
|
+
rewrites your `DATABASE_URL`. You own your schema; Ablo reads it.
|
|
48
|
+
- **New: `ablo connect`.** One command prints the exact, copy-pasteable setup for
|
|
49
|
+
your own Postgres — enable `wal_level=logical`, create the `ablo_publication`
|
|
50
|
+
publication and a least-privilege `ablo_replicator` role — and
|
|
51
|
+
`ablo connect --check` validates readiness (wal level, publication, replication
|
|
52
|
+
grant, replica identity). This is the single supported way to connect a real
|
|
53
|
+
database.
|
|
54
|
+
- **`ablo migrate` left the happy path.** It no longer creates roles, transfers
|
|
55
|
+
ownership, or rewrites your connection string, and `ablo dev` no longer attempts
|
|
56
|
+
a scoped-role creation on every watch loop. `migrate` is now an optional escape
|
|
57
|
+
hatch for generating starter DDL (`--dry-run` prints the SQL).
|
|
58
|
+
- **Clearer failures.** `ablo push` permission errors lead with the server's actual
|
|
59
|
+
reason code and per-code remediation instead of a generic "needs `schema:push`
|
|
60
|
+
scope," and the schema-conflict message names which environment/version a prior
|
|
61
|
+
push came from and when.
|
|
62
|
+
- **Logical-replication runtime is in Preview.** The setup (`ablo connect`) and the
|
|
63
|
+
connection model are live; the server-side WAL consumer that streams your changes
|
|
64
|
+
is implemented and journey-tested but not yet generally available.
|
|
65
|
+
|
|
66
|
+
The previous connection-string-operate and adapter/outbox modes are demoted to a
|
|
67
|
+
clearly-labeled **Legacy / not recommended** section — they still work, but new
|
|
68
|
+
integrations should use logical replication.
|
|
69
|
+
|
|
3
70
|
## 0.16.3
|
|
4
71
|
|
|
5
72
|
### Patch Changes
|
|
@@ -505,10 +505,11 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
505
505
|
* 1. REACTIVE — register `getToken` as the re-mint hook the FSM calls when a
|
|
506
506
|
* probe finds the key stale (`credential_stale`) or on a nudge.
|
|
507
507
|
* 2. PROACTIVE — keep the short-lived key fresh ahead of trouble: a refresh
|
|
508
|
-
* timer inside the TTL, plus re-mint on OS wake
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
508
|
+
* timer inside the TTL, plus re-mint on OS wake. The ENTIRE proactive
|
|
509
|
+
* block is browser-gated (`typeof window`): server/SSR has no socket to
|
|
510
|
+
* keep warm and the resolver is browser-oriented, so arming it in Node
|
|
511
|
+
* would fire a relative-URL fetch and throw. (Agents pass a static
|
|
512
|
+
* `apiKey` with no resolver, so this method is never called for them.)
|
|
512
513
|
*
|
|
513
514
|
* Config-driven and invisible, like Supabase's `autoRefreshToken` — consumers
|
|
514
515
|
* never call a refresh method. Idempotent (a second call replaces the first);
|
|
@@ -702,7 +703,9 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
702
703
|
* sync-group mutations). Does NOT schedule a flush — callers decide
|
|
703
704
|
* whether to debounce (live) or flush atomically (catch-up frame).
|
|
704
705
|
*/
|
|
705
|
-
protected enqueueDelta(delta: SyncDelta
|
|
706
|
+
protected enqueueDelta(delta: SyncDelta, options?: {
|
|
707
|
+
authoritative?: boolean;
|
|
708
|
+
}): boolean;
|
|
706
709
|
/** Debounce a flush for live single-delta traffic. */
|
|
707
710
|
protected scheduleDeltaFlush(): void;
|
|
708
711
|
/**
|
package/dist/BaseSyncedStore.js
CHANGED
|
@@ -739,9 +739,21 @@ export class BaseSyncedStore {
|
|
|
739
739
|
// A throw = transient (offline / mint endpoint unreachable / 5xx). The
|
|
740
740
|
// login may be perfectly valid; never sign out for this — back off and
|
|
741
741
|
// retry. Mirrors the `getToken` throw-vs-null contract end-to-end.
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
742
|
+
const message = error?.message ?? String(error);
|
|
743
|
+
// A relative-URL resolver invoked server-side (Node fetch has no origin
|
|
744
|
+
// to resolve against) emits the opaque "Failed to parse URL" / "Only
|
|
745
|
+
// absolute URLs are supported". Translate it into something actionable
|
|
746
|
+
// instead of a mystery transient blip — the proactive refresh is now
|
|
747
|
+
// browser-only, so hitting this means the resolver fired from SSR/RSC or
|
|
748
|
+
// a server route.
|
|
749
|
+
if (typeof window === 'undefined' && /parse URL|absolute URLs?/i.test(message)) {
|
|
750
|
+
getContext().logger.warn('credential resolver ran on the server with a relative URL — Node fetch needs an absolute URL. ' +
|
|
751
|
+
'Refresh the Ablo client in the browser, or build an absolute URL server-side ' +
|
|
752
|
+
"(e.g. new URL('/api/ablo-session', process.env.NEXT_PUBLIC_APP_URL)).", { error: message });
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
getContext().logger.warn('access-credential re-mint failed (transient)', { error: message });
|
|
756
|
+
}
|
|
745
757
|
return 'network_error';
|
|
746
758
|
}
|
|
747
759
|
})();
|
|
@@ -769,10 +781,11 @@ export class BaseSyncedStore {
|
|
|
769
781
|
* 1. REACTIVE — register `getToken` as the re-mint hook the FSM calls when a
|
|
770
782
|
* probe finds the key stale (`credential_stale`) or on a nudge.
|
|
771
783
|
* 2. PROACTIVE — keep the short-lived key fresh ahead of trouble: a refresh
|
|
772
|
-
* timer inside the TTL, plus re-mint on OS wake
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
784
|
+
* timer inside the TTL, plus re-mint on OS wake. The ENTIRE proactive
|
|
785
|
+
* block is browser-gated (`typeof window`): server/SSR has no socket to
|
|
786
|
+
* keep warm and the resolver is browser-oriented, so arming it in Node
|
|
787
|
+
* would fire a relative-URL fetch and throw. (Agents pass a static
|
|
788
|
+
* `apiKey` with no resolver, so this method is never called for them.)
|
|
776
789
|
*
|
|
777
790
|
* Config-driven and invisible, like Supabase's `autoRefreshToken` — consumers
|
|
778
791
|
* never call a refresh method. Idempotent (a second call replaces the first);
|
|
@@ -807,14 +820,25 @@ export class BaseSyncedStore {
|
|
|
807
820
|
// 'network_error' → transient (offline / mint hiccup); the next timer tick
|
|
808
821
|
// or the FSM's own probe retries. Never sign out for it.
|
|
809
822
|
};
|
|
810
|
-
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
823
|
+
const teardowns = [];
|
|
824
|
+
// The ENTIRE proactive pre-roll is BROWSER-ONLY. On the server (Next.js
|
|
825
|
+
// SSR/RSC evaluating the `providers` module) there is no live socket to keep
|
|
826
|
+
// warm AND the scaffolded credential resolver is browser-oriented (a
|
|
827
|
+
// relative-URL `fetch('/api/ablo-session')`). Arming the timer server-side
|
|
828
|
+
// fires that resolver in Node, where fetch has no origin to resolve a
|
|
829
|
+
// relative URL against → "Failed to parse URL" on every tick. Browser-only
|
|
830
|
+
// refresh is the unanimous vendor model (Supabase `autoRefreshToken: isBrowser()`,
|
|
831
|
+
// Clerk/Ably/Stripe refresh client-side). The reactive re-mint hook
|
|
832
|
+
// (`setCredentialRefresher` above) stays UNCONDITIONAL: it only fires on a
|
|
833
|
+
// real connection probe, which can't happen during a bare SSR module eval.
|
|
817
834
|
if (typeof window !== 'undefined') {
|
|
835
|
+
// Comfortably inside the 15m `ek_` TTL; a missed (background-throttled)
|
|
836
|
+
// tick is recovered by the next, or by the reactive probe. The timer is
|
|
837
|
+
// the sole proactive PRE-ROLL — it keeps the key warm ahead of expiry even
|
|
838
|
+
// while the socket sits healthy-`connected` (a state the FSM never probes).
|
|
839
|
+
const REFRESH_INTERVAL_MS = 10 * 60 * 1000;
|
|
840
|
+
const timer = setInterval(() => void refresh(), REFRESH_INTERVAL_MS);
|
|
841
|
+
teardowns.push(() => clearInterval(timer));
|
|
818
842
|
// OS-wake (desktop only): the Electron shell bridges `powerMonitor`
|
|
819
843
|
// 'resume' to this DOM event. This is the ONE event-trigger the lifecycle
|
|
820
844
|
// still owns, because `visibilitychange` does NOT fire on wake-from-sleep
|
|
@@ -1713,7 +1737,25 @@ export class BaseSyncedStore {
|
|
|
1713
1737
|
applyDeltaFrame(deltas) {
|
|
1714
1738
|
let enqueuedAny = false;
|
|
1715
1739
|
for (const delta of deltas) {
|
|
1716
|
-
|
|
1740
|
+
// A delta_batch frame is the server's AUTHORITATIVE, ordered answer to
|
|
1741
|
+
// "everything in my stream after cursor C" (reconnect/catch-up replay or
|
|
1742
|
+
// post-bootstrap drain). Apply every delta it carries — do NOT subject it
|
|
1743
|
+
// to the live-traffic watermark dedup (`id <= applied`).
|
|
1744
|
+
//
|
|
1745
|
+
// That watermark is only valid under in-order delivery, and reconnect
|
|
1746
|
+
// breaks the assumption: an in-flight LIVE broadcast for a gap delta can
|
|
1747
|
+
// land out of order BEFORE the catch-up fills the ids below it (e.g. the
|
|
1748
|
+
// server acks a write, then the test/client reconnects, then that write's
|
|
1749
|
+
// pending broadcast arrives on the fresh socket — id 4 live before the
|
|
1750
|
+
// catch-up's [2,3,4]). Applying id 4 advances `applied` to 4, and the
|
|
1751
|
+
// watermark would then drop 2 and 3 from the catch-up as "already seen" —
|
|
1752
|
+
// a poisoned gap and a cursor that lies (applied=4 with rows 2,3 absent).
|
|
1753
|
+
//
|
|
1754
|
+
// Re-applying a delta the live path already applied is safe: the
|
|
1755
|
+
// downstream `Database.processDeltaBatch` + `SyncClient.applyDeltaBatchToPool`
|
|
1756
|
+
// are idempotent (echo detection, no row resurrection, conflict
|
|
1757
|
+
// resolution), so the redundant id 4 is a no-op while 2 and 3 land.
|
|
1758
|
+
if (this.enqueueDelta(delta, { authoritative: true }))
|
|
1717
1759
|
enqueuedAny = true;
|
|
1718
1760
|
}
|
|
1719
1761
|
if (!enqueuedAny)
|
|
@@ -1734,10 +1776,15 @@ export class BaseSyncedStore {
|
|
|
1734
1776
|
* sync-group mutations). Does NOT schedule a flush — callers decide
|
|
1735
1777
|
* whether to debounce (live) or flush atomically (catch-up frame).
|
|
1736
1778
|
*/
|
|
1737
|
-
enqueueDelta(delta) {
|
|
1738
|
-
// Dedup guard — skip already-processed deltas
|
|
1739
|
-
|
|
1779
|
+
enqueueDelta(delta, options = {}) {
|
|
1780
|
+
// Dedup guard — skip already-processed deltas. The `applied` watermark is a
|
|
1781
|
+
// valid skip threshold ONLY for in-order live traffic; an authoritative
|
|
1782
|
+
// catch-up frame bypasses it (see `applyDeltaFrame`) so an out-of-order
|
|
1783
|
+
// live delta that advanced the watermark can't cause the frame's lower ids
|
|
1784
|
+
// to be silently dropped.
|
|
1785
|
+
if (!options.authoritative && delta.id > 0 && delta.id <= this.highestProcessedSyncId) {
|
|
1740
1786
|
return false;
|
|
1787
|
+
}
|
|
1741
1788
|
// Confirm awaiting transactions via sync ID threshold (before batching)
|
|
1742
1789
|
this.syncClient.onDeltaReceived(delta.id);
|
|
1743
1790
|
// Update version vector
|
package/dist/NetworkMonitor.js
CHANGED
|
@@ -31,7 +31,9 @@ export class NetworkMonitor extends EventEmitter {
|
|
|
31
31
|
const wasOnline = this.isOnline;
|
|
32
32
|
this.isOnline = false;
|
|
33
33
|
if (wasOnline) {
|
|
34
|
-
|
|
34
|
+
// Symmetric with 'Network connection restored' (info) — expected,
|
|
35
|
+
// transient connectivity state, not an actionable warning.
|
|
36
|
+
getContext().logger.info('Network connection lost');
|
|
35
37
|
this.emit('offline');
|
|
36
38
|
}
|
|
37
39
|
};
|