@abloatai/ablo 0.11.1 → 0.12.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 +49 -0
- package/README.md +10 -2
- package/dist/Model.d.ts +39 -0
- package/dist/Model.js +68 -0
- package/dist/ai-sdk/claim-broadcast.d.ts +4 -3
- package/dist/ai-sdk/claim-broadcast.js +2 -2
- package/dist/ai-sdk/wrap.d.ts +5 -4
- package/dist/ai-sdk/wrap.js +3 -3
- package/dist/auth/credentialPolicy.d.ts +145 -0
- package/dist/auth/credentialPolicy.js +130 -0
- package/dist/cli.cjs +42 -7
- package/dist/client/Ablo.d.ts +64 -91
- package/dist/client/Ablo.js +43 -103
- package/dist/client/ApiClient.d.ts +10 -1
- package/dist/client/ApiClient.js +45 -22
- package/dist/client/auth.d.ts +12 -5
- package/dist/client/auth.js +2 -1
- package/dist/client/createModelProxy.d.ts +64 -17
- package/dist/client/createModelProxy.js +18 -12
- package/dist/client/httpClient.d.ts +17 -3
- package/dist/client/httpClient.js +1 -0
- package/dist/client/identity.js +134 -122
- package/dist/client/index.d.ts +1 -1
- package/dist/client/sessionMint.d.ts +15 -0
- package/dist/client/sessionMint.js +86 -0
- package/dist/coordination/schema.d.ts +1 -1
- package/dist/coordination/schema.js +3 -1
- package/dist/errorCodes.d.ts +2 -0
- package/dist/errorCodes.js +2 -0
- package/dist/errors.d.ts +6 -3
- package/dist/errors.js +9 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -7
- package/dist/mutators/RecordingTransaction.js +14 -42
- package/dist/react/AbloProvider.d.ts +12 -13
- package/dist/react/AbloProvider.js +10 -10
- package/dist/react/context.d.ts +10 -45
- package/dist/react/context.js +12 -17
- package/dist/react/index.d.ts +8 -10
- package/dist/react/index.js +8 -11
- package/dist/react/useMutators.js +3 -2
- package/dist/react/useSyncStatus.d.ts +1 -1
- package/dist/react/useUndoScope.js +3 -2
- package/dist/realtime/index.d.ts +1 -1
- package/dist/schema/generate.js +1 -2
- package/dist/schema/model.d.ts +10 -3
- package/dist/schema/schema.d.ts +13 -2
- package/dist/schema/schema.js +26 -0
- package/dist/surface.d.ts +29 -0
- package/dist/surface.js +60 -0
- package/dist/sync/ConnectionManager.d.ts +16 -5
- package/dist/sync/ConnectionManager.js +42 -7
- package/dist/sync/createClaimStream.js +5 -4
- package/dist/sync/participants.js +1 -1
- package/dist/transactions/TransactionQueue.d.ts +0 -11
- package/dist/transactions/TransactionQueue.js +12 -56
- package/dist/types/global.d.ts +3 -0
- package/dist/types/streams.d.ts +17 -29
- package/dist/utils/mobx-setup.js +1 -0
- package/docs/api-keys.md +49 -0
- package/docs/api.md +3 -2
- package/docs/client-behavior.md +1 -0
- package/docs/coordination.md +75 -21
- package/docs/examples/existing-python-backend.md +9 -5
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/guarantees.md +4 -3
- package/docs/identity.md +89 -82
- package/docs/integration-guide.md +19 -10
- package/docs/migration.md +11 -3
- package/docs/quickstart.md +6 -2
- package/docs/react.md +3 -3
- package/docs/schema-contract.md +23 -5
- package/llms-full.txt +18 -16
- package/llms.txt +6 -6
- package/package.json +1 -1
- package/dist/api/index.d.ts +0 -10
- package/dist/api/index.js +0 -9
- package/dist/principal.d.ts +0 -44
- package/dist/principal.js +0 -49
- package/dist/react/SyncGroupProvider.d.ts +0 -19
- package/dist/react/SyncGroupProvider.js +0 -44
- package/dist/react/useClaim.d.ts +0 -29
- package/dist/react/useClaim.js +0 -42
- package/dist/react/usePresence.d.ts +0 -32
- package/dist/react/usePresence.js +0 -41
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SchemaRecord } from '../schema/schema.js';
|
|
2
|
+
import type { AbloSession, CreateSessionParams } from './Ablo.js';
|
|
3
|
+
/** The resolved control-plane context a mint needs. `fetch` is optional — the
|
|
4
|
+
* auth helpers fall back to the runtime global when omitted. */
|
|
5
|
+
export interface MintSessionContext {
|
|
6
|
+
readonly apiKey: string;
|
|
7
|
+
readonly baseUrl: string;
|
|
8
|
+
readonly fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Mint a session token from an already-resolved `sk_` credential + base URL.
|
|
12
|
+
* Discriminates the `{ user }` / `{ agent }` union onto the server's two mint
|
|
13
|
+
* doors and reshapes each flat response into the `AbloSession` resource.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mintSession<S extends SchemaRecord>(params: CreateSessionParams<S>, ctx: MintSessionContext): Promise<AbloSession>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mintSession` — the ONE implementation behind `sessions.create`, shared by the
|
|
3
|
+
* stateful `Ablo` client and the stateless protocol / HTTP client so the two can
|
|
4
|
+
* never drift on HOW a token is minted.
|
|
5
|
+
*
|
|
6
|
+
* Minting is a pure control-plane HTTP call (no socket, no synced pool): a backend
|
|
7
|
+
* holding a secret `sk_` exchanges it for a short-lived scoped token — `ek_` for an
|
|
8
|
+
* `{ user }` session (full end-user authority) or `rk_` for an `{ agent }` session
|
|
9
|
+
* (scoped to exactly the operations named in `can`). The two arms map to the
|
|
10
|
+
* server's two mint doors:
|
|
11
|
+
*
|
|
12
|
+
* `{ user }` → POST /auth/ephemeral-keys → `ek_`. The user-session door;
|
|
13
|
+
* routing this arm through /auth/capability is structurally
|
|
14
|
+
* impossible — that route rejects participantKind 'user' outright
|
|
15
|
+
* (`invalid_participant_kind`, the 2026-06-11 Pulse cascade where
|
|
16
|
+
* the SDK's own blessed pattern 403'd and integrators fell back to
|
|
17
|
+
* minting humans as agents).
|
|
18
|
+
* `{ agent }` → POST /auth/capability → scoped `rk_`. `can: { tasks: ['update'] }`
|
|
19
|
+
* serializes to the wire allowlist (`tasks.update`); the Hub matches
|
|
20
|
+
* it against every registered alias of the model.
|
|
21
|
+
*
|
|
22
|
+
* The caller supplies the resolved control-plane credential + base URL in `ctx`;
|
|
23
|
+
* WHICH key to use (the original `sk_`, never a derived `rk_` the startup exchange
|
|
24
|
+
* may have installed) is the caller's concern — see the two call sites.
|
|
25
|
+
*
|
|
26
|
+
* Type-only imports of `CreateSessionParams` / `AbloSession` keep this module a
|
|
27
|
+
* leaf (no runtime cycle back to `Ablo.ts`): at runtime it depends on `auth` +
|
|
28
|
+
* `schema` only.
|
|
29
|
+
*/
|
|
30
|
+
import { exchangeApiKey, mintUserSessionKey } from '../auth/index.js';
|
|
31
|
+
/**
|
|
32
|
+
* Mint a session token from an already-resolved `sk_` credential + base URL.
|
|
33
|
+
* Discriminates the `{ user }` / `{ agent }` union onto the server's two mint
|
|
34
|
+
* doors and reshapes each flat response into the `AbloSession` resource.
|
|
35
|
+
*/
|
|
36
|
+
export async function mintSession(params, ctx) {
|
|
37
|
+
const { apiKey, baseUrl } = ctx;
|
|
38
|
+
if (params.user) {
|
|
39
|
+
const res = await mintUserSessionKey({
|
|
40
|
+
apiKey,
|
|
41
|
+
baseUrl,
|
|
42
|
+
userId: params.user.id,
|
|
43
|
+
...(params.syncGroups ? { syncGroups: [...params.syncGroups] } : {}),
|
|
44
|
+
ttlSeconds: params.ttlSeconds ?? 900,
|
|
45
|
+
...(ctx.fetch ? { fetch: ctx.fetch } : {}),
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
object: 'session',
|
|
49
|
+
id: res.id,
|
|
50
|
+
token: res.token,
|
|
51
|
+
expiresAt: res.expiresAt,
|
|
52
|
+
organizationId: res.organizationId,
|
|
53
|
+
// The ephemeral mint stores scope on the key row; reshape its flat
|
|
54
|
+
// response into the session resource's scope block.
|
|
55
|
+
scope: {
|
|
56
|
+
organizationId: res.organizationId,
|
|
57
|
+
syncGroups: res.syncGroups,
|
|
58
|
+
operations: [],
|
|
59
|
+
participantKind: 'user',
|
|
60
|
+
participantId: res.participantId,
|
|
61
|
+
},
|
|
62
|
+
userMeta: params.userMeta ?? { id: res.participantId },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const operations = Object.entries(params.can).flatMap(([model, ops]) => (ops ?? []).map((op) => `${model.toLowerCase()}.${op}`));
|
|
66
|
+
const res = await exchangeApiKey({
|
|
67
|
+
apiKey,
|
|
68
|
+
baseUrl,
|
|
69
|
+
participantKind: 'agent',
|
|
70
|
+
participantId: params.agent.id,
|
|
71
|
+
...(params.syncGroups ? { syncGroups: [...params.syncGroups] } : {}),
|
|
72
|
+
operations,
|
|
73
|
+
ttlSeconds: params.ttlSeconds ?? 900,
|
|
74
|
+
...(params.userMeta ? { userMeta: params.userMeta } : {}),
|
|
75
|
+
...(ctx.fetch ? { fetch: ctx.fetch } : {}),
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
object: 'session',
|
|
79
|
+
id: res.capabilityId,
|
|
80
|
+
token: res.token,
|
|
81
|
+
expiresAt: res.expiresAt,
|
|
82
|
+
organizationId: res.organizationId,
|
|
83
|
+
scope: res.scope,
|
|
84
|
+
userMeta: res.userMeta,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -281,7 +281,7 @@ export declare const modelClaimSchema: z.ZodReadonly<z.ZodObject<{
|
|
|
281
281
|
agent: "agent";
|
|
282
282
|
system: "system";
|
|
283
283
|
}>>;
|
|
284
|
-
|
|
284
|
+
reason: z.ZodString;
|
|
285
285
|
description: z.ZodOptional<z.ZodString>;
|
|
286
286
|
field: z.ZodOptional<z.ZodString>;
|
|
287
287
|
status: z.ZodOptional<z.ZodEnum<{
|
|
@@ -203,7 +203,9 @@ export const modelClaimSchema = z
|
|
|
203
203
|
id: z.string(),
|
|
204
204
|
actor: z.string(),
|
|
205
205
|
participantKind: wireParticipantKindSchema,
|
|
206
|
-
|
|
206
|
+
/** Human-readable phase (`'editing'`). The public SDK field; the WS/HTTP
|
|
207
|
+
* wire carries the same value as `action` (healed on read). */
|
|
208
|
+
reason: z.string(),
|
|
207
209
|
description: z.string().optional(),
|
|
208
210
|
field: z.string().optional(),
|
|
209
211
|
status: z.enum(['active', 'queued']).optional(),
|
package/dist/errorCodes.d.ts
CHANGED
|
@@ -156,6 +156,7 @@ export declare const ERROR_CODES: {
|
|
|
156
156
|
readonly model_claimed: ErrorCodeSpec;
|
|
157
157
|
readonly model_claimed_timeout: ErrorCodeSpec;
|
|
158
158
|
readonly model_claim_not_configured: ErrorCodeSpec;
|
|
159
|
+
readonly model_watch_not_configured: ErrorCodeSpec;
|
|
159
160
|
readonly stale_context: ErrorCodeSpec;
|
|
160
161
|
readonly idempotency_conflict: ErrorCodeSpec;
|
|
161
162
|
readonly idempotency_key_too_long: ErrorCodeSpec;
|
|
@@ -195,6 +196,7 @@ export declare const ERROR_CODES: {
|
|
|
195
196
|
readonly schema_scope_kind_invalid: ErrorCodeSpec;
|
|
196
197
|
readonly schema_field_not_camelcase: ErrorCodeSpec;
|
|
197
198
|
readonly schema_field_consecutive_caps: ErrorCodeSpec;
|
|
199
|
+
readonly schema_reserved_field: ErrorCodeSpec;
|
|
198
200
|
readonly schema_grants_shape_invalid: ErrorCodeSpec;
|
|
199
201
|
readonly schema_grants_identifier_unsafe: ErrorCodeSpec;
|
|
200
202
|
readonly schema_grants_relation_kind: ErrorCodeSpec;
|
package/dist/errorCodes.js
CHANGED
|
@@ -159,6 +159,7 @@ export const ERROR_CODES = {
|
|
|
159
159
|
model_claimed: wire('claim', 409, false, 'The model instance is claimed by another participant.'),
|
|
160
160
|
model_claimed_timeout: wire('claim', 409, false, 'Timed out waiting for a model claim to clear.'),
|
|
161
161
|
model_claim_not_configured: client('claim', 'Claiming requires the collaboration runtime, which the standard Ablo({ schema, apiKey }) client wires up for every model automatically — 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).'),
|
|
162
|
+
model_watch_not_configured: client('claim', 'watch() opens a presence/claim subscription and needs a live WebSocket, so it is unavailable on the HTTP transport and on model proxies built without a socket. Use the standard Ablo({ schema, apiKey }) client (default WebSocket transport).'),
|
|
162
163
|
// ── stale context / idempotency (409) ──────────────────────────────
|
|
163
164
|
stale_context: wire('conflict', 409, true, 'The write carried a readAt watermark that is now stale; re-read and retry.'),
|
|
164
165
|
idempotency_conflict: wire('conflict', 409, false, 'The same Idempotency-Key was reused with a different request body.'),
|
|
@@ -210,6 +211,7 @@ export const ERROR_CODES = {
|
|
|
210
211
|
schema_scope_kind_invalid: wire('schema', 400, false, 'A scope kind in the schema is invalid.'),
|
|
211
212
|
schema_field_not_camelcase: wire('schema', 400, false, 'A schema field name is not camelCase.'),
|
|
212
213
|
schema_field_consecutive_caps: wire('schema', 400, false, 'A schema field name has consecutive capital letters.'),
|
|
214
|
+
schema_reserved_field: client('schema', 'A model redeclared a reserved base field (id, createdAt, updatedAt, organizationId, createdBy) that the SDK provides automatically.'),
|
|
213
215
|
schema_grants_shape_invalid: wire('schema', 400, false, 'A grants declaration has an invalid shape.'),
|
|
214
216
|
schema_grants_identifier_unsafe: wire('schema', 400, false, 'A grants declaration referenced an unsafe identifier.'),
|
|
215
217
|
schema_grants_relation_kind: wire('schema', 400, false, 'A grants relation referenced an invalid kind.'),
|
package/dist/errors.d.ts
CHANGED
|
@@ -161,7 +161,9 @@ export interface ClaimContext {
|
|
|
161
161
|
readonly claimId?: string;
|
|
162
162
|
readonly actor?: string;
|
|
163
163
|
readonly participantKind?: ParticipantKind;
|
|
164
|
-
|
|
164
|
+
/** Human-readable phase the holder is in (`'editing'`). Matches the public
|
|
165
|
+
* claim surface; the wire summary carries the same value as `action`. */
|
|
166
|
+
readonly reason?: string;
|
|
165
167
|
readonly description?: string;
|
|
166
168
|
readonly field?: string;
|
|
167
169
|
readonly status?: string;
|
|
@@ -186,8 +188,9 @@ export declare function formatClaimedErrorMessage(args: {
|
|
|
186
188
|
* The target entity is currently claimed by another participant and the caller
|
|
187
189
|
* asked the SDK not to read/write through that claim.
|
|
188
190
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
+
* Pass `ifClaimed: 'return'` to inspect active claims yourself instead of
|
|
192
|
+
* throwing; to wait for the claim to clear, take `ablo.<model>.claim({ id })`
|
|
193
|
+
* (it queues fairly) rather than blocking the read.
|
|
191
194
|
*/
|
|
192
195
|
export declare class AbloClaimedError extends AbloError {
|
|
193
196
|
readonly type: "AbloClaimedError";
|
package/dist/errors.js
CHANGED
|
@@ -162,7 +162,12 @@ export class AbloStaleContextError extends AbloError {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
function claimAction(claim) {
|
|
165
|
-
|
|
165
|
+
if (!claim)
|
|
166
|
+
return undefined;
|
|
167
|
+
// The public `ClaimContext` exposes the phase as `reason`; the wire
|
|
168
|
+
// `WireClaimSummary` projection still carries it under `action`. Read both.
|
|
169
|
+
const c = claim;
|
|
170
|
+
return c.reason ?? c.action;
|
|
166
171
|
}
|
|
167
172
|
function claimDescription(claim) {
|
|
168
173
|
if (!claim)
|
|
@@ -208,8 +213,9 @@ export function formatClaimedErrorMessage(args) {
|
|
|
208
213
|
* The target entity is currently claimed by another participant and the caller
|
|
209
214
|
* asked the SDK not to read/write through that claim.
|
|
210
215
|
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
216
|
+
* Pass `ifClaimed: 'return'` to inspect active claims yourself instead of
|
|
217
|
+
* throwing; to wait for the claim to clear, take `ablo.<model>.claim({ id })`
|
|
218
|
+
* (it queues fairly) rather than blocking the read.
|
|
213
219
|
*/
|
|
214
220
|
export class AbloClaimedError extends AbloError {
|
|
215
221
|
type = 'AbloClaimedError';
|
package/dist/index.d.ts
CHANGED
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
* Advanced — opt-in, most apps never import these (each is tagged
|
|
44
44
|
* "Advanced —" at its export below, with the one situation it's for):
|
|
45
45
|
* • `dataSource` / `abloSource` — only if your own DB stays canonical
|
|
46
|
-
* • `session` / `agent` — only for delegated agent principals
|
|
47
46
|
* • `defaultPolicy` — only to customize conflict resolution
|
|
48
47
|
* • `defineMutators` / `createTransaction` — only for custom mutators
|
|
49
48
|
* If you don't recognize one, you don't need it — the default path covers you.
|
|
@@ -51,11 +50,10 @@
|
|
|
51
50
|
export { Ablo } from './client/Ablo.js';
|
|
52
51
|
export type { MutationExecutor } from './interfaces/index.js';
|
|
53
52
|
export type { HttpClaimApi, InternalAbloOptions } from './client/Ablo.js';
|
|
54
|
-
export {
|
|
53
|
+
export { type AbloHttpClientOptions, type AbloHttpClient, type HttpModelClient, } from './client/httpClient.js';
|
|
55
54
|
export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './client/auth.js';
|
|
56
|
-
export type { AbloOptions,
|
|
55
|
+
export type { AbloOptions, LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ClaimHandle, ModelOperations, } from './client/Ablo.js';
|
|
57
56
|
export type { AbloPersistence } from './client/persistence.js';
|
|
58
|
-
export { session, agent } from './principal.js';
|
|
59
57
|
import { Ablo } from './client/Ablo.js';
|
|
60
58
|
export default Ablo;
|
|
61
59
|
export { dataSource, abloSource, sourceEventForOperation, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
|
|
@@ -70,6 +68,8 @@ export { writeOptionsSchema, onStaleModeSchema, assertWriteOptions, } from './cl
|
|
|
70
68
|
export type { WriteOptionsInput } from './client/writeOptionsSchema.js';
|
|
71
69
|
export type { WriteOptions, MutationOptions } from './interfaces/index.js';
|
|
72
70
|
export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
|
|
71
|
+
export { PUBLIC_MODEL_VERBS, PUBLIC_LIST_OPTION_KEYS, PUBLIC_ABLO_OPTION_KEYS, } from './surface.js';
|
|
72
|
+
export type { ModelVerb, ListOptionKey, AbloOptionKey } from './surface.js';
|
|
73
73
|
export type { Register, DefaultSyncShape } from './types/global.js';
|
|
74
74
|
export { defineMutators } from './mutators/defineMutators.js';
|
|
75
75
|
export { createTransaction, type Transaction } from './mutators/Transaction.js';
|
package/dist/index.js
CHANGED
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
* Advanced — opt-in, most apps never import these (each is tagged
|
|
44
44
|
* "Advanced —" at its export below, with the one situation it's for):
|
|
45
45
|
* • `dataSource` / `abloSource` — only if your own DB stays canonical
|
|
46
|
-
* • `session` / `agent` — only for delegated agent principals
|
|
47
46
|
* • `defaultPolicy` — only to customize conflict resolution
|
|
48
47
|
* • `defineMutators` / `createTransaction` — only for custom mutators
|
|
49
48
|
* If you don't recognize one, you don't need it — the default path covers you.
|
|
@@ -56,17 +55,11 @@
|
|
|
56
55
|
// `import Ablo from '@abloatai/ablo'` works; named export so
|
|
57
56
|
// `import { Ablo }` also compiles.
|
|
58
57
|
export { Ablo } from './client/Ablo.js';
|
|
59
|
-
export { createAbloHttpClient, } from './client/httpClient.js';
|
|
60
58
|
export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './client/auth.js';
|
|
61
59
|
// Participant types live under `Ablo.Participant.*` —
|
|
62
60
|
// `Ablo.Participant.Joined`, `Ablo.Participant.Manager`,
|
|
63
61
|
// `Ablo.Participant.JoinOptions`, etc. Same dot-access shape as
|
|
64
62
|
// `Ablo.Peer`, `Ablo.Claim`. No flat re-exports.
|
|
65
|
-
// Advanced — most apps never import this. Principal constructors for
|
|
66
|
-
// delegated agent paths (`Ablo({ kind: 'agent', as: session({...}) })`).
|
|
67
|
-
// The default `Ablo({ schema, apiKey })` resolves identity from the key;
|
|
68
|
-
// reach for these only when minting a delegated agent principal.
|
|
69
|
-
export { session, agent } from './principal.js';
|
|
70
63
|
import { Ablo } from './client/Ablo.js';
|
|
71
64
|
export default Ablo;
|
|
72
65
|
// Advanced — most apps never import this. Customer-owned storage adapter
|
|
@@ -100,6 +93,10 @@ export { writeOptionsSchema, onStaleModeSchema, assertWriteOptions, } from './cl
|
|
|
100
93
|
// Storage-wedge detection — lets app shells render a recovery screen when the
|
|
101
94
|
// IndexedDB backing store is stuck (see core/openIDBWithTimeout.ts).
|
|
102
95
|
export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
|
|
96
|
+
// Machine-checked surface manifest — the SDK's own description of its public
|
|
97
|
+
// verb/option names, compile-time-bound to the real types (see surface.ts).
|
|
98
|
+
// The MCP `get_api_surface` imports these so docs can't name a phantom verb.
|
|
99
|
+
export { PUBLIC_MODEL_VERBS, PUBLIC_LIST_OPTION_KEYS, PUBLIC_ABLO_OPTION_KEYS, } from './surface.js';
|
|
103
100
|
// Advanced — most apps never import this. Custom (Zero-style) mutators:
|
|
104
101
|
// `ablo.<model>.create/update/delete` already covers normal writes. Reach
|
|
105
102
|
// for `defineMutators` only when you need a named, multi-step mutation with
|
|
@@ -59,55 +59,27 @@ function wrapMutateForKey(modelKey, mutate, store, inverses, forwards) {
|
|
|
59
59
|
// wider shape is exactly right.
|
|
60
60
|
return model.toJSON();
|
|
61
61
|
};
|
|
62
|
+
// Before-image for the undo inverse. Delegates to `Model.capturePreviousValues`
|
|
63
|
+
// — the SINGLE shared implementation (the stream path's
|
|
64
|
+
// `TransactionQueue.extractPreviousData` calls the same method). `fallbackToLive`
|
|
65
|
+
// is ON here: the manual-record path wants the live value as a last resort for
|
|
66
|
+
// a field that was neither pre-mutated nor in the original snapshot. (The
|
|
67
|
+
// stream path passes `false` so it can omit-and-drop instead — that flag is
|
|
68
|
+
// the one intentional difference between the two callers.)
|
|
62
69
|
const snapshotFields = (id, fieldNames) => {
|
|
63
70
|
const model = store.pool.get(id);
|
|
64
71
|
if (!model)
|
|
65
72
|
return null;
|
|
66
|
-
|
|
67
|
-
// `modifiedProperties` is populated by M1's `observe()` listener the
|
|
68
|
-
// moment the caller mutates an observable field directly. Thanks to
|
|
69
|
-
// `Model.propertyChanged`'s first-old-wins policy, `.old` holds the TRUE
|
|
70
|
-
// pre-session baseline even after many in-place mutations (e.g. a drag
|
|
71
|
-
// frame loop). That makes it the authoritative source for the undo
|
|
72
|
-
// inverse when the caller pre-mutates before invoking the mutator.
|
|
73
|
-
//
|
|
74
|
-
// Fallback chain for models/fields that weren't pre-mutated (so no
|
|
75
|
-
// `modifiedProperties` entry exists yet): `getOriginalSnapshot()`
|
|
76
|
-
// (populated on load/`markAsPersisted`/sync-ack), then the live
|
|
77
|
-
// observable. The live read is correct only when the caller didn't
|
|
78
|
-
// touch the field first.
|
|
79
|
-
const original = model.getOriginalSnapshot();
|
|
80
|
-
for (const f of fieldNames) {
|
|
81
|
-
if (f === 'id')
|
|
82
|
-
continue;
|
|
83
|
-
const mod = model.modifiedProperties.get(f);
|
|
84
|
-
if (mod) {
|
|
85
|
-
out[f] = mod.old;
|
|
86
|
-
}
|
|
87
|
-
else if (original && f in original) {
|
|
88
|
-
out[f] = original[f];
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
out[f] = Reflect.get(model, f);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return out;
|
|
73
|
+
return model.capturePreviousValues(fieldNames, { fallbackToLive: true });
|
|
95
74
|
};
|
|
96
75
|
// After a mutator's `base.update` succeeds, drop the `modifiedProperties`
|
|
97
|
-
// entries we snapshotted from
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
76
|
+
// entries we snapshotted from so the next mutator call sees THIS update's
|
|
77
|
+
// result as its baseline, not the pre-session old value. The transaction
|
|
78
|
+
// queue already captured its frozen copy synchronously inside `store.save`,
|
|
79
|
+
// so this clear is safe for server rollback. Shared with the stream path via
|
|
80
|
+
// `Model.consumeModifiedFields`.
|
|
102
81
|
const consumeModifiedFields = (id, fieldNames) => {
|
|
103
|
-
|
|
104
|
-
if (!model)
|
|
105
|
-
return;
|
|
106
|
-
for (const f of fieldNames) {
|
|
107
|
-
if (f === 'id')
|
|
108
|
-
continue;
|
|
109
|
-
model.modifiedProperties.delete(f);
|
|
110
|
-
}
|
|
82
|
+
store.pool.get(id)?.consumeModifiedFields(fieldNames);
|
|
111
83
|
};
|
|
112
84
|
return {
|
|
113
85
|
// Overloaded — single row or array. The recorder dispatches the
|
|
@@ -15,7 +15,7 @@ import { type SyncStoreContract } from './context.js';
|
|
|
15
15
|
* - **One component, one import.** Consumers write the provider
|
|
16
16
|
* once at the root; nothing else needs to plumb the engine.
|
|
17
17
|
* - **Multiplayer is default.** React consumers are always browsers doing
|
|
18
|
-
* multiplayer UI, so `
|
|
18
|
+
* multiplayer UI, so `useWatch()` / `useAblo()` are always
|
|
19
19
|
* available. No opt-in prop.
|
|
20
20
|
* - **Declarative props for app glue.** `preventUnsavedChanges`,
|
|
21
21
|
* `onSessionExpired`, `postBootstrap`, `resolveUsers` — each
|
|
@@ -36,7 +36,7 @@ import { type SyncStoreContract } from './context.js';
|
|
|
36
36
|
* // Build once at module scope — a new instance per render tears down the socket.
|
|
37
37
|
* const ablo = Ablo({
|
|
38
38
|
* schema,
|
|
39
|
-
*
|
|
39
|
+
* apiKey: () =>
|
|
40
40
|
* fetch('/api/ablo-session', { method: 'POST' })
|
|
41
41
|
* .then((r) => r.json())
|
|
42
42
|
* .then((d) => d.token),
|
|
@@ -114,18 +114,13 @@ export interface AbloProviderProps<R extends SchemaRecord = SchemaRecord> {
|
|
|
114
114
|
export declare function AbloProvider<R extends SchemaRecord = SchemaRecord>(props: AbloProviderProps<R>): React.ReactElement;
|
|
115
115
|
export type { EngineParticipant, ParticipantScope, ParticipantStatus };
|
|
116
116
|
/**
|
|
117
|
-
* Options for `
|
|
117
|
+
* Options for `useWatch`. The hook reuses the engine's single
|
|
118
118
|
* WebSocket and opens a scoped claim on it when `scope` is provided:
|
|
119
119
|
* one TCP connection, N logical sub-syncgroup participants.
|
|
120
120
|
*/
|
|
121
|
-
export interface
|
|
121
|
+
export interface UseWatchOptions {
|
|
122
122
|
readonly scope?: ParticipantScope;
|
|
123
|
-
readonly label?: string;
|
|
124
|
-
readonly as?: unknown;
|
|
125
123
|
readonly ttlSeconds?: number | string | null;
|
|
126
|
-
readonly agent?: unknown;
|
|
127
|
-
readonly idempotencyKey?: string | null;
|
|
128
|
-
readonly autoRefreshThresholdSeconds?: number | null;
|
|
129
124
|
/** Tear down + don't re-join while true. */
|
|
130
125
|
readonly paused?: boolean;
|
|
131
126
|
/**
|
|
@@ -154,7 +149,7 @@ export interface UseParticipantOptions {
|
|
|
154
149
|
}
|
|
155
150
|
/** @deprecated Use `ParticipantStatus`. */
|
|
156
151
|
export type MeshParticipantStatus = ParticipantStatus;
|
|
157
|
-
export interface
|
|
152
|
+
export interface UseWatchReturn {
|
|
158
153
|
readonly participant: EngineParticipant | null;
|
|
159
154
|
/** Everyone else on the engine's sync groups (`participant.presence.others`), bridged to React. */
|
|
160
155
|
readonly peers: ReadonlyArray<Peer>;
|
|
@@ -168,15 +163,19 @@ export interface UseParticipantReturn {
|
|
|
168
163
|
* lifecycle status. Auto-cleans up on unmount or when `paused`
|
|
169
164
|
* flips to true.
|
|
170
165
|
*
|
|
166
|
+
* `useWatch` is the React form of `ablo.<model>.watch` — scope-level
|
|
167
|
+
* read-interest + presence; returns the reactive participant facade
|
|
168
|
+
* (peers/claims/status).
|
|
169
|
+
*
|
|
171
170
|
* The returned `participant` is an `EngineParticipant` — `.presence`
|
|
172
171
|
* + `.claims` only — backed by the engine's existing socket. For
|
|
173
172
|
* headless-bot patterns (a separate identity in the same browser
|
|
174
173
|
* tab), construct a second `Ablo({ kind: 'agent', ... })` directly.
|
|
175
174
|
*/
|
|
176
|
-
export declare function
|
|
175
|
+
export declare function useWatch(opts: UseWatchOptions): UseWatchReturn;
|
|
177
176
|
/**
|
|
178
177
|
* Read-only presence: the OTHER participants currently visible to this
|
|
179
|
-
* connection, bridged to React. Unlike {@link
|
|
178
|
+
* connection, bridged to React. Unlike {@link useWatch}, this does
|
|
180
179
|
* NOT enter/leave a scope (no `update_subscription`, no warm-TTL churn) —
|
|
181
180
|
* it is a pure reader of the engine's already-flowing presence stream.
|
|
182
181
|
*
|
|
@@ -189,7 +188,7 @@ export declare function useParticipant(opts: UseParticipantOptions): UseParticip
|
|
|
189
188
|
* Use this to answer "is anyone else here?" — e.g. suppressing live-cursor
|
|
190
189
|
* broadcasts while alone — when some OTHER mount already owns the scope's
|
|
191
190
|
* read interest (scope `leave` is not reference-counted, so a second
|
|
192
|
-
* `
|
|
191
|
+
* `useWatch` on the same scope would warm-drop the owner's
|
|
193
192
|
* subscription on unmount).
|
|
194
193
|
*
|
|
195
194
|
* ```ts
|
|
@@ -36,7 +36,7 @@ export function AbloProvider(props) {
|
|
|
36
36
|
// REACTIVE binding over it (context + bootstrap gate + error/session
|
|
37
37
|
// forwarding); it does NOT construct, configure, or own the connection. The
|
|
38
38
|
// client owns auth, the credential lifecycle (first mint, refresh, and
|
|
39
|
-
// wake/online/focus re-mint — see `Ablo({
|
|
39
|
+
// wake/online/focus re-mint — see `Ablo({ apiKey })`), transport, and
|
|
40
40
|
// `dispose()`. The CONSUMER built the client, so the consumer owns teardown;
|
|
41
41
|
// the provider never disposes it.
|
|
42
42
|
const engine = client;
|
|
@@ -204,12 +204,16 @@ const EMPTY_INTENTS = Object.freeze([]);
|
|
|
204
204
|
* lifecycle status. Auto-cleans up on unmount or when `paused`
|
|
205
205
|
* flips to true.
|
|
206
206
|
*
|
|
207
|
+
* `useWatch` is the React form of `ablo.<model>.watch` — scope-level
|
|
208
|
+
* read-interest + presence; returns the reactive participant facade
|
|
209
|
+
* (peers/claims/status).
|
|
210
|
+
*
|
|
207
211
|
* The returned `participant` is an `EngineParticipant` — `.presence`
|
|
208
212
|
* + `.claims` only — backed by the engine's existing socket. For
|
|
209
213
|
* headless-bot patterns (a separate identity in the same browser
|
|
210
214
|
* tab), construct a second `Ablo({ kind: 'agent', ... })` directly.
|
|
211
215
|
*/
|
|
212
|
-
export function
|
|
216
|
+
export function useWatch(opts) {
|
|
213
217
|
const ctx = useContext(AbloInternalContext);
|
|
214
218
|
const engine = ctx?.engine ?? null;
|
|
215
219
|
const { paused = false } = opts;
|
|
@@ -338,15 +342,11 @@ export function useParticipant(opts) {
|
|
|
338
342
|
unsubClaims();
|
|
339
343
|
};
|
|
340
344
|
}, [participant, paused]);
|
|
341
|
-
// `opts.as`, `opts.agent`, `opts.idempotencyKey`, and
|
|
342
|
-
// `opts.autoRefreshThresholdSeconds` remain migration placeholders
|
|
343
|
-
// for future capability-mint/attenuation wiring. `scope` is already
|
|
344
|
-
// active: it opens a multiplexed claim on the engine WebSocket.
|
|
345
345
|
return { participant, peers, claims, status, error };
|
|
346
346
|
}
|
|
347
347
|
/**
|
|
348
348
|
* Read-only presence: the OTHER participants currently visible to this
|
|
349
|
-
* connection, bridged to React. Unlike {@link
|
|
349
|
+
* connection, bridged to React. Unlike {@link useWatch}, this does
|
|
350
350
|
* NOT enter/leave a scope (no `update_subscription`, no warm-TTL churn) —
|
|
351
351
|
* it is a pure reader of the engine's already-flowing presence stream.
|
|
352
352
|
*
|
|
@@ -359,7 +359,7 @@ export function useParticipant(opts) {
|
|
|
359
359
|
* Use this to answer "is anyone else here?" — e.g. suppressing live-cursor
|
|
360
360
|
* broadcasts while alone — when some OTHER mount already owns the scope's
|
|
361
361
|
* read interest (scope `leave` is not reference-counted, so a second
|
|
362
|
-
* `
|
|
362
|
+
* `useWatch` on the same scope would warm-drop the owner's
|
|
363
363
|
* subscription on unmount).
|
|
364
364
|
*
|
|
365
365
|
* ```ts
|
|
@@ -370,7 +370,7 @@ export function useParticipant(opts) {
|
|
|
370
370
|
export function usePeers(scope) {
|
|
371
371
|
const ctx = useContext(AbloInternalContext);
|
|
372
372
|
const engine = ctx?.engine ?? null;
|
|
373
|
-
// Resolve scope → groups through the schema (same idiom as
|
|
373
|
+
// Resolve scope → groups through the schema (same idiom as useWatch).
|
|
374
374
|
// The stringified, sorted key is the stable effect dependency.
|
|
375
375
|
const scopeKey = JSON.stringify(resolveParticipantSyncGroups(scope, engine?.schema).sort());
|
|
376
376
|
const groups = useMemo(() => JSON.parse(scopeKey), [scopeKey]);
|
|
@@ -387,7 +387,7 @@ export function usePeers(scope) {
|
|
|
387
387
|
// Plain useState + onChange — presence changes on join/leave/activity
|
|
388
388
|
// only (never on cursor traffic, a separate channel), so this fires
|
|
389
389
|
// rarely; a frame of stale presence is harmless (same rationale as
|
|
390
|
-
//
|
|
390
|
+
// useWatch's peers bridge).
|
|
391
391
|
setPeers(compute());
|
|
392
392
|
return presence.onChange(() => setPeers(compute()));
|
|
393
393
|
}, [engine, scopeKey]);
|
package/dist/react/context.d.ts
CHANGED
|
@@ -137,29 +137,12 @@ export interface SyncReactContext {
|
|
|
137
137
|
* augmentation — see `src/types/global.ts`.
|
|
138
138
|
*/
|
|
139
139
|
schema?: Schema;
|
|
140
|
-
/**
|
|
141
|
-
* Optional presence source. When set, `usePresence()` returns this
|
|
142
|
-
* value cast to the consumer's `ResolvePresence` type (declared via
|
|
143
|
-
* `interface Register { Presence: ... }`). The SDK doesn't own a
|
|
144
|
-
* presence wire format — consumers plug whatever backs their cursors,
|
|
145
|
-
* status, or activity state (a MobX store, a Zustand slice, a custom
|
|
146
|
-
* subscription). The typed-global gives it a call-site-ergonomic
|
|
147
|
-
* type without the SDK dictating the transport.
|
|
148
|
-
*/
|
|
149
|
-
presence?: unknown;
|
|
150
|
-
/**
|
|
151
|
-
* Optional claim initiator. Same pattern as presence — consumers
|
|
152
|
-
* plug a function that turns an claim claim into a handle they
|
|
153
|
-
* control (WebSocket send, optimistic local update, whatever).
|
|
154
|
-
* `useClaim(name)` returns a typed invoker for the named claim
|
|
155
|
-
* from `interface Register { Claims: ... }`.
|
|
156
|
-
*/
|
|
157
|
-
beginClaim?: (claimName: string, claim: unknown) => unknown;
|
|
158
140
|
}
|
|
159
141
|
export declare const SyncContext: import("react").Context<SyncReactContext | null>;
|
|
160
142
|
/**
|
|
161
|
-
* Access the sync store from React components.
|
|
162
|
-
*
|
|
143
|
+
* Access the sync store from React components. The context is provided by
|
|
144
|
+
* `<AbloProvider>` (which renders the internal {@link SyncProvider}); public
|
|
145
|
+
* consumers wire `<AbloProvider client={ablo}>`, never this directly.
|
|
163
146
|
*/
|
|
164
147
|
export declare function useSyncContext(): SyncReactContext;
|
|
165
148
|
/**
|
|
@@ -177,33 +160,15 @@ export interface SyncProviderProps {
|
|
|
177
160
|
* their legacy `(schema, modelKey, …)` signatures.
|
|
178
161
|
*/
|
|
179
162
|
schema?: Schema;
|
|
180
|
-
/**
|
|
181
|
-
* Optional presence source for `usePresence()`. See
|
|
182
|
-
* {@link SyncReactContext.presence} — the consumer plugs whatever
|
|
183
|
-
* backs their presence state; the hook returns it with
|
|
184
|
-
* `ResolvePresence` typing.
|
|
185
|
-
*/
|
|
186
|
-
presence?: unknown;
|
|
187
|
-
/**
|
|
188
|
-
* Optional claim initiator for `useClaim()`. See
|
|
189
|
-
* {@link SyncReactContext.beginClaim}.
|
|
190
|
-
*/
|
|
191
|
-
beginClaim?: (claimName: string, claim: unknown) => unknown;
|
|
192
163
|
children?: ReactNode;
|
|
193
164
|
}
|
|
194
165
|
/**
|
|
195
|
-
* SyncProvider
|
|
196
|
-
* (useModel, useModels, useMutations) can
|
|
197
|
-
*
|
|
198
|
-
* @example
|
|
199
|
-
* import { SyncProvider } from '@abloatai/ablo/react';
|
|
166
|
+
* SyncProvider — the INTERNAL low-level provider that wires a built sync store
|
|
167
|
+
* into React so SDK hooks (useModel, useModels, useMutations) can reach it.
|
|
200
168
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* </SyncProvider>
|
|
206
|
-
* );
|
|
207
|
-
* }
|
|
169
|
+
* Public consumers do NOT use this directly (it is not exported from
|
|
170
|
+
* `@abloatai/ablo/react`). `<AbloProvider client={ablo}>` constructs the
|
|
171
|
+
* store from your `Ablo({ schema, apiKey })` client and renders this provider
|
|
172
|
+
* underneath — reach for `<AbloProvider>`.
|
|
208
173
|
*/
|
|
209
|
-
export declare function SyncProvider({ store, organizationId, schema,
|
|
174
|
+
export declare function SyncProvider({ store, organizationId, schema, children, }: SyncProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SyncReactContext | null>>;
|
package/dist/react/context.js
CHANGED
|
@@ -3,33 +3,28 @@ import { createContext, createElement, useContext } from 'react';
|
|
|
3
3
|
import { AbloValidationError } from '../errors.js';
|
|
4
4
|
export const SyncContext = createContext(null);
|
|
5
5
|
/**
|
|
6
|
-
* Access the sync store from React components.
|
|
7
|
-
*
|
|
6
|
+
* Access the sync store from React components. The context is provided by
|
|
7
|
+
* `<AbloProvider>` (which renders the internal {@link SyncProvider}); public
|
|
8
|
+
* consumers wire `<AbloProvider client={ablo}>`, never this directly.
|
|
8
9
|
*/
|
|
9
10
|
export function useSyncContext() {
|
|
10
11
|
const ctx = useContext(SyncContext);
|
|
11
12
|
if (!ctx) {
|
|
12
|
-
throw new AbloValidationError('
|
|
13
|
+
throw new AbloValidationError('Sync hooks must be used within an <AbloProvider>.', {
|
|
13
14
|
code: 'sync_context_missing_provider',
|
|
14
15
|
});
|
|
15
16
|
}
|
|
16
17
|
return ctx;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
|
-
* SyncProvider
|
|
20
|
-
* (useModel, useModels, useMutations) can
|
|
20
|
+
* SyncProvider — the INTERNAL low-level provider that wires a built sync store
|
|
21
|
+
* into React so SDK hooks (useModel, useModels, useMutations) can reach it.
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* return (
|
|
27
|
-
* <SyncProvider store={syncStore} organizationId={orgId}>
|
|
28
|
-
* <YourApp />
|
|
29
|
-
* </SyncProvider>
|
|
30
|
-
* );
|
|
31
|
-
* }
|
|
23
|
+
* Public consumers do NOT use this directly (it is not exported from
|
|
24
|
+
* `@abloatai/ablo/react`). `<AbloProvider client={ablo}>` constructs the
|
|
25
|
+
* store from your `Ablo({ schema, apiKey })` client and renders this provider
|
|
26
|
+
* underneath — reach for `<AbloProvider>`.
|
|
32
27
|
*/
|
|
33
|
-
export function SyncProvider({ store, organizationId, schema,
|
|
34
|
-
return createElement(SyncContext.Provider, { value: { store, organizationId, schema
|
|
28
|
+
export function SyncProvider({ store, organizationId, schema, children, }) {
|
|
29
|
+
return createElement(SyncContext.Provider, { value: { store, organizationId, schema } }, children);
|
|
35
30
|
}
|