@abloatai/ablo 0.11.0 → 0.11.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.
- package/CHANGELOG.md +24 -0
- package/README.md +62 -23
- package/dist/cli.cjs +115 -19
- package/dist/client/Ablo.js +16 -1
- package/dist/client/ApiClient.js +4 -1
- package/dist/client/auth.d.ts +9 -4
- package/dist/client/auth.js +40 -5
- package/dist/client/createModelProxy.d.ts +25 -0
- package/dist/client/createModelProxy.js +79 -4
- package/dist/errorCodes.js +1 -1
- package/dist/schema/schema.d.ts +3 -3
- package/dist/transactions/TransactionQueue.d.ts +11 -0
- package/dist/transactions/TransactionQueue.js +60 -4
- package/dist/types/global.d.ts +8 -3
- package/dist/types/global.js +8 -3
- package/docs/api.md +3 -3
- package/docs/client-behavior.md +6 -3
- package/docs/coordination.md +13 -3
- package/docs/data-sources.md +29 -9
- package/docs/migration.md +40 -0
- package/docs/quickstart.md +61 -33
- package/docs/react.md +46 -0
- package/llms-full.txt +25 -8
- package/llms.txt +11 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7f91f6e: DX hardening from a real onboarding session — onboarding, CLI, coordination, types, and docs.
|
|
8
|
+
|
|
9
|
+
**Client behavior**
|
|
10
|
+
- `databaseUrl` is now an explicit, server-only option: `Ablo(...)` no longer auto-reads `process.env.DATABASE_URL`. A stray `DATABASE_URL` (common — Prisma/Drizzle/docker set it) no longer silently flips the client into connection-string mode; a one-time warning points at the explicit option. Passing `databaseUrl: process.env.DATABASE_URL` explicitly is unchanged.
|
|
11
|
+
- Claims/presence are now observable from any client (including Node agents): reading a row enters its entity sync group (read-interest) and claiming pins it (write-intent), so `ablo.<model>.claim.state({ id })` reports co-participants without any manual subscribe step — whether the observer arrives before the claim (live delta) or after it (subscribe-time backfill). The claim **holder** now also sees its own claim via `claim.state`. **Requires a coordinated `sync-server` deploy** (the subscribe-time claim backfill + the entity-scope subscription gate that lets an org-authority agent key narrow into a row's group live server-side); the client package change alone does not deliver cross-client agent observation.
|
|
12
|
+
|
|
13
|
+
**CLI**
|
|
14
|
+
- `ablo init` detects the `src/app` layout (routes + the `@/ablo` import alias resolve correctly), writes the **real** stored sandbox key into `.env.local` instead of a placeholder, and scaffolds `ablo/register.ts` (a regular module, not a colliding `ablo.d.ts`).
|
|
15
|
+
- `ablo <command> --help` / `-h` now prints usage instead of erroring with "unknown flag", and `migrate` is listed in the top-level help.
|
|
16
|
+
- `ablo dev --no-watch` now exits after one push instead of watching forever.
|
|
17
|
+
|
|
18
|
+
**Types**
|
|
19
|
+
- Name the client with `typeof sync` (the value-inferred idiom, like tRPC's `typeof appRouter` / Drizzle's `typeof db`) — `ReturnType<typeof Ablo>` collapses to the untyped client and should not be used. No bespoke client-type generic is needed.
|
|
20
|
+
- `model_claim_not_configured` message clarified: claiming needs no per-model schema configuration; every model is claimable through the standard client.
|
|
21
|
+
|
|
22
|
+
**Docs**
|
|
23
|
+
- Reconciled the self-contradictory `databaseUrl` story (it is an explicit, server-only option, not auto-read from the environment; consistent casing), documented that the sandbox can host rows (apiKey only, no database), explained why a localhost Postgres can't be the system of record, and led the connect-your-database flow with `ablo pull`/`ablo check` over `ablo migrate`. Fixed stale `api.md` vocabulary (`object: 'claim'`, `participantKind: 'user' | 'agent' | 'system'`).
|
|
24
|
+
|
|
25
|
+
- 7f91f6e: Docs: document the completed `intent` → `claim` rename. Adds a 0.11.0 migration entry (`useIntent` → `useClaim`, `Register.Intents` → `Register.Claims`, `Ablo.Intent.*` → `Ablo.Claim.*`, and the coordinated client/server deploy for the `claim_*` wire frames), a `useClaim` section in the React reference, and fixes the stale `participantKind` union to the canonical `'user' | 'agent' | 'system'`.
|
|
26
|
+
|
|
3
27
|
## 0.11.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -54,10 +54,12 @@ claims are visible while the work is still in progress.
|
|
|
54
54
|
`llms.txt` · **upgrading?** see the
|
|
55
55
|
[Version History & Migration Guide](./docs/migration.md)
|
|
56
56
|
|
|
57
|
-
It works with the auth and database you already have. **
|
|
58
|
-
system of record
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
It works with the auth and database you already have. **In production, your
|
|
58
|
+
database is the system of record.** Ablo is the transaction layer on top of it:
|
|
59
|
+
realtime data is scoped to *sync groups* from your own identity, and every
|
|
60
|
+
committed row lives in your Postgres. (Trying Ablo with no database yet? The
|
|
61
|
+
hosted **sandbox** can host rows in Ablo's test plane — apiKey only, like
|
|
62
|
+
Stripe test mode — so you can explore before pointing it at your Postgres.)
|
|
61
63
|
|
|
62
64
|
**Built for** collaborative editors, AI agent workflows, and internal tools —
|
|
63
65
|
anywhere people and agents change shared state and everyone has to see it live.
|
|
@@ -65,18 +67,24 @@ anywhere people and agents change shared state and everyone has to see it live.
|
|
|
65
67
|
## Set up
|
|
66
68
|
|
|
67
69
|
The CLI takes you from nothing to a synced schema — it handles the account,
|
|
68
|
-
the key, and the env file. You bring one thing: a Postgres
|
|
69
|
-
(local, Neon, RDS — any will do
|
|
70
|
-
Ablo
|
|
70
|
+
the key, and the env file. You bring one thing: a Postgres you already have —
|
|
71
|
+
the same `DATABASE_URL` (local, Neon, RDS — any will do) that backs your auth,
|
|
72
|
+
audit, and log tables. Ablo syncs a *subset* of models against it; **in
|
|
73
|
+
production, your database is the system of record**.
|
|
71
74
|
|
|
72
75
|
```bash
|
|
73
76
|
npm install @abloatai/ablo
|
|
74
77
|
npx ablo login # opens the browser: sign in (or sign up) → a sk_test_ key is saved locally
|
|
75
78
|
npx ablo init # scaffolds ablo/schema.ts (offers to log in if you skipped it)
|
|
76
|
-
npx ablo
|
|
77
|
-
npx ablo push # pushes your schema (sandbox), writes ABLO_API_KEY to .env.local, watches for changes
|
|
79
|
+
npx ablo push # pushes your schema (sandbox), writes ABLO_API_KEY to .env.local, watches for changes
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
Then point Ablo at the tables for your synced models. Most teams **already
|
|
83
|
+
have those tables** (often Prisma- or Drizzle-managed) — adopt them with
|
|
84
|
+
`npx ablo pull` / `npx ablo check`, the common case. Let Ablo own its own
|
|
85
|
+
tables instead? `npx ablo migrate` provisions them in your Postgres (reads
|
|
86
|
+
`DATABASE_URL`). Either way your other tables are left untouched.
|
|
87
|
+
|
|
80
88
|
After `ablo push`, the [Quick Start](#quick-start) below runs as-is —
|
|
81
89
|
`ABLO_API_KEY` is already in `.env.local` (frameworks load it automatically;
|
|
82
90
|
plain Node: `node --env-file=.env.local app.ts`). `npx ablo status` shows
|
|
@@ -106,18 +114,24 @@ import Ablo from '@abloatai/ablo';
|
|
|
106
114
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
107
115
|
```
|
|
108
116
|
|
|
109
|
-
|
|
110
|
-
is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
117
|
+
The schema is registered once (init scaffolds `ablo/register.ts` for you), and
|
|
118
|
+
every type is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
111
119
|
|
|
112
120
|
```ts
|
|
113
|
-
// ablo.
|
|
114
|
-
import type { schema } from './
|
|
121
|
+
// ablo/register.ts — scaffolded by `npx ablo init`, sits beside ablo/schema.ts
|
|
122
|
+
import type { schema } from './schema';
|
|
115
123
|
declare module '@abloatai/ablo' {
|
|
116
124
|
interface Register { Schema: typeof schema }
|
|
117
125
|
}
|
|
118
126
|
export {};
|
|
119
127
|
```
|
|
120
128
|
|
|
129
|
+
It's a regular `.ts` module, not a hand-authored `.d.ts`. The top-level
|
|
130
|
+
`import type { schema }` makes the `declare module` block *merge* into (augment)
|
|
131
|
+
the SDK's `Register` interface instead of colliding with it — the same shape
|
|
132
|
+
[TanStack Router uses in `src/router.tsx`](https://tanstack.com/router/latest/docs/framework/react/guide/type-safety). Any `.ts` file in your
|
|
133
|
+
`tsconfig` `include` works; it never needs to be imported.
|
|
134
|
+
|
|
121
135
|
```ts
|
|
122
136
|
import type { Model } from '@abloatai/ablo/schema';
|
|
123
137
|
|
|
@@ -128,6 +142,25 @@ type WeatherReport = Model<'weatherReports'>; // fully typed from YOUR schema
|
|
|
128
142
|
TanStack-Router pattern: declare the source of truth once, everything
|
|
129
143
|
infers from it.)
|
|
130
144
|
|
|
145
|
+
### Naming the client type
|
|
146
|
+
|
|
147
|
+
When you need to pass the client around (a function parameter, a context value),
|
|
148
|
+
**infer the type from the value** — `type Sync = typeof sync`:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
export const sync = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
152
|
+
export type Sync = typeof sync; // fully-typed, schema-aware
|
|
153
|
+
|
|
154
|
+
function persist(client: Sync) { /* ... */ }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
This is the same idiom as tRPC's `type AppRouter = typeof appRouter` and
|
|
158
|
+
Drizzle's `typeof db` — the factory resolves the typed overload at the call
|
|
159
|
+
site, so `typeof sync` carries your schema. Do **not** write
|
|
160
|
+
`ReturnType<typeof Ablo>`: that collapses to the untyped last overload and
|
|
161
|
+
loses your model types. There is no bespoke client-type generic to import —
|
|
162
|
+
`typeof` your client value is the type.
|
|
163
|
+
|
|
131
164
|
```ts
|
|
132
165
|
const schema = defineSchema({
|
|
133
166
|
weatherReports: model({
|
|
@@ -140,7 +173,7 @@ const schema = defineSchema({
|
|
|
140
173
|
const ablo = Ablo({
|
|
141
174
|
schema,
|
|
142
175
|
apiKey: process.env.ABLO_API_KEY, // written to .env.local by `npx ablo push`
|
|
143
|
-
databaseUrl: process.env.DATABASE_URL, // your Postgres — rows live here
|
|
176
|
+
databaseUrl: process.env.DATABASE_URL, // your Postgres, passed explicitly — rows live here
|
|
144
177
|
});
|
|
145
178
|
|
|
146
179
|
await ablo.ready();
|
|
@@ -388,29 +421,35 @@ curl https://api.abloatai.com/v1/commits \
|
|
|
388
421
|
|
|
389
422
|
## Your Database
|
|
390
423
|
|
|
391
|
-
|
|
392
|
-
layer on top of it
|
|
424
|
+
In production, every schema model is backed by **your own database** — Ablo is
|
|
425
|
+
the transaction layer on top of it. Two ways to connect it:
|
|
393
426
|
|
|
394
427
|
| | How Ablo reaches your Postgres | Use when |
|
|
395
428
|
| --- | --- | --- |
|
|
396
|
-
| **Connection string** (
|
|
429
|
+
| **Connection string** (primary) | `databaseUrl` at init — passed explicitly, never auto-read from the environment. Ablo registers the connection once (sent over TLS, stored sealed, never echoed back) and commits each write directly — through a non-superuser role, behind row-level security. | You can hand over a scoped connection string. |
|
|
397
430
|
| **Signed endpoint** | Your app exposes one route built from an ORM adapter (`prismaDataSource` / `drizzleDataSource`); Ablo sends signed commit requests and your app writes its own database. | Database credentials must never leave your infrastructure. |
|
|
398
431
|
|
|
399
|
-
|
|
432
|
+
(No database yet? The hosted **sandbox** can host rows in Ablo's test plane —
|
|
433
|
+
omit `databaseUrl` and pass an `apiKey` only, like Stripe test mode — so you can
|
|
434
|
+
try Ablo before connecting your Postgres.)
|
|
435
|
+
|
|
436
|
+
Same product, same truth either way: in production your database is the system of
|
|
437
|
+
record. See
|
|
400
438
|
[Connect Your Database](./docs/data-sources.md) for both shapes.
|
|
401
439
|
|
|
402
440
|
## Configuration
|
|
403
441
|
|
|
404
|
-
`Ablo({ ... })` takes
|
|
405
|
-
|
|
406
|
-
[Data Source endpoint](./docs/data-sources.md) in your app.
|
|
407
|
-
|
|
442
|
+
`Ablo({ ... })` takes your schema, your key, and — in production — your database,
|
|
443
|
+
either as an explicit `databaseUrl` here or as a signed
|
|
444
|
+
[Data Source endpoint](./docs/data-sources.md) in your app. (`databaseUrl` is
|
|
445
|
+
never auto-read from the environment; omit it to try Ablo against the hosted
|
|
446
|
+
sandbox.) Every other option has correct defaults:
|
|
408
447
|
|
|
409
448
|
| Option | Type | Default | Purpose |
|
|
410
449
|
| --- | --- | --- | --- |
|
|
411
450
|
| `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
|
|
412
451
|
| `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
|
|
413
|
-
| `databaseUrl` | `string \| null` |
|
|
452
|
+
| `databaseUrl` | `string \| null` | `—` | Your Postgres, registered as the data plane. **Must be passed explicitly — it is not auto-read from the environment.** If you have a `DATABASE_URL` set for another tool (Prisma, Drizzle, docker-compose), `Ablo()` ignores it unless you pass `databaseUrl` explicitly. 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, or when trying Ablo against the hosted sandbox. |
|
|
414
453
|
|
|
415
454
|
Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
|
|
416
455
|
authenticates with the signed-in user's session; the raw-key path is gated
|
package/dist/cli.cjs
CHANGED
|
@@ -276803,7 +276803,7 @@ var ERROR_CODES = {
|
|
|
276803
276803
|
malformed_subscription: wire("validation", 400, false, "The update_subscription payload was malformed; expected { syncGroups: string[] }."),
|
|
276804
276804
|
model_claimed: wire("claim", 409, false, "The model instance is claimed by another participant."),
|
|
276805
276805
|
model_claimed_timeout: wire("claim", 409, false, "Timed out waiting for a model claim to clear."),
|
|
276806
|
-
model_claim_not_configured: client("claim", "Claiming
|
|
276806
|
+
model_claim_not_configured: client("claim", "Claiming requires the collaboration runtime, which the standard Ablo({ schema, apiKey }) client wires up for every model automatically \u2014 there is no per-model claim configuration to add. This appears only when a model proxy is constructed directly without that runtime (an internal/advanced path)."),
|
|
276807
276807
|
// ── stale context / idempotency (409) ──────────────────────────────
|
|
276808
276808
|
stale_context: wire("conflict", 409, true, "The write carried a readAt watermark that is now stale; re-read and retry."),
|
|
276809
276809
|
idempotency_conflict: wire("conflict", 409, false, "The same Idempotency-Key was reused with a different request body."),
|
|
@@ -279942,6 +279942,14 @@ async function push(argv) {
|
|
|
279942
279942
|
}
|
|
279943
279943
|
|
|
279944
279944
|
// src/cli/migrate.ts
|
|
279945
|
+
var MIGRATE_USAGE = ` ablo migrate \u2014 provision your schema's tables in your own Postgres (DATABASE_URL)
|
|
279946
|
+
|
|
279947
|
+
Usage:
|
|
279948
|
+
npx ablo migrate Create the synced-model tables (with row-level security)
|
|
279949
|
+
npx ablo migrate --dry-run Print the SQL without executing it
|
|
279950
|
+
npx ablo migrate --output schema.sql Write the SQL to a file instead of applying
|
|
279951
|
+
npx ablo migrate --schema <path> Use a schema file other than ablo/schema.ts
|
|
279952
|
+
npx ablo migrate --export <name> Use a named export other than \`schema\``;
|
|
279945
279953
|
var DEFAULT_SCHEMA_PATH2 = "ablo/schema.ts";
|
|
279946
279954
|
var DEFAULT_EXPORT2 = "schema";
|
|
279947
279955
|
function parseMigrateArgs(argv) {
|
|
@@ -280478,7 +280486,10 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
|
|
|
280478
280486
|
s.message("Provisioning a sandbox key\u2026");
|
|
280479
280487
|
const provRes = await fetch(`${AUTH_URL}/api/cli/provision-key`, {
|
|
280480
280488
|
method: "POST",
|
|
280481
|
-
headers: { authorization: `Bearer ${accessToken}
|
|
280489
|
+
headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
|
|
280490
|
+
// Pass the device_code so the server can scope the minted keys to the
|
|
280491
|
+
// project the user picked at /cli (login project picker). Harmless if none.
|
|
280492
|
+
body: JSON.stringify({ device_code: code.device_code })
|
|
280482
280493
|
}).catch(() => null);
|
|
280483
280494
|
if (!provRes || !provRes.ok) {
|
|
280484
280495
|
s.stop("Could not provision a key.");
|
|
@@ -280706,6 +280717,39 @@ async function projects(argv) {
|
|
|
280706
280717
|
);
|
|
280707
280718
|
return;
|
|
280708
280719
|
}
|
|
280720
|
+
if (sub === "rename") {
|
|
280721
|
+
const ref = argv[1];
|
|
280722
|
+
const name = argv.slice(2).join(" ").trim();
|
|
280723
|
+
if (!ref || ref.startsWith("-") || !name) {
|
|
280724
|
+
console.error(import_picocolors9.default.red(" usage: ablo projects rename <slug|id> <new name>"));
|
|
280725
|
+
process.exit(1);
|
|
280726
|
+
}
|
|
280727
|
+
const all = await fetchProjects();
|
|
280728
|
+
const target = all.find((p2) => p2.slug === ref || p2.id === ref);
|
|
280729
|
+
if (!target) {
|
|
280730
|
+
console.error(import_picocolors9.default.red(` No project "${ref}".`) + import_picocolors9.default.dim(" Run ablo projects list."));
|
|
280731
|
+
process.exit(1);
|
|
280732
|
+
}
|
|
280733
|
+
if (target.default) {
|
|
280734
|
+
console.error(import_picocolors9.default.red(" The default project cannot be renamed."));
|
|
280735
|
+
process.exit(1);
|
|
280736
|
+
}
|
|
280737
|
+
const { status: status2, body } = await request(`/api/v1/projects/${target.id}`, requireKey(), {
|
|
280738
|
+
method: "PATCH",
|
|
280739
|
+
body: { name }
|
|
280740
|
+
});
|
|
280741
|
+
if (status2 !== 200) {
|
|
280742
|
+
console.error(
|
|
280743
|
+
import_picocolors9.default.red(` Rename failed (${status2}): ${String(body.message ?? body.code ?? "")}`)
|
|
280744
|
+
);
|
|
280745
|
+
process.exit(1);
|
|
280746
|
+
}
|
|
280747
|
+
const updated = body;
|
|
280748
|
+
console.log(
|
|
280749
|
+
` ${import_picocolors9.default.green("\u2713")} Renamed ${import_picocolors9.default.bold(updated.slug)} \u2192 ${import_picocolors9.default.bold(updated.name ?? updated.slug)} ${import_picocolors9.default.dim(`(${updated.id})`)}`
|
|
280750
|
+
);
|
|
280751
|
+
return;
|
|
280752
|
+
}
|
|
280709
280753
|
if (sub === "use") {
|
|
280710
280754
|
const ref = argv[1];
|
|
280711
280755
|
if (!ref) {
|
|
@@ -280733,7 +280777,9 @@ async function projects(argv) {
|
|
|
280733
280777
|
return;
|
|
280734
280778
|
}
|
|
280735
280779
|
console.error(
|
|
280736
|
-
import_picocolors9.default.red(` unknown subcommand: ${sub}`) + import_picocolors9.default.dim(
|
|
280780
|
+
import_picocolors9.default.red(` unknown subcommand: ${sub}`) + import_picocolors9.default.dim(
|
|
280781
|
+
` (expected ${import_picocolors9.default.bold("list")}, ${import_picocolors9.default.bold("create")}, ${import_picocolors9.default.bold("rename")}, or ${import_picocolors9.default.bold("use")})`
|
|
280782
|
+
)
|
|
280737
280783
|
);
|
|
280738
280784
|
process.exit(1);
|
|
280739
280785
|
}
|
|
@@ -282130,8 +282176,18 @@ async function drizzlePull(argv) {
|
|
|
282130
282176
|
var LOGO = `
|
|
282131
282177
|
${brand("ablo")} ${import_picocolors18.default.dim("sync engine")}
|
|
282132
282178
|
`;
|
|
282179
|
+
var SUBCOMMAND_USAGE = {
|
|
282180
|
+
migrate: MIGRATE_USAGE
|
|
282181
|
+
};
|
|
282133
282182
|
async function main() {
|
|
282134
|
-
|
|
282183
|
+
let command = process.argv[2];
|
|
282184
|
+
if (command && process.argv.slice(3).some((a) => a === "--help" || a === "-h")) {
|
|
282185
|
+
if (SUBCOMMAND_USAGE[command]) {
|
|
282186
|
+
console.log(SUBCOMMAND_USAGE[command]);
|
|
282187
|
+
return;
|
|
282188
|
+
}
|
|
282189
|
+
command = void 0;
|
|
282190
|
+
}
|
|
282135
282191
|
if (command === "init") {
|
|
282136
282192
|
await init(process.argv.slice(3));
|
|
282137
282193
|
} else if (command === "login") {
|
|
@@ -282149,8 +282205,14 @@ async function main() {
|
|
|
282149
282205
|
} else if (command === "webhooks") {
|
|
282150
282206
|
await webhooks(process.argv.slice(3));
|
|
282151
282207
|
} else if (command === "dev") {
|
|
282152
|
-
|
|
282153
|
-
|
|
282208
|
+
const devArgs = process.argv.slice(3);
|
|
282209
|
+
const oneShot = devArgs.includes("--no-watch");
|
|
282210
|
+
console.log(
|
|
282211
|
+
import_picocolors18.default.dim(
|
|
282212
|
+
oneShot ? " `ablo dev --no-watch` is `ablo push` (push once, no watcher) \u2014 running that." : " `ablo dev` is now `ablo push --watch` \u2014 running that."
|
|
282213
|
+
)
|
|
282214
|
+
);
|
|
282215
|
+
await dev(oneShot ? devArgs : [...devArgs, "--watch"]);
|
|
282154
282216
|
} else if (command === "check") {
|
|
282155
282217
|
await check(process.argv.slice(3));
|
|
282156
282218
|
} else if (command === "pull") {
|
|
@@ -282208,6 +282270,8 @@ async function main() {
|
|
|
282208
282270
|
console.log(` npx ablo pull prisma [path] Generate schema.ts from a Prisma schema (keeps enums + relations)`);
|
|
282209
282271
|
console.log(` npx ablo pull drizzle <module> Generate schema.ts from a Drizzle schema (keeps enums + relations)`);
|
|
282210
282272
|
console.log(` npx ablo check Check your existing database fits the schema (read-only, creates no tables)`);
|
|
282273
|
+
console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (DATABASE_URL)`);
|
|
282274
|
+
console.log(` npx ablo migrate --dry-run Print the SQL without executing (preview)`);
|
|
282211
282275
|
console.log(` npx ablo push Upload your schema definition to Ablo (metadata only \u2014 rows stay in your DB)`);
|
|
282212
282276
|
console.log(` npx ablo push --force Allow destructive/unexecutable changes`);
|
|
282213
282277
|
console.log(` npx ablo push --rename a:b Treat model "a" as renamed to "b"`);
|
|
@@ -282285,6 +282349,10 @@ function detectOrm(override) {
|
|
|
282285
282349
|
}
|
|
282286
282350
|
return "none";
|
|
282287
282351
|
}
|
|
282352
|
+
function detectNextLayout() {
|
|
282353
|
+
const useSrc = (0, import_fs12.existsSync)((0, import_path7.join)("src", "app")) || !(0, import_fs12.existsSync)("app") && (0, import_fs12.existsSync)("src");
|
|
282354
|
+
return useSrc ? { appBase: (0, import_path7.join)("src", "app"), aliasBase: "src" } : { appBase: "app", aliasBase: "." };
|
|
282355
|
+
}
|
|
282288
282356
|
async function chooseOption(name, flagValue, fallback, allowed, interactive, prompt) {
|
|
282289
282357
|
if (flagValue !== void 0) {
|
|
282290
282358
|
if (!allowed.includes(flagValue)) {
|
|
@@ -282384,7 +282452,8 @@ async function init(args = []) {
|
|
|
282384
282452
|
"Non-interactive (no TTY / --yes)"
|
|
282385
282453
|
);
|
|
282386
282454
|
}
|
|
282387
|
-
const
|
|
282455
|
+
const layout = framework === "nextjs" ? detectNextLayout() : { appBase: "app", aliasBase: "." };
|
|
282456
|
+
const abloDir = (0, import_path7.join)(layout.aliasBase, "ablo");
|
|
282388
282457
|
(0, import_fs12.mkdirSync)(abloDir, { recursive: true });
|
|
282389
282458
|
const created = [];
|
|
282390
282459
|
let schemaSource = generateSchema();
|
|
@@ -282415,41 +282484,51 @@ async function init(args = []) {
|
|
|
282415
282484
|
created.push(`${abloDir}/schema.ts${schemaNote}`);
|
|
282416
282485
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "index.ts"), generateSyncConfig(auth, storage));
|
|
282417
282486
|
created.push(`${abloDir}/index.ts`);
|
|
282487
|
+
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "register.ts"), generateRegister());
|
|
282488
|
+
created.push(`${abloDir}/register.ts`);
|
|
282418
282489
|
const orm = detectOrm(opts.orm);
|
|
282419
282490
|
if (storage === "endpoint") {
|
|
282420
282491
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "data-source.ts"), generateDataSource(orm));
|
|
282421
282492
|
created.push(`${abloDir}/data-source.ts${orm === "drizzle" ? " (Drizzle)" : " (Prisma)"}`);
|
|
282422
282493
|
}
|
|
282423
282494
|
const envFile = framework === "nextjs" ? ".env.local" : ".env";
|
|
282495
|
+
const resolvedKey = process.env.ABLO_API_KEY ? void 0 : resolveApiKey("sandbox");
|
|
282496
|
+
const wireRealKey = envFile === ".env.local" && Boolean(resolvedKey);
|
|
282497
|
+
const envBody = generateEnv(storage, { includeApiKey: !wireRealKey });
|
|
282424
282498
|
if (!(0, import_fs12.existsSync)(envFile)) {
|
|
282425
|
-
(0, import_fs12.writeFileSync)(envFile,
|
|
282499
|
+
(0, import_fs12.writeFileSync)(envFile, envBody);
|
|
282426
282500
|
created.push(envFile);
|
|
282427
282501
|
} else {
|
|
282428
282502
|
const existing = (0, import_fs12.readFileSync)(envFile, "utf-8");
|
|
282429
282503
|
if (!existing.includes("ABLO_")) {
|
|
282430
|
-
(0, import_fs12.writeFileSync)(envFile, existing + "\n" +
|
|
282504
|
+
(0, import_fs12.writeFileSync)(envFile, existing + "\n" + envBody);
|
|
282431
282505
|
created.push(`${envFile} ${import_picocolors18.default.dim("(appended)")}`);
|
|
282432
282506
|
} else {
|
|
282433
282507
|
created.push(`${envFile} ${import_picocolors18.default.dim("(already configured)")}`);
|
|
282434
282508
|
}
|
|
282435
282509
|
}
|
|
282510
|
+
if (wireRealKey && resolvedKey) {
|
|
282511
|
+
wireEnvLocal(resolvedKey);
|
|
282512
|
+
created.push(`.env.local ${import_picocolors18.default.dim("(ABLO_API_KEY set from your login)")}`);
|
|
282513
|
+
}
|
|
282436
282514
|
if (agent) {
|
|
282437
282515
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "agent.ts"), generateAgent());
|
|
282438
282516
|
created.push(`${abloDir}/agent.ts`);
|
|
282439
282517
|
}
|
|
282440
282518
|
if (framework === "nextjs") {
|
|
282441
282519
|
if (storage === "endpoint") {
|
|
282442
|
-
const webhookDir = (0, import_path7.join)(
|
|
282520
|
+
const webhookDir = (0, import_path7.join)(layout.appBase, "api", "ablo", "webhooks");
|
|
282443
282521
|
(0, import_fs12.mkdirSync)(webhookDir, { recursive: true });
|
|
282444
282522
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(webhookDir, "route.ts"), generateWebhookRoute(orm));
|
|
282445
282523
|
created.push(`${webhookDir}/route.ts${orm === "prisma" ? " (Prisma mirror)" : " (add your database write)"}`);
|
|
282446
282524
|
}
|
|
282447
|
-
|
|
282448
|
-
|
|
282449
|
-
|
|
282525
|
+
const providersPath = (0, import_path7.join)(layout.appBase, "providers.tsx");
|
|
282526
|
+
(0, import_fs12.writeFileSync)(providersPath, generateProviders());
|
|
282527
|
+
created.push(`${providersPath} ${import_picocolors18.default.dim(`(wrap ${(0, import_path7.join)(layout.appBase, "layout.tsx")} in <Providers>)`)}`);
|
|
282528
|
+
const sessionDir = (0, import_path7.join)(layout.appBase, "api", "ablo-session");
|
|
282450
282529
|
(0, import_fs12.mkdirSync)(sessionDir, { recursive: true });
|
|
282451
282530
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(sessionDir, "route.ts"), generateSessionRoute());
|
|
282452
|
-
created.push(
|
|
282531
|
+
created.push(`${(0, import_path7.join)(sessionDir, "route.ts")} ${import_picocolors18.default.dim("(wire your auth)")}`);
|
|
282453
282532
|
}
|
|
282454
282533
|
if (framework !== "vanilla") {
|
|
282455
282534
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "TaskList.tsx"), generateComponent());
|
|
@@ -282478,7 +282557,7 @@ async function init(args = []) {
|
|
|
282478
282557
|
`Provision your DB: ${import_picocolors18.default.bold("npx ablo migrate")} (creates your Ablo-model tables + the adapter tables; keep your own migrations for everything else), then mount ${import_picocolors18.default.bold(`${abloDir}/data-source.ts`)} at ${import_picocolors18.default.bold("/api/ablo/source")}`
|
|
282479
282558
|
],
|
|
282480
282559
|
...framework === "nextjs" ? [
|
|
282481
|
-
`Wrap ${import_picocolors18.default.bold("
|
|
282560
|
+
`Wrap ${import_picocolors18.default.bold((0, import_path7.join)(layout.appBase, "layout.tsx"))} in ${import_picocolors18.default.bold("<Providers>")} (${(0, import_path7.join)(layout.appBase, "providers.tsx")}) and add your auth to ${import_picocolors18.default.bold((0, import_path7.join)(layout.appBase, "api", "ablo-session", "route.ts"))}`
|
|
282482
282561
|
] : [],
|
|
282483
282562
|
`Run ${import_picocolors18.default.bold(`${pm} run dev`)} and open two browser tabs \u2014 changes sync in real-time`,
|
|
282484
282563
|
...agent ? [
|
|
@@ -282557,14 +282636,31 @@ export const sync = Ablo({
|
|
|
282557
282636
|
apiKey: process.env.ABLO_API_KEY,${databaseLine}${authLine}
|
|
282558
282637
|
schema,
|
|
282559
282638
|
});
|
|
282639
|
+
|
|
282640
|
+
// Name the client's type off the constructed value \u2014 the overload resolves at
|
|
282641
|
+
// this call site, so this carries the full typed surface. (Like tRPC's
|
|
282642
|
+
// \`typeof appRouter\`, Drizzle's \`typeof db\`.) Prefer this over \`ReturnType<typeof Ablo>\`.
|
|
282643
|
+
export type Sync = typeof sync;
|
|
282644
|
+
`;
|
|
282645
|
+
}
|
|
282646
|
+
function generateRegister() {
|
|
282647
|
+
return `import type { schema } from './schema';
|
|
282648
|
+
|
|
282649
|
+
declare module '@abloatai/ablo' {
|
|
282650
|
+
interface Register {
|
|
282651
|
+
Schema: typeof schema;
|
|
282652
|
+
}
|
|
282653
|
+
}
|
|
282654
|
+
|
|
282655
|
+
export {};
|
|
282560
282656
|
`;
|
|
282561
282657
|
}
|
|
282562
|
-
function generateEnv(storage) {
|
|
282658
|
+
function generateEnv(storage, opts = {}) {
|
|
282659
|
+
const { includeApiKey = true } = opts;
|
|
282563
282660
|
const databaseBlock = storage === "direct" ? "# Your Postgres \u2014 the system of record. The client registers this connection\n# (sent once over TLS, stored sealed) and every row lives HERE, never with Ablo.\n# Use a dedicated non-superuser role; the browser never sees this value.\nDATABASE_URL=postgres://user:password@host:5432/db\n" : "# Used by ablo/data-source.ts (your DB endpoint) + `ablo migrate` \u2014 NOT the client.\n# Ablo never sees it; the browser never sees it. Your DB stays in your app.\nDATABASE_URL=postgres://user:password@host:5432/db\n";
|
|
282564
282661
|
const webhookBlock = storage === "endpoint" ? "# Signing secret for the webhook receiver (app/api/ablo/webhooks/route.ts).\n# Ablo mints this when you register the endpoint's URL (POST /v1/webhook_endpoints\n# or the dashboard) and returns it once \u2014 paste it here.\nABLO_WEBHOOK_SECRET=whsec_your_endpoint_secret_here\n" : "";
|
|
282565
|
-
|
|
282566
|
-
|
|
282567
|
-
${webhookBlock}${databaseBlock}`;
|
|
282662
|
+
const apiKeyBlock = includeApiKey ? "# Ablo Sync Engine \u2014 use a sk_test_ key for local dev (`npx ablo push`)\nABLO_API_KEY=sk_test_your_key_here\n" : "";
|
|
282663
|
+
return `${apiKeyBlock}${webhookBlock}${databaseBlock}`;
|
|
282568
282664
|
}
|
|
282569
282665
|
function generateDataSource(orm) {
|
|
282570
282666
|
return orm === "drizzle" ? drizzleDataSourceScaffold() : prismaDataSourceScaffold();
|
package/dist/client/Ablo.js
CHANGED
|
@@ -42,7 +42,7 @@ import { createProtocolClient, } from './ApiClient.js';
|
|
|
42
42
|
// Value import is cycle-safe: httpClient.js only value-imports ApiClient.js,
|
|
43
43
|
// which imports this module type-only.
|
|
44
44
|
import { createAbloHttpClient, } from './httpClient.js';
|
|
45
|
-
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, } from './auth.js';
|
|
45
|
+
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfDatabaseUrlEnvIgnored, } from './auth.js';
|
|
46
46
|
import { registerDataSource } from './registerDataSource.js';
|
|
47
47
|
import { shouldUseInMemoryPersistence, } from './persistence.js';
|
|
48
48
|
import { createModelProxy } from './createModelProxy.js';
|
|
@@ -725,6 +725,9 @@ export function Ablo(options) {
|
|
|
725
725
|
dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
|
|
726
726
|
});
|
|
727
727
|
const { logger = consoleLogger } = internalOptions;
|
|
728
|
+
// Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
|
|
729
|
+
// passed — the env value is no longer auto-adopted (see resolveDatabaseUrl).
|
|
730
|
+
warnIfDatabaseUrlEnvIgnored(authInput, (m) => logger.warn(m));
|
|
728
731
|
const schema = options.schema;
|
|
729
732
|
const url = resolveBaseURL(authInput);
|
|
730
733
|
// 1. Derive config from schema
|
|
@@ -1370,6 +1373,18 @@ export function Ablo(options) {
|
|
|
1370
1373
|
},
|
|
1371
1374
|
waitFor: (target, waitOptions) => publicClaims.waitFor({ model: target.model, id: target.id }, waitOptions),
|
|
1372
1375
|
selfParticipantId: participantId,
|
|
1376
|
+
selfParticipantKind: kind,
|
|
1377
|
+
// Read-interest / write-intent enrolment for the typed surface.
|
|
1378
|
+
// `enterScope`/`pinScope` resolve the `{ [schemaKey]: id }` scope
|
|
1379
|
+
// through the SAME resolver the claim path uses, landing this client in
|
|
1380
|
+
// the entity-scoped group the holder's claim presence fans out on.
|
|
1381
|
+
// Return the store promise so the claim write path can AWAIT pinScope
|
|
1382
|
+
// BEFORE acquiring the lease (closing the subscribe-vs-broadcast race);
|
|
1383
|
+
// read-interest callers (`retrieve`/`claim.state`) still `void` it and
|
|
1384
|
+
// stay fire-and-forget. SOFT either way — the store swallows reconcile
|
|
1385
|
+
// errors so read interest never makes a read reject or stall.
|
|
1386
|
+
enterScope: (scope) => store.enterScope(scope),
|
|
1387
|
+
pinScope: (scope) => store.pinScope(scope),
|
|
1373
1388
|
});
|
|
1374
1389
|
}
|
|
1375
1390
|
const commits = {
|
package/dist/client/ApiClient.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* nouns directly to HTTP routes on sync-server.
|
|
7
7
|
*/
|
|
8
8
|
import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError, translateHttpError, } from '../errors.js';
|
|
9
|
-
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, } from './auth.js';
|
|
9
|
+
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfDatabaseUrlEnvIgnored, } from './auth.js';
|
|
10
10
|
import { registerDataSource } from './registerDataSource.js';
|
|
11
11
|
import { toSeconds } from '../utils/duration.js';
|
|
12
12
|
import { assertWriteOptions } from './writeOptionsSchema.js';
|
|
@@ -17,6 +17,9 @@ export function createProtocolClient(options) {
|
|
|
17
17
|
const configuredApiKey = resolveApiKey(authInput);
|
|
18
18
|
const configuredAuthToken = resolveAuthToken(authInput);
|
|
19
19
|
const configuredDatabaseUrl = resolveDatabaseUrl(authInput);
|
|
20
|
+
// Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
|
|
21
|
+
// passed — no logger on this path, so the helper falls back to console.warn.
|
|
22
|
+
warnIfDatabaseUrlEnvIgnored(authInput);
|
|
20
23
|
assertBrowserSafety({
|
|
21
24
|
apiKey: configuredApiKey,
|
|
22
25
|
databaseUrl: configuredDatabaseUrl,
|
package/dist/client/auth.d.ts
CHANGED
|
@@ -45,12 +45,17 @@ export declare function resolveAuthToken(input: AuthResolveInput): string | null
|
|
|
45
45
|
/**
|
|
46
46
|
* Resolve the direct-URL connector's Postgres connection string.
|
|
47
47
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* `databaseUrl` is an EXPLICIT, opt-in option: Ablo registers a dedicated
|
|
49
|
+
* tenant database only when the caller passes it to `Ablo(...)`. It is NOT
|
|
50
|
+
* read from `process.env.DATABASE_URL` — per this module's invariant
|
|
51
|
+
* (`ABLO_API_KEY` is the only environment fallback), an app's `DATABASE_URL`
|
|
52
|
+
* (commonly set for Prisma/Drizzle/docker) must never silently flip the client
|
|
53
|
+
* into connection-string mode. The default Data Source path keeps `DATABASE_URL`
|
|
54
|
+
* in the app and exposes `dataSource(...)`; that path leaves this null.
|
|
55
|
+
* `warnIfDatabaseUrlEnvIgnored` nudges callers who set the env but omitted the option.
|
|
52
56
|
*/
|
|
53
57
|
export declare function resolveDatabaseUrl(input: AuthResolveInput): string | null;
|
|
58
|
+
export declare function warnIfDatabaseUrlEnvIgnored(input: AuthResolveInput, warn?: (message: string) => void): void;
|
|
54
59
|
export declare const ABLO_HOSTED_API_DOMAIN = "api.abloatai.com";
|
|
55
60
|
export declare const ABLO_HOSTED_HTTP_BASE_URL = "https://api.abloatai.com";
|
|
56
61
|
export declare const ABLO_DEFAULT_BASE_URL = "https://api.abloatai.com";
|
package/dist/client/auth.js
CHANGED
|
@@ -30,13 +30,48 @@ export function resolveAuthToken(input) {
|
|
|
30
30
|
/**
|
|
31
31
|
* Resolve the direct-URL connector's Postgres connection string.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* `databaseUrl` is an EXPLICIT, opt-in option: Ablo registers a dedicated
|
|
34
|
+
* tenant database only when the caller passes it to `Ablo(...)`. It is NOT
|
|
35
|
+
* read from `process.env.DATABASE_URL` — per this module's invariant
|
|
36
|
+
* (`ABLO_API_KEY` is the only environment fallback), an app's `DATABASE_URL`
|
|
37
|
+
* (commonly set for Prisma/Drizzle/docker) must never silently flip the client
|
|
38
|
+
* into connection-string mode. The default Data Source path keeps `DATABASE_URL`
|
|
39
|
+
* in the app and exposes `dataSource(...)`; that path leaves this null.
|
|
40
|
+
* `warnIfDatabaseUrlEnvIgnored` nudges callers who set the env but omitted the option.
|
|
37
41
|
*/
|
|
38
42
|
export function resolveDatabaseUrl(input) {
|
|
39
|
-
return input.options.databaseUrl ??
|
|
43
|
+
return input.options.databaseUrl ?? null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* One-time migration nudge for the dropped `DATABASE_URL` env fallback.
|
|
47
|
+
*
|
|
48
|
+
* Earlier versions silently adopted `process.env.DATABASE_URL` when `databaseUrl`
|
|
49
|
+
* was not passed, registering a direct connector behind the caller's back — which
|
|
50
|
+
* surprised any app that keeps `DATABASE_URL` for another tool (Prisma, Drizzle,
|
|
51
|
+
* docker-compose) and, on localhost, tried to register a database Ablo's cloud
|
|
52
|
+
* cannot reach. The env value is now ignored; this points the developer at the
|
|
53
|
+
* explicit option instead of flipping their mode for them. Warns once per process
|
|
54
|
+
* so it never spams, and falls back to `console.warn` when no logger is supplied
|
|
55
|
+
* (the `transport: 'api'` client has none).
|
|
56
|
+
*/
|
|
57
|
+
let warnedDatabaseUrlEnvIgnored = false;
|
|
58
|
+
export function warnIfDatabaseUrlEnvIgnored(input, warn) {
|
|
59
|
+
if (warnedDatabaseUrlEnvIgnored)
|
|
60
|
+
return;
|
|
61
|
+
if (input.options.databaseUrl != null)
|
|
62
|
+
return;
|
|
63
|
+
const envUrl = input.env.DATABASE_URL;
|
|
64
|
+
if (typeof envUrl !== 'string' || envUrl.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
warnedDatabaseUrlEnvIgnored = true;
|
|
67
|
+
const message = 'Found DATABASE_URL in the environment but `databaseUrl` was not passed to Ablo(...). ' +
|
|
68
|
+
'Ablo no longer auto-adopts DATABASE_URL — the environment value is ignored. ' +
|
|
69
|
+
'To register your Postgres directly, pass `databaseUrl: process.env.DATABASE_URL` explicitly; ' +
|
|
70
|
+
'otherwise ignore this (the hosted sandbox and signed Data Source endpoints need no databaseUrl).';
|
|
71
|
+
if (warn)
|
|
72
|
+
warn(message);
|
|
73
|
+
else if (typeof console !== 'undefined')
|
|
74
|
+
console.warn('[sync]', message);
|
|
40
75
|
}
|
|
41
76
|
export const ABLO_HOSTED_API_DOMAIN = 'api.abloatai.com';
|
|
42
77
|
export const ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
|
|
@@ -136,6 +136,31 @@ export interface ModelCollaboration<T> {
|
|
|
136
136
|
* from "someone else holds it" in `claimOrWait`.
|
|
137
137
|
*/
|
|
138
138
|
readonly selfParticipantId: string;
|
|
139
|
+
/**
|
|
140
|
+
* The local participant's kind (`'user' | 'agent' | 'system'`). Used to
|
|
141
|
+
* stamp the synthesized self-claim returned from `claim.state` when the
|
|
142
|
+
* LOCAL proxy holds the lease (server presence frames exclude one's own
|
|
143
|
+
* claims, so the holder must build its own view).
|
|
144
|
+
*/
|
|
145
|
+
readonly selfParticipantKind?: 'user' | 'agent' | 'system';
|
|
146
|
+
/**
|
|
147
|
+
* Subscribe the connection to a scope's sync group(s) (read-interest).
|
|
148
|
+
* The typed surface calls this on single-entity reads/claim observation so
|
|
149
|
+
* a Node/agent client lands in the SAME entity-scoped group the holder's
|
|
150
|
+
* claim presence fans out on — otherwise a peer subscribed only to
|
|
151
|
+
* `org:`/`user:` groups never sees claim broadcasts. Fire-and-forget and
|
|
152
|
+
* SOFT: read interest is best-effort and must never make a read reject or
|
|
153
|
+
* stall (see `AreaOfInterestManager.reconcile`). Optional so minimal test
|
|
154
|
+
* doubles can omit it. Forwards to `BaseSyncedStore.enterScope`.
|
|
155
|
+
*/
|
|
156
|
+
enterScope?(scope: Record<string, string>): void | Promise<void>;
|
|
157
|
+
/**
|
|
158
|
+
* Pin a scope's sync group(s) (write-intent / prominence): a row this
|
|
159
|
+
* client holds an active claim on stays subscribed regardless of
|
|
160
|
+
* navigation. Same fire-and-forget, soft semantics as `enterScope`.
|
|
161
|
+
* Forwards to `BaseSyncedStore.pinScope`.
|
|
162
|
+
*/
|
|
163
|
+
pinScope?(scope: Record<string, string>): void | Promise<void>;
|
|
139
164
|
}
|
|
140
165
|
export interface ClaimTargetOptions<T = Record<string, unknown>> {
|
|
141
166
|
/** Phase shown to observers while held. Defaults to `'editing'`. */
|