@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/dist/policy/types.d.ts
CHANGED
|
@@ -48,6 +48,14 @@ export interface IntentHeldConflict extends ConflictBase {
|
|
|
48
48
|
readonly entityId: string;
|
|
49
49
|
/** Holder's intent expiry (ms since epoch). */
|
|
50
50
|
readonly expiresAt: number;
|
|
51
|
+
/**
|
|
52
|
+
* The committer's granted capability operations (the key's allowlist). A
|
|
53
|
+
* policy is a pure function of the conflict value, so it can only authorize
|
|
54
|
+
* on what's carried here — this is what lets a policy express "preempt iff
|
|
55
|
+
* the committer holds `intent.preempt`" (see `capabilityPreemptPolicy`).
|
|
56
|
+
* Empty for a human session with no allowlist.
|
|
57
|
+
*/
|
|
58
|
+
readonly committerOperations: readonly string[];
|
|
51
59
|
}
|
|
52
60
|
/**
|
|
53
61
|
* The discriminated union the policy receives. Switch on `.kind` to
|
|
@@ -61,6 +69,20 @@ export type ConflictDecision = {
|
|
|
61
69
|
} | {
|
|
62
70
|
readonly action: 'allow';
|
|
63
71
|
readonly note?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Evict the current holder and grant the target to the committer. Only
|
|
75
|
+
* meaningful for an `intent_held` conflict at claim time (`intent_begin`):
|
|
76
|
+
* the holder receives an `intent_lost` (reason `'preempted'`) and the
|
|
77
|
+
* preemptor takes the lease, jumping ahead of any FIFO waiters. This is the
|
|
78
|
+
* authorization seam for preemption — a policy returns `preempt` only for a
|
|
79
|
+
* committer it deems higher-priority (e.g. a supervisor over its sub-agents,
|
|
80
|
+
* or an identity holding a preempt capability). At commit time there is no
|
|
81
|
+
* holder to evict, so a `preempt` decision there is treated as `allow`.
|
|
82
|
+
*/
|
|
83
|
+
| {
|
|
84
|
+
readonly action: 'preempt';
|
|
85
|
+
readonly reason?: string;
|
|
64
86
|
};
|
|
65
87
|
/**
|
|
66
88
|
* Pluggable decision function. Sync or async.
|
|
@@ -81,4 +103,13 @@ export type ConflictPolicy = (conflict: Conflict) => ConflictDecision | Promise<
|
|
|
81
103
|
* intent-conflicting write through.
|
|
82
104
|
*/
|
|
83
105
|
export declare const defaultPolicy: ConflictPolicy;
|
|
106
|
+
/**
|
|
107
|
+
* Capability-gated preemption. An `intent_held` conflict is PREEMPTED when the
|
|
108
|
+
* committer holds the `intent.preempt` operation in its capability allowlist
|
|
109
|
+
* (the holder is evicted, the committer takes the lease); everything else falls
|
|
110
|
+
* back to `defaultPolicy` (reject). Opt-in — wire it as a `conflictPolicies`
|
|
111
|
+
* global to let a privileged identity jump a held entity without a bespoke
|
|
112
|
+
* policy. The authorization is the capability, not an identity string.
|
|
113
|
+
*/
|
|
114
|
+
export declare const capabilityPreemptPolicy: ConflictPolicy;
|
|
84
115
|
export {};
|
package/dist/policy/types.js
CHANGED
|
@@ -15,3 +15,18 @@ export const defaultPolicy = (conflict) => ({
|
|
|
15
15
|
action: 'reject',
|
|
16
16
|
reason: conflict.kind === 'stale_context' ? 'stale_context' : 'intent_conflict',
|
|
17
17
|
});
|
|
18
|
+
/**
|
|
19
|
+
* Capability-gated preemption. An `intent_held` conflict is PREEMPTED when the
|
|
20
|
+
* committer holds the `intent.preempt` operation in its capability allowlist
|
|
21
|
+
* (the holder is evicted, the committer takes the lease); everything else falls
|
|
22
|
+
* back to `defaultPolicy` (reject). Opt-in — wire it as a `conflictPolicies`
|
|
23
|
+
* global to let a privileged identity jump a held entity without a bespoke
|
|
24
|
+
* policy. The authorization is the capability, not an identity string.
|
|
25
|
+
*/
|
|
26
|
+
export const capabilityPreemptPolicy = (conflict) => {
|
|
27
|
+
if (conflict.kind === 'intent_held' &&
|
|
28
|
+
conflict.committerOperations.includes('intent.preempt')) {
|
|
29
|
+
return { action: 'preempt', reason: 'capability:intent.preempt' };
|
|
30
|
+
}
|
|
31
|
+
return defaultPolicy(conflict);
|
|
32
|
+
};
|
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
|
*/
|
|
@@ -114,7 +114,19 @@ export interface AbloProviderProps<R extends SchemaRecord = SchemaRecord> {
|
|
|
114
114
|
sessionErrorDetector?: SessionErrorDetector;
|
|
115
115
|
onlineStatus?: OnlineStatusProvider;
|
|
116
116
|
configOverrides?: SyncEngineConfig;
|
|
117
|
+
/**
|
|
118
|
+
* Raw sync-group strings for the initial connection. Prefer {@link scope} —
|
|
119
|
+
* the model form (`{ decks: deckId }`) that the engine resolves through the
|
|
120
|
+
* schema's `scope`, so you never hand-write a `deck:<id>` string. Both merge.
|
|
121
|
+
*/
|
|
117
122
|
syncGroups?: string[];
|
|
123
|
+
/**
|
|
124
|
+
* Model-form connection scope: `{ decks: deckId, documents: documentId }` or
|
|
125
|
+
* entity refs. Resolved through the schema's per-model `scope` into group
|
|
126
|
+
* strings (so typename `SlideDeck` → `deck:<id>`), unioned with {@link syncGroups}.
|
|
127
|
+
* Memoize the object if it's derived, to avoid rotating the engine each render.
|
|
128
|
+
*/
|
|
129
|
+
scope?: ParticipantScope;
|
|
118
130
|
bootstrapBaseUrl?: string;
|
|
119
131
|
maxPoolSize?: number;
|
|
120
132
|
/**
|
|
@@ -212,7 +224,7 @@ export declare function useParticipant(opts: UseParticipantOptions): UseParticip
|
|
|
212
224
|
/**
|
|
213
225
|
* Returns the raw `SyncEngine` proxy. Typically you want the typed
|
|
214
226
|
* hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
|
|
215
|
-
* where you need direct access (e.g., `sync.tasks.
|
|
227
|
+
* where you need direct access (e.g., `sync.tasks.onChange(cb)`).
|
|
216
228
|
*
|
|
217
229
|
* The generic parameter narrows the return type to your schema's
|
|
218
230
|
* model record so call sites get typed `sync.tasks.findMany()` /
|
|
@@ -32,7 +32,7 @@ function createErrorEmitter() {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
export function AbloProvider(props) {
|
|
35
|
-
const { schema, url = 'wss://mesh.ablo.finance', userId, teamIds, apiKey, preventUnsavedChanges, onSessionExpired, onError, observability, logger, mutationExecutor, mutationDispatcher, sessionErrorDetector, onlineStatus, configOverrides, syncGroups, bootstrapBaseUrl, maxPoolSize, persistence, bootstrapMode, fallback = _jsx(DefaultFallback, {}), children, } = props;
|
|
35
|
+
const { schema, url = 'wss://mesh.ablo.finance', userId, teamIds, apiKey, preventUnsavedChanges, onSessionExpired, onError, observability, logger, mutationExecutor, mutationDispatcher, sessionErrorDetector, onlineStatus, configOverrides, syncGroups, scope, bootstrapBaseUrl, maxPoolSize, persistence, bootstrapMode, fallback = _jsx(DefaultFallback, {}), children, } = props;
|
|
36
36
|
// Account scope is no longer accepted from props. The engine learns
|
|
37
37
|
// it from auth (capability token) at bootstrap and we read it back
|
|
38
38
|
// out of `_store.orgId` once `engine.ready()` resolves.
|
|
@@ -86,7 +86,11 @@ export function AbloProvider(props) {
|
|
|
86
86
|
mutationExecutor,
|
|
87
87
|
mutationDispatcher,
|
|
88
88
|
configOverrides,
|
|
89
|
-
|
|
89
|
+
// Union raw strings with model-form `scope` resolved through the schema,
|
|
90
|
+
// so `scope={{ decks: id }}` becomes `deck:<id>` via the model's `scope`.
|
|
91
|
+
syncGroups: scope
|
|
92
|
+
? [...(syncGroups ?? []), ...resolveParticipantSyncGroups(scope, schema)]
|
|
93
|
+
: syncGroups,
|
|
90
94
|
bootstrapBaseUrl,
|
|
91
95
|
maxPoolSize,
|
|
92
96
|
persistence,
|
|
@@ -251,7 +255,11 @@ export function useParticipant(opts) {
|
|
|
251
255
|
const ctx = useContext(AbloInternalContext);
|
|
252
256
|
const engine = ctx?.engine ?? null;
|
|
253
257
|
const { paused = false } = opts;
|
|
254
|
-
|
|
258
|
+
// Resolve the model-form scope ({ decks: id } / refs) THROUGH the schema, so a
|
|
259
|
+
// model's declared `scope` kind is honored (typename `SlideDeck` → `deck:<id>`,
|
|
260
|
+
// not the `type:id` string fallback). Schema appears once the engine is ready;
|
|
261
|
+
// until then refs resolve by convention, then re-resolve when it arrives.
|
|
262
|
+
const scopeKey = JSON.stringify(resolveParticipantSyncGroups(opts.scope, engine?.schema).sort());
|
|
255
263
|
const scopedSyncGroups = useMemo(() => JSON.parse(scopeKey), [scopeKey]);
|
|
256
264
|
const [claimError, setClaimError] = useState(null);
|
|
257
265
|
const [claimConnected, setClaimConnected] = useState(false);
|
|
@@ -326,10 +334,10 @@ export function useParticipant(opts) {
|
|
|
326
334
|
}
|
|
327
335
|
setPeers(participant.presence.others);
|
|
328
336
|
setClaims(participant.intents.others);
|
|
329
|
-
const unsubPresence = participant.presence.
|
|
337
|
+
const unsubPresence = participant.presence.onChange(() => {
|
|
330
338
|
setPeers(participant.presence.others);
|
|
331
339
|
});
|
|
332
|
-
const unsubIntents = participant.intents.
|
|
340
|
+
const unsubIntents = participant.intents.onChange(() => {
|
|
333
341
|
setClaims(participant.intents.others);
|
|
334
342
|
});
|
|
335
343
|
return () => {
|
|
@@ -347,7 +355,7 @@ export function useParticipant(opts) {
|
|
|
347
355
|
/**
|
|
348
356
|
* Returns the raw `SyncEngine` proxy. Typically you want the typed
|
|
349
357
|
* hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
|
|
350
|
-
* where you need direct access (e.g., `sync.tasks.
|
|
358
|
+
* where you need direct access (e.g., `sync.tasks.onChange(cb)`).
|
|
351
359
|
*
|
|
352
360
|
* The generic parameter narrows the return type to your schema's
|
|
353
361
|
* 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>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema → Postgres DDL — the one pure SQL emitter shared by every consumer.
|
|
3
|
+
*
|
|
4
|
+
* `defineSchema(...)` (serialized to {@link SchemaJSON}) is the single source of
|
|
5
|
+
* truth; this module lowers it to ordered DDL strings. Both the hosted server
|
|
6
|
+
* (which applies it to Ablo-managed Postgres on `schema push`) and the
|
|
7
|
+
* `ablo migrate` CLI (which applies it to a customer's own Postgres) call these
|
|
8
|
+
* generators, so the SQL — column types, RLS, enum checks — is identical no
|
|
9
|
+
* matter who runs it. There is no second type map.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is pure (returns strings; no DB, no I/O); the execution side
|
|
12
|
+
* (transaction + advisory lock) lives with each consumer because it's coupled
|
|
13
|
+
* to that consumer's Postgres client and error type.
|
|
14
|
+
*
|
|
15
|
+
* - `generateProvisionPlan` — additive + idempotent (CREATE/ADD … IF NOT
|
|
16
|
+
* EXISTS + RLS). Never loses data. The "create my tables" primitive.
|
|
17
|
+
* - `generateMigrationPlan` — the destructive-aware counterpart driven by the
|
|
18
|
+
* {@link diffSchema} step list (drops, renames, type casts, backfills).
|
|
19
|
+
*/
|
|
20
|
+
import type { SchemaJSON, ModelJSON } from './serialize.js';
|
|
21
|
+
import type { MigrationStep, BackfillValue } from './diff.js';
|
|
22
|
+
export interface ProvisionPlan {
|
|
23
|
+
/** The Postgres schema the tables live in (`app_<id>` or `public`). */
|
|
24
|
+
readonly appSchema: string;
|
|
25
|
+
/** Ordered, idempotent DDL statements. Safe to run repeatedly. */
|
|
26
|
+
readonly statements: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
export interface MigrationPlan {
|
|
29
|
+
/** The app Postgres schema the DDL targets (`app_<id>` or `public`). */
|
|
30
|
+
readonly appSchema: string;
|
|
31
|
+
/** Ordered DDL statements (expand → contract). */
|
|
32
|
+
readonly statements: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
/** Per-app schema name for an app (organization) id. */
|
|
35
|
+
export declare function appSchemaName(organizationId: string): string;
|
|
36
|
+
export declare function camelToSnake(identifier: string): string;
|
|
37
|
+
/** Quote an identifier (defense-in-depth; inputs are already slug/snake). */
|
|
38
|
+
export declare function q(identifier: string): string;
|
|
39
|
+
export declare function sqlType(fieldType: ModelJSON['fields'][string]['type']): string;
|
|
40
|
+
/**
|
|
41
|
+
* Build the additive, idempotent provisioning plan for an app. Pure — no DB
|
|
42
|
+
* access.
|
|
43
|
+
*
|
|
44
|
+
* `targetSchema` is where the tables live: the app's schema `app_<id>` on the
|
|
45
|
+
* shared tier, or `public` on a dedicated tenant's own database (where the DB
|
|
46
|
+
* itself is the isolation boundary). For `public` the `CREATE SCHEMA` is
|
|
47
|
+
* skipped (it always exists).
|
|
48
|
+
*/
|
|
49
|
+
export declare function generateProvisionPlan(schema: SchemaJSON, targetSchema: string): ProvisionPlan;
|
|
50
|
+
/**
|
|
51
|
+
* Lower an ordered migration step list to DDL. `next` is the schema being pushed
|
|
52
|
+
* (the target column shapes are read from it), `prev` the active one (used to
|
|
53
|
+
* resolve the *old* table name on a model rename).
|
|
54
|
+
*/
|
|
55
|
+
export declare function generateMigrationPlan(steps: readonly MigrationStep[], opts: {
|
|
56
|
+
readonly prev: SchemaJSON | null;
|
|
57
|
+
readonly next: SchemaJSON;
|
|
58
|
+
readonly targetSchema: string;
|
|
59
|
+
/** Constant seed values that let a required-field add / made-required step
|
|
60
|
+
* set NOT NULL on a non-empty table. Keyed by (model, field). */
|
|
61
|
+
readonly backfills?: readonly BackfillValue[];
|
|
62
|
+
}): MigrationPlan;
|