@abloatai/ablo 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +248 -124
- package/dist/BaseSyncedStore.d.ts +3 -3
- package/dist/BaseSyncedStore.js +3 -3
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.js +1 -1
- package/dist/client/Ablo.d.ts +91 -93
- package/dist/client/Ablo.js +122 -60
- package/dist/client/ApiClient.d.ts +14 -14
- package/dist/client/ApiClient.js +81 -55
- package/dist/client/createInternalComponents.d.ts +2 -3
- package/dist/client/createInternalComponents.js +2 -3
- package/dist/client/createModelProxy.d.ts +116 -90
- package/dist/client/createModelProxy.js +128 -128
- package/dist/client/index.d.ts +6 -7
- package/dist/client/index.js +4 -5
- package/dist/client/validateAbloOptions.js +5 -5
- package/dist/coordination/index.d.ts +6 -0
- package/dist/coordination/index.js +6 -0
- package/dist/coordination/schema.d.ts +329 -0
- package/dist/coordination/schema.js +209 -0
- package/dist/core/QueryView.d.ts +4 -1
- package/dist/core/QueryView.js +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +7 -0
- package/dist/core/query-utils.d.ts +7 -10
- package/dist/core/query-utils.js +2 -3
- package/dist/errorCodes.d.ts +264 -0
- package/dist/errorCodes.js +251 -0
- package/dist/errors.d.ts +59 -14
- package/dist/errors.js +73 -12
- package/dist/index.d.ts +11 -9
- package/dist/index.js +8 -12
- package/dist/interfaces/index.d.ts +2 -10
- package/dist/mutators/Transaction.d.ts +2 -2
- package/dist/mutators/Transaction.js +2 -2
- package/dist/mutators/mutateActions.d.ts +44 -0
- package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
- package/dist/mutators/readerActions.d.ts +32 -0
- package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/index.js +1 -1
- package/dist/policy/types.d.ts +31 -0
- package/dist/policy/types.js +15 -0
- package/dist/query/types.d.ts +1 -1
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +14 -6
- package/dist/react/context.d.ts +4 -4
- package/dist/react/index.d.ts +4 -5
- package/dist/react/index.js +3 -7
- package/dist/react/useAblo.d.ts +14 -14
- package/dist/react/useAblo.js +26 -26
- package/dist/react/useIntent.d.ts +2 -2
- package/dist/react/useIntent.js +2 -2
- package/dist/react/useMutators.d.ts +1 -1
- package/dist/react/usePresence.d.ts +3 -3
- package/dist/react/usePresence.js +4 -4
- package/dist/react/useUndoScope.d.ts +1 -1
- package/dist/schema/ddl.d.ts +62 -0
- package/dist/schema/ddl.js +317 -0
- package/dist/schema/diff.d.ts +167 -0
- package/dist/schema/diff.js +280 -0
- package/dist/schema/field.d.ts +16 -19
- package/dist/schema/field.js +30 -17
- package/dist/schema/generate.d.ts +19 -0
- package/dist/schema/generate.js +87 -0
- package/dist/schema/index.d.ts +9 -3
- package/dist/schema/index.js +14 -2
- package/dist/schema/model.d.ts +87 -25
- package/dist/schema/model.js +33 -3
- package/dist/schema/relation.d.ts +17 -0
- package/dist/schema/roles.d.ts +148 -0
- package/dist/schema/roles.js +149 -0
- package/dist/schema/schema.d.ts +10 -69
- package/dist/schema/schema.js +58 -24
- package/dist/schema/select.d.ts +25 -0
- package/dist/schema/select.js +55 -0
- package/dist/schema/serialize.d.ts +96 -0
- package/dist/schema/serialize.js +231 -0
- package/dist/schema/sugar.d.ts +20 -3
- package/dist/schema/sugar.js +5 -1
- package/dist/schema/tenancy.d.ts +66 -0
- package/dist/schema/tenancy.js +58 -0
- package/dist/sync/HydrationCoordinator.d.ts +2 -0
- package/dist/sync/HydrationCoordinator.js +23 -17
- package/dist/sync/SyncWebSocket.d.ts +17 -0
- package/dist/sync/SyncWebSocket.js +46 -1
- package/dist/sync/awaitIntentGrant.d.ts +26 -0
- package/dist/sync/awaitIntentGrant.js +60 -0
- package/dist/sync/createIntentStream.d.ts +2 -1
- package/dist/sync/createIntentStream.js +89 -5
- package/dist/sync/createPresenceStream.js +1 -1
- package/dist/sync/participants.d.ts +2 -2
- package/dist/sync/participants.js +9 -18
- package/dist/types/global.d.ts +43 -52
- package/dist/types/global.js +16 -18
- package/dist/types/streams.d.ts +90 -42
- package/docs/api-keys.md +44 -0
- package/docs/api.md +72 -173
- package/docs/audit.md +5 -5
- package/docs/cli.md +212 -0
- package/docs/client-behavior.md +42 -43
- package/docs/coordination.md +343 -0
- package/docs/data-sources.md +16 -16
- package/docs/examples/agent-human.md +30 -32
- package/docs/examples/ai-sdk-tool.md +32 -33
- package/docs/examples/existing-python-backend.md +38 -36
- package/docs/examples/nextjs.md +24 -25
- package/docs/examples/scoped-agent.md +78 -0
- package/docs/examples/server-agent.md +20 -61
- package/docs/guarantees.md +34 -56
- package/docs/identity.md +529 -0
- package/docs/index.md +18 -24
- package/docs/integration-guide.md +130 -144
- package/docs/interaction-model.md +32 -95
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +1 -1
- package/docs/mcp.md +11 -26
- package/docs/quickstart.md +43 -49
- package/docs/react.md +74 -24
- package/docs/roadmap.md +17 -7
- package/llms.txt +34 -39
- package/package.json +8 -1
- package/dist/react/useMutate.d.ts +0 -83
- package/dist/react/useQuery.d.ts +0 -123
- package/dist/react/useQuery.js +0 -145
- package/dist/react/useReader.d.ts +0 -69
- package/docs/capabilities.md +0 -163
package/llms.txt
CHANGED
|
@@ -11,34 +11,26 @@ import Ablo from '@abloatai/ablo';
|
|
|
11
11
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
12
12
|
|
|
13
13
|
const schema = defineSchema({
|
|
14
|
-
|
|
14
|
+
weatherReports: model({
|
|
15
15
|
id: z.string(),
|
|
16
|
-
|
|
17
|
-
status: z.enum(['
|
|
18
|
-
|
|
16
|
+
location: z.string(),
|
|
17
|
+
status: z.enum(['pending', 'ready']),
|
|
18
|
+
forecast: z.string().optional(),
|
|
19
19
|
}),
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
23
23
|
|
|
24
|
-
const [
|
|
25
|
-
if (!
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'task_123',
|
|
35
|
-
{ status: 'done', summary: await summarize(task) },
|
|
36
|
-
{
|
|
37
|
-
readAt: snap.stamp,
|
|
38
|
-
onStale: 'reject',
|
|
39
|
-
wait: 'confirmed',
|
|
40
|
-
},
|
|
41
|
-
);
|
|
24
|
+
const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
|
|
25
|
+
if (!report) throw new Error('Row not found');
|
|
26
|
+
|
|
27
|
+
const updated = await ablo.weatherReports.claim('report_stockholm', async (report) => {
|
|
28
|
+
return ablo.weatherReports.update(
|
|
29
|
+
report.id,
|
|
30
|
+
{ status: 'ready', forecast: await getForecast(report) },
|
|
31
|
+
{ wait: 'confirmed' },
|
|
32
|
+
);
|
|
33
|
+
});
|
|
42
34
|
```
|
|
43
35
|
|
|
44
36
|
That is the normal app path: declare models in a schema, then use `ablo.<model>.load(...)`, `ablo.<model>.retrieve(...)`, `ablo.<model>.create(...)`, `ablo.<model>.update(...)`, and `ablo.<model>.delete(...)`.
|
|
@@ -54,7 +46,7 @@ defaults to `'live'`, with `'archived'` and `'all'` for lifecycle-aware reads.
|
|
|
54
46
|
Advanced schema-less agents exist for workers that cannot import the app schema,
|
|
55
47
|
but do not teach that path first.
|
|
56
48
|
|
|
57
|
-
React reads should use selector `useAblo`: `useAblo((ablo) => ablo.
|
|
49
|
+
React reads should use selector `useAblo`: `useAblo((ablo) => ablo.weatherReports.retrieve(id))`.
|
|
58
50
|
Use zero-argument `useAblo()` only when a component needs the client for an
|
|
59
51
|
event handler or effect. Treat `useQuery`, `useOne`, `useReader`, and
|
|
60
52
|
`useMutate` as compatibility hooks for older string-keyed integrations, not the
|
|
@@ -64,36 +56,39 @@ first integration path.
|
|
|
64
56
|
|
|
65
57
|
Multiplayer is not a separate mode. When human UI, server actions, and agents use
|
|
66
58
|
the same schema client and write through `ablo.<model>`, Ablo coordinates the
|
|
67
|
-
shared
|
|
68
|
-
|
|
59
|
+
shared model stream: confirmed deltas fan out to subscribers, active claims are
|
|
60
|
+
visible through `claimState(id)`, and stale writes can be rejected with `readAt`.
|
|
69
61
|
|
|
70
62
|
If an app writes directly to its own database outside Ablo, that write bypasses
|
|
71
63
|
coordination until the app reports it through Data Source events.
|
|
72
64
|
|
|
73
65
|
## Nouns
|
|
74
66
|
|
|
75
|
-
- `Model
|
|
76
|
-
- `
|
|
67
|
+
- `Model client` is the typed `ablo.<model>` object generated from schema.
|
|
68
|
+
- `Claim` holds a model row while slow work runs; `claimState(id)` observes it.
|
|
77
69
|
- `Commit` is the durable protocol write behind `ablo.<model>.update(...)`.
|
|
78
70
|
- `Receipt` confirms the commit.
|
|
79
|
-
- `Capability` scopes what an agent may do. `agent.run(...)` handles the common case.
|
|
80
|
-
- `Task` is one agent run, with audit and cost attribution.
|
|
81
71
|
|
|
82
|
-
##
|
|
72
|
+
## Claimed Behavior
|
|
83
73
|
|
|
84
|
-
Reads never silently block.
|
|
74
|
+
Reads never silently block. Schema reads stay open while a row is claimed.
|
|
75
|
+
Server reads through `ablo.model(name)` can pass `ifClaimed: 'return'` to
|
|
76
|
+
receive active claims, `ifClaimed: 'fail'` to throw `AbloClaimedError`, or
|
|
77
|
+
`ifClaimed: 'wait'` to wait until the active claim clears.
|
|
85
78
|
|
|
86
|
-
Schema clients wait from the realtime
|
|
79
|
+
Schema clients wait from the realtime claim stream. Schema-less HTTP callers must provide an explicit `claimedPollInterval` when using `ifClaimed: 'wait'`; Ablo does not hide a hard-coded polling loop.
|
|
87
80
|
|
|
88
|
-
Use `
|
|
81
|
+
Use `claimedTimeout` only as a maximum wait, not as the coordination mechanism.
|
|
89
82
|
|
|
90
83
|
## Guarantees
|
|
91
84
|
|
|
92
85
|
`wait: 'confirmed'` means the server accepted the write. Schema model writes are optimistic by default; server rejection rolls back local state. Use `snapshot(...)` plus `readAt` and `onStale: 'reject'` to prevent lost updates.
|
|
93
86
|
|
|
94
|
-
|
|
87
|
+
Claims coordinate writers; they do not block readers. Most users should stay on
|
|
88
|
+
schema-backed reads/writes and `claim(...)`; do not teach manual protocol
|
|
89
|
+
bookkeeping in the happy path.
|
|
95
90
|
|
|
96
|
-
All SDK errors extend `AbloError`. Important classes: `
|
|
91
|
+
All SDK errors extend `AbloError`. Important classes: `AbloClaimedError`, `AbloStaleContextError`, `AbloAuthenticationError`, `AbloPermissionError`, `AbloRateLimitError`, `AbloIdempotencyError`, `AbloConnectionError`, `AbloValidationError`, and `AbloServerError`.
|
|
97
92
|
|
|
98
93
|
## Schema Scope
|
|
99
94
|
|
|
@@ -114,7 +109,7 @@ import { dataSource } from '@abloatai/ablo';
|
|
|
114
109
|
## Sandboxes
|
|
115
110
|
|
|
116
111
|
Public `/sandbox` is a deterministic visual demo. It should teach shared state,
|
|
117
|
-
|
|
112
|
+
claims, stale-write rejection, receipts, and deltas, but it does not use a real
|
|
118
113
|
API key. It also exposes a Claude Code / Codex handoff prompt. Prefer that shape
|
|
119
114
|
when an agent is asked to "make Ablo work" in an existing app.
|
|
120
115
|
|
|
@@ -124,16 +119,16 @@ sandbox like Stripe test mode: it has an isolated sync group prefix and mints
|
|
|
124
119
|
Resetting a sandbox creates a clean future stream without touching live data.
|
|
125
120
|
Use `sk_live_*` only for production.
|
|
126
121
|
|
|
127
|
-
For coding agents, the sandbox success path is: pick one shared
|
|
122
|
+
For coding agents, the sandbox success path is: pick one shared model,
|
|
128
123
|
declare schema, create the Ablo client, replace one direct mutation with a typed
|
|
129
124
|
`ablo.<model>.update(...)`, use selector `useAblo` for live reads, and add a
|
|
130
|
-
two-writer stale/
|
|
125
|
+
two-writer stale/claim smoke test.
|
|
131
126
|
|
|
132
127
|
## Public Surface
|
|
133
128
|
|
|
134
129
|
Import from these public paths only:
|
|
135
130
|
|
|
136
|
-
- `@abloatai/ablo` — `Ablo`, errors, typed model
|
|
131
|
+
- `@abloatai/ablo` — `Ablo`, errors, typed model clients, claims, `dataSource`, and advanced protocol models.
|
|
137
132
|
- `@abloatai/ablo/schema` — schema DSL.
|
|
138
133
|
- `@abloatai/ablo/react` — React provider and hooks.
|
|
139
134
|
- `@abloatai/ablo/testing` — test harnesses and mocks.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/ablo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "State control API for AI agents and collaborative apps.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"import": "./dist/schema/index.js",
|
|
21
21
|
"default": "./dist/schema/index.js"
|
|
22
22
|
},
|
|
23
|
+
"./coordination": {
|
|
24
|
+
"types": "./dist/coordination/index.d.ts",
|
|
25
|
+
"import": "./dist/coordination/index.js",
|
|
26
|
+
"default": "./dist/coordination/index.js"
|
|
27
|
+
},
|
|
23
28
|
"./react": {
|
|
24
29
|
"types": "./dist/react/index.d.ts",
|
|
25
30
|
"import": "./dist/react/index.js",
|
|
@@ -71,6 +76,8 @@
|
|
|
71
76
|
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
72
77
|
"pack:check": "npm_config_cache=${TMPDIR:-/tmp}/ablo-npm-cache npm pack --dry-run",
|
|
73
78
|
"lint:imports": "node scripts/check-js-extensions.mjs",
|
|
79
|
+
"generate:errors": "tsx scripts/generate-error-docs.mts",
|
|
80
|
+
"lint:errors": "tsx scripts/check-error-docs.mts",
|
|
74
81
|
"lint:pkg": "publint",
|
|
75
82
|
"prepublishOnly": "npm run build && npm run lint:pkg",
|
|
76
83
|
"test": "jest",
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { Schema, InferModel, InferCreate } from '../schema/schema.js';
|
|
2
|
-
import type { ResolveSchema } from '../types/global.js';
|
|
3
|
-
import type { SyncStoreContract } from './context.js';
|
|
4
|
-
type GlobalMutateKey = ResolveSchema extends {
|
|
5
|
-
models: infer M;
|
|
6
|
-
} ? keyof M & string : string;
|
|
7
|
-
type GlobalMutateActions<K extends string> = ResolveSchema extends Schema ? K extends keyof ResolveSchema['models'] & string ? MutateActions<ResolveSchema, K> : MutateActions<Schema, string> : MutateActions<Schema, string>;
|
|
8
|
-
/**
|
|
9
|
-
* Compatibility mutation hook. Returns CRUD methods for a single model type.
|
|
10
|
-
*
|
|
11
|
-
* Prefer `useAblo()` and call `ablo.<model>.create/update/delete` inside
|
|
12
|
-
* callbacks for new integrations. This hook remains for older string-keyed code.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* import { schema } from '@ablo/schema';
|
|
16
|
-
* import { useMutate } from '@abloatai/ablo/react';
|
|
17
|
-
*
|
|
18
|
-
* const tasks = useMutate(schema, 'tasks');
|
|
19
|
-
*
|
|
20
|
-
* // Create — fields are type-checked against the schema's Zod shape
|
|
21
|
-
* await tasks.create({ title: 'Fix bug', status: 'todo', projectId });
|
|
22
|
-
*
|
|
23
|
-
* // Update — id + partial changes, no need to hold a model instance
|
|
24
|
-
* await tasks.update({ id: task.id, status: 'done', completedAt: new Date() });
|
|
25
|
-
*
|
|
26
|
-
* // Delete / archive / unarchive — by id
|
|
27
|
-
* await tasks.delete(task.id);
|
|
28
|
-
* await tasks.archive(task.id);
|
|
29
|
-
*
|
|
30
|
-
* Mirrors the Zero pattern: `zero.mutate.task.update({ id, status: 'done' })`.
|
|
31
|
-
*/
|
|
32
|
-
/**
|
|
33
|
-
* `create` / `update` / `delete` are overloaded: pass one row or an
|
|
34
|
-
* array. Drizzle and Prisma use the same shape (`db.insert(table).values(rowOrRows)`).
|
|
35
|
-
* Avoids the `*Many` suffix while keeping the semantics: every entry in
|
|
36
|
-
* an array call lands in the same synchronous tick (Promise.all under
|
|
37
|
-
* the hood), so the microtask coalescer in `TransactionQueue` collapses
|
|
38
|
-
* N pushes into one wire commit with one `batchIndex` — structurally
|
|
39
|
-
* identical to Zero's mutator-boundary commit.
|
|
40
|
-
*/
|
|
41
|
-
type UpdatePatch<S extends Schema, K extends keyof S['models'] & string> = {
|
|
42
|
-
id: string;
|
|
43
|
-
} & Partial<InferModel<S, K>>;
|
|
44
|
-
export interface MutateActions<S extends Schema, K extends keyof S['models'] & string> {
|
|
45
|
-
/**
|
|
46
|
-
* Create one entity, or an array of entities in a single tick. ID,
|
|
47
|
-
* createdAt, updatedAt, organizationId default automatically per row.
|
|
48
|
-
*/
|
|
49
|
-
create(data: InferCreate<S, K>): Promise<InferModel<S, K>>;
|
|
50
|
-
create(data: InferCreate<S, K>[]): Promise<InferModel<S, K>[]>;
|
|
51
|
-
/**
|
|
52
|
-
* Update one row, or an array of rows in a single tick. Each patch is
|
|
53
|
-
* `{ id, ...changes }` — missing ids throw. Schema-generated models
|
|
54
|
-
* are MobX-observable, so direct assignment fires reactivity.
|
|
55
|
-
*/
|
|
56
|
-
update(patch: UpdatePatch<S, K>): Promise<InferModel<S, K>>;
|
|
57
|
-
update(patches: UpdatePatch<S, K>[]): Promise<InferModel<S, K>[]>;
|
|
58
|
-
/**
|
|
59
|
-
* Delete one row by id, or an array of ids in a single tick. Missing
|
|
60
|
-
* ids are silently ignored.
|
|
61
|
-
*/
|
|
62
|
-
delete(id: string): Promise<void>;
|
|
63
|
-
delete(ids: string[]): Promise<void>;
|
|
64
|
-
/** Soft-archive by ID. */
|
|
65
|
-
archive: (id: string) => Promise<void>;
|
|
66
|
-
/** Restore an archived entity by ID. */
|
|
67
|
-
unarchive: (id: string) => Promise<void>;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Pure factory — testable without React. The hook just wraps this in
|
|
71
|
-
* useMemo with the React context.
|
|
72
|
-
*/
|
|
73
|
-
export declare function createMutateActions<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, store: SyncStoreContract, organizationId: string): MutateActions<S, K>;
|
|
74
|
-
/** @deprecated Prefer `useAblo()` plus `ablo.<model>.create/update/delete`. */
|
|
75
|
-
export declare function useMutate<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K): MutateActions<S, K>;
|
|
76
|
-
/** Typed CRUD via the `AbloSync` global augmentation. The schema is
|
|
77
|
-
* resolved from the `SyncProvider`'s context — consumer doesn't pass it
|
|
78
|
-
* at the call site.
|
|
79
|
-
*
|
|
80
|
-
* @deprecated Prefer `useAblo()` plus `ablo.<model>.create/update/delete`.
|
|
81
|
-
*/
|
|
82
|
-
export declare function useMutate<K extends GlobalMutateKey>(modelKey: K): GlobalMutateActions<K>;
|
|
83
|
-
export {};
|
package/dist/react/useQuery.d.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import type { ModelScope } from '../types/index.js';
|
|
2
|
-
import type { Schema } from '../schema/schema.js';
|
|
3
|
-
import type { InferModel } from '../schema/schema.js';
|
|
4
|
-
import type { ResolveSchema } from '../types/global.js';
|
|
5
|
-
/** Narrow model-key union for the zero-arg overload. */
|
|
6
|
-
type GlobalModelKey = ResolveSchema extends {
|
|
7
|
-
models: infer M;
|
|
8
|
-
} ? keyof M & string : string;
|
|
9
|
-
/** Typed entity shape for a given model key. Falls back to a loose shape
|
|
10
|
-
* when the resolved schema doesn't extend the full `Schema` contract
|
|
11
|
-
* (i.e., no global augmentation present). */
|
|
12
|
-
type GlobalEntity<K extends string> = ResolveSchema extends Schema ? K extends keyof ResolveSchema['models'] ? InferModel<ResolveSchema, K> : Record<string, unknown> : Record<string, unknown>;
|
|
13
|
-
/**
|
|
14
|
-
* Compatibility query hook for entity collections.
|
|
15
|
-
*
|
|
16
|
-
* Prefer selector reads for new integrations:
|
|
17
|
-
*
|
|
18
|
-
* ```ts
|
|
19
|
-
* const tasks = useAblo((ablo) =>
|
|
20
|
-
* ablo.tasks.list({ where: { status: 'todo' } }),
|
|
21
|
-
* );
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* This hook remains for older string-keyed integrations.
|
|
25
|
-
*
|
|
26
|
-
* **Typed overload:**
|
|
27
|
-
* ```ts
|
|
28
|
-
* import { schema } from '@/sync/schema';
|
|
29
|
-
* const chats = useQuery(schema, 'chats', { where: { userId } });
|
|
30
|
-
* // chats is fully typed: Chat[] with displayTitle, icon, color, etc.
|
|
31
|
-
* ```
|
|
32
|
-
*
|
|
33
|
-
* **Untyped overload (legacy):**
|
|
34
|
-
* ```ts
|
|
35
|
-
* const chats = useQuery('Chat');
|
|
36
|
-
* // chats is Record<string, unknown>[]
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
export interface QueryOptions<T = Record<string, unknown>> {
|
|
40
|
-
/** Declarative field-level filter. Shallow match: all specified fields must match. */
|
|
41
|
-
where?: Partial<T>;
|
|
42
|
-
/** Arbitrary predicate function for complex logic. Applied AFTER where. */
|
|
43
|
-
filter?: (entity: T) => boolean;
|
|
44
|
-
/** Sort field name. */
|
|
45
|
-
orderBy?: keyof T & string;
|
|
46
|
-
/** Sort direction. Default: 'asc'. */
|
|
47
|
-
order?: 'asc' | 'desc';
|
|
48
|
-
/** Max results. */
|
|
49
|
-
limit?: number;
|
|
50
|
-
/** Skip N results (pagination). */
|
|
51
|
-
offset?: number;
|
|
52
|
-
/** Filter by model scope (live, archived, all). Default: live. */
|
|
53
|
-
scope?: ModelScope;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Typed query (explicit schema arg).
|
|
57
|
-
*
|
|
58
|
-
* @deprecated Prefer `useAblo((ablo) => ablo.<model>.list(options))` for new
|
|
59
|
-
* integrations. This overload remains for compatibility with older
|
|
60
|
-
* string-keyed React code.
|
|
61
|
-
*
|
|
62
|
-
* ```ts
|
|
63
|
-
* const tasks = useQuery(schema, 'tasks', { where: { status: 'todo' } });
|
|
64
|
-
* // tasks: Task[] — fully typed from Zod shape + computed getters
|
|
65
|
-
* ```
|
|
66
|
-
*/
|
|
67
|
-
export declare function useQuery<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, options?: QueryOptions<InferModel<S, K>>): InferModel<S, K>[];
|
|
68
|
-
/**
|
|
69
|
-
* Typed query (global-augmented): pass just the model key. Resolves
|
|
70
|
-
* the schema from the `AbloSync` global augmentation the consumer
|
|
71
|
-
* declared in a `.d.ts`. No `schema` arg at the call site — this is
|
|
72
|
-
* the Liveblocks-style ergonomic path.
|
|
73
|
-
*
|
|
74
|
-
* @deprecated Prefer `useAblo((ablo) => ablo.<model>.list(options))` for new
|
|
75
|
-
* integrations. This overload remains for compatibility with older
|
|
76
|
-
* string-keyed React code.
|
|
77
|
-
*
|
|
78
|
-
* ```ts
|
|
79
|
-
* // apps/your-app/src/ablo-sync.d.ts
|
|
80
|
-
* declare global { interface AbloSync { Schema: typeof schema } }
|
|
81
|
-
*
|
|
82
|
-
* // any component
|
|
83
|
-
* const tasks = useQuery('tasks', { where: { status: 'todo' } });
|
|
84
|
-
* // tasks: Task[] — typed via the declared global
|
|
85
|
-
* ```
|
|
86
|
-
*
|
|
87
|
-
* When no global augmentation exists, `GlobalEntity` falls back to
|
|
88
|
-
* `Record<string, unknown>` — same ergonomics as the legacy untyped
|
|
89
|
-
* overload, with the key still validated against the resolved schema's
|
|
90
|
-
* model keys when that schema is declared.
|
|
91
|
-
*/
|
|
92
|
-
export declare function useQuery<K extends GlobalModelKey>(modelKey: K, options?: QueryOptions<GlobalEntity<K>>): GlobalEntity<K>[];
|
|
93
|
-
/** @deprecated Prefer selector reads through `useAblo`. */
|
|
94
|
-
export declare function useQuery<T = Record<string, unknown>>(typename: string, options?: QueryOptions<T>): T[];
|
|
95
|
-
/**
|
|
96
|
-
* Compatibility single-entity lookup. Prefer selector reads:
|
|
97
|
-
*
|
|
98
|
-
* ```ts
|
|
99
|
-
* const task = useAblo((ablo) => ablo.tasks.retrieve(taskId));
|
|
100
|
-
* ```
|
|
101
|
-
*
|
|
102
|
-
* ```ts
|
|
103
|
-
* // Typed
|
|
104
|
-
* const task = useOne(schema, 'tasks', taskId);
|
|
105
|
-
*
|
|
106
|
-
* // Untyped (legacy)
|
|
107
|
-
* const task = useOne(taskId);
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
/** @deprecated Prefer `useAblo((ablo) => ablo.<model>.retrieve(id))`. */
|
|
111
|
-
export declare function useOne<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, id?: string): InferModel<S, K> | undefined;
|
|
112
|
-
/** Typed single-entity lookup via the `AbloSync` global augmentation.
|
|
113
|
-
*
|
|
114
|
-
* @deprecated Prefer `useAblo((ablo) => ablo.<model>.retrieve(id))`.
|
|
115
|
-
*
|
|
116
|
-
* The pool `.get(id)` call doesn't actually need the typename at runtime
|
|
117
|
-
* — the return is already keyed by id globally — so the model key serves
|
|
118
|
-
* as a compile-time narrowing hint for consumers who want the specific
|
|
119
|
-
* entity type at the call site. */
|
|
120
|
-
export declare function useOne<K extends GlobalModelKey>(modelKey: K, id?: string): GlobalEntity<K> | undefined;
|
|
121
|
-
/** @deprecated Prefer `useAblo((ablo) => ablo.<model>.retrieve(id))`. */
|
|
122
|
-
export declare function useOne<T = Record<string, unknown>>(id?: string): T | undefined;
|
|
123
|
-
export {};
|
package/dist/react/useQuery.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { useMemo, useEffect, useRef, useCallback } from 'react';
|
|
3
|
-
import { useSyncContext } from './context.js';
|
|
4
|
-
import { useReactive } from './useReactive.js';
|
|
5
|
-
// ── Stable key helper ───────────────────────────────────────────────
|
|
6
|
-
/**
|
|
7
|
-
* Produce a stable string key for QueryOptions so useMemo only recreates
|
|
8
|
-
* the view when the logical query changes.
|
|
9
|
-
*
|
|
10
|
-
* - `where` is serialized via JSON.stringify (deterministic for simple values).
|
|
11
|
-
* - `filter` is a function — we track its reference identity.
|
|
12
|
-
* - Primitives (orderBy, order, limit, offset, scope) are included directly.
|
|
13
|
-
*/
|
|
14
|
-
function useStableKey(options) {
|
|
15
|
-
// We use a ref to track the previous filter reference. When it changes
|
|
16
|
-
// the key changes and the view is recreated.
|
|
17
|
-
const filterRef = useRef(undefined);
|
|
18
|
-
// Bump a generation counter when the filter reference changes
|
|
19
|
-
const genRef = useRef(0);
|
|
20
|
-
if (options?.filter !== filterRef.current) {
|
|
21
|
-
filterRef.current = options?.filter;
|
|
22
|
-
genRef.current++;
|
|
23
|
-
}
|
|
24
|
-
return useMemo(() => {
|
|
25
|
-
if (!options)
|
|
26
|
-
return '';
|
|
27
|
-
const parts = [];
|
|
28
|
-
if (options.where)
|
|
29
|
-
parts.push('w:' + JSON.stringify(options.where));
|
|
30
|
-
if (options.filter)
|
|
31
|
-
parts.push('f:' + genRef.current);
|
|
32
|
-
if (options.orderBy)
|
|
33
|
-
parts.push('ob:' + String(options.orderBy));
|
|
34
|
-
if (options.order)
|
|
35
|
-
parts.push('o:' + options.order);
|
|
36
|
-
if (options.limit !== undefined)
|
|
37
|
-
parts.push('l:' + options.limit);
|
|
38
|
-
if (options.offset !== undefined)
|
|
39
|
-
parts.push('off:' + options.offset);
|
|
40
|
-
if (options.scope)
|
|
41
|
-
parts.push('s:' + options.scope);
|
|
42
|
-
return parts.join('|');
|
|
43
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
|
-
}, [
|
|
45
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
-
options?.where ? JSON.stringify(options.where) : '',
|
|
47
|
-
genRef.current,
|
|
48
|
-
options?.orderBy,
|
|
49
|
-
options?.order,
|
|
50
|
-
options?.limit,
|
|
51
|
-
options?.offset,
|
|
52
|
-
options?.scope,
|
|
53
|
-
]);
|
|
54
|
-
}
|
|
55
|
-
// ── Implementation ──────────────────────────────────────────────────
|
|
56
|
-
export function useQuery(schemaOrTypename, modelKeyOrOptions, maybeOptions) {
|
|
57
|
-
const ctx = useSyncContext();
|
|
58
|
-
const { store } = ctx;
|
|
59
|
-
let typename;
|
|
60
|
-
let options;
|
|
61
|
-
if (typeof schemaOrTypename === 'string') {
|
|
62
|
-
// First arg is a string. Could be either the new zero-arg typed
|
|
63
|
-
// overload (a schema model key, resolved via `ctx.schema`) or the
|
|
64
|
-
// legacy untyped overload (a raw typename like 'Chat'). When a
|
|
65
|
-
// schema is present on the context and the string maps to a known
|
|
66
|
-
// model key, we look up the real typename from the schema's
|
|
67
|
-
// `ModelDef.typename`. Otherwise we treat the string as a typename
|
|
68
|
-
// directly — preserving the legacy behavior for any non-opting
|
|
69
|
-
// consumer. Both paths converge on the same runtime lookup.
|
|
70
|
-
const key = schemaOrTypename;
|
|
71
|
-
const ctxSchema = ctx.schema;
|
|
72
|
-
const modelDef = ctxSchema
|
|
73
|
-
? ctxSchema.models[key]
|
|
74
|
-
: undefined;
|
|
75
|
-
typename = modelDef?.typename ?? key;
|
|
76
|
-
options = modelKeyOrOptions;
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
// Explicit schema path: useQuery(schema, 'chats', options?)
|
|
80
|
-
const schema = schemaOrTypename;
|
|
81
|
-
const modelKey = modelKeyOrOptions;
|
|
82
|
-
const modelDef = schema.models[modelKey];
|
|
83
|
-
typename = modelDef?.typename ?? modelKey;
|
|
84
|
-
options = maybeOptions;
|
|
85
|
-
}
|
|
86
|
-
const optionsKey = useStableKey(options);
|
|
87
|
-
// The QueryView is generic-erased to `Record<string, unknown>`, but
|
|
88
|
-
// the caller's filter is typed in `T`. Wrap rather than cast: the
|
|
89
|
-
// view passes a Record at runtime and the wrapper narrows to T —
|
|
90
|
-
// single typed boundary, no `as unknown as` chain.
|
|
91
|
-
const userFilter = options?.filter;
|
|
92
|
-
const viewOptions = options
|
|
93
|
-
? {
|
|
94
|
-
where: options.where,
|
|
95
|
-
filter: userFilter
|
|
96
|
-
? (entity) => userFilter(entity)
|
|
97
|
-
: undefined,
|
|
98
|
-
orderBy: options.orderBy,
|
|
99
|
-
order: options.order,
|
|
100
|
-
limit: options.limit,
|
|
101
|
-
offset: options.offset,
|
|
102
|
-
scope: options.scope,
|
|
103
|
-
}
|
|
104
|
-
: undefined;
|
|
105
|
-
const view = useMemo(() => store.pool.createView(typename, viewOptions),
|
|
106
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
|
-
[store.pool, typename, optionsKey]);
|
|
108
|
-
useEffect(() => () => view.dispose(), [view]);
|
|
109
|
-
// Self-subscribing — consumers never wrap their component in
|
|
110
|
-
// `observer`. `useReactive` tracks the observables read inside the
|
|
111
|
-
// compute function (`view.results`), recomputes on change, and
|
|
112
|
-
// returns a stable slice so downstream `.sort()` / `.reverse()`
|
|
113
|
-
// calls don't trip MobX error 37. The default structural equality
|
|
114
|
-
// check prevents re-renders when nothing actually moved.
|
|
115
|
-
//
|
|
116
|
-
// The compute closure MUST be stable when `view` is stable. Without
|
|
117
|
-
// useCallback([view]), each render passes a fresh arrow to
|
|
118
|
-
// useReactive, which then can't distinguish "swapped to a new
|
|
119
|
-
// QueryView" from "same view, new render" — the wrong call would
|
|
120
|
-
// either re-subscribe every render (waste) or never re-subscribe
|
|
121
|
-
// when view actually swaps (stale snapshot bug returning a previous
|
|
122
|
-
// view's results forever).
|
|
123
|
-
const compute = useCallback(() => view.results.slice(), [view]);
|
|
124
|
-
return useReactive(compute);
|
|
125
|
-
}
|
|
126
|
-
export function useOne(schemaOrIdOrKey, modelKeyOrId, maybeId) {
|
|
127
|
-
const { store } = useSyncContext();
|
|
128
|
-
if (schemaOrIdOrKey === undefined) {
|
|
129
|
-
return undefined;
|
|
130
|
-
}
|
|
131
|
-
if (typeof schemaOrIdOrKey === 'string') {
|
|
132
|
-
// Either `useOne(id)` (legacy, one arg) or `useOne(modelKey, id)` (global).
|
|
133
|
-
// Disambiguate by whether a second arg was passed — both paths
|
|
134
|
-
// converge on the same runtime pool lookup because entity IDs are
|
|
135
|
-
// globally unique across model types.
|
|
136
|
-
if (modelKeyOrId !== undefined) {
|
|
137
|
-
return store.pool.get(modelKeyOrId);
|
|
138
|
-
}
|
|
139
|
-
return store.pool.get(schemaOrIdOrKey);
|
|
140
|
-
}
|
|
141
|
-
// Explicit schema path: useOne(schema, 'tasks', id)
|
|
142
|
-
if (!maybeId)
|
|
143
|
-
return undefined;
|
|
144
|
-
return store.pool.get(maybeId);
|
|
145
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { Schema, InferModel } from '../schema/schema.js';
|
|
2
|
-
import type { ResolveSchema } from '../types/global.js';
|
|
3
|
-
import type { SyncStoreContract } from './context.js';
|
|
4
|
-
type GlobalReaderKey = ResolveSchema extends {
|
|
5
|
-
models: infer M;
|
|
6
|
-
} ? keyof M & string : string;
|
|
7
|
-
type GlobalReaderActions<K extends string> = ResolveSchema extends Schema ? K extends keyof ResolveSchema['models'] & string ? ReaderActions<ResolveSchema, K> : ReaderActions<Schema, string> : ReaderActions<Schema, string>;
|
|
8
|
-
/**
|
|
9
|
-
* Compatibility schema-typed imperative reader. Returns functions for one-off lookups
|
|
10
|
-
* without subscribing the component to collection changes.
|
|
11
|
-
*
|
|
12
|
-
* Prefer `useAblo()` and call `ablo.<model>.retrieve/list` inside callbacks and
|
|
13
|
-
* effects in new integrations. This hook remains for older string-keyed code.
|
|
14
|
-
*
|
|
15
|
-
* Use this inside event handlers, mutation callbacks, or effects where you
|
|
16
|
-
* need a current snapshot of the pool but don't want to trigger re-renders
|
|
17
|
-
* on every entity change.
|
|
18
|
-
*
|
|
19
|
-
* For reactive reads, use selector reads through
|
|
20
|
-
* `useAblo((ablo) => ablo.<model>.retrieve(id))`.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* import { schema } from '@ablo/schema';
|
|
24
|
-
* import { useReader } from '@abloatai/ablo/react';
|
|
25
|
-
*
|
|
26
|
-
* function useTaskMutations() {
|
|
27
|
-
* const read = useReader(schema, 'tasks');
|
|
28
|
-
*
|
|
29
|
-
* return {
|
|
30
|
-
* create: async (data) => {
|
|
31
|
-
* // Imperative read — uses FK index when available (O(1))
|
|
32
|
-
* const existing = read.findMany({ where: { projectId: data.projectId } });
|
|
33
|
-
* const order = existing.reduce((m, t) => Math.max(m, t.order ?? 0), 0) + 1;
|
|
34
|
-
* // ...
|
|
35
|
-
* },
|
|
36
|
-
* };
|
|
37
|
-
* }
|
|
38
|
-
*/
|
|
39
|
-
export interface ReaderFindOptions<T> {
|
|
40
|
-
/** Equality filter — uses FK index when the field is registered. */
|
|
41
|
-
where?: Partial<T>;
|
|
42
|
-
/** Predicate applied AFTER `where` filtering. */
|
|
43
|
-
filter?: (entity: T) => boolean;
|
|
44
|
-
/** Sort field. */
|
|
45
|
-
orderBy?: keyof T & string;
|
|
46
|
-
/** Sort direction. Default: 'asc'. */
|
|
47
|
-
order?: 'asc' | 'desc';
|
|
48
|
-
/** Max results. */
|
|
49
|
-
limit?: number;
|
|
50
|
-
/** Skip N results. */
|
|
51
|
-
offset?: number;
|
|
52
|
-
}
|
|
53
|
-
export interface ReaderActions<S extends Schema, K extends keyof S['models'] & string> {
|
|
54
|
-
/** Get a single entity by id. Returns undefined if not in pool. */
|
|
55
|
-
retrieve: (id: string) => InferModel<S, K> | undefined;
|
|
56
|
-
/** Read a collection with optional filters. Snapshot — not reactive. */
|
|
57
|
-
list: (options?: ReaderFindOptions<InferModel<S, K>>) => InferModel<S, K>[];
|
|
58
|
-
/** Count entities matching the options. */
|
|
59
|
-
count: (options?: ReaderFindOptions<InferModel<S, K>>) => number;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Pure factory — testable without React. `useReader` wraps this in useMemo.
|
|
63
|
-
*/
|
|
64
|
-
export declare function createReaderActions<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, store: SyncStoreContract): ReaderActions<S, K>;
|
|
65
|
-
/** @deprecated Prefer `useAblo()` plus `ablo.<model>.retrieve/list` in callbacks/effects. */
|
|
66
|
-
export declare function useReader<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K): ReaderActions<S, K>;
|
|
67
|
-
/** @deprecated Prefer `useAblo()` plus `ablo.<model>.retrieve/list` in callbacks/effects. */
|
|
68
|
-
export declare function useReader<K extends GlobalReaderKey>(modelKey: K): GlobalReaderActions<K>;
|
|
69
|
-
export {};
|