@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
package/dist/react/index.d.ts
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* @abloatai/ablo/react — React bindings (v0.3.0)
|
|
3
3
|
*
|
|
4
4
|
* Umbrella provider:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* const ablo = Ablo({ schema, apiKey }) // build once — module scope or useMemo
|
|
6
|
+
* <AbloProvider client={ablo} fallback={<Skeleton/>}>
|
|
7
|
+
* — `client` is the only required prop (construct it yourself; the provider
|
|
8
|
+
* is the thin reactive binding, like `<Elements stripe={...}>`). `userId`
|
|
9
|
+
* is optional + informational. Owns sync engine + multiplayer lifecycle;
|
|
10
|
+
* the `fallback` prop
|
|
7
11
|
* gates children on first bootstrap. Pass `fallback="passthrough"`
|
|
8
12
|
* to disable the gate.
|
|
9
|
-
* <SyncGroupProvider id="matter:..."> — per-entity scope
|
|
10
13
|
* <ClientSideSuspense fallback={<Skeleton/>}> — NESTED gate inside an
|
|
11
14
|
* already-ready provider. Use only when you need a separate gate
|
|
12
15
|
* for a heavy subtree (e.g. a canvas) while app chrome renders
|
|
@@ -28,9 +31,7 @@
|
|
|
28
31
|
*
|
|
29
32
|
* Multiplayer (always available — `<AbloProvider>` always constructs a client):
|
|
30
33
|
* useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
|
|
31
|
-
*
|
|
32
|
-
* usePresence() — typed presence view
|
|
33
|
-
* useClaim(name) — typed claim dispatcher
|
|
34
|
+
* useWatch({ scope }) — join multiplayer for a scope, get peers/claims
|
|
34
35
|
*
|
|
35
36
|
* ── Breaking changes from v0.2.x ───────────────────────────────────
|
|
36
37
|
* Removed: <SyncProvider>, SyncContext, useSyncContext — folded into
|
|
@@ -44,8 +45,7 @@
|
|
|
44
45
|
* migration notes in CHANGELOG.md.
|
|
45
46
|
*/
|
|
46
47
|
export type { DefaultSyncShape, ResolveSchema, ResolvePresence, ResolveClaims, ResolveUserMeta, ResolveModelKey, } from '../types/global.js';
|
|
47
|
-
export { AbloProvider,
|
|
48
|
-
export { SyncGroupProvider, useSyncGroup, type SyncGroupProviderProps, } from './SyncGroupProvider.js';
|
|
48
|
+
export { AbloProvider, useWatch, usePeers, useSync, useSyncStore, type AbloProviderProps, type ParticipantScope, type ParticipantStatus, type UseWatchOptions, type UseWatchReturn, type MeshParticipantStatus, } from './AbloProvider.js';
|
|
49
49
|
export { ClientSideSuspense, type ClientSideSuspenseProps, } from './ClientSideSuspense.js';
|
|
50
50
|
export { DefaultFallback } from './DefaultFallback.js';
|
|
51
51
|
export type { SyncStoreContract } from './context.js';
|
|
@@ -59,6 +59,4 @@ export type { ReaderActions, ReaderFindOptions } from '../mutators/readerActions
|
|
|
59
59
|
export { useMutators, type MutatorInvokers, type InvokerFor, type UseMutatorsOptions, } from './useMutators.js';
|
|
60
60
|
export { useUndoScope, type UseUndoScopeResult } from './useUndoScope.js';
|
|
61
61
|
export { useAblo, type UseAbloHydratedModelResult, type UseAbloModelOptions, type UseAbloModelResult, } from './useAblo.js';
|
|
62
|
-
export { usePresence } from './usePresence.js';
|
|
63
|
-
export { useClaim } from './useClaim.js';
|
|
64
62
|
export { ModelScope } from '../types/index.js';
|
package/dist/react/index.js
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* @abloatai/ablo/react — React bindings (v0.3.0)
|
|
3
3
|
*
|
|
4
4
|
* Umbrella provider:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* const ablo = Ablo({ schema, apiKey }) // build once — module scope or useMemo
|
|
6
|
+
* <AbloProvider client={ablo} fallback={<Skeleton/>}>
|
|
7
|
+
* — `client` is the only required prop (construct it yourself; the provider
|
|
8
|
+
* is the thin reactive binding, like `<Elements stripe={...}>`). `userId`
|
|
9
|
+
* is optional + informational. Owns sync engine + multiplayer lifecycle;
|
|
10
|
+
* the `fallback` prop
|
|
7
11
|
* gates children on first bootstrap. Pass `fallback="passthrough"`
|
|
8
12
|
* to disable the gate.
|
|
9
|
-
* <SyncGroupProvider id="matter:..."> — per-entity scope
|
|
10
13
|
* <ClientSideSuspense fallback={<Skeleton/>}> — NESTED gate inside an
|
|
11
14
|
* already-ready provider. Use only when you need a separate gate
|
|
12
15
|
* for a heavy subtree (e.g. a canvas) while app chrome renders
|
|
@@ -28,9 +31,7 @@
|
|
|
28
31
|
*
|
|
29
32
|
* Multiplayer (always available — `<AbloProvider>` always constructs a client):
|
|
30
33
|
* useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
|
|
31
|
-
*
|
|
32
|
-
* usePresence() — typed presence view
|
|
33
|
-
* useClaim(name) — typed claim dispatcher
|
|
34
|
+
* useWatch({ scope }) — join multiplayer for a scope, get peers/claims
|
|
34
35
|
*
|
|
35
36
|
* ── Breaking changes from v0.2.x ───────────────────────────────────
|
|
36
37
|
* Removed: <SyncProvider>, SyncContext, useSyncContext — folded into
|
|
@@ -44,8 +45,7 @@
|
|
|
44
45
|
* migration notes in CHANGELOG.md.
|
|
45
46
|
*/
|
|
46
47
|
// ── Umbrella provider + lifecycle hooks ────────────────────────────
|
|
47
|
-
export { AbloProvider,
|
|
48
|
-
export { SyncGroupProvider, useSyncGroup, } from './SyncGroupProvider.js';
|
|
48
|
+
export { AbloProvider, useWatch, usePeers, useSync, useSyncStore, } from './AbloProvider.js';
|
|
49
49
|
export { ClientSideSuspense, } from './ClientSideSuspense.js';
|
|
50
50
|
export { DefaultFallback } from './DefaultFallback.js';
|
|
51
51
|
// ── Status + errors + identity ─────────────────────────────────────
|
|
@@ -63,8 +63,5 @@ export { useReactive } from './useReactive.js';
|
|
|
63
63
|
export { useMutators, } from './useMutators.js';
|
|
64
64
|
export { useUndoScope } from './useUndoScope.js';
|
|
65
65
|
export { useAblo, } from './useAblo.js';
|
|
66
|
-
// ── Presence + claim (typed via Register module augmentation) ─────
|
|
67
|
-
export { usePresence } from './usePresence.js';
|
|
68
|
-
export { useClaim } from './useClaim.js';
|
|
69
66
|
// ── ModelScope re-export ───────────────────────────────────────────
|
|
70
67
|
export { ModelScope } from '../types/index.js';
|
|
@@ -17,8 +17,9 @@ export function useMutators(schemaOrMutators, mutatorsOrOptions, maybeOptions) {
|
|
|
17
17
|
const mutators = (isExplicit ? mutatorsOrOptions : schemaOrMutators);
|
|
18
18
|
const options = (isExplicit ? maybeOptions : mutatorsOrOptions);
|
|
19
19
|
if (!schema) {
|
|
20
|
-
throw new AbloValidationError('useMutators: no schema available. Pass the schema as the first arg ' +
|
|
21
|
-
'or
|
|
20
|
+
throw new AbloValidationError('useMutators: no schema available. Pass the schema as the first arg, ' +
|
|
21
|
+
'or build the <AbloProvider> above with `Ablo({ schema })` so the ' +
|
|
22
|
+
'zero-arg overload can read it from context.', { code: 'mutators_schema_missing' });
|
|
22
23
|
}
|
|
23
24
|
const { undoScope } = options ?? {};
|
|
24
25
|
return useMemo(() => {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* `reason` carries the human-readable close reason when available.
|
|
18
18
|
* - `disconnected` — network failure, server error, or the retry loop
|
|
19
19
|
* gave up. Show the offline / error UI.
|
|
20
|
-
* - `needs-auth` — server rejected the
|
|
20
|
+
* - `needs-auth` — server rejected the auth token (1008/4001/4003). The
|
|
21
21
|
* consumer's `onSessionExpired` callback has already been invoked
|
|
22
22
|
* by `<AbloProvider>`; this variant exists for UI that wants to
|
|
23
23
|
* reflect the auth state itself.
|
|
@@ -34,8 +34,9 @@ export function useUndoScope(schemaOrName, nameOrOptions, maybeOptions) {
|
|
|
34
34
|
const name = isExplicit ? nameOrOptions : schemaOrName;
|
|
35
35
|
const options = (isExplicit ? maybeOptions : nameOrOptions);
|
|
36
36
|
if (!schema) {
|
|
37
|
-
throw new AbloValidationError('useUndoScope: no schema available. Pass the schema as the first arg ' +
|
|
38
|
-
'or
|
|
37
|
+
throw new AbloValidationError('useUndoScope: no schema available. Pass the schema as the first arg, ' +
|
|
38
|
+
'or build the <AbloProvider> above with `Ablo({ schema })` so the ' +
|
|
39
|
+
'zero-arg overload can read it from context.', { code: 'undo_scope_schema_missing' });
|
|
39
40
|
}
|
|
40
41
|
const scope = useMemo(() => {
|
|
41
42
|
// Store is the identity for the manager — one per SyncProvider.
|
package/dist/realtime/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
* subscriptions, presence, offline queueing, and a long-lived WebSocket.
|
|
6
6
|
*/
|
|
7
7
|
export { Ablo, computeFKDepthPriority } from '../client/Ablo.js';
|
|
8
|
-
export type { AbloOptions, InternalAbloOptions,
|
|
8
|
+
export type { AbloOptions, InternalAbloOptions, LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelOperations, } from '../client/Ablo.js';
|
|
9
9
|
import { Ablo } from '../client/Ablo.js';
|
|
10
10
|
export default Ablo;
|
package/dist/schema/generate.js
CHANGED
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
* unions). Relations are resolved by the runtime SDK's typed accessors and are
|
|
15
15
|
* not expanded here.
|
|
16
16
|
*/
|
|
17
|
-
|
|
18
|
-
const BASE_FIELDS = ['id', 'createdAt', 'updatedAt', 'organizationId', 'createdBy'];
|
|
17
|
+
import { BASE_FIELDS } from './schema.js';
|
|
19
18
|
function tsType(meta) {
|
|
20
19
|
switch (meta.type) {
|
|
21
20
|
case 'string':
|
package/dist/schema/model.d.ts
CHANGED
|
@@ -95,9 +95,16 @@ export interface ModelOptions {
|
|
|
95
95
|
*/
|
|
96
96
|
tableName?: string;
|
|
97
97
|
/**
|
|
98
|
-
* Whether this model's table has an organization_id column.
|
|
99
|
-
*
|
|
100
|
-
*
|
|
98
|
+
* Whether this model's table has an `organization_id` column. Default: true.
|
|
99
|
+
* When false, the bootstrap/read query omits the `WHERE organization_id = $1`
|
|
100
|
+
* tenant filter for this model.
|
|
101
|
+
*
|
|
102
|
+
* ⚠ SECURITY — `orgScoped: false` makes the table GLOBALLY READABLE: every
|
|
103
|
+
* client of every tenant sees every row. It is ONLY correct for genuinely
|
|
104
|
+
* tenant-less tables (the `organizations` table itself, global lookups). If
|
|
105
|
+
* rows belong to a tenant through a foreign key but this table has no
|
|
106
|
+
* `organization_id` of its own, use {@link scopedVia} INSTEAD — reaching for
|
|
107
|
+
* `orgScoped: false` there silently exposes the entire table cross-tenant.
|
|
101
108
|
*/
|
|
102
109
|
orgScoped?: boolean;
|
|
103
110
|
/**
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -78,6 +78,15 @@ export declare const baseFieldsSchema: z.ZodObject<{
|
|
|
78
78
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
79
79
|
createdBy: z.ZodOptional<z.ZodString>;
|
|
80
80
|
}, z.core.$strip>;
|
|
81
|
+
/**
|
|
82
|
+
* The base-column names every model carries automatically (the keys of
|
|
83
|
+
* {@link baseFieldsSchema}). The single source of truth — `generate.ts`
|
|
84
|
+
* imports this to avoid double-emitting a redeclared base column, and the
|
|
85
|
+
* `defineSchema` field loop uses it to reject a model that tries to redeclare
|
|
86
|
+
* one (Zod `.merge` would otherwise silently overwrite the base field with the
|
|
87
|
+
* user's, producing a `string & Date` type that breaks the build).
|
|
88
|
+
*/
|
|
89
|
+
export declare const BASE_FIELDS: readonly ["id", "createdAt", "updatedAt", "organizationId", "createdBy"];
|
|
81
90
|
/** The base fields type — pure data columns. */
|
|
82
91
|
export type BaseModelFields = z.infer<typeof baseFieldsSchema>;
|
|
83
92
|
/**
|
|
@@ -138,7 +147,8 @@ export type Model<A, B = never> = [B] extends [never] ? A extends keyof Register
|
|
|
138
147
|
* Drizzle deprecated its own `InferModel` for the same reason. Kept as an
|
|
139
148
|
* alias; no behavior difference.
|
|
140
149
|
*/
|
|
141
|
-
export type InferModel<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape, infer R, infer C> ?
|
|
150
|
+
export type InferModel<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape, infer R, infer C> ? // `Omit<…, keyof BaseModelFields>` so a model that (wrongly) redeclares a
|
|
151
|
+
Omit<z.infer<z.ZodObject<Shape>>, keyof BaseModelFields> & BaseModelFields & BaseModelMethods & InferComputed<C> & InferRelations<S, R> : never;
|
|
142
152
|
/**
|
|
143
153
|
* Infer relation accessor types from a model's relations record.
|
|
144
154
|
*
|
|
@@ -184,7 +194,8 @@ export type InferComputed<C> = string extends keyof C ? unknown : {
|
|
|
184
194
|
* // createdAt, updatedAt are NOT accepted — they're auto-generated
|
|
185
195
|
* ```
|
|
186
196
|
*/
|
|
187
|
-
export type InferCreate<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape> ?
|
|
197
|
+
export type InferCreate<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape> ? // Same reserved-field guard as InferModel: drop any (wrongly) redeclared
|
|
198
|
+
Omit<z.input<z.ZodObject<Shape>>, keyof BaseModelFields> & Partial<BaseModelFields> : never;
|
|
188
199
|
/**
|
|
189
200
|
* Extract all model names from a schema.
|
|
190
201
|
*/
|
package/dist/schema/schema.js
CHANGED
|
@@ -58,6 +58,21 @@ export const baseFieldsSchema = z.object({
|
|
|
58
58
|
organizationId: z.string().optional(),
|
|
59
59
|
createdBy: z.string().optional(),
|
|
60
60
|
});
|
|
61
|
+
/**
|
|
62
|
+
* The base-column names every model carries automatically (the keys of
|
|
63
|
+
* {@link baseFieldsSchema}). The single source of truth — `generate.ts`
|
|
64
|
+
* imports this to avoid double-emitting a redeclared base column, and the
|
|
65
|
+
* `defineSchema` field loop uses it to reject a model that tries to redeclare
|
|
66
|
+
* one (Zod `.merge` would otherwise silently overwrite the base field with the
|
|
67
|
+
* user's, producing a `string & Date` type that breaks the build).
|
|
68
|
+
*/
|
|
69
|
+
export const BASE_FIELDS = [
|
|
70
|
+
'id',
|
|
71
|
+
'createdAt',
|
|
72
|
+
'updatedAt',
|
|
73
|
+
'organizationId',
|
|
74
|
+
'createdBy',
|
|
75
|
+
];
|
|
61
76
|
// ── Factory ───────────────────────────────────────────────────────────────
|
|
62
77
|
/**
|
|
63
78
|
* Define a sync engine schema.
|
|
@@ -146,6 +161,17 @@ export function defineSchema(models, options) {
|
|
|
146
161
|
// failure immediate and unambiguous.
|
|
147
162
|
for (const fieldName of Object.keys(def.shape)) {
|
|
148
163
|
assertRoundTrippableCamelCase(name, fieldName);
|
|
164
|
+
// Reserved base columns are merged in below via `baseFieldsSchema.merge`,
|
|
165
|
+
// and Zod `.merge` silently OVERWRITES the base field with the user's —
|
|
166
|
+
// e.g. a model declaring `createdAt: z.string()` ends up with a field
|
|
167
|
+
// typed `string & Date`, which breaks the build. Reject the collision at
|
|
168
|
+
// definition time so the author sees an unambiguous error instead.
|
|
169
|
+
if (BASE_FIELDS.includes(fieldName)) {
|
|
170
|
+
throw new AbloValidationError(`[defineSchema] ${name}.${fieldName}: field \`${fieldName}\` collides with a ` +
|
|
171
|
+
`reserved field that the SDK provides automatically ` +
|
|
172
|
+
`(${BASE_FIELDS.join(', ')}). Remove it from your model — redeclaring it ` +
|
|
173
|
+
`produces a \`string & Date\` type and breaks the build.`, { code: 'schema_reserved_field', param: `${name}.${fieldName}` });
|
|
174
|
+
}
|
|
149
175
|
}
|
|
150
176
|
validators[name] = baseFieldsSchema.merge(def.schema);
|
|
151
177
|
// Resolve every relation's `foreignKeyColumn` once, now. The builder
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-checked public API-surface manifest — the SDK owns the description of
|
|
3
|
+
* its OWN surface, bound to the real exported types at COMPILE TIME so the MCP
|
|
4
|
+
* `get_api_surface` / docs can never drift from reality.
|
|
5
|
+
*
|
|
6
|
+
* This exists because the hand-authored surface (apps/sync-web/.../api-surface.ts)
|
|
7
|
+
* once named `load` / `count` / `scope` — verbs/options that don't exist — with no
|
|
8
|
+
* coupling to the code. The fix: the name lists live HERE, next to the types, and
|
|
9
|
+
* each is proven EXACTLY equal to the keys of its source interface via
|
|
10
|
+
* `Expect<Equal<…>>`. Add or remove a verb/option without updating the matching
|
|
11
|
+
* tuple and THIS FILE FAILS TO COMPILE (the `Equal` constraint is checked eagerly
|
|
12
|
+
* at the alias declaration — both directions: no phantom name, no missing name).
|
|
13
|
+
*
|
|
14
|
+
* Consumers (the MCP `get_api_surface`) import these NAME tuples and build their
|
|
15
|
+
* prose from them, so a summary can never reference a verb that doesn't exist.
|
|
16
|
+
* NAMES are guaranteed; descriptions stay hand-written (prose can't be type-checked).
|
|
17
|
+
*/
|
|
18
|
+
/** Every method on `ablo.<model>` (the stateful `ModelOperations`). The single
|
|
19
|
+
* source of truth for the model-verb names the docs/MCP may describe. */
|
|
20
|
+
export declare const PUBLIC_MODEL_VERBS: readonly ["retrieve", "list", "get", "getAll", "getCount", "create", "update", "delete", "claim", "watch", "onChange"];
|
|
21
|
+
/** Keys accepted by `list`/`getAll`/`onChange` options (`LocalReadOptions`).
|
|
22
|
+
* Note `state` (lifecycle filter) — NOT `scope` (a historic doc drift). */
|
|
23
|
+
export declare const PUBLIC_LIST_OPTION_KEYS: readonly ["where", "filter", "orderBy", "limit", "offset", "state"];
|
|
24
|
+
/** Public keys of `AbloOptions`. `schema` is required; the rest are optional
|
|
25
|
+
* (the locked happy path is `Ablo({ schema, apiKey, databaseUrl, transport })`). */
|
|
26
|
+
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "databaseUrl", "persistence", "transport", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
|
|
27
|
+
export type ModelVerb = (typeof PUBLIC_MODEL_VERBS)[number];
|
|
28
|
+
export type ListOptionKey = (typeof PUBLIC_LIST_OPTION_KEYS)[number];
|
|
29
|
+
export type AbloOptionKey = (typeof PUBLIC_ABLO_OPTION_KEYS)[number];
|
package/dist/surface.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-checked public API-surface manifest — the SDK owns the description of
|
|
3
|
+
* its OWN surface, bound to the real exported types at COMPILE TIME so the MCP
|
|
4
|
+
* `get_api_surface` / docs can never drift from reality.
|
|
5
|
+
*
|
|
6
|
+
* This exists because the hand-authored surface (apps/sync-web/.../api-surface.ts)
|
|
7
|
+
* once named `load` / `count` / `scope` — verbs/options that don't exist — with no
|
|
8
|
+
* coupling to the code. The fix: the name lists live HERE, next to the types, and
|
|
9
|
+
* each is proven EXACTLY equal to the keys of its source interface via
|
|
10
|
+
* `Expect<Equal<…>>`. Add or remove a verb/option without updating the matching
|
|
11
|
+
* tuple and THIS FILE FAILS TO COMPILE (the `Equal` constraint is checked eagerly
|
|
12
|
+
* at the alias declaration — both directions: no phantom name, no missing name).
|
|
13
|
+
*
|
|
14
|
+
* Consumers (the MCP `get_api_surface`) import these NAME tuples and build their
|
|
15
|
+
* prose from them, so a summary can never reference a verb that doesn't exist.
|
|
16
|
+
* NAMES are guaranteed; descriptions stay hand-written (prose can't be type-checked).
|
|
17
|
+
*/
|
|
18
|
+
// ── the per-`ablo.<model>` verb surface ────────────────────────────────────
|
|
19
|
+
/** Every method on `ablo.<model>` (the stateful `ModelOperations`). The single
|
|
20
|
+
* source of truth for the model-verb names the docs/MCP may describe. */
|
|
21
|
+
export const PUBLIC_MODEL_VERBS = [
|
|
22
|
+
'retrieve',
|
|
23
|
+
'list',
|
|
24
|
+
'get',
|
|
25
|
+
'getAll',
|
|
26
|
+
'getCount',
|
|
27
|
+
'create',
|
|
28
|
+
'update',
|
|
29
|
+
'delete',
|
|
30
|
+
'claim',
|
|
31
|
+
'watch',
|
|
32
|
+
'onChange',
|
|
33
|
+
];
|
|
34
|
+
// ── the read/list query option surface ─────────────────────────────────────
|
|
35
|
+
/** Keys accepted by `list`/`getAll`/`onChange` options (`LocalReadOptions`).
|
|
36
|
+
* Note `state` (lifecycle filter) — NOT `scope` (a historic doc drift). */
|
|
37
|
+
export const PUBLIC_LIST_OPTION_KEYS = [
|
|
38
|
+
'where',
|
|
39
|
+
'filter',
|
|
40
|
+
'orderBy',
|
|
41
|
+
'limit',
|
|
42
|
+
'offset',
|
|
43
|
+
'state',
|
|
44
|
+
];
|
|
45
|
+
// ── the `Ablo({ … })` constructor option surface ───────────────────────────
|
|
46
|
+
/** Public keys of `AbloOptions`. `schema` is required; the rest are optional
|
|
47
|
+
* (the locked happy path is `Ablo({ schema, apiKey, databaseUrl, transport })`). */
|
|
48
|
+
export const PUBLIC_ABLO_OPTION_KEYS = [
|
|
49
|
+
'schema',
|
|
50
|
+
'apiKey',
|
|
51
|
+
'databaseUrl',
|
|
52
|
+
'persistence',
|
|
53
|
+
'transport',
|
|
54
|
+
'authToken',
|
|
55
|
+
'baseURL',
|
|
56
|
+
'fetch',
|
|
57
|
+
'defaultHeaders',
|
|
58
|
+
'defaultQuery',
|
|
59
|
+
'dangerouslyAllowBrowser',
|
|
60
|
+
];
|
|
@@ -20,18 +20,29 @@
|
|
|
20
20
|
* Designed to be embedded by `BaseSyncedStore`: one instance per store,
|
|
21
21
|
* started on first successful connect, disposed on teardown.
|
|
22
22
|
*
|
|
23
|
-
* CONNECTED
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* CONNECTED ──(socket drop)──► PROBING_NETWORK ──► RECONNECTING ──► CONNECTED
|
|
24
|
+
* │ │ │
|
|
25
|
+
* (network lost) ▼ ▼
|
|
26
|
+
* ▼ SESSION_EXPIRED BACKOFF ──► PROBING_NETWORK
|
|
27
|
+
* OFFLINE ──(online)──► PROBING_NETWORK
|
|
28
|
+
* │
|
|
29
|
+
* ▼
|
|
30
|
+
* WAITING_FOR_NETWORK
|
|
27
31
|
*
|
|
28
|
-
* Includes
|
|
32
|
+
* Includes three fixes over the original app-side FSM:
|
|
29
33
|
* 1. `backoff` accepts `NETWORK_ONLINE` / `TAB_VISIBLE` — jumps to
|
|
30
34
|
* probing immediately when the network comes back, without
|
|
31
35
|
* waiting for the backoff timer to elapse.
|
|
32
36
|
* 2. `scheduleBackoff` parks in `waiting_for_network` (resetting
|
|
33
37
|
* `attempt`) when `navigator.onLine === false` at max retries,
|
|
34
38
|
* instead of hard-reloading an already-offline browser.
|
|
39
|
+
* 3. A socket drop (`WS_DISCONNECTED`, typically code 1006) goes
|
|
40
|
+
* STRAIGHT to `probing_network`, not the passive `offline` state.
|
|
41
|
+
* 1006 is browser-local and carries no connectivity signal, so on a
|
|
42
|
+
* healthy machine no `online`/`offline` event ever fires — parking in
|
|
43
|
+
* `offline` stranded recovery until the 30s watchdog, long enough for
|
|
44
|
+
* queued commits to roll back. Only a genuine OS-level `NETWORK_LOST`
|
|
45
|
+
* parks in `offline` and waits for the `online` event.
|
|
35
46
|
*/
|
|
36
47
|
import { type ProbeResult } from './NetworkProbe.js';
|
|
37
48
|
import type { AuthTokenGetter } from '../auth/credentialSource.js';
|
|
@@ -20,18 +20,29 @@
|
|
|
20
20
|
* Designed to be embedded by `BaseSyncedStore`: one instance per store,
|
|
21
21
|
* started on first successful connect, disposed on teardown.
|
|
22
22
|
*
|
|
23
|
-
* CONNECTED
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* CONNECTED ──(socket drop)──► PROBING_NETWORK ──► RECONNECTING ──► CONNECTED
|
|
24
|
+
* │ │ │
|
|
25
|
+
* (network lost) ▼ ▼
|
|
26
|
+
* ▼ SESSION_EXPIRED BACKOFF ──► PROBING_NETWORK
|
|
27
|
+
* OFFLINE ──(online)──► PROBING_NETWORK
|
|
28
|
+
* │
|
|
29
|
+
* ▼
|
|
30
|
+
* WAITING_FOR_NETWORK
|
|
27
31
|
*
|
|
28
|
-
* Includes
|
|
32
|
+
* Includes three fixes over the original app-side FSM:
|
|
29
33
|
* 1. `backoff` accepts `NETWORK_ONLINE` / `TAB_VISIBLE` — jumps to
|
|
30
34
|
* probing immediately when the network comes back, without
|
|
31
35
|
* waiting for the backoff timer to elapse.
|
|
32
36
|
* 2. `scheduleBackoff` parks in `waiting_for_network` (resetting
|
|
33
37
|
* `attempt`) when `navigator.onLine === false` at max retries,
|
|
34
38
|
* instead of hard-reloading an already-offline browser.
|
|
39
|
+
* 3. A socket drop (`WS_DISCONNECTED`, typically code 1006) goes
|
|
40
|
+
* STRAIGHT to `probing_network`, not the passive `offline` state.
|
|
41
|
+
* 1006 is browser-local and carries no connectivity signal, so on a
|
|
42
|
+
* healthy machine no `online`/`offline` event ever fires — parking in
|
|
43
|
+
* `offline` stranded recovery until the 30s watchdog, long enough for
|
|
44
|
+
* queued commits to roll back. Only a genuine OS-level `NETWORK_LOST`
|
|
45
|
+
* parks in `offline` and waits for the `online` event.
|
|
35
46
|
*/
|
|
36
47
|
import { makeAutoObservable, runInAction } from 'mobx';
|
|
37
48
|
import { getContext } from '../context.js';
|
|
@@ -142,8 +153,19 @@ export class ConnectionManager {
|
|
|
142
153
|
case 'connected':
|
|
143
154
|
switch (event.type) {
|
|
144
155
|
case 'NETWORK_LOST':
|
|
145
|
-
|
|
156
|
+
// The OS reported the NIC down — park passively in `offline` and
|
|
157
|
+
// wait for the `online` event. Probing a downed adapter is wasted
|
|
158
|
+
// work.
|
|
146
159
|
return 'offline';
|
|
160
|
+
case 'WS_DISCONNECTED':
|
|
161
|
+
// The socket died (typically code 1006) but the OS network is
|
|
162
|
+
// almost certainly fine — 1006 is generated locally when the TCP
|
|
163
|
+
// conn vanishes and carries NO connectivity signal, so the browser
|
|
164
|
+
// fires no online/offline event. Probe IMMEDIATELY rather than
|
|
165
|
+
// landing in the passive `offline` dead-end (which only escaped via
|
|
166
|
+
// the 30s watchdog, long after queued commits rolled back). The
|
|
167
|
+
// probe fast-fails if we genuinely ARE offline → waiting_for_network.
|
|
168
|
+
return 'probing_network';
|
|
147
169
|
case 'WS_SESSION_ERROR':
|
|
148
170
|
case 'BOOTSTRAP_FAILED_SESSION':
|
|
149
171
|
return 'session_expired';
|
|
@@ -301,7 +323,7 @@ export class ConnectionManager {
|
|
|
301
323
|
}
|
|
302
324
|
}
|
|
303
325
|
// ── Side effects per state ───────────────────────────────────────────
|
|
304
|
-
onEnterState(state,
|
|
326
|
+
onEnterState(state, event) {
|
|
305
327
|
switch (state) {
|
|
306
328
|
case 'connected':
|
|
307
329
|
this.clearBackoffTimer();
|
|
@@ -314,6 +336,19 @@ export class ConnectionManager {
|
|
|
314
336
|
this.callbacks?.onDisconnectWebSocket();
|
|
315
337
|
break;
|
|
316
338
|
case 'probing_network':
|
|
339
|
+
// A socket drop (`WS_DISCONNECTED`) now lands here directly so recovery
|
|
340
|
+
// starts immediately. Tear the dead socket down FIRST — this is what
|
|
341
|
+
// sets SyncWebSocket's `isManualClose=true` and suppresses its own
|
|
342
|
+
// scheduleReconnect, keeping the FSM the single reconnect authority on
|
|
343
|
+
// the human path. The teardown runs synchronously inside the
|
|
344
|
+
// `disconnected` emit, before `SyncWebSocket.onclose` checks the flag,
|
|
345
|
+
// so the timing matches the previous `offline`-entry teardown. We gate
|
|
346
|
+
// on the drop event specifically: the other paths into `probing_network`
|
|
347
|
+
// (TAB_VISIBLE re-validation, handshake retry, backoff elapse) must NOT
|
|
348
|
+
// tear down a socket that may still be live.
|
|
349
|
+
if (event.type === 'WS_DISCONNECTED') {
|
|
350
|
+
this.callbacks?.onDisconnectWebSocket();
|
|
351
|
+
}
|
|
317
352
|
this.runProbe();
|
|
318
353
|
break;
|
|
319
354
|
case 'waiting_for_network':
|
|
@@ -192,7 +192,8 @@ export function createClaimStream(config, transport = null) {
|
|
|
192
192
|
entityId: claim.entityId,
|
|
193
193
|
path: claim.path,
|
|
194
194
|
range: claim.range,
|
|
195
|
-
action
|
|
195
|
+
// Wire field stays `action` (coordination schema); source is `reason`.
|
|
196
|
+
action: claim.reason,
|
|
196
197
|
field: claim.field,
|
|
197
198
|
meta: claim.meta,
|
|
198
199
|
estimatedMs: claim.estimatedMs,
|
|
@@ -244,7 +245,7 @@ export function createClaimStream(config, transport = null) {
|
|
|
244
245
|
range: args.range,
|
|
245
246
|
field: args.field,
|
|
246
247
|
meta: args.meta,
|
|
247
|
-
|
|
248
|
+
reason: args.reason,
|
|
248
249
|
estimatedMs,
|
|
249
250
|
queue: args.queue,
|
|
250
251
|
};
|
|
@@ -261,7 +262,7 @@ export function createClaimStream(config, transport = null) {
|
|
|
261
262
|
return {
|
|
262
263
|
object: 'claim',
|
|
263
264
|
claimId,
|
|
264
|
-
|
|
265
|
+
reason: args.reason,
|
|
265
266
|
target: {
|
|
266
267
|
model: args.entityType,
|
|
267
268
|
id: args.entityId,
|
|
@@ -294,7 +295,7 @@ export function createClaimStream(config, transport = null) {
|
|
|
294
295
|
range: resolved.range,
|
|
295
296
|
field: resolved.field,
|
|
296
297
|
meta: withDescription(resolved.meta, opts?.description),
|
|
297
|
-
|
|
298
|
+
reason: opts?.reason ?? 'editing',
|
|
298
299
|
ttl: opts?.ttl,
|
|
299
300
|
queue: opts?.queue,
|
|
300
301
|
});
|
|
@@ -427,17 +427,6 @@ export declare class TransactionQueue extends EventEmitter {
|
|
|
427
427
|
private extractUpdateData;
|
|
428
428
|
private buildUpdateInput;
|
|
429
429
|
private extractPreviousData;
|
|
430
|
-
/**
|
|
431
|
-
* Re-baseline `modifiedProperties` for the fields a freshly-staged update just
|
|
432
|
-
* committed. Called right after {@link extractPreviousData} freezes their
|
|
433
|
-
* `.old` into the transaction, so the NEXT update to the same field sees this
|
|
434
|
-
* update's result as its baseline rather than the stale pre-session `.old`
|
|
435
|
-
* preserved by `Model.propertyChanged`'s first-old-wins policy. Only consumes
|
|
436
|
-
* keys present in this update — untouched fields keep their baselines. Safe
|
|
437
|
-
* because the wire payload lives on `transaction.data` and rollback restores
|
|
438
|
-
* from `transaction.previousData`; neither re-reads `modifiedProperties`.
|
|
439
|
-
*/
|
|
440
|
-
private consumeModifiedFields;
|
|
441
430
|
/**
|
|
442
431
|
* Public API
|
|
443
432
|
*/
|
|
@@ -780,8 +780,8 @@ export class TransactionQueue extends EventEmitter {
|
|
|
780
780
|
// instead of THIS update's result — corrupting the stream-recorded undo
|
|
781
781
|
// inverse (the second move's "before" would point all the way back). The
|
|
782
782
|
// wire payload is already frozen in `transaction.data`, so dropping the
|
|
783
|
-
// consumed entries is safe.
|
|
784
|
-
|
|
783
|
+
// consumed entries is safe.
|
|
784
|
+
model.consumeModifiedFields(Object.keys(updateInput));
|
|
785
785
|
const modelKey = normalizeModelKey(actualModelName);
|
|
786
786
|
const priorityScore = this.computePriorityScore('update', actualModelName);
|
|
787
787
|
const transaction = {
|
|
@@ -1999,63 +1999,19 @@ export class TransactionQueue extends EventEmitter {
|
|
|
1999
1999
|
// model ever needs to surface previous-state outside `modifiedProperties`,
|
|
2000
2000
|
// expose a typed `getPreviousData()` accessor on Model and call that.
|
|
2001
2001
|
extractPreviousData(model, updateInput) {
|
|
2002
|
-
const prev = { id: model.id };
|
|
2003
|
-
const modified = model.modifiedProperties instanceof Map ? model.modifiedProperties : null;
|
|
2004
2002
|
// When the update's written keys are known, capture a before-image for
|
|
2005
2003
|
// EXACTLY those keys so the recorded undo inverse can revert them and only
|
|
2006
2004
|
// them (a full-row inverse would clobber concurrent edits to unrelated
|
|
2007
|
-
// fields).
|
|
2008
|
-
//
|
|
2009
|
-
//
|
|
2010
|
-
//
|
|
2011
|
-
//
|
|
2012
|
-
//
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
const original = model.getOriginalSnapshot();
|
|
2018
|
-
for (const key of Object.keys(updateInput)) {
|
|
2019
|
-
if (key === 'id')
|
|
2020
|
-
continue;
|
|
2021
|
-
const mod = modified?.get(key);
|
|
2022
|
-
if (mod) {
|
|
2023
|
-
prev[key] = mod.old;
|
|
2024
|
-
}
|
|
2025
|
-
else if (original && key in original) {
|
|
2026
|
-
prev[key] = original[key];
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
return prev;
|
|
2030
|
-
}
|
|
2031
|
-
if (modified && modified.size > 0) {
|
|
2032
|
-
for (const [key, change] of modified) {
|
|
2033
|
-
prev[key] = change.old;
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
return prev;
|
|
2037
|
-
}
|
|
2038
|
-
/**
|
|
2039
|
-
* Re-baseline `modifiedProperties` for the fields a freshly-staged update just
|
|
2040
|
-
* committed. Called right after {@link extractPreviousData} freezes their
|
|
2041
|
-
* `.old` into the transaction, so the NEXT update to the same field sees this
|
|
2042
|
-
* update's result as its baseline rather than the stale pre-session `.old`
|
|
2043
|
-
* preserved by `Model.propertyChanged`'s first-old-wins policy. Only consumes
|
|
2044
|
-
* keys present in this update — untouched fields keep their baselines. Safe
|
|
2045
|
-
* because the wire payload lives on `transaction.data` and rollback restores
|
|
2046
|
-
* from `transaction.previousData`; neither re-reads `modifiedProperties`.
|
|
2047
|
-
*/
|
|
2048
|
-
consumeModifiedFields(model, updateInput) {
|
|
2049
|
-
if (!(model.modifiedProperties instanceof Map) || model.modifiedProperties.size === 0) {
|
|
2050
|
-
return;
|
|
2051
|
-
}
|
|
2052
|
-
for (const key of [...model.modifiedProperties.keys()]) {
|
|
2053
|
-
if (key === 'id')
|
|
2054
|
-
continue;
|
|
2055
|
-
if (updateInput && !(key in updateInput))
|
|
2056
|
-
continue;
|
|
2057
|
-
model.modifiedProperties.delete(key);
|
|
2058
|
-
}
|
|
2005
|
+
// fields). `fallbackToLive: false` makes `Model.capturePreviousValues` OMIT
|
|
2006
|
+
// any key it can't resolve from `modifiedProperties.old` / the original
|
|
2007
|
+
// snapshot — `buildUndoOps` then drops an un-revertible inverse rather than
|
|
2008
|
+
// inventing one. With no `updateInput` (full extract) fall back to every
|
|
2009
|
+
// tracked field. `Model.capturePreviousValues` is the single before-image
|
|
2010
|
+
// source shared with `RecordingTransaction.snapshotFields`.
|
|
2011
|
+
const keys = updateInput
|
|
2012
|
+
? Object.keys(updateInput)
|
|
2013
|
+
: [...(model.modifiedProperties instanceof Map ? model.modifiedProperties.keys() : [])];
|
|
2014
|
+
return { id: model.id, ...model.capturePreviousValues(keys, { fallbackToLive: false }) };
|
|
2059
2015
|
}
|
|
2060
2016
|
/**
|
|
2061
2017
|
* Public API
|
package/dist/types/global.d.ts
CHANGED
|
@@ -60,6 +60,9 @@ export interface DefaultSyncShape {
|
|
|
60
60
|
* Empty by default — every SDK resolver falls back to {@link DefaultSyncShape}
|
|
61
61
|
* when an expected key is absent. Exported from the package root so the module
|
|
62
62
|
* augmentation merges into this declaration.
|
|
63
|
+
*
|
|
64
|
+
* The `Schema` augmentation key holds the type produced by `defineSchema`, so
|
|
65
|
+
* the same noun reads consistently here and in {@link ResolveSchema}.
|
|
63
66
|
*/
|
|
64
67
|
export interface Register {
|
|
65
68
|
}
|