@abloatai/ablo 0.3.1 → 0.5.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 +54 -1
- package/NOTICE +2 -2
- package/README.md +99 -78
- package/dist/BaseSyncedStore.d.ts +3 -2
- package/dist/agent/Agent.d.ts +1 -1
- package/dist/agent/Agent.js +1 -1
- package/dist/agent/index.d.ts +4 -4
- package/dist/agent/index.js +6 -6
- package/dist/agent/types.d.ts +1 -1
- package/dist/ai-sdk/index.d.ts +3 -3
- package/dist/ai-sdk/index.js +3 -3
- package/dist/ai-sdk/intent-broadcast.d.ts +1 -1
- package/dist/ai-sdk/intent-broadcast.js +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/client/Ablo.d.ts +53 -27
- package/dist/client/Ablo.js +32 -1
- package/dist/client/auth.d.ts +3 -3
- package/dist/client/auth.js +5 -5
- package/dist/client/createModelProxy.d.ts +118 -32
- package/dist/client/createModelProxy.js +87 -44
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.js +3 -3
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +2 -2
- package/dist/errors.d.ts +9 -7
- package/dist/errors.js +9 -7
- package/dist/index.d.ts +20 -6
- package/dist/index.js +41 -22
- package/dist/interfaces/headless.d.ts +1 -1
- package/dist/interfaces/headless.js +2 -2
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +2 -2
- package/dist/policy/types.d.ts +10 -0
- package/dist/principal.d.ts +3 -3
- package/dist/principal.js +3 -3
- package/dist/query/client.d.ts +7 -6
- package/dist/react/AbloProvider.d.ts +44 -1
- package/dist/react/AbloProvider.js +3 -1
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/SyncGroupProvider.js +1 -1
- package/dist/react/context.d.ts +1 -1
- package/dist/react/context.js +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutate.d.ts +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useReader.d.ts +1 -1
- package/dist/schema/field.d.ts +1 -1
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +2 -2
- package/dist/schema/model.d.ts +2 -2
- package/dist/schema/model.js +2 -2
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -1
- package/dist/schema/relation.d.ts +1 -1
- package/dist/schema/relation.js +1 -1
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/source/index.d.ts +22 -28
- package/dist/source/index.js +23 -20
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +2 -2
- package/dist/sync/SyncWebSocket.d.ts +20 -5
- package/dist/sync/createIntentStream.js +7 -0
- package/dist/testing/fixtures/models.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -1
- package/dist/testing/helpers/react-wrapper.d.ts +2 -2
- package/dist/testing/helpers/react-wrapper.js +2 -2
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/types/streams.d.ts +41 -1
- package/docs/api.md +78 -20
- package/docs/data-sources.md +50 -16
- package/docs/examples/ai-sdk-tool.md +14 -31
- package/docs/examples/existing-python-backend.md +6 -6
- package/docs/integration-guide.md +8 -7
- package/docs/interaction-model.md +16 -4
- package/docs/mcp.md +1 -1
- package/docs/quickstart.md +20 -18
- package/examples/data-source/README.md +1 -1
- package/examples/data-source/ablo-driver.ts +5 -5
- package/examples/data-source/customer-server.ts +10 -10
- package/examples/data-source/run.ts +9 -11
- package/examples/data-source/schema.ts +1 -1
- package/examples/quickstart.ts +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
package/dist/client/Ablo.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* bootstrap, offline queue, DI adapters) behind a single function call.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
* import { Ablo } from '@ablo/
|
|
8
|
+
* import { Ablo } from '@abloatai/ablo/client';
|
|
9
9
|
* import { schema } from './schema';
|
|
10
10
|
*
|
|
11
11
|
* const sync = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
@@ -21,7 +21,7 @@ import { ObjectPool } from '../ObjectPool.js';
|
|
|
21
21
|
import type { SyncStoreContract } from '../react/context.js';
|
|
22
22
|
import type { SyncWebSocket } from '../sync/SyncWebSocket.js';
|
|
23
23
|
import { type SyncStatus } from '../BaseSyncedStore.js';
|
|
24
|
-
import type { IntentStream, PresenceStream, Snapshot } from '../types/streams.js';
|
|
24
|
+
import type { IntentStream, IntentWaitOptions, PresenceStream, Snapshot } from '../types/streams.js';
|
|
25
25
|
import type { ParticipantManager } from '../sync/participants.js';
|
|
26
26
|
import type { ActiveIntent, Duration, TargetRange } from '../types/streams.js';
|
|
27
27
|
import { type AbloApi, type AbloApiClientOptions, type AbloApiIntents } from './ApiClient.js';
|
|
@@ -48,34 +48,75 @@ export interface Turn {
|
|
|
48
48
|
* `ApiKeySetter` exactly so any rotation pattern that works with
|
|
49
49
|
* `@anthropic-ai/sdk` works here.
|
|
50
50
|
*
|
|
51
|
-
* Re-exported from `./auth` so existing import paths (`@ablo
|
|
51
|
+
* Re-exported from `./auth` so existing import paths (`@abloatai/ablo`)
|
|
52
52
|
* keep resolving; the canonical definition lives there alongside the
|
|
53
53
|
* resolvers that consume it.
|
|
54
54
|
*/
|
|
55
55
|
export type { ApiKeySetter } from './auth.js';
|
|
56
56
|
import type { ApiKeySetter } from './auth.js';
|
|
57
57
|
import { type AbloPersistence } from './persistence.js';
|
|
58
|
+
/**
|
|
59
|
+
* Options for `Ablo({...})`.
|
|
60
|
+
*
|
|
61
|
+
* The only required field is `schema`. The default path is one line:
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* `apiKey` itself defaults to `process.env.ABLO_API_KEY`, so in most
|
|
68
|
+
* server setups `Ablo({ schema })` is enough. Every other field is
|
|
69
|
+
* optional tuning (timeouts, retries, custom fetch, persistence) —
|
|
70
|
+
* if you're not sure whether you need one, you don't. Reach for them
|
|
71
|
+
* the way you'd reach for the equivalent option on the Stripe / OpenAI
|
|
72
|
+
* / Anthropic clients: rarely, and deliberately.
|
|
73
|
+
*
|
|
74
|
+
* @see https://docs.ablo.finance — full option reference
|
|
75
|
+
*/
|
|
58
76
|
export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
77
|
+
/**
|
|
78
|
+
* TypeScript schema defined with `defineSchema()`. Required — it's what
|
|
79
|
+
* makes `ablo.tasks.update(...)` typed. This is the one field you must
|
|
80
|
+
* pass; start here.
|
|
81
|
+
*/
|
|
82
|
+
schema: Schema<S>;
|
|
59
83
|
/**
|
|
60
84
|
* API key used for authentication.
|
|
61
85
|
*
|
|
62
86
|
* Accepts a static string (`sk_live_...`) or an async function that
|
|
63
|
-
* resolves to one. Defaults to `process.env['ABLO_API_KEY']
|
|
87
|
+
* resolves to one. Defaults to `process.env['ABLO_API_KEY']`, so you
|
|
88
|
+
* usually don't pass this explicitly server-side.
|
|
64
89
|
*/
|
|
65
90
|
apiKey?: string | ApiKeySetter | null | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Local persistence mode. Pass `indexeddb` only when you want offline
|
|
93
|
+
* queueing and a reload-surviving browser cache.
|
|
94
|
+
*
|
|
95
|
+
* @default 'volatile'
|
|
96
|
+
*/
|
|
97
|
+
persistence?: AbloPersistence;
|
|
66
98
|
/**
|
|
67
99
|
* Bearer auth token. Hosted-cloud consumers pass `apiKey`; self-hosted
|
|
68
100
|
* deployments may pass a bearer token minted by their own auth layer.
|
|
69
101
|
*/
|
|
70
102
|
authToken?: string | null | undefined;
|
|
71
103
|
/**
|
|
72
|
-
* Override the Ablo API base URL. Defaults to hosted production
|
|
73
|
-
* `process.env['ABLO_BASE_URL']` if unset.
|
|
104
|
+
* Override the Ablo API base URL. Defaults to hosted production.
|
|
74
105
|
*/
|
|
75
106
|
baseURL?: string | null | undefined;
|
|
76
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Maximum time (ms) to wait for a single request before timing out.
|
|
109
|
+
* Timed-out requests are retried, so worst-case wait can exceed this.
|
|
110
|
+
*
|
|
111
|
+
* @default 600_000
|
|
112
|
+
*/
|
|
77
113
|
timeout?: number | undefined;
|
|
78
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Maximum retries on transient failure (network error / 5xx / 429).
|
|
116
|
+
* Honors `Retry-After`.
|
|
117
|
+
*
|
|
118
|
+
* @default 2
|
|
119
|
+
*/
|
|
79
120
|
maxRetries?: number | undefined;
|
|
80
121
|
/** Custom fetch implementation for tests, proxies, or non-standard runtimes. */
|
|
81
122
|
fetch?: typeof fetch | undefined;
|
|
@@ -89,16 +130,6 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
89
130
|
* key or a controlled server proxy.
|
|
90
131
|
*/
|
|
91
132
|
dangerouslyAllowBrowser?: boolean | undefined;
|
|
92
|
-
/**
|
|
93
|
-
* TypeScript schema defined with `defineSchema()`. This enables typed
|
|
94
|
-
* resources such as `ablo.tasks.update(...)`.
|
|
95
|
-
*/
|
|
96
|
-
schema: Schema<S>;
|
|
97
|
-
/**
|
|
98
|
-
* Local persistence mode. Defaults to `volatile`. Pass `indexeddb` only
|
|
99
|
-
* when you want offline queueing and a reload-surviving browser cache.
|
|
100
|
-
*/
|
|
101
|
-
persistence?: AbloPersistence;
|
|
102
133
|
}
|
|
103
134
|
export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
104
135
|
/**
|
|
@@ -118,7 +149,7 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
118
149
|
apiKey?: string | ApiKeySetter | null | undefined;
|
|
119
150
|
/**
|
|
120
151
|
* Bearer auth token. Sent as `Authorization: Bearer <token>` on
|
|
121
|
-
* every request.
|
|
152
|
+
* every request.
|
|
122
153
|
*
|
|
123
154
|
* Use this for self-hosted deployments where your auth layer mints
|
|
124
155
|
* cap tokens directly. Hosted-cloud consumers pass `apiKey` instead;
|
|
@@ -129,7 +160,6 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
129
160
|
* Override the default base URL. Defaults to
|
|
130
161
|
* `wss://mesh.ablo.finance` for hosted production; pass an explicit
|
|
131
162
|
* URL for self-hosted or staging (e.g. `wss://mesh-staging.ablo.finance`).
|
|
132
|
-
* Reads `process.env['ABLO_BASE_URL']` if unset.
|
|
133
163
|
*/
|
|
134
164
|
baseURL?: string | null | undefined;
|
|
135
165
|
/**
|
|
@@ -327,7 +357,7 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
327
357
|
* deprecated aliases for one release cycle so consumers can migrate
|
|
328
358
|
* without a flag day.
|
|
329
359
|
*/
|
|
330
|
-
export type { ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions,
|
|
360
|
+
export type { ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ModelIntentAcquireOptions, ModelIntentHandle, ModelOperations, } from './createModelProxy.js';
|
|
331
361
|
import type { ModelOperations } from './createModelProxy.js';
|
|
332
362
|
export type ResourceOperationAction = 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
|
|
333
363
|
export type CommitWait = 'queued' | 'confirmed';
|
|
@@ -366,11 +396,7 @@ export interface BusyOptions {
|
|
|
366
396
|
/** HTTP API polling interval while waiting. WebSocket clients ignore it. */
|
|
367
397
|
readonly busyPollInterval?: number;
|
|
368
398
|
}
|
|
369
|
-
export
|
|
370
|
-
readonly timeout?: number;
|
|
371
|
-
readonly pollInterval?: number;
|
|
372
|
-
readonly signal?: AbortSignal;
|
|
373
|
-
}
|
|
399
|
+
export type { IntentWaitOptions } from '../types/streams.js';
|
|
374
400
|
export interface ResourceReadOptions extends BusyOptions {
|
|
375
401
|
}
|
|
376
402
|
export interface IntentCreateOptions {
|
|
@@ -816,7 +842,7 @@ export declare namespace Ablo {
|
|
|
816
842
|
type Event = import('../source/index.js').SourceEvent;
|
|
817
843
|
type EventsResult = import('../source/index.js').SourceEventsResult;
|
|
818
844
|
type Scope = import('../source/index.js').SourceScope;
|
|
819
|
-
type
|
|
845
|
+
type ApiKey = import('../source/index.js').SourceApiKey;
|
|
820
846
|
type Options<S extends _SchemaTypes.SchemaRecord = _SchemaTypes.SchemaRecord, TAuth = unknown> = import('../source/index.js').AbloSourceOptions<S, TAuth>;
|
|
821
847
|
type ModelHandlers<Row, CreateInput, TAuth = unknown> = import('../source/index.js').SourceModelHandlers<Row, CreateInput, TAuth>;
|
|
822
848
|
type SignatureVerificationResult = import('../source/index.js').SourceSignatureVerificationResult;
|
package/dist/client/Ablo.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* bootstrap, offline queue, DI adapters) behind a single function call.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
* import { Ablo } from '@ablo/
|
|
8
|
+
* import { Ablo } from '@abloatai/ablo/client';
|
|
9
9
|
* import { schema } from './schema';
|
|
10
10
|
*
|
|
11
11
|
* const sync = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
@@ -1147,6 +1147,37 @@ export function Ablo(options) {
|
|
|
1147
1147
|
getLastSyncId: () => store.getSyncWebSocket()?.getLastSyncId() ?? store.lastSyncId ?? 0,
|
|
1148
1148
|
entities: { [modelKey]: id },
|
|
1149
1149
|
}),
|
|
1150
|
+
observe: (target) => {
|
|
1151
|
+
// The live intent stream only tracks *open* (active) claims;
|
|
1152
|
+
// terminal states (committed / expired / canceled) drop out of
|
|
1153
|
+
// the list entirely — exactly the ephemeral coordination model.
|
|
1154
|
+
// So a present entry is, by definition, `status: 'active'`.
|
|
1155
|
+
const held = publicIntents.list({
|
|
1156
|
+
resource: target.resource,
|
|
1157
|
+
id: target.id,
|
|
1158
|
+
})[0];
|
|
1159
|
+
if (!held)
|
|
1160
|
+
return null;
|
|
1161
|
+
return {
|
|
1162
|
+
object: 'intent',
|
|
1163
|
+
id: held.id,
|
|
1164
|
+
status: 'active',
|
|
1165
|
+
target: {
|
|
1166
|
+
type: held.target.resource,
|
|
1167
|
+
id: held.target.id,
|
|
1168
|
+
...(held.target.path ? { path: held.target.path } : {}),
|
|
1169
|
+
...(held.target.range ? { range: held.target.range } : {}),
|
|
1170
|
+
...(held.target.field ? { field: held.target.field } : {}),
|
|
1171
|
+
...(held.target.meta ? { meta: held.target.meta } : {}),
|
|
1172
|
+
},
|
|
1173
|
+
action: held.action,
|
|
1174
|
+
heldBy: held.actor,
|
|
1175
|
+
participantKind: held.participantKind,
|
|
1176
|
+
expiresAt: held.expiresAt,
|
|
1177
|
+
};
|
|
1178
|
+
},
|
|
1179
|
+
waitFor: (target, waitOptions) => publicIntents.waitFor({ resource: target.resource, id: target.id }, waitOptions),
|
|
1180
|
+
selfParticipantId: participantId,
|
|
1150
1181
|
});
|
|
1151
1182
|
}
|
|
1152
1183
|
const commits = {
|
package/dist/client/auth.d.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* with an actionable message — so the constructor reads as a
|
|
8
8
|
* sequence of named decisions rather than a stream of `??`-chains.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Customer-facing env surface is intentionally small: `ABLO_API_KEY`
|
|
11
|
+
* is the only environment fallback. Other routing/auth overrides are
|
|
12
|
+
* explicit options so generated apps do not accrete hidden env knobs.
|
|
13
13
|
*/
|
|
14
14
|
/**
|
|
15
15
|
* Async callable that resolves to a fresh API key. Mirrors the shape
|
package/dist/client/auth.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* with an actionable message — so the constructor reads as a
|
|
8
8
|
* sequence of named decisions rather than a stream of `??`-chains.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Customer-facing env surface is intentionally small: `ABLO_API_KEY`
|
|
11
|
+
* is the only environment fallback. Other routing/auth overrides are
|
|
12
|
+
* explicit options so generated apps do not accrete hidden env knobs.
|
|
13
13
|
*/
|
|
14
14
|
import { AbloAuthenticationError } from '../errors.js';
|
|
15
15
|
/**
|
|
@@ -25,11 +25,11 @@ export function resolveApiKey(input) {
|
|
|
25
25
|
return input.options.apiKey ?? input.env.ABLO_API_KEY ?? null;
|
|
26
26
|
}
|
|
27
27
|
export function resolveAuthToken(input) {
|
|
28
|
-
return input.options.authToken ??
|
|
28
|
+
return input.options.authToken ?? null;
|
|
29
29
|
}
|
|
30
30
|
export const ABLO_DEFAULT_BASE_URL = 'wss://mesh.ablo.finance';
|
|
31
31
|
export function resolveBaseURL(input) {
|
|
32
|
-
return
|
|
32
|
+
return input.options.baseURL ?? ABLO_DEFAULT_BASE_URL;
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
35
|
* Browser guard — apiKey is server-side-only by default. Same check
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Each schema model gets one `ModelOperations<T, CreateInput>` —
|
|
10
10
|
* exposes `retrieve`, `list`, `count`, `create`, `update`, `delete`,
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* `intent` (the coordination handle), `subscribe`, and `load`. The
|
|
12
|
+
* factory returns a plain object; the client assembles the
|
|
13
|
+
* `ablo.<model>` lookup table from these.
|
|
14
14
|
*/
|
|
15
15
|
import type { MutationOptions } from '../interfaces/index.js';
|
|
16
16
|
import type { ModelRegistry } from '../ModelRegistry.js';
|
|
@@ -19,7 +19,7 @@ import type { SyncClient } from '../SyncClient.js';
|
|
|
19
19
|
import type { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
|
|
20
20
|
import type { LoadWhere } from '../query/types.js';
|
|
21
21
|
import { ModelScope } from '../types/index.js';
|
|
22
|
-
import type { Duration, Snapshot } from '../types/streams.js';
|
|
22
|
+
import type { Duration, Intent, IntentStatus, IntentWaitOptions, Snapshot } from '../types/streams.js';
|
|
23
23
|
export interface ModelResourceMeta {
|
|
24
24
|
readonly key: string;
|
|
25
25
|
readonly typename: string;
|
|
@@ -66,30 +66,7 @@ export interface ModelLoadOptions<T> {
|
|
|
66
66
|
*/
|
|
67
67
|
expand?: readonly string[];
|
|
68
68
|
}
|
|
69
|
-
export interface
|
|
70
|
-
/**
|
|
71
|
-
* Human-readable activity shown to other participants while this handle
|
|
72
|
-
* is open. Examples: `editing`, `summarizing`, `rewriting`, `reviewing`.
|
|
73
|
-
*/
|
|
74
|
-
activity?: string;
|
|
75
|
-
/** Optional field-level target for UI affordances such as busy badges. */
|
|
76
|
-
field?: keyof T & string;
|
|
77
|
-
/** Lease duration for the visible activity. Runtime death is cleaned up by TTL. */
|
|
78
|
-
ttl?: Duration;
|
|
79
|
-
/** Default wait mode for `handle.update(...)`. Defaults to `confirmed`. */
|
|
80
|
-
wait?: MutationOptions['wait'];
|
|
81
|
-
}
|
|
82
|
-
export interface ModelEditHandle<T> extends AsyncDisposable {
|
|
83
|
-
readonly id: string;
|
|
84
|
-
readonly intentId: string;
|
|
85
|
-
readonly activity: string;
|
|
86
|
-
readonly current: T;
|
|
87
|
-
readonly signal: AbortSignal;
|
|
88
|
-
update(data: Partial<T>, options?: MutationOptions): Promise<T>;
|
|
89
|
-
release(): Promise<void>;
|
|
90
|
-
revoke(): void;
|
|
91
|
-
}
|
|
92
|
-
export interface ModelIntentHandle {
|
|
69
|
+
export interface IntentLeaseHandle {
|
|
93
70
|
readonly id: string;
|
|
94
71
|
release(): Promise<void>;
|
|
95
72
|
revoke(): void;
|
|
@@ -103,8 +80,107 @@ export interface ModelCollaboration<T> {
|
|
|
103
80
|
};
|
|
104
81
|
action: string;
|
|
105
82
|
ttl?: Duration;
|
|
106
|
-
}): Promise<
|
|
83
|
+
}): Promise<IntentLeaseHandle>;
|
|
107
84
|
createSnapshot(modelKey: string, id: string): Snapshot;
|
|
85
|
+
/**
|
|
86
|
+
* Current coordination state on a target — who (if anyone) holds it.
|
|
87
|
+
* Synchronous reactive snapshot read off the presence/intent stream;
|
|
88
|
+
* `null` when the target is free. The wiring site computes it because
|
|
89
|
+
* only it knows the local participant id (needed to distinguish "I
|
|
90
|
+
* hold it" from "someone else holds it").
|
|
91
|
+
*/
|
|
92
|
+
observe(target: {
|
|
93
|
+
resource: string;
|
|
94
|
+
id: string;
|
|
95
|
+
}): Intent | null;
|
|
96
|
+
/**
|
|
97
|
+
* Resolve once no participant holds an active intent on the target.
|
|
98
|
+
* The contender's "wait until it's free" — delegates to the intent
|
|
99
|
+
* stream's `waitFor`.
|
|
100
|
+
*/
|
|
101
|
+
waitFor(target: {
|
|
102
|
+
resource: string;
|
|
103
|
+
id: string;
|
|
104
|
+
}, options?: IntentWaitOptions): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* The local participant's id. Used to distinguish "I already hold this"
|
|
107
|
+
* from "someone else holds it" in `claimOrWait`.
|
|
108
|
+
*/
|
|
109
|
+
readonly selfParticipantId: string;
|
|
110
|
+
}
|
|
111
|
+
/** Options for acquiring a per-model coordination intent. */
|
|
112
|
+
export interface ModelIntentAcquireOptions {
|
|
113
|
+
/** Phase shown to others while held. Defaults to `'editing'`. */
|
|
114
|
+
action?: string;
|
|
115
|
+
/** Field-level target for busy badges. */
|
|
116
|
+
field?: string;
|
|
117
|
+
/** Lease duration; runtime death is cleaned up by TTL. */
|
|
118
|
+
ttl?: Duration;
|
|
119
|
+
/** Default wait mode for `handle.update(...)`. Defaults to `confirmed`. */
|
|
120
|
+
wait?: MutationOptions['wait'];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Per-entity coordination handle, returned synchronously by
|
|
124
|
+
* `ablo.<model>.intent(id)`. It lets humans and agents claim a row before
|
|
125
|
+
* they work on it, so two of them don't edit the same thing at once.
|
|
126
|
+
*
|
|
127
|
+
* The lifecycle reads like a sentence:
|
|
128
|
+
*
|
|
129
|
+
* ```ts
|
|
130
|
+
* const report = ablo.weatherReports.intent('weather_stockholm');
|
|
131
|
+
*
|
|
132
|
+
* if (report.current) await report.whenFree(); // someone's on it — wait
|
|
133
|
+
* await report.claim({ action: 'checking_weather' }); // it's mine now
|
|
134
|
+
* await report.update({ status: 'ready' }); // write, then auto-finish
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* `current` is the live `Intent` (or `null` if free). `claim()` announces
|
|
138
|
+
* you're working so others yield. `whenFree()` waits for whoever holds it.
|
|
139
|
+
* `claimOrWait()` does both — claim, or wait your turn then claim — which
|
|
140
|
+
* is what you bind to an agent's write tool so it never reasons about
|
|
141
|
+
* coordination itself. `finish()`/`cancel()` give the claim back.
|
|
142
|
+
*/
|
|
143
|
+
export interface ModelIntentHandle<T> extends AsyncDisposable {
|
|
144
|
+
/** The target entity id this handle coordinates. */
|
|
145
|
+
readonly id: string;
|
|
146
|
+
/**
|
|
147
|
+
* Live coordination state on this target — `null` when free, otherwise
|
|
148
|
+
* the holder's `Intent` (who, what phase, until when). Reactive
|
|
149
|
+
* snapshot; pair with the model's `subscribe` for change notifications.
|
|
150
|
+
*/
|
|
151
|
+
readonly current: Intent | null;
|
|
152
|
+
/** Convenience: `current?.status ?? 'idle'`. */
|
|
153
|
+
readonly status: IntentStatus | 'idle';
|
|
154
|
+
/**
|
|
155
|
+
* Claim this row so other participants yield while you work. Resolves
|
|
156
|
+
* once the claim is announced. Throws if someone else already holds it
|
|
157
|
+
* — call `whenFree()` first, or use `claimOrWait()` to do both.
|
|
158
|
+
*/
|
|
159
|
+
claim(options?: ModelIntentAcquireOptions): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Claim the row, or — if someone else holds it — wait for them to
|
|
162
|
+
* finish, re-read the (now-changed) row, then claim. The caller never
|
|
163
|
+
* branches on who holds it; it just gets the row safely. A claim you
|
|
164
|
+
* already hold is treated as yours and taken without waiting. Bind this
|
|
165
|
+
* to an agent's write-tool boundary.
|
|
166
|
+
*/
|
|
167
|
+
claimOrWait(options?: ModelIntentAcquireOptions): Promise<void>;
|
|
168
|
+
/**
|
|
169
|
+
* Optimistic update guarded by the claim this handle holds. Rejects
|
|
170
|
+
* with `AbloStaleContextError` if the row changed under you, then
|
|
171
|
+
* auto-finishes. Call `claim()` first.
|
|
172
|
+
*/
|
|
173
|
+
update(data: Partial<T>, options?: MutationOptions): Promise<T>;
|
|
174
|
+
/** Finish: give back a claim you hold once the work is committed. */
|
|
175
|
+
finish(): Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* Wait until the row is free, then resolve. On resolution your cached
|
|
178
|
+
* copy may be stale — re-read before writing (the stale-context guard
|
|
179
|
+
* enforces this if you go through `claim()` + `update()`).
|
|
180
|
+
*/
|
|
181
|
+
whenFree(options?: IntentWaitOptions): Promise<void>;
|
|
182
|
+
/** Cancel: drop a claim you hold without committing any work. */
|
|
183
|
+
cancel(): void;
|
|
108
184
|
}
|
|
109
185
|
export interface ModelOperations<T, CreateInput> {
|
|
110
186
|
/**
|
|
@@ -135,10 +211,20 @@ export interface ModelOperations<T, CreateInput> {
|
|
|
135
211
|
/** Delete an entity by id — optimistic, offline-first (see `create`). */
|
|
136
212
|
delete(id: string, options?: MutationOptions): Promise<void>;
|
|
137
213
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
214
|
+
* Coordination accessor for one entity — the same `ablo.<model>(id)`
|
|
215
|
+
* shape as `create`/`update`/`retrieve`, but on the coordination plane
|
|
216
|
+
* (ephemeral, TTL'd, never persisted). Returns a handle synchronously:
|
|
217
|
+
* read `.current` to see who's editing, `claim()` to take it, `update()`
|
|
218
|
+
* to write under the claim, `whenFree()` to wait for a holder to finish.
|
|
219
|
+
*
|
|
220
|
+
* ```ts
|
|
221
|
+
* const lock = ablo.slide.intent(slideId);
|
|
222
|
+
* if (lock.current) await lock.whenFree(); // someone's editing — wait
|
|
223
|
+
* await lock.claim({ action: 'editing' });
|
|
224
|
+
* await lock.update({ title: 'New' }); // auto-finishes
|
|
225
|
+
* ```
|
|
140
226
|
*/
|
|
141
|
-
|
|
227
|
+
intent(id: string): ModelIntentHandle<T>;
|
|
142
228
|
/** Subscribe to changes (callback called on every change). */
|
|
143
229
|
subscribe(callback: (entities: T[]) => void, options?: ModelListOptions<T>): () => void;
|
|
144
230
|
/**
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Each schema model gets one `ModelOperations<T, CreateInput>` —
|
|
10
10
|
* exposes `retrieve`, `list`, `count`, `create`, `update`, `delete`,
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* `intent` (the coordination handle), `subscribe`, and `load`. The
|
|
12
|
+
* factory returns a plain object; the client assembles the
|
|
13
|
+
* `ablo.<model>` lookup table from these.
|
|
14
14
|
*/
|
|
15
15
|
import { autorun } from 'mobx';
|
|
16
16
|
import { AbloStaleContextError, AbloValidationError } from '../errors.js';
|
|
@@ -111,54 +111,96 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
111
111
|
syncClient.delete(model, options);
|
|
112
112
|
await waitForMutation(model, options);
|
|
113
113
|
},
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
let model = objectPool.get(id);
|
|
119
|
-
if (!model) {
|
|
120
|
-
await load({ where: { id } });
|
|
121
|
-
model = objectPool.get(id);
|
|
122
|
-
}
|
|
123
|
-
if (!model) {
|
|
124
|
-
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
125
|
-
}
|
|
126
|
-
const activity = options?.activity ?? 'editing';
|
|
127
|
-
const snapshot = collaboration.createSnapshot(schemaKey, id);
|
|
128
|
-
const intent = await collaboration.createIntent({
|
|
129
|
-
target: {
|
|
130
|
-
resource: schemaKey,
|
|
131
|
-
id,
|
|
132
|
-
...(options?.field ? { field: options.field } : {}),
|
|
133
|
-
},
|
|
134
|
-
action: activity,
|
|
135
|
-
ttl: options?.ttl,
|
|
136
|
-
});
|
|
114
|
+
intent(id) {
|
|
115
|
+
const target = { resource: schemaKey, id };
|
|
116
|
+
let acquired = null;
|
|
117
|
+
let snapshot = null;
|
|
137
118
|
let released = false;
|
|
138
|
-
|
|
119
|
+
let acquireWait;
|
|
120
|
+
// Public `cancel()` — drop the claim without committing. Calls the
|
|
121
|
+
// lower-level lease handle's `revoke()` (a different API; leave it).
|
|
122
|
+
const cancel = () => {
|
|
139
123
|
if (released)
|
|
140
124
|
return;
|
|
141
125
|
released = true;
|
|
142
|
-
snapshot
|
|
143
|
-
|
|
126
|
+
if (snapshot)
|
|
127
|
+
snapshot.signal.removeEventListener('abort', cancel);
|
|
128
|
+
acquired?.revoke();
|
|
144
129
|
};
|
|
145
|
-
|
|
130
|
+
// Public `finish()` — give the claim back after committing. Calls the
|
|
131
|
+
// lower-level lease handle's `release()` (a different API; leave it).
|
|
132
|
+
const finish = async () => {
|
|
146
133
|
if (released)
|
|
147
134
|
return;
|
|
148
135
|
released = true;
|
|
149
|
-
snapshot
|
|
150
|
-
|
|
136
|
+
if (snapshot)
|
|
137
|
+
snapshot.signal.removeEventListener('abort', cancel);
|
|
138
|
+
await acquired?.release();
|
|
139
|
+
};
|
|
140
|
+
const whenFree = async (options) => {
|
|
141
|
+
if (!collaboration)
|
|
142
|
+
return;
|
|
143
|
+
await collaboration.waitFor(target, options);
|
|
144
|
+
};
|
|
145
|
+
const claim = async (options) => {
|
|
146
|
+
if (!collaboration) {
|
|
147
|
+
throw new AbloValidationError(`Model "${schemaKey}" cannot claim an intent without collaboration wiring.`, { code: 'model_intent_not_configured' });
|
|
148
|
+
}
|
|
149
|
+
if (acquired)
|
|
150
|
+
return;
|
|
151
|
+
acquireWait = options?.wait;
|
|
152
|
+
// Load the row so update() has a snapshot to guard against.
|
|
153
|
+
let model = objectPool.get(id);
|
|
154
|
+
if (!model) {
|
|
155
|
+
await load({ where: { id } });
|
|
156
|
+
model = objectPool.get(id);
|
|
157
|
+
}
|
|
158
|
+
if (!model) {
|
|
159
|
+
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
160
|
+
}
|
|
161
|
+
const snap = collaboration.createSnapshot(schemaKey, id);
|
|
162
|
+
snap.signal.addEventListener('abort', cancel, { once: true });
|
|
163
|
+
snapshot = snap;
|
|
164
|
+
released = false;
|
|
165
|
+
acquired = await collaboration.createIntent({
|
|
166
|
+
target: {
|
|
167
|
+
resource: schemaKey,
|
|
168
|
+
id,
|
|
169
|
+
...(options?.field ? { field: options.field } : {}),
|
|
170
|
+
},
|
|
171
|
+
action: options?.action ?? 'editing',
|
|
172
|
+
ttl: options?.ttl,
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
const claimOrWait = async (options) => {
|
|
176
|
+
if (!collaboration) {
|
|
177
|
+
throw new AbloValidationError(`Model "${schemaKey}" cannot claim an intent without collaboration wiring.`, { code: 'model_intent_not_configured' });
|
|
178
|
+
}
|
|
179
|
+
const held = collaboration.observe(target);
|
|
180
|
+
// A foreign holder: wait for them to finish, then re-read before
|
|
181
|
+
// claiming. Our own claim (or a free target) goes straight to claim.
|
|
182
|
+
if (held && held.heldBy !== collaboration.selfParticipantId) {
|
|
183
|
+
await whenFree();
|
|
184
|
+
await load({ where: { id } });
|
|
185
|
+
}
|
|
186
|
+
await claim(options);
|
|
151
187
|
};
|
|
152
|
-
snapshot.signal.addEventListener('abort', revoke, { once: true });
|
|
153
188
|
const handle = {
|
|
154
189
|
id,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
190
|
+
get current() {
|
|
191
|
+
return collaboration?.observe(target) ?? null;
|
|
192
|
+
},
|
|
193
|
+
get status() {
|
|
194
|
+
return collaboration?.observe(target)?.status ?? 'idle';
|
|
195
|
+
},
|
|
196
|
+
claim,
|
|
197
|
+
claimOrWait,
|
|
159
198
|
async update(data, updateOptions) {
|
|
199
|
+
if (!acquired || !snapshot) {
|
|
200
|
+
throw new AbloValidationError(`Call claim() before update() on ablo.${schemaKey}.intent(${id}).`, { code: 'intent_not_acquired' });
|
|
201
|
+
}
|
|
160
202
|
if (snapshot.signal.aborted) {
|
|
161
|
-
throw new AbloStaleContextError(`
|
|
203
|
+
throw new AbloStaleContextError(`Intent context is stale for ${schemaKey}/${id}. Re-read the row and retry.`, {
|
|
162
204
|
code: 'edit_context_stale',
|
|
163
205
|
readAt: snapshot.stamp,
|
|
164
206
|
cause: snapshot.signal.reason,
|
|
@@ -166,20 +208,21 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
166
208
|
}
|
|
167
209
|
try {
|
|
168
210
|
return await operations.update(id, data, {
|
|
169
|
-
wait:
|
|
211
|
+
wait: acquireWait ?? 'confirmed',
|
|
170
212
|
readAt: snapshot.stamp,
|
|
171
213
|
onStale: 'reject',
|
|
172
214
|
...updateOptions,
|
|
173
|
-
intent,
|
|
215
|
+
intent: acquired,
|
|
174
216
|
});
|
|
175
217
|
}
|
|
176
218
|
finally {
|
|
177
|
-
await
|
|
219
|
+
await finish();
|
|
178
220
|
}
|
|
179
221
|
},
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
222
|
+
finish,
|
|
223
|
+
whenFree,
|
|
224
|
+
cancel,
|
|
225
|
+
[Symbol.asyncDispose]: finish,
|
|
183
226
|
};
|
|
184
227
|
return handle;
|
|
185
228
|
},
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @ablo/
|
|
2
|
+
* @abloatai/ablo/client — Consumer API
|
|
3
3
|
*
|
|
4
4
|
* The one-liner entry point for external consumers.
|
|
5
5
|
*
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* when you want the realtime sync engine with typed model proxies.
|
|
8
8
|
*
|
|
9
9
|
* ```ts
|
|
10
|
-
* import { Ablo } from '@ablo/
|
|
10
|
+
* import { Ablo } from '@abloatai/ablo/client';
|
|
11
11
|
* import { schema } from './schema';
|
|
12
12
|
*
|
|
13
13
|
* const ablo = Ablo({
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* ```
|
|
21
21
|
*
|
|
22
22
|
* For headless agents (workers, bots), pass `kind: 'agent'` plus a
|
|
23
|
-
*
|
|
23
|
+
* restricted (`rk_`) API key as `capabilityToken`:
|
|
24
24
|
*
|
|
25
25
|
* ```ts
|
|
26
26
|
* const bot = Ablo({
|
package/dist/client/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @ablo/
|
|
2
|
+
* @abloatai/ablo/client — Consumer API
|
|
3
3
|
*
|
|
4
4
|
* The one-liner entry point for external consumers.
|
|
5
5
|
*
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* when you want the realtime sync engine with typed model proxies.
|
|
8
8
|
*
|
|
9
9
|
* ```ts
|
|
10
|
-
* import { Ablo } from '@ablo/
|
|
10
|
+
* import { Ablo } from '@abloatai/ablo/client';
|
|
11
11
|
* import { schema } from './schema';
|
|
12
12
|
*
|
|
13
13
|
* const ablo = Ablo({
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* ```
|
|
21
21
|
*
|
|
22
22
|
* For headless agents (workers, bots), pass `kind: 'agent'` plus a
|
|
23
|
-
*
|
|
23
|
+
* restricted (`rk_`) API key as `capabilityToken`:
|
|
24
24
|
*
|
|
25
25
|
* ```ts
|
|
26
26
|
* const bot = Ablo({
|
package/dist/config/index.d.ts
CHANGED