@abloatai/ablo 0.5.1 → 0.6.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 +16 -0
- package/README.md +217 -122
- package/dist/BaseSyncedStore.d.ts +2 -2
- package/dist/BaseSyncedStore.js +2 -2
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.js +1 -1
- package/dist/client/Ablo.d.ts +90 -93
- package/dist/client/Ablo.js +121 -60
- package/dist/client/ApiClient.d.ts +14 -14
- package/dist/client/ApiClient.js +81 -55
- package/dist/client/createInternalComponents.d.ts +2 -3
- package/dist/client/createInternalComponents.js +2 -3
- package/dist/client/createModelProxy.d.ts +90 -87
- package/dist/client/createModelProxy.js +124 -127
- package/dist/client/index.d.ts +6 -7
- package/dist/client/index.js +4 -5
- package/dist/client/validateAbloOptions.js +3 -3
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +7 -0
- package/dist/errors.d.ts +8 -8
- package/dist/errors.js +18 -10
- package/dist/index.d.ts +9 -8
- package/dist/index.js +7 -11
- package/dist/interfaces/index.d.ts +2 -10
- package/dist/mutators/Transaction.d.ts +2 -2
- package/dist/mutators/Transaction.js +2 -2
- package/dist/mutators/mutateActions.d.ts +44 -0
- package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
- package/dist/mutators/readerActions.d.ts +32 -0
- package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
- package/dist/query/types.d.ts +1 -1
- package/dist/react/AbloProvider.d.ts +1 -1
- package/dist/react/AbloProvider.js +3 -3
- package/dist/react/context.d.ts +4 -4
- package/dist/react/index.d.ts +4 -5
- package/dist/react/index.js +3 -7
- package/dist/react/useAblo.d.ts +14 -14
- package/dist/react/useAblo.js +26 -26
- package/dist/react/useIntent.d.ts +2 -2
- package/dist/react/useIntent.js +2 -2
- package/dist/react/useMutators.d.ts +1 -1
- package/dist/react/usePresence.d.ts +3 -3
- package/dist/react/usePresence.js +4 -4
- package/dist/react/useUndoScope.d.ts +1 -1
- package/dist/schema/diff.d.ts +161 -0
- package/dist/schema/diff.js +262 -0
- package/dist/schema/generate.d.ts +19 -0
- package/dist/schema/generate.js +87 -0
- package/dist/schema/index.d.ts +4 -1
- package/dist/schema/index.js +7 -1
- package/dist/schema/schema.d.ts +83 -32
- package/dist/schema/schema.js +58 -12
- package/dist/schema/serialize.d.ts +92 -0
- package/dist/schema/serialize.js +227 -0
- package/dist/sync/SyncWebSocket.d.ts +17 -0
- package/dist/sync/SyncWebSocket.js +46 -1
- package/dist/sync/awaitIntentGrant.d.ts +26 -0
- package/dist/sync/awaitIntentGrant.js +60 -0
- package/dist/sync/createIntentStream.js +43 -4
- package/dist/sync/createPresenceStream.js +1 -1
- package/dist/sync/participants.d.ts +2 -2
- package/dist/sync/participants.js +4 -4
- package/dist/types/global.d.ts +43 -52
- package/dist/types/global.js +16 -18
- package/dist/types/streams.d.ts +37 -9
- package/docs/api.md +68 -158
- package/docs/audit.md +5 -5
- package/docs/client-behavior.md +41 -42
- package/docs/coordination.md +294 -0
- package/docs/data-sources.md +14 -14
- package/docs/examples/agent-human.md +30 -32
- package/docs/examples/ai-sdk-tool.md +32 -33
- package/docs/examples/existing-python-backend.md +35 -33
- package/docs/examples/nextjs.md +24 -25
- package/docs/examples/server-agent.md +20 -61
- package/docs/guarantees.md +30 -55
- package/docs/identity.md +458 -0
- package/docs/index.md +12 -24
- package/docs/integration-guide.md +106 -116
- package/docs/interaction-model.md +29 -95
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +1 -1
- package/docs/mcp.md +11 -26
- package/docs/quickstart.md +43 -49
- package/docs/react.md +73 -23
- package/docs/roadmap.md +5 -7
- package/llms.txt +34 -39
- package/package.json +1 -1
- package/dist/react/useMutate.d.ts +0 -83
- package/dist/react/useQuery.d.ts +0 -123
- package/dist/react/useQuery.js +0 -145
- package/dist/react/useReader.d.ts +0 -69
- package/docs/capabilities.md +0 -163
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Schema, InferModel, InferCreate } from '../schema/schema.js';
|
|
2
|
+
import type { SyncStoreContract } from '../react/context.js';
|
|
3
|
+
/**
|
|
4
|
+
* `create` / `update` / `delete` are overloaded: pass one row or an array
|
|
5
|
+
* (Drizzle/Prisma `values(rowOrRows)` shape). Every entry in an array call
|
|
6
|
+
* lands in the same synchronous tick (`Promise.all`), so the microtask
|
|
7
|
+
* coalescer in `TransactionQueue` collapses N pushes into one wire commit.
|
|
8
|
+
*
|
|
9
|
+
* This module is the React-free core of CRUD staging. The transaction system
|
|
10
|
+
* (`Transaction` / `RecordingTransaction`) and `BaseSyncedStore` build on it;
|
|
11
|
+
* there is no React hook here (the legacy `useMutate` hook was removed —
|
|
12
|
+
* callers use `ablo.<model>.create/update/delete`).
|
|
13
|
+
*/
|
|
14
|
+
type UpdatePatch<S extends Schema, K extends keyof S['models'] & string> = {
|
|
15
|
+
id: string;
|
|
16
|
+
} & Partial<InferModel<S, K>>;
|
|
17
|
+
export interface MutateActions<S extends Schema, K extends keyof S['models'] & string> {
|
|
18
|
+
/**
|
|
19
|
+
* Create one entity, or an array of entities in a single tick. ID,
|
|
20
|
+
* createdAt, updatedAt, organizationId default automatically per row.
|
|
21
|
+
*/
|
|
22
|
+
create(data: InferCreate<S, K>): Promise<InferModel<S, K>>;
|
|
23
|
+
create(data: InferCreate<S, K>[]): Promise<InferModel<S, K>[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Update one row, or an array of rows in a single tick. Each patch is
|
|
26
|
+
* `{ id, ...changes }` — missing ids throw. Schema-generated models are
|
|
27
|
+
* MobX-observable, so direct assignment fires reactivity.
|
|
28
|
+
*/
|
|
29
|
+
update(patch: UpdatePatch<S, K>): Promise<InferModel<S, K>>;
|
|
30
|
+
update(patches: UpdatePatch<S, K>[]): Promise<InferModel<S, K>[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Delete one row by id, or an array of ids in a single tick. Missing ids
|
|
33
|
+
* are silently ignored.
|
|
34
|
+
*/
|
|
35
|
+
delete(id: string): Promise<void>;
|
|
36
|
+
delete(ids: string[]): Promise<void>;
|
|
37
|
+
/** Soft-archive by ID. */
|
|
38
|
+
archive: (id: string) => Promise<void>;
|
|
39
|
+
/** Restore an archived entity by ID. */
|
|
40
|
+
unarchive: (id: string) => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/** Pure factory — builds CRUD actions over a store for one model. React-free. */
|
|
43
|
+
export declare function createMutateActions<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, store: SyncStoreContract, organizationId: string): MutateActions<S, K>;
|
|
44
|
+
export {};
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { useMemo } from 'react';
|
|
3
1
|
import { Model, modelAsRow } from '../Model.js';
|
|
4
2
|
import { AbloValidationError } from '../errors.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Pure factory — testable without React. The hook just wraps this in
|
|
8
|
-
* useMemo with the React context.
|
|
9
|
-
*/
|
|
3
|
+
/** Pure factory — builds CRUD actions over a store for one model. React-free. */
|
|
10
4
|
export function createMutateActions(schema, modelKey, store, organizationId) {
|
|
11
5
|
const modelDef = schema.models[modelKey];
|
|
12
6
|
const typename = modelDef?.typename ?? modelKey;
|
|
@@ -25,7 +19,7 @@ export function createMutateActions(schema, modelKey, store, organizationId) {
|
|
|
25
19
|
};
|
|
26
20
|
const model = store.pool.createFromData(fullData);
|
|
27
21
|
if (!model) {
|
|
28
|
-
throw new AbloValidationError(`
|
|
22
|
+
throw new AbloValidationError(`createMutateActions: failed to create ${typename} — no constructor in registry`, { code: 'mutate_create_unknown_model' });
|
|
29
23
|
}
|
|
30
24
|
return model;
|
|
31
25
|
};
|
|
@@ -34,14 +28,13 @@ export function createMutateActions(schema, modelKey, store, organizationId) {
|
|
|
34
28
|
const { id, ...changes } = patch;
|
|
35
29
|
const model = store.pool.get(id);
|
|
36
30
|
if (!model) {
|
|
37
|
-
throw new AbloValidationError(`
|
|
31
|
+
throw new AbloValidationError(`createMutateActions: ${typename} with id "${id}" not found in pool`, { code: 'mutate_update_entity_not_found' });
|
|
38
32
|
}
|
|
39
|
-
// Schema-derived patch keys are validated at the call-site type
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// guarantees these keys resolve at runtime.
|
|
33
|
+
// Schema-derived patch keys are validated at the call-site type signature
|
|
34
|
+
// (`UpdatePatch<S, K>`); writes here are dynamic-class field assignments.
|
|
35
|
+
// `Reflect.set` is the typed bridge — Model carries no index signature, but
|
|
36
|
+
// the dynamic field installation in `createDynamicModelClass` guarantees
|
|
37
|
+
// these keys resolve at runtime.
|
|
45
38
|
for (const [fieldName, value] of Object.entries(changes)) {
|
|
46
39
|
Reflect.set(model, fieldName, value);
|
|
47
40
|
}
|
|
@@ -49,9 +42,9 @@ export function createMutateActions(schema, modelKey, store, organizationId) {
|
|
|
49
42
|
return model;
|
|
50
43
|
};
|
|
51
44
|
return {
|
|
52
|
-
// Overloaded — runtime
|
|
53
|
-
//
|
|
54
|
-
//
|
|
45
|
+
// Overloaded — runtime `Array.isArray` decides shape. Both branches stage
|
|
46
|
+
// via `Promise.all` so the microtask coalescer collapses N pushes into one
|
|
47
|
+
// wire commit.
|
|
55
48
|
create: (async (data) => {
|
|
56
49
|
const now = new Date();
|
|
57
50
|
if (Array.isArray(data)) {
|
|
@@ -110,13 +103,3 @@ export function createMutateActions(schema, modelKey, store, organizationId) {
|
|
|
110
103
|
},
|
|
111
104
|
};
|
|
112
105
|
}
|
|
113
|
-
export function useMutate(schemaOrKey, maybeKey) {
|
|
114
|
-
const { store, organizationId, schema: ctxSchema } = useSyncContext();
|
|
115
|
-
const resolvedSchema = typeof schemaOrKey === 'string' ? ctxSchema : schemaOrKey;
|
|
116
|
-
const resolvedKey = typeof schemaOrKey === 'string' ? schemaOrKey : maybeKey;
|
|
117
|
-
if (!resolvedSchema) {
|
|
118
|
-
throw new AbloValidationError('useMutate: no schema available. Pass the schema as the first arg ' +
|
|
119
|
-
'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'mutate_schema_missing' });
|
|
120
|
-
}
|
|
121
|
-
return useMemo(() => createMutateActions(resolvedSchema, resolvedKey, store, organizationId), [store, organizationId, resolvedSchema, resolvedKey]);
|
|
122
|
-
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Schema, InferModel } from '../schema/schema.js';
|
|
2
|
+
import type { SyncStoreContract } from '../react/context.js';
|
|
3
|
+
/**
|
|
4
|
+
* React-free imperative reads over a store: one-off `retrieve`/`list`/`count`
|
|
5
|
+
* snapshots that do NOT subscribe to changes. Used by the transaction system
|
|
6
|
+
* and `BaseSyncedStore`. For reactive reads in components use
|
|
7
|
+
* `useAblo((ablo) => ablo.<model>.retrieve(id) / .list(opts))`.
|
|
8
|
+
*/
|
|
9
|
+
export interface ReaderFindOptions<T> {
|
|
10
|
+
/** Equality filter — uses FK index when the field is registered. */
|
|
11
|
+
where?: Partial<T>;
|
|
12
|
+
/** Predicate applied AFTER `where` filtering. */
|
|
13
|
+
filter?: (entity: T) => boolean;
|
|
14
|
+
/** Sort field. */
|
|
15
|
+
orderBy?: keyof T & string;
|
|
16
|
+
/** Sort direction. Default: 'asc'. */
|
|
17
|
+
order?: 'asc' | 'desc';
|
|
18
|
+
/** Max results. */
|
|
19
|
+
limit?: number;
|
|
20
|
+
/** Skip N results. */
|
|
21
|
+
offset?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ReaderActions<S extends Schema, K extends keyof S['models'] & string> {
|
|
24
|
+
/** Get a single entity by id. Returns undefined if not in pool. */
|
|
25
|
+
retrieve: (id: string) => InferModel<S, K> | undefined;
|
|
26
|
+
/** Read a collection with optional filters. Snapshot — not reactive. */
|
|
27
|
+
list: (options?: ReaderFindOptions<InferModel<S, K>>) => InferModel<S, K>[];
|
|
28
|
+
/** Count entities matching the options. */
|
|
29
|
+
count: (options?: ReaderFindOptions<InferModel<S, K>>) => number;
|
|
30
|
+
}
|
|
31
|
+
/** Pure factory — builds imperative read actions over a store for one model. */
|
|
32
|
+
export declare function createReaderActions<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, store: SyncStoreContract): ReaderActions<S, K>;
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
import { useMemo } from 'react';
|
|
3
|
-
import { useSyncContext } from './context.js';
|
|
4
|
-
import { AbloValidationError } from '../errors.js';
|
|
5
|
-
/**
|
|
6
|
-
* Pure factory — testable without React. `useReader` wraps this in useMemo.
|
|
7
|
-
*/
|
|
1
|
+
/** Pure factory — builds imperative read actions over a store for one model. */
|
|
8
2
|
export function createReaderActions(schema, modelKey, store) {
|
|
9
3
|
const modelDef = schema.models[modelKey];
|
|
10
4
|
const typename = modelDef?.typename ?? modelKey;
|
|
11
5
|
function read(options) {
|
|
12
|
-
// FK index fast path: single-field `where` on a registered FK index → O(1)
|
|
6
|
+
// FK index fast path: single-field `where` on a registered FK index → O(1).
|
|
13
7
|
let candidates;
|
|
14
8
|
const whereEntries = options?.where ? Object.entries(options.where) : [];
|
|
15
9
|
const singleWhere = whereEntries.length === 1 ? whereEntries[0] : undefined;
|
|
@@ -61,13 +55,3 @@ export function createReaderActions(schema, modelKey, store) {
|
|
|
61
55
|
count: (options) => read(options).length,
|
|
62
56
|
};
|
|
63
57
|
}
|
|
64
|
-
export function useReader(schemaOrKey, maybeKey) {
|
|
65
|
-
const { store, schema: ctxSchema } = useSyncContext();
|
|
66
|
-
const resolvedSchema = typeof schemaOrKey === 'string' ? ctxSchema : schemaOrKey;
|
|
67
|
-
const resolvedKey = typeof schemaOrKey === 'string' ? schemaOrKey : maybeKey;
|
|
68
|
-
if (!resolvedSchema) {
|
|
69
|
-
throw new AbloValidationError('useReader: no schema available. Pass the schema as the first arg ' +
|
|
70
|
-
'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'reader_schema_missing' });
|
|
71
|
-
}
|
|
72
|
-
return useMemo(() => createReaderActions(resolvedSchema, resolvedKey, store), [store, resolvedSchema, resolvedKey]);
|
|
73
|
-
}
|
package/dist/query/types.d.ts
CHANGED
|
@@ -135,7 +135,7 @@ export interface QueryBatchResult {
|
|
|
135
135
|
*/
|
|
136
136
|
results: unknown[];
|
|
137
137
|
/**
|
|
138
|
-
* Server watermark observed after the batch ran. Public
|
|
138
|
+
* Server watermark observed after the batch ran. Public model reads
|
|
139
139
|
* expose this as `stamp` and callers thread it into `commits.create({
|
|
140
140
|
* readAt })` to reject stale writes.
|
|
141
141
|
*/
|
|
@@ -212,7 +212,7 @@ export declare function useParticipant(opts: UseParticipantOptions): UseParticip
|
|
|
212
212
|
/**
|
|
213
213
|
* Returns the raw `SyncEngine` proxy. Typically you want the typed
|
|
214
214
|
* hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
|
|
215
|
-
* where you need direct access (e.g., `sync.tasks.
|
|
215
|
+
* where you need direct access (e.g., `sync.tasks.onChange(cb)`).
|
|
216
216
|
*
|
|
217
217
|
* The generic parameter narrows the return type to your schema's
|
|
218
218
|
* model record so call sites get typed `sync.tasks.findMany()` /
|
|
@@ -326,10 +326,10 @@ export function useParticipant(opts) {
|
|
|
326
326
|
}
|
|
327
327
|
setPeers(participant.presence.others);
|
|
328
328
|
setClaims(participant.intents.others);
|
|
329
|
-
const unsubPresence = participant.presence.
|
|
329
|
+
const unsubPresence = participant.presence.onChange(() => {
|
|
330
330
|
setPeers(participant.presence.others);
|
|
331
331
|
});
|
|
332
|
-
const unsubIntents = participant.intents.
|
|
332
|
+
const unsubIntents = participant.intents.onChange(() => {
|
|
333
333
|
setClaims(participant.intents.others);
|
|
334
334
|
});
|
|
335
335
|
return () => {
|
|
@@ -347,7 +347,7 @@ export function useParticipant(opts) {
|
|
|
347
347
|
/**
|
|
348
348
|
* Returns the raw `SyncEngine` proxy. Typically you want the typed
|
|
349
349
|
* hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
|
|
350
|
-
* where you need direct access (e.g., `sync.tasks.
|
|
350
|
+
* where you need direct access (e.g., `sync.tasks.onChange(cb)`).
|
|
351
351
|
*
|
|
352
352
|
* The generic parameter narrows the return type to your schema's
|
|
353
353
|
* model record so call sites get typed `sync.tasks.findMany()` /
|
package/dist/react/context.d.ts
CHANGED
|
@@ -85,14 +85,14 @@ export interface SyncReactContext {
|
|
|
85
85
|
* The stored reference is untyped here (`Schema` with default
|
|
86
86
|
* parameters) because the React context is a single runtime value
|
|
87
87
|
* shared by every hook. The compile-time types flow from the
|
|
88
|
-
* consumer's `declare
|
|
88
|
+
* consumer's `declare module '@abloatai/ablo' { interface Register { Schema: ... } }`
|
|
89
89
|
* augmentation — see `src/types/global.ts`.
|
|
90
90
|
*/
|
|
91
91
|
schema?: Schema;
|
|
92
92
|
/**
|
|
93
93
|
* Optional presence source. When set, `usePresence()` returns this
|
|
94
94
|
* value cast to the consumer's `ResolvePresence` type (declared via
|
|
95
|
-
* `interface
|
|
95
|
+
* `interface Register { Presence: ... }`). The SDK doesn't own a
|
|
96
96
|
* presence wire format — consumers plug whatever backs their cursors,
|
|
97
97
|
* status, or activity state (a MobX store, a Zustand slice, a custom
|
|
98
98
|
* subscription). The typed-global gives it a call-site-ergonomic
|
|
@@ -104,7 +104,7 @@ export interface SyncReactContext {
|
|
|
104
104
|
* plug a function that turns an intent claim into a handle they
|
|
105
105
|
* control (WebSocket send, optimistic local update, whatever).
|
|
106
106
|
* `useIntent(name)` returns a typed invoker for the named intent
|
|
107
|
-
* from `interface
|
|
107
|
+
* from `interface Register { Intents: ... }`.
|
|
108
108
|
*/
|
|
109
109
|
beginIntent?: (intentName: string, claim: unknown) => unknown;
|
|
110
110
|
}
|
|
@@ -125,7 +125,7 @@ export interface SyncProviderProps {
|
|
|
125
125
|
/**
|
|
126
126
|
* Optional schema. Wire this when you want compatibility string-keyed hooks
|
|
127
127
|
* (`useQuery('tasks')`) — the schema type also narrows via the
|
|
128
|
-
* consumer's
|
|
128
|
+
* consumer's `Register` registration. Omit to keep hooks on
|
|
129
129
|
* their legacy `(schema, modelKey, …)` signatures.
|
|
130
130
|
*/
|
|
131
131
|
schema?: Schema;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* Data hooks:
|
|
16
16
|
* useAblo((ablo) => ablo.tasks.retrieve(id)) — primary React read API
|
|
17
17
|
* useAblo() — typed client for callbacks/effects
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* (reads: ablo.<model>.retrieve/list;
|
|
19
|
+
* writes: ablo.<model>.create/update/delete)
|
|
20
20
|
* useMutators(defs, opts?) — Zero-style custom mutators
|
|
21
21
|
* useUndoScope(name) — per-surface undo/redo
|
|
22
22
|
*
|
|
@@ -53,9 +53,8 @@ export { useErrorListener } from './useErrorListener.js';
|
|
|
53
53
|
export { useMutationFailureListener, type MutationFailurePayload, } from './useMutationFailureListener.js';
|
|
54
54
|
export { useCurrentUserId } from './useCurrentUserId.js';
|
|
55
55
|
export { useReactive } from './useReactive.js';
|
|
56
|
-
export {
|
|
57
|
-
export {
|
|
58
|
-
export { useReader, type ReaderActions, type ReaderFindOptions } from './useReader.js';
|
|
56
|
+
export type { MutateActions } from '../mutators/mutateActions.js';
|
|
57
|
+
export type { ReaderActions, ReaderFindOptions } from '../mutators/readerActions.js';
|
|
59
58
|
export { useMutators, type MutatorInvokers, type InvokerFor, type UseMutatorsOptions, } from './useMutators.js';
|
|
60
59
|
export { useUndoScope, type UseUndoScopeResult } from './useUndoScope.js';
|
|
61
60
|
export { useAblo, type UseAbloHydratedModelResult, type UseAbloModelOptions, type UseAbloModelResult, } from './useAblo.js';
|
package/dist/react/index.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* Data hooks:
|
|
16
16
|
* useAblo((ablo) => ablo.tasks.retrieve(id)) — primary React read API
|
|
17
17
|
* useAblo() — typed client for callbacks/effects
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* (reads: ablo.<model>.retrieve/list;
|
|
19
|
+
* writes: ablo.<model>.create/update/delete)
|
|
20
20
|
* useMutators(defs, opts?) — Zero-style custom mutators
|
|
21
21
|
* useUndoScope(name) — per-surface undo/redo
|
|
22
22
|
*
|
|
@@ -59,14 +59,10 @@ export { useCurrentUserId } from './useCurrentUserId.js';
|
|
|
59
59
|
// lower-level `useSyncExternalStore`. Hides the cached-snapshot
|
|
60
60
|
// contract and handles default structural equality for arrays.
|
|
61
61
|
export { useReactive } from './useReactive.js';
|
|
62
|
-
// ── Data hooks ─────────────────────────────────────────────────────
|
|
63
|
-
export { useQuery, useOne } from './useQuery.js';
|
|
64
|
-
export { useMutate } from './useMutate.js';
|
|
65
|
-
export { useReader } from './useReader.js';
|
|
66
62
|
export { useMutators, } from './useMutators.js';
|
|
67
63
|
export { useUndoScope } from './useUndoScope.js';
|
|
68
64
|
export { useAblo, } from './useAblo.js';
|
|
69
|
-
// ── Presence + intent (typed via
|
|
65
|
+
// ── Presence + intent (typed via Register module augmentation) ─────
|
|
70
66
|
export { usePresence } from './usePresence.js';
|
|
71
67
|
export { useIntent } from './useIntent.js';
|
|
72
68
|
// ── ModelScope re-export ───────────────────────────────────────────
|
package/dist/react/useAblo.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Ablo,
|
|
1
|
+
import type { Ablo, ModelClaim } from '../client/Ablo.js';
|
|
2
2
|
import type { ModelOperations } from '../client/createModelProxy.js';
|
|
3
3
|
import type { SchemaRecord } from '../schema/schema.js';
|
|
4
4
|
import type { ResolveSchema } from '../types/global.js';
|
|
5
5
|
/**
|
|
6
6
|
* Resolved schema-record type for the consumer's app. Reads the
|
|
7
|
-
* `
|
|
7
|
+
* `Register` module augmentation if declared, falls back to the
|
|
8
8
|
* loose `SchemaRecord` if not. This lets `useAblo()` produce a
|
|
9
9
|
* fully typed engine handle without the consumer having to pass
|
|
10
10
|
* `<(typeof schema)['models']>` at every call site.
|
|
@@ -12,12 +12,12 @@ import type { ResolveSchema } from '../types/global.js';
|
|
|
12
12
|
type DefaultModels = ResolveSchema extends {
|
|
13
13
|
models: infer M;
|
|
14
14
|
} ? M extends SchemaRecord ? M : SchemaRecord : SchemaRecord;
|
|
15
|
-
type
|
|
15
|
+
type ModelClientSelector<R extends SchemaRecord, T, C> = (ablo: Ablo<R>) => ModelOperations<T, C>;
|
|
16
16
|
type AbloSelector<R extends SchemaRecord, T> = (ablo: Ablo<R>) => T;
|
|
17
17
|
export interface UseAbloModelOptions<T> {
|
|
18
18
|
/**
|
|
19
19
|
* Initial row, usually from a Server Component or loader. The hook returns it
|
|
20
|
-
* until the model
|
|
20
|
+
* until the model client has a newer row in the local pool.
|
|
21
21
|
*/
|
|
22
22
|
readonly initial?: T;
|
|
23
23
|
}
|
|
@@ -25,9 +25,9 @@ export interface UseAbloModelResult<T> {
|
|
|
25
25
|
/** Current row for the id, or `initial` until the row has hydrated. */
|
|
26
26
|
readonly data: T | undefined;
|
|
27
27
|
/** Active work claims on this model row. */
|
|
28
|
-
readonly
|
|
28
|
+
readonly claims: readonly ModelClaim[];
|
|
29
29
|
/** Convenience flag for disabling UI while another participant is active. */
|
|
30
|
-
readonly
|
|
30
|
+
readonly claimed: boolean;
|
|
31
31
|
}
|
|
32
32
|
export type UseAbloHydratedModelResult<T> = Omit<UseAbloModelResult<T>, 'data'> & {
|
|
33
33
|
readonly data: T;
|
|
@@ -36,20 +36,20 @@ export type UseAbloHydratedModelResult<T> = Omit<UseAbloModelResult<T>, 'data'>
|
|
|
36
36
|
* useAblo — access the typed engine instance, or subscribe to a specific
|
|
37
37
|
* `ablo.<model>` row from inside an `<AbloProvider>` subtree.
|
|
38
38
|
*
|
|
39
|
-
* Zero-arg when the consumer declares the `
|
|
40
|
-
* augmentation (`declare
|
|
39
|
+
* Zero-arg when the consumer declares the `Register` global
|
|
40
|
+
* augmentation (`declare module '@abloatai/ablo' { interface Register { Schema:
|
|
41
41
|
* typeof schema } }`). The default generic resolves through
|
|
42
42
|
* `ResolveSchema['models']` so call sites stay clean:
|
|
43
43
|
*
|
|
44
44
|
* ```ts
|
|
45
|
-
* // With
|
|
45
|
+
* // With Register augmentation (recommended):
|
|
46
46
|
* const ablo = useAblo();
|
|
47
47
|
* if (!ablo) return <Loading />;
|
|
48
48
|
* const docs = await ablo.documents.load({ where: { id } });
|
|
49
49
|
*
|
|
50
50
|
* // Reactive selector:
|
|
51
51
|
* const doc = useAblo((ablo) => ablo.documents.retrieve(id)) ?? serverDoc;
|
|
52
|
-
* const
|
|
52
|
+
* const active = useAblo((ablo) => ablo.documents.claimState(id));
|
|
53
53
|
*
|
|
54
54
|
* // Without augmentation, pass the schema generic:
|
|
55
55
|
* const ablo = useAblo<(typeof schema)['models']>();
|
|
@@ -61,12 +61,12 @@ export type UseAbloHydratedModelResult<T> = Omit<UseAbloModelResult<T>, 'data'>
|
|
|
61
61
|
*/
|
|
62
62
|
export declare function useAblo<R extends SchemaRecord = DefaultModels>(): Ablo<R> | null;
|
|
63
63
|
export declare function useAblo<R extends SchemaRecord = DefaultModels, T = unknown>(select: AbloSelector<R, T>): T | undefined;
|
|
64
|
-
export declare function useAblo<T, C>(
|
|
64
|
+
export declare function useAblo<T, C>(modelClient: ModelOperations<T, C>, id: string, options: UseAbloModelOptions<T> & {
|
|
65
65
|
readonly initial: T;
|
|
66
66
|
}): UseAbloHydratedModelResult<T>;
|
|
67
|
-
export declare function useAblo<R extends SchemaRecord = DefaultModels, T = Record<string, unknown>, C = unknown>(select:
|
|
67
|
+
export declare function useAblo<R extends SchemaRecord = DefaultModels, T = Record<string, unknown>, C = unknown>(select: ModelClientSelector<R, T, C>, id: string, options: UseAbloModelOptions<T> & {
|
|
68
68
|
readonly initial: T;
|
|
69
69
|
}): UseAbloHydratedModelResult<T>;
|
|
70
|
-
export declare function useAblo<T, C>(
|
|
71
|
-
export declare function useAblo<R extends SchemaRecord = DefaultModels, T = Record<string, unknown>, C = unknown>(select:
|
|
70
|
+
export declare function useAblo<T, C>(modelClient: ModelOperations<T, C>, id: string, options?: UseAbloModelOptions<T>): UseAbloModelResult<T>;
|
|
71
|
+
export declare function useAblo<R extends SchemaRecord = DefaultModels, T = Record<string, unknown>, C = unknown>(select: ModelClientSelector<R, T, C>, id: string, options?: UseAbloModelOptions<T>): UseAbloModelResult<T>;
|
|
72
72
|
export {};
|
package/dist/react/useAblo.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useContext, useEffect, useState } from 'react';
|
|
3
3
|
import { AbloInternalContext } from './internalContext.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getModelClientMeta } from '../client/createModelProxy.js';
|
|
5
5
|
import { Model, modelAsRow } from '../Model.js';
|
|
6
6
|
import { useReactive } from './useReactive.js';
|
|
7
|
-
const
|
|
8
|
-
function readModelResult(engine,
|
|
9
|
-
if (!
|
|
10
|
-
return { data: initial,
|
|
7
|
+
const EMPTY_CLAIMS = Object.freeze([]);
|
|
8
|
+
function readModelResult(engine, modelClient, id, initial) {
|
|
9
|
+
if (!modelClient || id === undefined) {
|
|
10
|
+
return { data: initial, claims: EMPTY_CLAIMS, claimed: false };
|
|
11
11
|
}
|
|
12
|
-
const data = snapshotValue(
|
|
13
|
-
const meta =
|
|
14
|
-
const
|
|
15
|
-
? engine.intents.list({
|
|
16
|
-
:
|
|
17
|
-
return { data,
|
|
12
|
+
const data = snapshotValue(modelClient.retrieve(id) ?? initial);
|
|
13
|
+
const meta = getModelClientMeta(modelClient);
|
|
14
|
+
const claims = meta && engine
|
|
15
|
+
? engine.intents.list({ model: meta.key, id })
|
|
16
|
+
: EMPTY_CLAIMS;
|
|
17
|
+
return { data, claims, claimed: claims.length > 0 };
|
|
18
18
|
}
|
|
19
19
|
function snapshotValue(value) {
|
|
20
20
|
if (value instanceof Model) {
|
|
@@ -25,39 +25,39 @@ function snapshotValue(value) {
|
|
|
25
25
|
}
|
|
26
26
|
return value;
|
|
27
27
|
}
|
|
28
|
-
export function useAblo(
|
|
28
|
+
export function useAblo(modelOrSelect, id, options) {
|
|
29
29
|
const ctx = useContext(AbloInternalContext);
|
|
30
30
|
const engine = ctx?.engine ?? null;
|
|
31
31
|
const initial = options?.initial;
|
|
32
|
-
const hasSelection =
|
|
33
|
-
const isSelectorOnly = typeof
|
|
34
|
-
const
|
|
32
|
+
const hasSelection = modelOrSelect !== undefined;
|
|
33
|
+
const isSelectorOnly = typeof modelOrSelect === 'function' && id === undefined;
|
|
34
|
+
const modelClient = typeof modelOrSelect === 'function' && id !== undefined
|
|
35
35
|
? engine
|
|
36
|
-
?
|
|
36
|
+
? modelOrSelect(engine)
|
|
37
37
|
: undefined
|
|
38
|
-
: typeof
|
|
38
|
+
: typeof modelOrSelect === 'function'
|
|
39
39
|
? undefined
|
|
40
|
-
:
|
|
41
|
-
const [
|
|
40
|
+
: modelOrSelect;
|
|
41
|
+
const [claimVersion, setClaimVersion] = useState(0);
|
|
42
42
|
useEffect(() => {
|
|
43
43
|
if (!engine || !hasSelection)
|
|
44
44
|
return;
|
|
45
|
-
return engine.intents.
|
|
45
|
+
return engine.intents.onChange(() => setClaimVersion((version) => version + 1));
|
|
46
46
|
}, [engine, hasSelection]);
|
|
47
47
|
const selected = useReactive(() => {
|
|
48
|
-
void
|
|
49
|
-
if (!engine || !isSelectorOnly || typeof
|
|
48
|
+
void claimVersion;
|
|
49
|
+
if (!engine || !isSelectorOnly || typeof modelOrSelect !== 'function') {
|
|
50
50
|
return undefined;
|
|
51
51
|
}
|
|
52
|
-
return snapshotValue(
|
|
52
|
+
return snapshotValue(modelOrSelect(engine));
|
|
53
53
|
});
|
|
54
54
|
const modelResult = useReactive(() => {
|
|
55
|
-
void
|
|
56
|
-
return readModelResult(engine,
|
|
55
|
+
void claimVersion;
|
|
56
|
+
return readModelResult(engine, modelClient, id, initial);
|
|
57
57
|
});
|
|
58
58
|
if (isSelectorOnly)
|
|
59
59
|
return selected;
|
|
60
|
-
if (
|
|
60
|
+
if (modelOrSelect)
|
|
61
61
|
return modelResult;
|
|
62
62
|
return engine;
|
|
63
63
|
}
|
|
@@ -5,8 +5,8 @@ import type { ResolveIntents } from '../types/global.js';
|
|
|
5
5
|
* The consumer declares their intent vocabulary in the global:
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
|
-
* declare
|
|
9
|
-
* interface
|
|
8
|
+
* declare module '@abloatai/ablo' {
|
|
9
|
+
* interface Register {
|
|
10
10
|
* Intents: {
|
|
11
11
|
* editLayer: { slideId: string; layerId: string };
|
|
12
12
|
* generateWithAI: { entityId: string; tool: string };
|
package/dist/react/useIntent.js
CHANGED
|
@@ -8,8 +8,8 @@ import { AbloValidationError } from '../errors.js';
|
|
|
8
8
|
* The consumer declares their intent vocabulary in the global:
|
|
9
9
|
*
|
|
10
10
|
* ```ts
|
|
11
|
-
* declare
|
|
12
|
-
* interface
|
|
11
|
+
* declare module '@abloatai/ablo' {
|
|
12
|
+
* interface Register {
|
|
13
13
|
* Intents: {
|
|
14
14
|
* editLayer: { slideId: string; layerId: string };
|
|
15
15
|
* generateWithAI: { entityId: string; tool: string };
|
|
@@ -50,7 +50,7 @@ export interface UseMutatorsOptions<S extends Schema> {
|
|
|
50
50
|
}
|
|
51
51
|
/** Mutator invokers (explicit schema arg). */
|
|
52
52
|
export declare function useMutators<S extends Schema, M extends MutatorDefs<S>>(schema: S, mutators: M, options?: UseMutatorsOptions<S>): MutatorInvokers<M>;
|
|
53
|
-
/** Mutator invokers via the `
|
|
53
|
+
/** Mutator invokers via the `Register` module augmentation. Schema comes
|
|
54
54
|
* from the `SyncProvider`'s context; the mutator tree is typed against
|
|
55
55
|
* `ResolveSchema` at the call site. */
|
|
56
56
|
export declare function useMutators<M extends ResolveSchema extends Schema ? MutatorDefs<ResolveSchema> : MutatorDefs<Schema>>(mutators: M, options?: UseMutatorsOptions<ResolveSchema extends Schema ? ResolveSchema : Schema>): MutatorInvokers<M>;
|
|
@@ -2,7 +2,7 @@ import type { ResolvePresence } from '../types/global.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Read the consumer-supplied presence state with `ResolvePresence`d
|
|
4
4
|
* typing — the shape the consumer declared in
|
|
5
|
-
* `declare
|
|
5
|
+
* `declare module '@abloatai/ablo' { interface Register { Presence: ... } }`.
|
|
6
6
|
*
|
|
7
7
|
* The SDK doesn't own a presence wire format. Consumers plug whatever
|
|
8
8
|
* backs their cursors, status, or activity (a MobX store, a custom
|
|
@@ -11,8 +11,8 @@ import type { ResolvePresence } from '../types/global.js';
|
|
|
11
11
|
*
|
|
12
12
|
* ```ts
|
|
13
13
|
* // apps/your-app/src/ablo-sync.d.ts
|
|
14
|
-
* declare
|
|
15
|
-
* interface
|
|
14
|
+
* declare module '@abloatai/ablo' {
|
|
15
|
+
* interface Register {
|
|
16
16
|
* Presence: { cursor: { x: number; y: number } | null; status: 'away' | 'online' };
|
|
17
17
|
* }
|
|
18
18
|
* }
|
|
@@ -3,7 +3,7 @@ import { useSyncContext } from './context.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Read the consumer-supplied presence state with `ResolvePresence`d
|
|
5
5
|
* typing — the shape the consumer declared in
|
|
6
|
-
* `declare
|
|
6
|
+
* `declare module '@abloatai/ablo' { interface Register { Presence: ... } }`.
|
|
7
7
|
*
|
|
8
8
|
* The SDK doesn't own a presence wire format. Consumers plug whatever
|
|
9
9
|
* backs their cursors, status, or activity (a MobX store, a custom
|
|
@@ -12,8 +12,8 @@ import { useSyncContext } from './context.js';
|
|
|
12
12
|
*
|
|
13
13
|
* ```ts
|
|
14
14
|
* // apps/your-app/src/ablo-sync.d.ts
|
|
15
|
-
* declare
|
|
16
|
-
* interface
|
|
15
|
+
* declare module '@abloatai/ablo' {
|
|
16
|
+
* interface Register {
|
|
17
17
|
* Presence: { cursor: { x: number; y: number } | null; status: 'away' | 'online' };
|
|
18
18
|
* }
|
|
19
19
|
* }
|
|
@@ -35,7 +35,7 @@ export function usePresence() {
|
|
|
35
35
|
// The runtime value is whatever the consumer passed to `SyncProvider`.
|
|
36
36
|
// The type assertion reflects the consumer's declared global, which
|
|
37
37
|
// the hook can't verify at runtime — but the consumer controls both
|
|
38
|
-
// ends (the
|
|
38
|
+
// ends (the registration and the provider prop) so this is a
|
|
39
39
|
// single-source-of-truth contract, not blind trust.
|
|
40
40
|
return ctx.presence;
|
|
41
41
|
}
|
|
@@ -32,5 +32,5 @@ export interface UseUndoScopeResult<S extends Schema> {
|
|
|
32
32
|
}
|
|
33
33
|
/** Per-surface undo/redo (explicit schema arg). */
|
|
34
34
|
export declare function useUndoScope<S extends Schema>(schema: S, name: string, options?: UndoScopeOptions): UseUndoScopeResult<S>;
|
|
35
|
-
/** Per-surface undo/redo via the `
|
|
35
|
+
/** Per-surface undo/redo via the `Register` module augmentation. */
|
|
36
36
|
export declare function useUndoScope(name: string, options?: UndoScopeOptions): UseUndoScopeResult<ResolveSchema extends Schema ? ResolveSchema : Schema>;
|