@abloatai/ablo 0.5.1 → 0.7.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 +61 -0
- package/README.md +248 -124
- package/dist/BaseSyncedStore.d.ts +3 -3
- package/dist/BaseSyncedStore.js +3 -3
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.js +1 -1
- package/dist/client/Ablo.d.ts +91 -93
- package/dist/client/Ablo.js +122 -60
- package/dist/client/ApiClient.d.ts +14 -14
- package/dist/client/ApiClient.js +81 -55
- package/dist/client/createInternalComponents.d.ts +2 -3
- package/dist/client/createInternalComponents.js +2 -3
- package/dist/client/createModelProxy.d.ts +116 -90
- package/dist/client/createModelProxy.js +128 -128
- package/dist/client/index.d.ts +6 -7
- package/dist/client/index.js +4 -5
- package/dist/client/validateAbloOptions.js +5 -5
- package/dist/coordination/index.d.ts +6 -0
- package/dist/coordination/index.js +6 -0
- package/dist/coordination/schema.d.ts +329 -0
- package/dist/coordination/schema.js +209 -0
- package/dist/core/QueryView.d.ts +4 -1
- package/dist/core/QueryView.js +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +7 -0
- package/dist/core/query-utils.d.ts +7 -10
- package/dist/core/query-utils.js +2 -3
- package/dist/errorCodes.d.ts +264 -0
- package/dist/errorCodes.js +251 -0
- package/dist/errors.d.ts +59 -14
- package/dist/errors.js +73 -12
- package/dist/index.d.ts +11 -9
- package/dist/index.js +8 -12
- package/dist/interfaces/index.d.ts +2 -10
- package/dist/mutators/Transaction.d.ts +2 -2
- package/dist/mutators/Transaction.js +2 -2
- package/dist/mutators/mutateActions.d.ts +44 -0
- package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
- package/dist/mutators/readerActions.d.ts +32 -0
- package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/index.js +1 -1
- package/dist/policy/types.d.ts +31 -0
- package/dist/policy/types.js +15 -0
- package/dist/query/types.d.ts +1 -1
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +14 -6
- package/dist/react/context.d.ts +4 -4
- package/dist/react/index.d.ts +4 -5
- package/dist/react/index.js +3 -7
- package/dist/react/useAblo.d.ts +14 -14
- package/dist/react/useAblo.js +26 -26
- package/dist/react/useIntent.d.ts +2 -2
- package/dist/react/useIntent.js +2 -2
- package/dist/react/useMutators.d.ts +1 -1
- package/dist/react/usePresence.d.ts +3 -3
- package/dist/react/usePresence.js +4 -4
- package/dist/react/useUndoScope.d.ts +1 -1
- package/dist/schema/ddl.d.ts +62 -0
- package/dist/schema/ddl.js +317 -0
- package/dist/schema/diff.d.ts +167 -0
- package/dist/schema/diff.js +280 -0
- package/dist/schema/field.d.ts +16 -19
- package/dist/schema/field.js +30 -17
- package/dist/schema/generate.d.ts +19 -0
- package/dist/schema/generate.js +87 -0
- package/dist/schema/index.d.ts +9 -3
- package/dist/schema/index.js +14 -2
- package/dist/schema/model.d.ts +87 -25
- package/dist/schema/model.js +33 -3
- package/dist/schema/relation.d.ts +17 -0
- package/dist/schema/roles.d.ts +148 -0
- package/dist/schema/roles.js +149 -0
- package/dist/schema/schema.d.ts +10 -69
- package/dist/schema/schema.js +58 -24
- package/dist/schema/select.d.ts +25 -0
- package/dist/schema/select.js +55 -0
- package/dist/schema/serialize.d.ts +96 -0
- package/dist/schema/serialize.js +231 -0
- package/dist/schema/sugar.d.ts +20 -3
- package/dist/schema/sugar.js +5 -1
- package/dist/schema/tenancy.d.ts +66 -0
- package/dist/schema/tenancy.js +58 -0
- package/dist/sync/HydrationCoordinator.d.ts +2 -0
- package/dist/sync/HydrationCoordinator.js +23 -17
- package/dist/sync/SyncWebSocket.d.ts +17 -0
- package/dist/sync/SyncWebSocket.js +46 -1
- package/dist/sync/awaitIntentGrant.d.ts +26 -0
- package/dist/sync/awaitIntentGrant.js +60 -0
- package/dist/sync/createIntentStream.d.ts +2 -1
- package/dist/sync/createIntentStream.js +89 -5
- package/dist/sync/createPresenceStream.js +1 -1
- package/dist/sync/participants.d.ts +2 -2
- package/dist/sync/participants.js +9 -18
- package/dist/types/global.d.ts +43 -52
- package/dist/types/global.js +16 -18
- package/dist/types/streams.d.ts +90 -42
- package/docs/api-keys.md +44 -0
- package/docs/api.md +72 -173
- package/docs/audit.md +5 -5
- package/docs/cli.md +212 -0
- package/docs/client-behavior.md +42 -43
- package/docs/coordination.md +343 -0
- package/docs/data-sources.md +16 -16
- package/docs/examples/agent-human.md +30 -32
- package/docs/examples/ai-sdk-tool.md +32 -33
- package/docs/examples/existing-python-backend.md +38 -36
- package/docs/examples/nextjs.md +24 -25
- package/docs/examples/scoped-agent.md +78 -0
- package/docs/examples/server-agent.md +20 -61
- package/docs/guarantees.md +34 -56
- package/docs/identity.md +529 -0
- package/docs/index.md +18 -24
- package/docs/integration-guide.md +130 -144
- package/docs/interaction-model.md +32 -95
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +1 -1
- package/docs/mcp.md +11 -26
- package/docs/quickstart.md +43 -49
- package/docs/react.md +74 -24
- package/docs/roadmap.md +17 -7
- package/llms.txt +34 -39
- package/package.json +8 -1
- package/dist/react/useMutate.d.ts +0 -83
- package/dist/react/useQuery.d.ts +0 -123
- package/dist/react/useQuery.js +0 -145
- package/dist/react/useReader.d.ts +0 -69
- package/docs/capabilities.md +0 -163
package/dist/types/global.d.ts
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Type registration point for SDK consumers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `usePresence`, `useIntent` — reads its types from the
|
|
7
|
-
* No generics at call sites
|
|
4
|
+
* A consumer registers their Schema, Presence, Intents, and UserMeta ONCE by
|
|
5
|
+
* augmenting the {@link Register} interface, and every SDK hook — `useAblo`,
|
|
6
|
+
* `useQuery`, `useOne`, `usePresence`, `useIntent` — reads its types from the
|
|
7
|
+
* resolved registration. No generics at call sites, no `schema` arg per call.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* and every consumer of the resolved types below picks up the augmentation
|
|
15
|
-
* automatically.
|
|
9
|
+
* Registration is done via **module augmentation** of `@abloatai/ablo` —
|
|
10
|
+
* the same pattern TanStack Router uses for its `Register` interface. The brand
|
|
11
|
+
* lives in the module specifier, so the interface is just `Register` (not a
|
|
12
|
+
* global, not prefixed). It's a language feature, not a library trick: any file
|
|
13
|
+
* in the compilation can augment it and every resolver below picks it up.
|
|
16
14
|
*
|
|
17
15
|
* Consumer example:
|
|
18
16
|
*
|
|
19
17
|
* ```ts
|
|
20
|
-
* // apps/your-app/src/ablo
|
|
18
|
+
* // apps/your-app/src/ablo.d.ts
|
|
21
19
|
* import type { schema } from './your-schema';
|
|
22
20
|
*
|
|
23
|
-
* declare
|
|
24
|
-
* interface
|
|
21
|
+
* declare module '@abloatai/ablo' {
|
|
22
|
+
* interface Register {
|
|
25
23
|
* Schema: typeof schema;
|
|
26
24
|
* Presence: { cursor: { x: number; y: number } | null };
|
|
27
25
|
* Intents: { editLayer: { layerId: string } };
|
|
@@ -31,17 +29,15 @@
|
|
|
31
29
|
* export {};
|
|
32
30
|
* ```
|
|
33
31
|
*
|
|
34
|
-
* If `
|
|
35
|
-
*
|
|
36
|
-
*
|
|
32
|
+
* If `Register` is never augmented, every resolver falls back to
|
|
33
|
+
* {@link DefaultSyncShape} — a loose shape that keeps consumers compiling
|
|
34
|
+
* without typed benefits until they opt in.
|
|
37
35
|
*/
|
|
38
36
|
/**
|
|
39
|
-
* Default fallback shapes used when the consumer hasn't
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* without producing a typed entity shape. Once the consumer augments the
|
|
44
|
-
* global, every resolver below picks up the augmented types automatically.
|
|
37
|
+
* Default fallback shapes used when the consumer hasn't augmented
|
|
38
|
+
* {@link Register}. `DefaultSyncShape.Schema` is intentionally structural — it
|
|
39
|
+
* carries `{ models: Record<string, unknown> }` so hooks can still validate the
|
|
40
|
+
* model key argument against *something*, just without a typed entity shape.
|
|
45
41
|
*/
|
|
46
42
|
export interface DefaultSyncShape {
|
|
47
43
|
readonly Schema: {
|
|
@@ -53,54 +49,49 @@ export interface DefaultSyncShape {
|
|
|
53
49
|
readonly id: string;
|
|
54
50
|
};
|
|
55
51
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
52
|
+
/**
|
|
53
|
+
* The registration interface. Consumers augment it via
|
|
54
|
+
* `declare module '@abloatai/ablo' { interface Register { Schema: ...; … } }`.
|
|
55
|
+
* Empty by default — every SDK resolver falls back to {@link DefaultSyncShape}
|
|
56
|
+
* when an expected key is absent. Exported from the package root so the module
|
|
57
|
+
* augmentation merges into this declaration.
|
|
58
|
+
*/
|
|
59
|
+
export interface Register {
|
|
65
60
|
}
|
|
66
61
|
/**
|
|
67
|
-
* The consumer's schema, or the default shape if
|
|
68
|
-
*
|
|
69
|
-
* returned from queries/mutations.
|
|
62
|
+
* The consumer's schema, or the default shape if unregistered. Hooks use this
|
|
63
|
+
* to type their model-key argument and infer the entity type returned.
|
|
70
64
|
*/
|
|
71
|
-
export type ResolveSchema =
|
|
65
|
+
export type ResolveSchema = Register extends {
|
|
72
66
|
Schema: infer S;
|
|
73
67
|
} ? S extends {
|
|
74
68
|
models: Record<string, unknown>;
|
|
75
69
|
} ? S : DefaultSyncShape['Schema'] : DefaultSyncShape['Schema'];
|
|
76
70
|
/**
|
|
77
|
-
* The consumer's presence shape, or the default
|
|
78
|
-
*
|
|
79
|
-
* the consumer wants to broadcast per session.
|
|
71
|
+
* The consumer's presence shape, or the default if unregistered. Used by
|
|
72
|
+
* `usePresence`. Free-form — any serializable JSON broadcast per session.
|
|
80
73
|
*/
|
|
81
|
-
export type ResolvePresence =
|
|
74
|
+
export type ResolvePresence = Register extends {
|
|
82
75
|
Presence: infer P;
|
|
83
76
|
} ? P : DefaultSyncShape['Presence'];
|
|
84
77
|
/**
|
|
85
|
-
* The consumer's intent vocabulary, or the default if
|
|
86
|
-
*
|
|
87
|
-
*
|
|
78
|
+
* The consumer's intent vocabulary, or the default if unregistered. Keys are
|
|
79
|
+
* intent names; values are the claim payload for each intent. Used by
|
|
80
|
+
* `useIntent(intentName)`.
|
|
88
81
|
*/
|
|
89
|
-
export type ResolveIntents =
|
|
82
|
+
export type ResolveIntents = Register extends {
|
|
90
83
|
Intents: infer I;
|
|
91
84
|
} ? I : DefaultSyncShape['Intents'];
|
|
92
85
|
/**
|
|
93
|
-
* The consumer's user-metadata shape, or the default if
|
|
94
|
-
*
|
|
95
|
-
* the SDK doesn't validate this.
|
|
86
|
+
* The consumer's user-metadata shape, or the default if unregistered. Carries
|
|
87
|
+
* identity info the consumer trusts from their auth layer — not SDK-validated.
|
|
96
88
|
*/
|
|
97
|
-
export type ResolveUserMeta =
|
|
89
|
+
export type ResolveUserMeta = Register extends {
|
|
98
90
|
UserMeta: infer U;
|
|
99
91
|
} ? U : DefaultSyncShape['UserMeta'];
|
|
100
92
|
/**
|
|
101
|
-
* The keys of the consumer's schema models. `useQuery(modelKey)` narrows
|
|
102
|
-
*
|
|
103
|
-
* compile time.
|
|
93
|
+
* The keys of the consumer's schema models. `useQuery(modelKey)` narrows its
|
|
94
|
+
* first argument to this union, so unknown key literals fail at compile time.
|
|
104
95
|
*/
|
|
105
96
|
export type ResolveModelKey = ResolveSchema extends {
|
|
106
97
|
models: infer M;
|
package/dist/types/global.js
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Type registration point for SDK consumers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `usePresence`, `useIntent` — reads its types from the
|
|
7
|
-
* No generics at call sites
|
|
4
|
+
* A consumer registers their Schema, Presence, Intents, and UserMeta ONCE by
|
|
5
|
+
* augmenting the {@link Register} interface, and every SDK hook — `useAblo`,
|
|
6
|
+
* `useQuery`, `useOne`, `usePresence`, `useIntent` — reads its types from the
|
|
7
|
+
* resolved registration. No generics at call sites, no `schema` arg per call.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* and every consumer of the resolved types below picks up the augmentation
|
|
15
|
-
* automatically.
|
|
9
|
+
* Registration is done via **module augmentation** of `@abloatai/ablo` —
|
|
10
|
+
* the same pattern TanStack Router uses for its `Register` interface. The brand
|
|
11
|
+
* lives in the module specifier, so the interface is just `Register` (not a
|
|
12
|
+
* global, not prefixed). It's a language feature, not a library trick: any file
|
|
13
|
+
* in the compilation can augment it and every resolver below picks it up.
|
|
16
14
|
*
|
|
17
15
|
* Consumer example:
|
|
18
16
|
*
|
|
19
17
|
* ```ts
|
|
20
|
-
* // apps/your-app/src/ablo
|
|
18
|
+
* // apps/your-app/src/ablo.d.ts
|
|
21
19
|
* import type { schema } from './your-schema';
|
|
22
20
|
*
|
|
23
|
-
* declare
|
|
24
|
-
* interface
|
|
21
|
+
* declare module '@abloatai/ablo' {
|
|
22
|
+
* interface Register {
|
|
25
23
|
* Schema: typeof schema;
|
|
26
24
|
* Presence: { cursor: { x: number; y: number } | null };
|
|
27
25
|
* Intents: { editLayer: { layerId: string } };
|
|
@@ -31,8 +29,8 @@
|
|
|
31
29
|
* export {};
|
|
32
30
|
* ```
|
|
33
31
|
*
|
|
34
|
-
* If `
|
|
35
|
-
*
|
|
36
|
-
*
|
|
32
|
+
* If `Register` is never augmented, every resolver falls back to
|
|
33
|
+
* {@link DefaultSyncShape} — a loose shape that keeps consumers compiling
|
|
34
|
+
* without typed benefits until they opt in.
|
|
37
35
|
*/
|
|
38
36
|
export {};
|
package/dist/types/streams.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* the shared coordination substrate.
|
|
10
10
|
*/
|
|
11
11
|
import type { InferModel, Schema } from '../schema/schema.js';
|
|
12
|
+
import type { TargetRange, OnStaleMode, IntentClaim, PresenceKind } from '../coordination/schema.js';
|
|
13
|
+
export type { TargetRange, OnStaleMode, IntentClaim, PresenceKind };
|
|
12
14
|
/**
|
|
13
15
|
* Any JSON-serializable value. Used where the SDK accepts free-form
|
|
14
16
|
* metadata that will be persisted / transported as JSON — avoids
|
|
@@ -113,13 +115,6 @@ export interface ContextChange {
|
|
|
113
115
|
* snapshot. Defaults to `'reject'` when `readAt` is provided without
|
|
114
116
|
* `onStale`.
|
|
115
117
|
*/
|
|
116
|
-
export type OnStaleMode = 'reject' | 'flag' | 'merge' | 'force';
|
|
117
|
-
export interface TargetRange {
|
|
118
|
-
readonly startLine: number;
|
|
119
|
-
readonly endLine: number;
|
|
120
|
-
readonly startColumn?: number;
|
|
121
|
-
readonly endColumn?: number;
|
|
122
|
-
}
|
|
123
118
|
/**
|
|
124
119
|
* A pointer to one entity, optionally narrowed to a structured
|
|
125
120
|
* subtarget. `type` and `id` are customer schema vocabulary; `path`,
|
|
@@ -175,7 +170,7 @@ export interface PresenceStream {
|
|
|
175
170
|
/**
|
|
176
171
|
* Reactive view of every OTHER participant's current activity on
|
|
177
172
|
* this participant's sync groups. Reads return the current snapshot;
|
|
178
|
-
* pair with `
|
|
173
|
+
* pair with `onChange(listener)` below to get notified on changes.
|
|
179
174
|
*
|
|
180
175
|
* An LLM pipeline can include `presence.others` in its system prompt
|
|
181
176
|
* so the model literally reasons with knowledge of what other
|
|
@@ -209,7 +204,7 @@ export interface PresenceStream {
|
|
|
209
204
|
* });
|
|
210
205
|
* ```
|
|
211
206
|
*/
|
|
212
|
-
|
|
207
|
+
onChange(listener: () => void): () => void;
|
|
213
208
|
/**
|
|
214
209
|
* Async-iterable view of the peer roster. Each iteration yields the
|
|
215
210
|
* current `others` snapshot on every mutation — so the consumer
|
|
@@ -304,32 +299,6 @@ export interface Peer {
|
|
|
304
299
|
/** Pending-mutation intents this participant has declared. */
|
|
305
300
|
readonly activeIntents?: ReadonlyArray<IntentClaim>;
|
|
306
301
|
}
|
|
307
|
-
/**
|
|
308
|
-
* Pending-mutation intent on the wire. Declared via `intent_begin`,
|
|
309
|
-
* cleared on `intent_abandon` / commit / disconnect / TTL expiry.
|
|
310
|
-
* Server stamps `declaredAt` and `expiresAt` (ms epoch). The SDK's
|
|
311
|
-
* `IntentStream.others` exposes a richer `ActiveIntent` view (defined
|
|
312
|
-
* below) that adds `heldBy` so callers know which participant owns it.
|
|
313
|
-
*/
|
|
314
|
-
export interface IntentClaim {
|
|
315
|
-
readonly intentId: string;
|
|
316
|
-
readonly entityType: string;
|
|
317
|
-
readonly entityId: string;
|
|
318
|
-
readonly path?: string;
|
|
319
|
-
readonly range?: TargetRange;
|
|
320
|
-
readonly action: string;
|
|
321
|
-
readonly field?: string;
|
|
322
|
-
readonly meta?: Record<string, unknown>;
|
|
323
|
-
readonly declaredAt: number;
|
|
324
|
-
readonly expiresAt: number;
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Transition type carried on every presence frame from the server.
|
|
328
|
-
* - `'enter'` — first frame the receiver sees for this peer.
|
|
329
|
-
* - `'update'` — activity / intent change on an already-known peer.
|
|
330
|
-
* - `'leave'` — peer departed (explicit disconnect or TTL expiry).
|
|
331
|
-
*/
|
|
332
|
-
export type PresenceKind = 'enter' | 'update' | 'leave';
|
|
333
302
|
/** Outbound `presence_update` payload. */
|
|
334
303
|
export interface PresenceUpdatePayload {
|
|
335
304
|
readonly status: 'online' | 'away' | 'offline' | (string & {});
|
|
@@ -371,6 +340,15 @@ export interface ClaimOptions extends IntentOptions {
|
|
|
371
340
|
* app-specific phases.
|
|
372
341
|
*/
|
|
373
342
|
readonly reason?: string;
|
|
343
|
+
/**
|
|
344
|
+
* Join the server's fair FIFO queue on contention instead of being
|
|
345
|
+
* rejected. The grant arrives asynchronously (`intent_acquired` if the
|
|
346
|
+
* target was free, `intent_granted` once promoted to the head of the line).
|
|
347
|
+
* The low-level `claim` returns its handle immediately regardless; callers
|
|
348
|
+
* that need to *wait* for the grant use the awaiting wrappers
|
|
349
|
+
* (`ablo.<model>.claim`), which pair this flag with `awaitIntentGrant`.
|
|
350
|
+
*/
|
|
351
|
+
readonly queue?: boolean;
|
|
374
352
|
}
|
|
375
353
|
export interface IntentStream {
|
|
376
354
|
/**
|
|
@@ -394,6 +372,23 @@ export interface IntentStream {
|
|
|
394
372
|
* below to get notified on change.
|
|
395
373
|
*/
|
|
396
374
|
readonly others: ReadonlyArray<ActiveIntent>;
|
|
375
|
+
/**
|
|
376
|
+
* Reactive view of the wait queue on one target — the FIFO line of
|
|
377
|
+
* `status: 'queued'` intents behind the current holder, each with its
|
|
378
|
+
* `action`, `heldBy`, and `position`. Synced from the server's per-entity
|
|
379
|
+
* `intent_queue` frame; empty when no one's waiting. Pair with
|
|
380
|
+
* `subscribe(...)` for change notifications.
|
|
381
|
+
*/
|
|
382
|
+
queueFor(target: PresenceTarget): readonly Intent[];
|
|
383
|
+
/**
|
|
384
|
+
* Re-rank the wait queue on a target — move the listed waiters to the front
|
|
385
|
+
* in the given order; unlisted waiters keep their relative FIFO order behind
|
|
386
|
+
* them. Pass the `Intent[]` from `queueFor(target)` in the order you want
|
|
387
|
+
* (each `Intent` carries its `heldBy` + `id`). Privileged: the server gates
|
|
388
|
+
* it (a participant lacking the `intent.reorder` capability is denied), so
|
|
389
|
+
* this is fire-and-forget — the new order arrives reactively via `queueFor`.
|
|
390
|
+
*/
|
|
391
|
+
reorder(target: PresenceTarget, order: readonly Intent[]): void;
|
|
397
392
|
/**
|
|
398
393
|
* Framework-agnostic reactivity. Same contract as
|
|
399
394
|
* `PresenceStream.subscribe` — register a listener fired on every
|
|
@@ -401,7 +396,7 @@ export interface IntentStream {
|
|
|
401
396
|
* returns an unsubscribe fn. Use `useSyncExternalStore` in React or
|
|
402
397
|
* `autorun` in MobX.
|
|
403
398
|
*/
|
|
404
|
-
|
|
399
|
+
onChange(listener: () => void): () => void;
|
|
405
400
|
/**
|
|
406
401
|
* Observe server-side intent rejections. Fires when the server
|
|
407
402
|
* rejects an `intents.writing(...)` / `announce(...)` call because
|
|
@@ -418,6 +413,23 @@ export interface IntentStream {
|
|
|
418
413
|
* Returns an unsubscribe fn.
|
|
419
414
|
*/
|
|
420
415
|
onRejected(listener: (rejection: IntentRejection) => void): () => void;
|
|
416
|
+
/**
|
|
417
|
+
* Observe LOSING an intent you held — distinct from `onRejected` (a claim the
|
|
418
|
+
* server refused). Fires on the server's `intent_lost` frame, carrying why:
|
|
419
|
+
* `'preempted'` (a privileged participant evicted you) or `'expired'` (your
|
|
420
|
+
* TTL lapsed). Lets a holder react — re-plan vs re-claim — instead of
|
|
421
|
+
* silently discovering the lease gone via presence.
|
|
422
|
+
*
|
|
423
|
+
* ```ts
|
|
424
|
+
* participant.intents.onLost((lost) => {
|
|
425
|
+
* if (lost.reason === 'preempted') replanAgainst(lost.target);
|
|
426
|
+
* else reclaim(lost.target);
|
|
427
|
+
* });
|
|
428
|
+
* ```
|
|
429
|
+
*
|
|
430
|
+
* Returns an unsubscribe fn.
|
|
431
|
+
*/
|
|
432
|
+
onLost(listener: (lost: IntentLost) => void): () => void;
|
|
421
433
|
/**
|
|
422
434
|
* Async-iterable view of everyone else's open intents. Each
|
|
423
435
|
* iteration yields the current snapshot on every mutation.
|
|
@@ -456,6 +468,31 @@ export interface IntentRejection {
|
|
|
456
468
|
/** When the existing claim expires (ms since epoch). */
|
|
457
469
|
readonly heldByExpiresAt: number;
|
|
458
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* You LOST an intent you were HOLDING — distinct from `IntentRejection` (a
|
|
473
|
+
* claim the server refused you). Delivered via `onLost`.
|
|
474
|
+
*/
|
|
475
|
+
export interface IntentLost {
|
|
476
|
+
/** The held claim's id that you just lost. */
|
|
477
|
+
readonly intentId: string;
|
|
478
|
+
/**
|
|
479
|
+
* How you lost it. `'preempted'`: a privileged participant (one holding the
|
|
480
|
+
* `intent.preempt` capability) evicted you and took the lease — its work now
|
|
481
|
+
* supersedes yours, so re-plan against the new holder rather than blindly
|
|
482
|
+
* re-claiming. `'expired'`: your TTL lapsed without finishing — re-claim if
|
|
483
|
+
* you still need it.
|
|
484
|
+
*/
|
|
485
|
+
readonly reason: 'expired' | 'preempted';
|
|
486
|
+
/** The target you no longer hold. */
|
|
487
|
+
readonly target: {
|
|
488
|
+
readonly entityType: string;
|
|
489
|
+
readonly entityId: string;
|
|
490
|
+
readonly path?: string;
|
|
491
|
+
readonly range?: TargetRange;
|
|
492
|
+
readonly field?: string;
|
|
493
|
+
readonly meta?: Record<string, unknown>;
|
|
494
|
+
};
|
|
495
|
+
}
|
|
459
496
|
export interface IntentDeclaration {
|
|
460
497
|
readonly target: EntityRef;
|
|
461
498
|
/** Human-readable reason — "rewriting title" / "restyling chart". */
|
|
@@ -494,8 +531,13 @@ export interface ActiveIntent extends IntentDeclaration {
|
|
|
494
531
|
readonly announcedAt: string;
|
|
495
532
|
readonly expiresAt: string;
|
|
496
533
|
}
|
|
497
|
-
/**
|
|
498
|
-
|
|
534
|
+
/**
|
|
535
|
+
* Every lifecycle state of a coordination intent, in one enum.
|
|
536
|
+
* `active` = the current holder (the lock). `queued` = waiting in the FIFO
|
|
537
|
+
* line behind the holder (carries `position`). The terminal states drop the
|
|
538
|
+
* intent from the synced set.
|
|
539
|
+
*/
|
|
540
|
+
export type IntentStatus = 'active' | 'queued' | 'committed' | 'expired' | 'canceled';
|
|
499
541
|
/** Options for waiting on a target to become free. */
|
|
500
542
|
export interface IntentWaitOptions {
|
|
501
543
|
readonly timeout?: number;
|
|
@@ -509,10 +551,11 @@ export interface IntentWaitOptions {
|
|
|
509
551
|
*
|
|
510
552
|
* Deliberately omits a Stripe-style `next_action`: a contender's only
|
|
511
553
|
* response is "wait until free, then re-read", and the runtime performs
|
|
512
|
-
* that uniformly
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
* object exists
|
|
554
|
+
* that uniformly — `claim` serializes behind the holder via the server
|
|
555
|
+
* FIFO queue (or low-level `intents.waitFor` to wait without claiming), and the
|
|
556
|
+
* stale-context guard forces the re-read. Encoding a constant instruction
|
|
557
|
+
* the engine always takes would be the kind of ceremony this object exists
|
|
558
|
+
* to remove.
|
|
516
559
|
*/
|
|
517
560
|
export interface Intent {
|
|
518
561
|
readonly object: 'intent';
|
|
@@ -532,4 +575,9 @@ export interface Intent {
|
|
|
532
575
|
readonly createdAt?: string;
|
|
533
576
|
/** Ms-epoch the server auto-expires it if the holder doesn't finish. */
|
|
534
577
|
readonly expiresAt: string;
|
|
578
|
+
/**
|
|
579
|
+
* 0-based place in the FIFO line — present only when `status: 'queued'`
|
|
580
|
+
* (`0` = next in line behind the holder). Absent for the active holder.
|
|
581
|
+
*/
|
|
582
|
+
readonly position?: number;
|
|
535
583
|
}
|
package/docs/api-keys.md
CHANGED
|
@@ -22,3 +22,47 @@ Use API keys from trusted runtimes:
|
|
|
22
22
|
- webhooks
|
|
23
23
|
|
|
24
24
|
Never ship a secret API key to a browser bundle.
|
|
25
|
+
|
|
26
|
+
## Test mode and sandboxes
|
|
27
|
+
|
|
28
|
+
Test and live keys are the same shape; the prefix names the environment:
|
|
29
|
+
|
|
30
|
+
- `sk_test_…` — a key bound to a **sandbox**. Its reads and writes are isolated
|
|
31
|
+
to that sandbox and are invisible to live keys (and to other sandboxes).
|
|
32
|
+
- `sk_live_…` — a key against your live data.
|
|
33
|
+
|
|
34
|
+
Every org has a default **Test mode** sandbox, plus any number of additional
|
|
35
|
+
sandboxes you create. **Data is isolated per sandbox; the schema is shared
|
|
36
|
+
across the whole org.** A schema you push from a test key defines the same
|
|
37
|
+
models your live keys see — only the rows differ. This mirrors how Stripe
|
|
38
|
+
separates test and live data while keeping the API shape identical.
|
|
39
|
+
|
|
40
|
+
## Scopes
|
|
41
|
+
|
|
42
|
+
Keys carry scopes following the principle of least privilege — each key gets
|
|
43
|
+
only what its job needs. A secret key with **no scopes** has full org authority
|
|
44
|
+
(the default for a `sk_live_` backend key); a key with a non-empty scope set is
|
|
45
|
+
restricted to exactly those grants:
|
|
46
|
+
|
|
47
|
+
- `schema:push` — author the org schema (`ablo schema push`, `ablo dev`). A
|
|
48
|
+
high-risk, org-wide grant: because schema is shared, a push affects the live
|
|
49
|
+
table shape. A full-authority key has it implicitly; a *restricted* key (such
|
|
50
|
+
as a sandbox key) needs it granted explicitly.
|
|
51
|
+
- `sandbox:<id>` — marks the key as belonging to a sandbox (its data isolation
|
|
52
|
+
comes from the sandbox binding, not this scope string).
|
|
53
|
+
|
|
54
|
+
A key minted from the default **Test mode** sandbox carries `schema:push`, so
|
|
55
|
+
`ablo dev` works out of the box. Keys from other sandboxes are **data-only** by
|
|
56
|
+
default — enable "schema authoring" when minting if you want that key to push
|
|
57
|
+
schema too. Hand data-only keys to embedded apps and CI agents; reserve
|
|
58
|
+
schema-authoring keys for the developer running `ablo dev`.
|
|
59
|
+
|
|
60
|
+
### `ablo dev`
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
ABLO_API_KEY=sk_test_… npx ablo dev
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Pushes your `ablo/schema.ts` to the test sandbox, prints the one line you need
|
|
67
|
+
in `.env.local`, and re-pushes on every save. It refuses `sk_live_` keys so a
|
|
68
|
+
tight save loop can never churn production data.
|