@abloatai/ablo 0.7.0 → 0.9.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 +72 -1
- package/README.md +80 -66
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +179 -5
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +6 -5
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +26 -36
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +259 -33
- package/dist/client/Ablo.js +276 -73
- package/dist/client/ApiClient.d.ts +52 -4
- package/dist/client/ApiClient.js +236 -66
- package/dist/client/auth.d.ts +21 -2
- package/dist/client/auth.js +77 -5
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +187 -79
- package/dist/client/createModelProxy.js +203 -68
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +63 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +59 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +92 -1
- package/dist/errorCodes.js +139 -7
- package/dist/errors.d.ts +54 -3
- package/dist/errors.js +192 -44
- package/dist/index.d.ts +23 -10
- package/dist/index.js +21 -8
- package/dist/keys/index.d.ts +76 -0
- package/dist/keys/index.js +171 -0
- package/dist/mutators/UndoManager.d.ts +86 -50
- package/dist/mutators/UndoManager.js +129 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +22 -14
- package/dist/react/AbloProvider.d.ts +23 -101
- package/dist/react/AbloProvider.js +61 -103
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/SyncGroupProvider.d.ts +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +3 -2
- package/dist/react/useAblo.d.ts +4 -4
- package/dist/react/useAblo.js +10 -5
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/react/useReactive.js +16 -3
- package/dist/schema/ddl.d.ts +26 -3
- package/dist/schema/ddl.js +152 -4
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.js +12 -0
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +7 -3
- package/dist/schema/serialize.js +6 -2
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +59 -0
- package/dist/source/adapter.js +19 -0
- package/dist/source/adapters/drizzle.d.ts +34 -0
- package/dist/source/adapters/drizzle.js +147 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +199 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +143 -0
- package/dist/source/contract.js +98 -0
- package/dist/source/index.d.ts +61 -10
- package/dist/source/index.js +98 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +56 -42
- package/dist/sync/ConnectionManager.d.ts +57 -1
- package/dist/sync/ConnectionManager.js +186 -11
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +241 -41
- package/dist/sync/NetworkProbe.d.ts +60 -18
- package/dist/sync/NetworkProbe.js +121 -23
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +113 -89
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/sync/participants.js +5 -2
- package/dist/transactions/TransactionQueue.js +13 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api-keys.md +5 -5
- package/docs/api.md +125 -65
- package/docs/audit.md +16 -9
- package/docs/cli.md +57 -47
- package/docs/client-behavior.md +54 -40
- package/docs/coordination.md +66 -80
- package/docs/data-sources.md +56 -34
- package/docs/examples/agent-human.md +74 -28
- package/docs/examples/ai-sdk-tool.md +29 -22
- package/docs/examples/existing-python-backend.md +41 -26
- package/docs/examples/nextjs.md +32 -17
- package/docs/examples/scoped-agent.md +43 -28
- package/docs/examples/server-agent.md +40 -15
- package/docs/guarantees.md +38 -27
- package/docs/identity.md +65 -59
- package/docs/index.md +30 -19
- package/docs/integration-guide.md +78 -78
- package/docs/interaction-model.md +43 -35
- package/docs/mcp/claude-code.md +11 -19
- package/docs/mcp/cursor.md +7 -25
- package/docs/mcp/windsurf.md +7 -20
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +63 -61
- package/docs/react.md +24 -16
- package/docs/roadmap.md +13 -13
- package/docs/schema-contract.md +111 -0
- package/docs/the-loop.md +21 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +10 -7
- package/examples/data-source/customer-server.ts +27 -25
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +55 -21
- package/package.json +48 -3
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
4
|
-
import { Ablo } from '../client/Ablo.js';
|
|
5
4
|
import { createParticipantClaimId, parseParticipantTtlSeconds, resolveParticipantSyncGroups, } from '../sync/participants.js';
|
|
6
5
|
import { SyncContext } from './context.js';
|
|
7
6
|
import { AbloInternalContext } from './internalContext.js';
|
|
@@ -32,75 +31,52 @@ function createErrorEmitter() {
|
|
|
32
31
|
};
|
|
33
32
|
}
|
|
34
33
|
export function AbloProvider(props) {
|
|
35
|
-
const {
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
34
|
+
const { client, userId, preventUnsavedChanges, onSessionExpired, onError, fallback = _jsx(DefaultFallback, {}), children, } = props;
|
|
35
|
+
// The client IS the engine — synchronous, never null. This provider is a
|
|
36
|
+
// REACTIVE binding over it (context + bootstrap gate + error/session
|
|
37
|
+
// forwarding); it does NOT construct, configure, or own the connection. The
|
|
38
|
+
// client owns auth, the credential lifecycle (first mint, refresh, and
|
|
39
|
+
// wake/online/focus re-mint — see `Ablo({ getToken })`), transport, and
|
|
40
|
+
// `dispose()`. The CONSUMER built the client, so the consumer owns teardown;
|
|
41
|
+
// the provider never disposes it.
|
|
42
|
+
const engine = client;
|
|
43
|
+
const schema = engine.schema;
|
|
44
|
+
// Account scope isn't a prop — read it from `_store.orgId` once `ready()`
|
|
45
|
+
// resolves the identity from the client's auth.
|
|
39
46
|
const [resolvedAccountScope, setResolvedAccountScope] = useState(null);
|
|
40
47
|
// ── Error emitter (provider-instance scoped) ─────────────────────
|
|
41
|
-
//
|
|
42
|
-
// Built once, reused for the lifetime of this provider. Survives
|
|
43
|
-
// engine rotations so error listeners don't need to resubscribe.
|
|
44
48
|
const errorEmitterRef = useRef(null);
|
|
45
49
|
if (!errorEmitterRef.current) {
|
|
46
50
|
errorEmitterRef.current = createErrorEmitter();
|
|
47
51
|
}
|
|
48
52
|
const errorEmitter = errorEmitterRef.current;
|
|
49
|
-
// Stash
|
|
50
|
-
//
|
|
51
|
-
// time, matching the `useEventCallback` idiom.
|
|
53
|
+
// Stash callbacks in refs so a new identity each render doesn't re-run the
|
|
54
|
+
// start effect (the `useEventCallback` idiom).
|
|
52
55
|
const onErrorRef = useRef(onError);
|
|
53
56
|
onErrorRef.current = onError;
|
|
54
57
|
useEffect(() => {
|
|
55
58
|
return errorEmitter.subscribe((err) => onErrorRef.current?.(err));
|
|
56
59
|
}, [errorEmitter]);
|
|
57
|
-
|
|
60
|
+
const onSessionExpiredRef = useRef(onSessionExpired);
|
|
61
|
+
onSessionExpiredRef.current = onSessionExpired;
|
|
62
|
+
// Re-key the bootstrap gate when the client INSTANCE changes — a genuinely new
|
|
63
|
+
// engine is a fresh "first bootstrap". Stable for the common single-client app.
|
|
64
|
+
const clientGenRef = useRef({ client, gen: 0 });
|
|
65
|
+
if (clientGenRef.current.client !== client) {
|
|
66
|
+
clientGenRef.current = { client, gen: clientGenRef.current.gen + 1 };
|
|
67
|
+
}
|
|
68
|
+
const engineKey = String(clientGenRef.current.gen);
|
|
69
|
+
// ── Start + session-error wiring ─────────────────────────────────
|
|
58
70
|
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
});
|
|
67
|
-
const [engineState, setEngineState] = useState({ key: engineKey, engine: null });
|
|
68
|
-
// Keep a ref to the current engine key so the rotation effect can
|
|
69
|
-
// detect late-arriving prop changes without causing React churn.
|
|
70
|
-
const currentKeyRef = useRef(engineState.key);
|
|
71
|
-
currentKeyRef.current = engineState.key;
|
|
71
|
+
// Two reactive jobs only:
|
|
72
|
+
// 1. Forward a SERVER session-rejection → purge (wipe IndexedDB so the next
|
|
73
|
+
// login starts clean) → onSessionExpired (the app redirects). The
|
|
74
|
+
// offline/transient-vs-terminal credential logic lives in the CLIENT now.
|
|
75
|
+
// 2. Drive `ready()` (idempotent) so bootstrap starts on mount, then read the
|
|
76
|
+
// resolved org scope for SyncContext.
|
|
77
|
+
// It does NOT dispose the client (consumer-owned) and does NOT touch auth.
|
|
72
78
|
useEffect(() => {
|
|
73
|
-
|
|
74
|
-
let isStale = false;
|
|
75
|
-
setResolvedAccountScope(null);
|
|
76
|
-
// Construct engine + multiplayer streams for this key.
|
|
77
|
-
const engineOptions = {
|
|
78
|
-
baseURL: url,
|
|
79
|
-
schema,
|
|
80
|
-
...(userId ? { user: { id: userId, teamIds } } : {}),
|
|
81
|
-
apiKey,
|
|
82
|
-
logger,
|
|
83
|
-
observability,
|
|
84
|
-
sessionErrorDetector,
|
|
85
|
-
onlineStatus,
|
|
86
|
-
mutationExecutor,
|
|
87
|
-
mutationDispatcher,
|
|
88
|
-
configOverrides,
|
|
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,
|
|
94
|
-
bootstrapBaseUrl,
|
|
95
|
-
maxPoolSize,
|
|
96
|
-
persistence,
|
|
97
|
-
...(bootstrapMode ? { bootstrapMode } : {}),
|
|
98
|
-
autoStart: false,
|
|
99
|
-
};
|
|
100
|
-
const engine = Ablo(engineOptions);
|
|
101
|
-
setEngineState({ key: engineKey, engine });
|
|
102
|
-
// Forward session-error events to the consumer. Purge first so
|
|
103
|
-
// the IndexedDB is wiped before the app redirects to /signin.
|
|
79
|
+
let stale = false;
|
|
104
80
|
const unsubscribeSession = engine.onSessionError(async (err) => {
|
|
105
81
|
errorEmitter.emit(err);
|
|
106
82
|
try {
|
|
@@ -108,55 +84,37 @@ export function AbloProvider(props) {
|
|
|
108
84
|
}
|
|
109
85
|
catch { }
|
|
110
86
|
try {
|
|
111
|
-
await
|
|
87
|
+
await onSessionExpiredRef.current?.();
|
|
112
88
|
}
|
|
113
89
|
catch (hookErr) {
|
|
114
90
|
errorEmitter.emit(hookErr);
|
|
115
91
|
}
|
|
116
92
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
catch (err) {
|
|
130
|
-
if (isStale || abort.signal.aborted)
|
|
131
|
-
return;
|
|
132
|
-
errorEmitter.emit(err);
|
|
133
|
-
}
|
|
134
|
-
})();
|
|
93
|
+
engine
|
|
94
|
+
.ready()
|
|
95
|
+
.then(() => {
|
|
96
|
+
if (stale)
|
|
97
|
+
return;
|
|
98
|
+
setResolvedAccountScope(engine._store.orgId ?? null);
|
|
99
|
+
})
|
|
100
|
+
.catch((err) => {
|
|
101
|
+
if (stale)
|
|
102
|
+
return;
|
|
103
|
+
errorEmitter.emit(err);
|
|
104
|
+
});
|
|
135
105
|
return () => {
|
|
136
|
-
|
|
137
|
-
abort.abort();
|
|
106
|
+
stale = true;
|
|
138
107
|
unsubscribeSession();
|
|
139
|
-
void engine.dispose();
|
|
140
|
-
// AbloClient is stateless-ish — participants manage their own
|
|
141
|
-
// WebSocket connections via `participant.disconnect()`. No client
|
|
142
|
-
// close is needed.
|
|
143
108
|
};
|
|
144
|
-
|
|
145
|
-
// captured at first render; rotating the engine on every
|
|
146
|
-
// `mutationExecutor` identity change would destroy the WebSocket.
|
|
147
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
148
|
-
}, [engineKey]);
|
|
109
|
+
}, [engine, errorEmitter]);
|
|
149
110
|
// ── beforeunload + preventUnsavedChanges ─────────────────────────
|
|
150
111
|
useEffect(() => {
|
|
151
112
|
if (typeof window === 'undefined')
|
|
152
113
|
return;
|
|
153
114
|
const handler = (event) => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// Best-effort: dispose on unload. The async work may not
|
|
158
|
-
// complete before the tab closes — that's fine for IDB, which
|
|
159
|
-
// flushes pending writes transactionally.
|
|
115
|
+
// Best-effort IDB flush on TAB CLOSE — the client is going away with the
|
|
116
|
+
// page regardless. This is NOT an unmount teardown: the consumer owns the
|
|
117
|
+
// client's lifecycle and the provider never disposes it on unmount.
|
|
160
118
|
void engine.dispose();
|
|
161
119
|
if (preventUnsavedChanges && engine._store.hasUnsyncedChanges) {
|
|
162
120
|
event.preventDefault();
|
|
@@ -165,30 +123,30 @@ export function AbloProvider(props) {
|
|
|
165
123
|
};
|
|
166
124
|
window.addEventListener('beforeunload', handler);
|
|
167
125
|
return () => window.removeEventListener('beforeunload', handler);
|
|
168
|
-
}, [
|
|
126
|
+
}, [engine, preventUnsavedChanges]);
|
|
169
127
|
// ── SyncContext value (for useQuery/useOne/useMutate hooks) ──────
|
|
128
|
+
//
|
|
129
|
+
// The engine is always present (it's the `client` prop), but its org scope is
|
|
130
|
+
// unknown until `ready()` resolves identity — so `syncValue` is null until
|
|
131
|
+
// then, which drives the initial fallback below.
|
|
170
132
|
const syncValue = useMemo(() => {
|
|
171
|
-
if (!engineState.engine)
|
|
172
|
-
return null;
|
|
173
133
|
const currentAccountScope = resolvedAccountScope ??
|
|
174
|
-
|
|
134
|
+
engine._store.orgId;
|
|
175
135
|
if (!currentAccountScope)
|
|
176
136
|
return null;
|
|
177
137
|
return {
|
|
178
|
-
store:
|
|
138
|
+
store: engine._store,
|
|
179
139
|
organizationId: currentAccountScope,
|
|
180
140
|
schema,
|
|
181
141
|
};
|
|
182
|
-
}, [
|
|
142
|
+
}, [engine, resolvedAccountScope, schema]);
|
|
183
143
|
// ── Internal context (currentUserId + error subscription) ────────
|
|
184
144
|
const internalValue = useMemo(() => ({
|
|
185
145
|
currentUserId: userId ?? null,
|
|
186
146
|
subscribeError: errorEmitter.subscribe,
|
|
187
147
|
emitError: errorEmitter.emit,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
engine: engineState.engine,
|
|
191
|
-
}), [userId, errorEmitter, engineState.engine]);
|
|
148
|
+
engine: engine,
|
|
149
|
+
}), [userId, errorEmitter, engine]);
|
|
192
150
|
// ── Render ───────────────────────────────────────────────────────
|
|
193
151
|
//
|
|
194
152
|
// Two-phase gate (see `BootstrapGate` below for the latch logic):
|
|
@@ -212,7 +170,7 @@ export function AbloProvider(props) {
|
|
|
212
170
|
if (!syncValue) {
|
|
213
171
|
return (_jsx(AbloInternalContext.Provider, { value: internalValue, children: initialFallback }));
|
|
214
172
|
}
|
|
215
|
-
return (_jsx(AbloInternalContext.Provider, { value: internalValue, children: _jsx(SyncContext.Provider, { value: syncValue, children: passthrough ? (children) : (_jsx(BootstrapGate, { fallback: fallback, children: children },
|
|
173
|
+
return (_jsx(AbloInternalContext.Provider, { value: internalValue, children: _jsx(SyncContext.Provider, { value: syncValue, children: passthrough ? (children) : (_jsx(BootstrapGate, { fallback: fallback, children: children }, engineKey)) }) }));
|
|
216
174
|
}
|
|
217
175
|
/**
|
|
218
176
|
* Internal gate that renders `fallback` only during the very first
|
|
@@ -33,4 +33,4 @@ export interface ClientSideSuspenseProps {
|
|
|
33
33
|
/** What to render once the subtree is cleared to render. */
|
|
34
34
|
children: ReactNode;
|
|
35
35
|
}
|
|
36
|
-
export declare function ClientSideSuspense({ fallback, children }: ClientSideSuspenseProps): import("react
|
|
36
|
+
export declare function ClientSideSuspense({ fallback, children }: ClientSideSuspenseProps): import("react").JSX.Element;
|
|
@@ -21,4 +21,4 @@
|
|
|
21
21
|
* pass `fallback={null}`. Consumers who want to skip the gate entirely
|
|
22
22
|
* pass `fallback="passthrough"`.
|
|
23
23
|
*/
|
|
24
|
-
export declare function DefaultFallback(): import("react
|
|
24
|
+
export declare function DefaultFallback(): import("react").JSX.Element;
|
|
@@ -4,7 +4,7 @@ export interface SyncGroupProviderProps {
|
|
|
4
4
|
id: string;
|
|
5
5
|
children: ReactNode;
|
|
6
6
|
}
|
|
7
|
-
export declare function SyncGroupProvider({ id, children }: SyncGroupProviderProps): import("react
|
|
7
|
+
export declare function SyncGroupProvider({ id, children }: SyncGroupProviderProps): import("react").JSX.Element;
|
|
8
8
|
/**
|
|
9
9
|
* Returns the ID of the nearest `<SyncGroupProvider>`. Throws if
|
|
10
10
|
* called outside one — sync-group awareness is mandatory by design,
|
package/dist/react/index.d.ts
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
* immediately. The provider-level `fallback` is the default path.
|
|
14
14
|
*
|
|
15
15
|
* Data hooks:
|
|
16
|
-
* useAblo((ablo) => ablo.tasks.
|
|
16
|
+
* useAblo((ablo) => ablo.tasks.get(id)) — primary React read API (sync local snapshot)
|
|
17
17
|
* useAblo() — typed client for callbacks/effects
|
|
18
|
-
* (reads: ablo.<model>.
|
|
18
|
+
* (sync local reads: ablo.<model>.get/getAll;
|
|
19
|
+
* async server reads: ablo.<model>.retrieve/list;
|
|
19
20
|
* writes: ablo.<model>.create/update/delete)
|
|
20
21
|
* useMutators(defs, opts?) — Zero-style custom mutators
|
|
21
22
|
* useUndoScope(name) — per-surface undo/redo
|
package/dist/react/index.js
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
* immediately. The provider-level `fallback` is the default path.
|
|
14
14
|
*
|
|
15
15
|
* Data hooks:
|
|
16
|
-
* useAblo((ablo) => ablo.tasks.
|
|
16
|
+
* useAblo((ablo) => ablo.tasks.get(id)) — primary React read API (sync local snapshot)
|
|
17
17
|
* useAblo() — typed client for callbacks/effects
|
|
18
|
-
* (reads: ablo.<model>.
|
|
18
|
+
* (sync local reads: ablo.<model>.get/getAll;
|
|
19
|
+
* async server reads: ablo.<model>.retrieve/list;
|
|
19
20
|
* writes: ablo.<model>.create/update/delete)
|
|
20
21
|
* useMutators(defs, opts?) — Zero-style custom mutators
|
|
21
22
|
* useUndoScope(name) — per-surface undo/redo
|
package/dist/react/useAblo.d.ts
CHANGED
|
@@ -45,11 +45,11 @@ export type UseAbloHydratedModelResult<T> = Omit<UseAbloModelResult<T>, 'data'>
|
|
|
45
45
|
* // With Register augmentation (recommended):
|
|
46
46
|
* const ablo = useAblo();
|
|
47
47
|
* if (!ablo) return <Loading />;
|
|
48
|
-
* const
|
|
48
|
+
* const doc = await ablo.documents.retrieve({ id }); // async server read
|
|
49
49
|
*
|
|
50
|
-
* // Reactive selector:
|
|
51
|
-
* const doc = useAblo((ablo) => ablo.documents.
|
|
52
|
-
* const active = useAblo((ablo) => ablo.documents.
|
|
50
|
+
* // Reactive selector (sync local-graph snapshot):
|
|
51
|
+
* const doc = useAblo((ablo) => ablo.documents.get(id)) ?? serverDoc;
|
|
52
|
+
* const active = useAblo((ablo) => ablo.documents.claim.state({ id }));
|
|
53
53
|
*
|
|
54
54
|
* // Without augmentation, pass the schema generic:
|
|
55
55
|
* const ablo = useAblo<(typeof schema)['models']>();
|
package/dist/react/useAblo.js
CHANGED
|
@@ -9,7 +9,7 @@ function readModelResult(engine, modelClient, id, initial) {
|
|
|
9
9
|
if (!modelClient || id === undefined) {
|
|
10
10
|
return { data: initial, claims: EMPTY_CLAIMS, claimed: false };
|
|
11
11
|
}
|
|
12
|
-
const data = snapshotValue(modelClient.
|
|
12
|
+
const data = snapshotValue(modelClient.get(id) ?? initial);
|
|
13
13
|
const meta = getModelClientMeta(modelClient);
|
|
14
14
|
const claims = meta && engine
|
|
15
15
|
? engine.intents.list({ model: meta.key, id })
|
|
@@ -29,7 +29,6 @@ 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 = modelOrSelect !== undefined;
|
|
33
32
|
const isSelectorOnly = typeof modelOrSelect === 'function' && id === undefined;
|
|
34
33
|
const modelClient = typeof modelOrSelect === 'function' && id !== undefined
|
|
35
34
|
? engine
|
|
@@ -38,14 +37,20 @@ export function useAblo(modelOrSelect, id, options) {
|
|
|
38
37
|
: typeof modelOrSelect === 'function'
|
|
39
38
|
? undefined
|
|
40
39
|
: modelOrSelect;
|
|
40
|
+
// Claims live on a non-MobX event emitter (engine.intents), so the useReactive
|
|
41
|
+
// reactions below cannot track them — we bridge changes through a setState bump.
|
|
42
|
+
// ONLY the model-row form (`id !== undefined`) actually reads claims, so gate the
|
|
43
|
+
// subscription on `id`. The selector-only form (`useAblo((a) => a.x.get/getAll)`)
|
|
44
|
+
// never reads claims; subscribing it to the workspace-global intent stream would
|
|
45
|
+
// re-render + double-compute it on every intent/presence delta anywhere (a real
|
|
46
|
+
// storm during AI editing / live collaboration) for a value that can't change.
|
|
41
47
|
const [claimVersion, setClaimVersion] = useState(0);
|
|
42
48
|
useEffect(() => {
|
|
43
|
-
if (!engine ||
|
|
49
|
+
if (!engine || id === undefined)
|
|
44
50
|
return;
|
|
45
51
|
return engine.intents.onChange(() => setClaimVersion((version) => version + 1));
|
|
46
|
-
}, [engine,
|
|
52
|
+
}, [engine, id]);
|
|
47
53
|
const selected = useReactive(() => {
|
|
48
|
-
void claimVersion;
|
|
49
54
|
if (!engine || !isSelectorOnly || typeof modelOrSelect !== 'function') {
|
|
50
55
|
return undefined;
|
|
51
56
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* const userId = useCurrentUserId();
|
|
14
14
|
* const ablo = useAblo();
|
|
15
15
|
* if (!userId) return null;
|
|
16
|
-
* return <button onClick={() => ablo?.tasks.update(id, { assigneeId: userId })}>
|
|
16
|
+
* return <button onClick={() => ablo?.tasks.update({ id, data: { assigneeId: userId } })}>
|
|
17
17
|
* Assign to me
|
|
18
18
|
* </button>;
|
|
19
19
|
* }
|
|
@@ -17,7 +17,7 @@ import { AbloValidationError } from '../errors.js';
|
|
|
17
17
|
* const userId = useCurrentUserId();
|
|
18
18
|
* const ablo = useAblo();
|
|
19
19
|
* if (!userId) return null;
|
|
20
|
-
* return <button onClick={() => ablo?.tasks.update(id, { assigneeId: userId })}>
|
|
20
|
+
* return <button onClick={() => ablo?.tasks.update({ id, data: { assigneeId: userId } })}>
|
|
21
21
|
* Assign to me
|
|
22
22
|
* </button>;
|
|
23
23
|
* }
|
|
@@ -34,19 +34,26 @@ export function useMutators(schemaOrMutators, mutatorsOrOptions, maybeOptions) {
|
|
|
34
34
|
invokers[mutatorName] = async (args) => {
|
|
35
35
|
// Recording path: wrap the transaction so each write snapshots its
|
|
36
36
|
// inverse. On success, push the captured entry to the scope.
|
|
37
|
+
//
|
|
38
|
+
// The whole snapshot → write → record sequence runs on the scope's
|
|
39
|
+
// serialization chain so concurrent invocations (the slides UI fires
|
|
40
|
+
// writes un-awaited) record in *invocation* order and never
|
|
41
|
+
// interleave their shared-model snapshots. See UndoScope.runRecorded.
|
|
37
42
|
if (undoScope) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
return undoScope.runRecorded(async () => {
|
|
44
|
+
const recording = createRecordingTransaction(schema, store, organizationId);
|
|
45
|
+
try {
|
|
46
|
+
const result = await fn({ tx: recording.tx, args });
|
|
47
|
+
const entry = recording.getEntry(label);
|
|
48
|
+
if (entry)
|
|
49
|
+
undoScope.record(entry);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
getContext().logger.error(`[useMutators] mutator "${label}" threw`, { error: err });
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
50
57
|
}
|
|
51
58
|
// Non-recording path — plain transaction, identical to pre-undo V1.
|
|
52
59
|
const tx = createTransaction(schema, store, organizationId);
|
|
@@ -66,16 +66,29 @@ export function useReactive(compute, equals = defaultEquals) {
|
|
|
66
66
|
const subscribeVersionRef = useRef(0);
|
|
67
67
|
if (snapshotRef.current === null) {
|
|
68
68
|
snapshotRef.current = { value: compute() };
|
|
69
|
-
computeRef.current = compute;
|
|
70
69
|
}
|
|
71
70
|
else if (computeRef.current !== compute) {
|
|
71
|
+
// `compute` is a fresh inline arrow at virtually every call site, so this
|
|
72
|
+
// branch runs on essentially every render. Reconcile the snapshot against
|
|
73
|
+
// the latest closure, but only force a re-subscription when the value
|
|
74
|
+
// ACTUALLY changed. For the dominant case (same observable source, new
|
|
75
|
+
// arrow identity, unchanged value) this avoids tearing down + recreating
|
|
76
|
+
// the MobX reaction — and its double-compute — on every render. A genuine
|
|
77
|
+
// source swap (a memoized compute closing over a new observable source)
|
|
78
|
+
// changes the value, which both updates the snapshot and bumps
|
|
79
|
+
// `subscribeVersion` so the reaction below re-subscribes and re-tracks the
|
|
80
|
+
// new source's observables.
|
|
72
81
|
const next = compute();
|
|
73
82
|
if (!equals(snapshotRef.current.value, next)) {
|
|
74
83
|
snapshotRef.current = { value: next };
|
|
84
|
+
subscribeVersionRef.current++;
|
|
75
85
|
}
|
|
76
|
-
computeRef.current = compute;
|
|
77
|
-
subscribeVersionRef.current++;
|
|
78
86
|
}
|
|
87
|
+
// Point the long-lived reaction at the latest closure every render. The
|
|
88
|
+
// reaction expression reads `computeRef.current` at fire time, so it always
|
|
89
|
+
// runs the newest compute (and re-tracks its observables) even when we did
|
|
90
|
+
// not re-subscribe above.
|
|
91
|
+
computeRef.current = compute;
|
|
79
92
|
const subscribeVersion = subscribeVersionRef.current;
|
|
80
93
|
const subscribe = useCallback((onChange) => {
|
|
81
94
|
return reaction(() => computeRef.current(), (next) => {
|
package/dist/schema/ddl.d.ts
CHANGED
|
@@ -22,14 +22,34 @@ import type { MigrationStep, BackfillValue } from './diff.js';
|
|
|
22
22
|
export interface ProvisionPlan {
|
|
23
23
|
/** The Postgres schema the tables live in (`app_<id>` or `public`). */
|
|
24
24
|
readonly appSchema: string;
|
|
25
|
-
/** Ordered, idempotent DDL statements. Safe to run repeatedly.
|
|
25
|
+
/** Ordered, idempotent DDL statements. Safe to run repeatedly. Executors run
|
|
26
|
+
* these together in ONE transaction. */
|
|
26
27
|
readonly statements: readonly string[];
|
|
28
|
+
/** Post-commit, NON-transactional DDL (`VALIDATE CONSTRAINT`, `CREATE INDEX
|
|
29
|
+
* CONCURRENTLY`) — run AFTER {@link statements} commit, each outside any
|
|
30
|
+
* transaction, best-effort. Keeps the lock-heavy / scan-heavy work off the
|
|
31
|
+
* main transaction so adding a foreign key never freezes a large, live BYO
|
|
32
|
+
* table. Optional + back-compat: absent = nothing to run. */
|
|
33
|
+
readonly concurrent?: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
export interface ProvisionOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Emit `DEFERRABLE INITIALLY DEFERRED` FOREIGN KEY constraints for every
|
|
38
|
+
* `parent: true` belongsTo relation (true ownership edges only — see
|
|
39
|
+
* {@link foreignKeyStatements}). Off by default: the soft-reference model keeps
|
|
40
|
+
* out-of-order sync robust on Ablo-managed tables. Turn on for a customer's own
|
|
41
|
+
* (BYO / dedicated) database, where a clean, navigable relational schema is
|
|
42
|
+
* wanted and the DB starts empty (nothing for the constraint to fail against).
|
|
43
|
+
*/
|
|
44
|
+
readonly foreignKeys?: boolean;
|
|
27
45
|
}
|
|
28
46
|
export interface MigrationPlan {
|
|
29
47
|
/** The app Postgres schema the DDL targets (`app_<id>` or `public`). */
|
|
30
48
|
readonly appSchema: string;
|
|
31
|
-
/** Ordered DDL statements (expand → contract). */
|
|
49
|
+
/** Ordered DDL statements (expand → contract). Run in ONE transaction. */
|
|
32
50
|
readonly statements: readonly string[];
|
|
51
|
+
/** Post-commit, non-transactional DDL — see {@link ProvisionPlan.concurrent}. */
|
|
52
|
+
readonly concurrent?: readonly string[];
|
|
33
53
|
}
|
|
34
54
|
/** Per-app schema name for an app (organization) id. */
|
|
35
55
|
export declare function appSchemaName(organizationId: string): string;
|
|
@@ -46,7 +66,7 @@ export declare function sqlType(fieldType: ModelJSON['fields'][string]['type']):
|
|
|
46
66
|
* itself is the isolation boundary). For `public` the `CREATE SCHEMA` is
|
|
47
67
|
* skipped (it always exists).
|
|
48
68
|
*/
|
|
49
|
-
export declare function generateProvisionPlan(schema: SchemaJSON, targetSchema: string): ProvisionPlan;
|
|
69
|
+
export declare function generateProvisionPlan(schema: SchemaJSON, targetSchema: string, opts?: ProvisionOptions): ProvisionPlan;
|
|
50
70
|
/**
|
|
51
71
|
* Lower an ordered migration step list to DDL. `next` is the schema being pushed
|
|
52
72
|
* (the target column shapes are read from it), `prev` the active one (used to
|
|
@@ -59,4 +79,7 @@ export declare function generateMigrationPlan(steps: readonly MigrationStep[], o
|
|
|
59
79
|
/** Constant seed values that let a required-field add / made-required step
|
|
60
80
|
* set NOT NULL on a non-empty table. Keyed by (model, field). */
|
|
61
81
|
readonly backfills?: readonly BackfillValue[];
|
|
82
|
+
/** Emit DEFERRABLE FK constraints for `parent: true` edges of newly-created
|
|
83
|
+
* models. Off by default — see {@link ProvisionOptions.foreignKeys}. */
|
|
84
|
+
readonly foreignKeys?: boolean;
|
|
62
85
|
}): MigrationPlan;
|