@abloatai/humans 0.63.0 → 0.64.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/dist/Ablo.d.ts +4 -0
- package/dist/client.d.ts +11 -31
- package/dist/local/client/reactiveEngine.js +4 -5
- package/dist/{react/useSyncStatus.d.ts → local/client/status.d.ts} +4 -3
- package/dist/local/client/status.js +14 -0
- package/dist/local/client/storeLifecycle.js +2 -2
- package/dist/local/storeContract.d.ts +3 -4
- package/dist/presence/index.js +6 -2
- package/dist/react/AbloProvider.d.ts +59 -139
- package/dist/react/AbloProvider.js +60 -181
- package/dist/react/createAbloReact.d.ts +10 -32
- package/dist/react/createAbloReact.js +10 -54
- package/dist/react/internalContext.d.ts +3 -20
- package/dist/react/snapshot.d.ts +4 -0
- package/dist/react/snapshot.js +75 -0
- package/dist/react/useAblo.d.ts +28 -45
- package/dist/react/useAblo.js +39 -77
- package/dist/react/useMutators.d.ts +20 -17
- package/dist/react/usePresence.js +7 -6
- package/dist/react/useReactive.d.ts +12 -0
- package/dist/react/useReactive.js +57 -0
- package/dist/react/useUndoScope.d.ts +15 -29
- package/dist/react/useUndoScope.js +17 -0
- package/dist/react.d.ts +7 -19
- package/dist/react.js +7 -15
- package/package.json +4 -3
- package/src/Ablo.ts +5 -0
- package/src/client.ts +12 -31
- package/src/local/client/reactiveEngine.ts +4 -5
- package/src/local/client/status.ts +20 -0
- package/src/local/client/storeLifecycle.ts +2 -2
- package/src/local/storeContract.ts +3 -4
- package/src/presence/index.ts +6 -2
- package/src/react/AbloProvider.tsx +94 -309
- package/src/react/createAbloReact.ts +18 -99
- package/src/react/internalContext.ts +3 -20
- package/src/react/snapshot.ts +67 -0
- package/src/react/useAblo.ts +71 -140
- package/src/react/useMutators.ts +30 -26
- package/src/react/usePresence.ts +7 -12
- package/src/react/useReactive.ts +56 -0
- package/src/react/useUndoScope.ts +18 -14
- package/src/react.ts +7 -69
- package/dist/react/ClientSideSuspense.d.ts +0 -36
- package/dist/react/ClientSideSuspense.js +0 -17
- package/dist/react/useCurrentUserId.d.ts +0 -2
- package/dist/react/useCurrentUserId.js +0 -12
- package/dist/react/useErrorListener.d.ts +0 -2
- package/dist/react/useErrorListener.js +0 -14
- package/dist/react/useMutationFailureListener.d.ts +0 -8
- package/dist/react/useMutationFailureListener.js +0 -19
- package/dist/react/useSyncStatus.js +0 -37
- package/dist/useReactive.d.ts +0 -6
- package/dist/useReactive.js +0 -43
- package/src/react/ClientSideSuspense.tsx +0 -57
- package/src/react/useCurrentUserId.ts +0 -17
- package/src/react/useErrorListener.ts +0 -22
- package/src/react/useMutationFailureListener.ts +0 -34
- package/src/react/useSyncStatus.ts +0 -42
- package/src/useReactive.ts +0 -51
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
useCallback,
|
|
5
|
-
useContext,
|
|
6
5
|
useEffect,
|
|
7
6
|
useMemo,
|
|
8
7
|
useRef,
|
|
@@ -12,34 +11,14 @@ import {
|
|
|
12
11
|
} from 'react';
|
|
13
12
|
import type { Schema, SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
14
13
|
import type { AbloClient as Ablo } from '../client.js';
|
|
15
|
-
import type { PresenceSession } from '@abloatai/transaction/presence';
|
|
16
|
-
import type { GroupScope } from '../local/sync/scopeGroups.js';
|
|
17
|
-
import { resolveScopeGroups } from '../local/sync/scopeGroups.js';
|
|
18
14
|
import { SyncContext, type SyncStoreContract } from './context.js';
|
|
19
15
|
import { AbloInternalContext, type AbloInternalContextValue } from './internalContext.js';
|
|
20
16
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
21
|
-
import {
|
|
17
|
+
import { useAblo } from './useAblo.js';
|
|
22
18
|
import { DefaultFallback } from './DefaultFallback.js';
|
|
23
|
-
import { presenceOfClient } from '../presence/index.js';
|
|
24
19
|
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
* the full lifecycle (Strict-Mode-safe singleton, `beforeunload`,
|
|
28
|
-
* session-expiry handling, post-bootstrap hooks).
|
|
29
|
-
*
|
|
30
|
-
* Design goals:
|
|
31
|
-
*
|
|
32
|
-
* - **One component, one import.** Consumers write the provider
|
|
33
|
-
* once at the root; nothing else needs to plumb the engine.
|
|
34
|
-
* - **Multiplayer is default.** React consumers share the client's scoped
|
|
35
|
-
* groups, presence stream, and model surface without another join step.
|
|
36
|
-
* - **Declarative props for app glue.** `preventUnsavedChanges`,
|
|
37
|
-
* `onSessionExpired`, `postBootstrap`, `resolveUsers` — each
|
|
38
|
-
* absorbs a class of integration code that previously lived in
|
|
39
|
-
* userland.
|
|
40
|
-
* - **Singleton safety.** The engine lives in a ref and rotates
|
|
41
|
-
* only when `userId` / account scope / `url` change. React
|
|
42
|
-
* Strict Mode double-mount does not leak a second WebSocket.
|
|
20
|
+
/** Reactive binding over an application-owned client. Starts readiness,
|
|
21
|
+
* forwards errors and gates bootstrap; the application owns client disposal.
|
|
43
22
|
*/
|
|
44
23
|
|
|
45
24
|
// ── Props ────────────────────────────────────────────────────────────
|
|
@@ -62,107 +41,19 @@ import { presenceOfClient } from '../presence/index.js';
|
|
|
62
41
|
* </AbloProvider>
|
|
63
42
|
* ```
|
|
64
43
|
*
|
|
65
|
-
* That's it for most apps.
|
|
44
|
+
* That's it for most apps. The `fallback`,
|
|
66
45
|
* `preventUnsavedChanges`, and `on*` props are opt-in app glue; and the
|
|
67
46
|
* block tagged "Optional DI (advanced)" below is escape-hatch wiring for
|
|
68
47
|
* tests and platform builders — if you don't recognize a prop there, you
|
|
69
48
|
* don't need it.
|
|
70
49
|
*/
|
|
71
|
-
export interface AbloProviderProps<R extends SchemaRecord = SchemaRecord> {
|
|
72
|
-
/**
|
|
73
|
-
* A prebuilt {@link Ablo} client — **the only way to configure the engine.**
|
|
74
|
-
* Construct it yourself with `Ablo({ schema, apiKey, ... })` and pass the
|
|
75
|
-
* instance: the CLIENT owns auth, the credential lifecycle, transport, and
|
|
76
|
-
* connection; this provider is the thin REACTIVE binding over it (context,
|
|
77
|
-
* the bootstrap gate, error/session forwarding).
|
|
78
|
-
*
|
|
79
|
-
* Memoize it (build it once, e.g. with `useMemo` or module scope) — a new
|
|
80
|
-
* instance each render re-keys the bootstrap gate and tears down the socket.
|
|
81
|
-
*/
|
|
82
|
-
client: Ablo<R>;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The app user id, surfaced via `useCurrentUserId()` for app-owned fields.
|
|
86
|
-
* Purely informational for the React tree — sync identity is resolved by the
|
|
87
|
-
* client from its auth, not from this. Optional.
|
|
88
|
-
*/
|
|
89
|
-
userId?: string;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Block tab close while there are unsynced local writes (the standard
|
|
93
|
-
* `beforeunload` prompt). Browsers ignore custom messages — don't pass one.
|
|
94
|
-
*/
|
|
95
|
-
preventUnsavedChanges?: boolean;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Fired after the client has completed its terminal authentication cleanup
|
|
99
|
-
* (or surfaced a cleanup failure). Use it for app side effects such as a
|
|
100
|
-
* redirect to sign-in or clearing analytics identity.
|
|
101
|
-
*/
|
|
102
|
-
onSessionExpired?: () => void | Promise<void>;
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Fired on any error the provider surfaces (engine/WebSocket/bootstrap). For
|
|
106
|
-
* Sentry/Datadog. React-only consumers can use `useErrorListener()` instead.
|
|
107
|
-
*/
|
|
108
|
-
onError?: (error: Error) => void;
|
|
109
|
-
|
|
110
|
-
/** @internal placeholder so the old WS-URL prop shape doesn't silently leak in. */
|
|
111
|
-
url?: never;
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Rendered in place of `children` during the *first* bootstrap pass —
|
|
115
|
-
* while the engine is actively transitioning from `initial` →
|
|
116
|
-
* `connected` and has never successfully connected before. Once the
|
|
117
|
-
* engine reaches `connected` the gate latches open for the lifetime
|
|
118
|
-
* of this provider instance; transient `reconnecting` / `needs-auth`
|
|
119
|
-
* states do NOT re-show the fallback (the app's own UI handles those
|
|
120
|
-
* by then).
|
|
121
|
-
*
|
|
122
|
-
* Defaults to `<DefaultFallback />` — a neutral theme-adaptive
|
|
123
|
-
* spinner that uses `currentColor`, ships with zero design-system
|
|
124
|
-
* dependencies, and self-centers in a full-parent container. Pass
|
|
125
|
-
* your own `<Skeleton />` for a branded loading UX. Pass `null` to
|
|
126
|
-
* render nothing during bootstrap. Pass the string literal
|
|
127
|
-
* `"passthrough"` to opt out of the gate entirely — children render
|
|
128
|
-
* immediately and consumers are responsible for their own gating
|
|
129
|
-
* (`<ClientSideSuspense>` or manual `useSyncStatus()` checks).
|
|
130
|
-
* Useful for pages that mount debug helpers, error boundaries, or
|
|
131
|
-
* analytics that must run pre-ready.
|
|
132
|
-
*/
|
|
133
|
-
fallback?: ReactNode | 'passthrough';
|
|
134
|
-
|
|
135
|
-
children: ReactNode;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
50
|
// ── Implementation ───────────────────────────────────────────────────
|
|
139
51
|
|
|
140
|
-
/**
|
|
141
|
-
* Lightweight event emitter for provider-level errors. Lives on the
|
|
142
|
-
* provider instance (ref-based) so `useErrorListener` subscriptions
|
|
143
|
-
* survive re-renders without thrashing.
|
|
144
|
-
*/
|
|
145
|
-
function createErrorEmitter() {
|
|
146
|
-
const listeners = new Set<(err: Error) => void>();
|
|
147
|
-
return {
|
|
148
|
-
subscribe(fn: (err: Error) => void): () => void {
|
|
149
|
-
listeners.add(fn);
|
|
150
|
-
return () => { listeners.delete(fn); };
|
|
151
|
-
},
|
|
152
|
-
emit(err: Error): void {
|
|
153
|
-
for (const fn of listeners) {
|
|
154
|
-
try { fn(err); } catch {}
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
52
|
export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
161
|
-
props:
|
|
53
|
+
props: AbloProvider.Props<R>,
|
|
162
54
|
): React.ReactElement {
|
|
163
55
|
const {
|
|
164
56
|
client,
|
|
165
|
-
userId,
|
|
166
57
|
preventUnsavedChanges,
|
|
167
58
|
onSessionExpired,
|
|
168
59
|
onError,
|
|
@@ -182,22 +73,15 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
182
73
|
|
|
183
74
|
// Account scope isn't a prop — read it from `_store.orgId` once `ready()`
|
|
184
75
|
// resolves the identity from the client's auth.
|
|
185
|
-
const [
|
|
186
|
-
|
|
187
|
-
// ── Error emitter (provider-instance scoped) ─────────────────────
|
|
188
|
-
const errorEmitterRef = useRef<ReturnType<typeof createErrorEmitter> | null>(null);
|
|
189
|
-
if (!errorEmitterRef.current) {
|
|
190
|
-
errorEmitterRef.current = createErrorEmitter();
|
|
191
|
-
}
|
|
192
|
-
const errorEmitter = errorEmitterRef.current;
|
|
76
|
+
const [resolvedScope, setResolvedScope] = useState<{ engine: typeof engine; account: string | null } | null>(null);
|
|
193
77
|
|
|
194
78
|
// Stash callbacks in refs so a new identity each render doesn't re-run the
|
|
195
79
|
// start effect (the `useEventCallback` idiom).
|
|
196
80
|
const onErrorRef = useRef(onError);
|
|
197
81
|
onErrorRef.current = onError;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}, [
|
|
82
|
+
const reportError = useCallback((error: Error) => {
|
|
83
|
+
try { onErrorRef.current?.(error); } catch { /* Error reporting must not interrupt session cleanup. */ }
|
|
84
|
+
}, []);
|
|
201
85
|
const onSessionExpiredRef = useRef(onSessionExpired);
|
|
202
86
|
onSessionExpiredRef.current = onSessionExpired;
|
|
203
87
|
|
|
@@ -222,15 +106,15 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
222
106
|
let stale = false;
|
|
223
107
|
|
|
224
108
|
const unsubscribeSession = engine.onSessionError((err) => {
|
|
225
|
-
|
|
109
|
+
reportError(err);
|
|
226
110
|
void (async () => {
|
|
227
111
|
try {
|
|
228
112
|
await onSessionExpiredRef.current?.();
|
|
229
113
|
} catch (hookErr) {
|
|
230
|
-
|
|
114
|
+
reportError(hookErr as Error);
|
|
231
115
|
}
|
|
232
116
|
})().catch(() => {
|
|
233
|
-
//
|
|
117
|
+
// This was
|
|
234
118
|
// already the error-reporting path, so swallow rather than surface
|
|
235
119
|
// an unhandled rejection loop.
|
|
236
120
|
});
|
|
@@ -240,20 +124,21 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
240
124
|
.ready()
|
|
241
125
|
.then(() => {
|
|
242
126
|
if (stale) return;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
127
|
+
setResolvedScope({
|
|
128
|
+
engine,
|
|
129
|
+
account: (engine._store as SyncStoreContract & { orgId?: string }).orgId ?? null,
|
|
130
|
+
});
|
|
246
131
|
})
|
|
247
132
|
.catch((err) => {
|
|
248
133
|
if (stale) return;
|
|
249
|
-
|
|
134
|
+
reportError(err as Error);
|
|
250
135
|
});
|
|
251
136
|
|
|
252
137
|
return () => {
|
|
253
138
|
stale = true;
|
|
254
139
|
unsubscribeSession();
|
|
255
140
|
};
|
|
256
|
-
}, [engine,
|
|
141
|
+
}, [engine, reportError]);
|
|
257
142
|
|
|
258
143
|
// ── beforeunload + preventUnsavedChanges ─────────────────────────
|
|
259
144
|
|
|
@@ -280,7 +165,7 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
280
165
|
// then, which drives the initial fallback below.
|
|
281
166
|
const syncValue = useMemo(() => {
|
|
282
167
|
const currentAccountScope =
|
|
283
|
-
|
|
168
|
+
(resolvedScope?.engine === engine ? resolvedScope.account : null) ??
|
|
284
169
|
(engine._store as SyncStoreContract & { orgId?: string }).orgId;
|
|
285
170
|
if (!currentAccountScope) return null;
|
|
286
171
|
return {
|
|
@@ -288,57 +173,30 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
288
173
|
organizationId: currentAccountScope,
|
|
289
174
|
schema,
|
|
290
175
|
};
|
|
291
|
-
}, [engine,
|
|
176
|
+
}, [engine, resolvedScope, schema]);
|
|
292
177
|
|
|
293
|
-
//
|
|
178
|
+
// The React tree holds the same client used by core code.
|
|
294
179
|
|
|
295
180
|
const internalValue = useMemo<AbloInternalContextValue>(() => ({
|
|
296
|
-
currentUserId: userId ?? null,
|
|
297
|
-
subscribeError: errorEmitter.subscribe,
|
|
298
|
-
emitError: errorEmitter.emit,
|
|
299
181
|
engine: engine as Ablo<SchemaRecord>,
|
|
300
|
-
}), [
|
|
182
|
+
}), [engine]);
|
|
301
183
|
|
|
302
184
|
// ── Render ───────────────────────────────────────────────────────
|
|
303
185
|
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
// 1. Engine is null on first render (constructed in the effect
|
|
307
|
-
// above, not in render). We render `fallback` directly — there
|
|
308
|
-
// is no SyncContext to read status from, and by definition the
|
|
309
|
-
// engine hasn't started bootstrapping.
|
|
310
|
-
// 2. Engine exists. Mount SyncContext. `BootstrapGate` then reads
|
|
311
|
-
// `useSyncStatus()` and shows `fallback` only during the very
|
|
312
|
-
// first `connecting` transition; children render on every
|
|
313
|
-
// subsequent state change, including reconnects and auth
|
|
314
|
-
// failures (the app's own UI handles those).
|
|
315
|
-
//
|
|
316
|
-
// `fallback === 'passthrough'` short-circuits both branches — children
|
|
317
|
-
// render immediately without any gate, restoring pre-gate behavior
|
|
318
|
-
// for consumers who need debug helpers / error boundaries / analytics
|
|
319
|
-
// to mount before the engine is ready.
|
|
320
|
-
|
|
186
|
+
// Keep the context tree stable during startup so passthrough children retain
|
|
187
|
+
// their component state when authenticated row scope becomes available.
|
|
321
188
|
const passthrough = fallback === 'passthrough';
|
|
322
|
-
const initialFallback = passthrough ? children : fallback;
|
|
323
|
-
|
|
324
|
-
if (!syncValue) {
|
|
325
|
-
return (
|
|
326
|
-
<AbloInternalContext.Provider value={internalValue}>
|
|
327
|
-
{initialFallback}
|
|
328
|
-
</AbloInternalContext.Provider>
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
189
|
|
|
332
190
|
return (
|
|
333
191
|
<AbloInternalContext.Provider value={internalValue}>
|
|
334
192
|
<SyncContext.Provider value={syncValue}>
|
|
335
193
|
{passthrough ? (
|
|
336
194
|
children
|
|
337
|
-
) : (
|
|
195
|
+
) : syncValue ? (
|
|
338
196
|
<BootstrapGate key={engineKey} fallback={fallback}>
|
|
339
197
|
{children}
|
|
340
198
|
</BootstrapGate>
|
|
341
|
-
)}
|
|
199
|
+
) : fallback}
|
|
342
200
|
</SyncContext.Provider>
|
|
343
201
|
</AbloInternalContext.Provider>
|
|
344
202
|
);
|
|
@@ -352,8 +210,7 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
352
210
|
* re-show the fallback, because by then the app has already rendered
|
|
353
211
|
* once and its own reconnect UI should take over.
|
|
354
212
|
*
|
|
355
|
-
* Re-keyed
|
|
356
|
-
* (userId/org/url change) reset the latch — a new engine genuinely IS
|
|
213
|
+
* Re-keyed when the client instance changes so account rotations reset the latch — a new engine genuinely IS
|
|
357
214
|
* a new "first bootstrap" cycle.
|
|
358
215
|
*/
|
|
359
216
|
function BootstrapGate({
|
|
@@ -363,155 +220,83 @@ function BootstrapGate({
|
|
|
363
220
|
readonly fallback: ReactNode;
|
|
364
221
|
readonly children: ReactNode;
|
|
365
222
|
}): ReactNode {
|
|
366
|
-
const status =
|
|
223
|
+
const status = useAblo(ablo => ablo.status);
|
|
367
224
|
const [everConnected, setEverConnected] = useState(false);
|
|
368
225
|
|
|
369
226
|
useEffect(() => {
|
|
370
227
|
if (
|
|
371
|
-
status
|
|
372
|
-
status
|
|
373
|
-
status
|
|
228
|
+
status?.name === 'connected' ||
|
|
229
|
+
status?.name === 'reconnecting' ||
|
|
230
|
+
status?.name === 'disconnected'
|
|
374
231
|
) {
|
|
375
232
|
setEverConnected(true);
|
|
376
233
|
}
|
|
377
|
-
}, [status
|
|
234
|
+
}, [status?.name]);
|
|
378
235
|
|
|
379
|
-
const showFallback = !everConnected && status
|
|
236
|
+
const showFallback = !everConnected && status?.name === 'connecting';
|
|
380
237
|
return <>{showFallback ? fallback : children}</>;
|
|
381
238
|
}
|
|
382
239
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
export
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// ── Escape-hatches: raw engine/store access ──────────────────────────
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Returns the raw `SyncEngine` proxy. Typically you want the typed
|
|
450
|
-
* hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
|
|
451
|
-
* where you need direct access (e.g., `sync.items.onChange(cb)`).
|
|
452
|
-
*
|
|
453
|
-
* The generic parameter narrows the return type to your schema's
|
|
454
|
-
* model record so call sites get typed `sync.items.findMany()` /
|
|
455
|
-
* `sync.sections.create(...)` without a cast at the call site:
|
|
456
|
-
*
|
|
457
|
-
* ```ts
|
|
458
|
-
* const sync = useSync<(typeof schema)['models']>();
|
|
459
|
-
* ```
|
|
460
|
-
*
|
|
461
|
-
* The runtime value is the exact engine the provider constructed;
|
|
462
|
-
* the generic just widens the compile-time type.
|
|
463
|
-
*/
|
|
464
|
-
export function useSync<R extends SchemaRecord = SchemaRecord>(): Ablo<R> {
|
|
465
|
-
const ctx = useContext(AbloInternalContext);
|
|
466
|
-
if (!ctx) {
|
|
467
|
-
throw new AbloValidationError(
|
|
468
|
-
'useSync: no <AbloProvider> mounted above this component.',
|
|
469
|
-
{ code: 'no_ablo_provider' },
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
if (!ctx.engine) {
|
|
473
|
-
throw new AbloValidationError(
|
|
474
|
-
'useSync: the sync engine has not yet initialized. Wrap your ' +
|
|
475
|
-
'consumer in <ClientSideSuspense> or guard on useSyncStatus().',
|
|
476
|
-
{ code: 'sync_not_ready' },
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
return rebindProviderEngine(ctx.engine);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function rebindProviderEngine<R extends SchemaRecord>(
|
|
483
|
-
engine: Ablo<SchemaRecord>,
|
|
484
|
-
): Ablo<R> {
|
|
485
|
-
return engine as Ablo<R>;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Returns the underlying `SyncStoreContract` (the BaseSyncedStore).
|
|
490
|
-
* Most consumers should prefer the typed hooks (`useQuery` etc.); this
|
|
491
|
-
* is for advanced cases like direct InstanceCache access or custom
|
|
492
|
-
* reactive bridges. Throws if the provider hasn't mounted the store
|
|
493
|
-
* yet — wrap consumers in `<ClientSideSuspense>` to gate correctly.
|
|
494
|
-
*
|
|
495
|
-
* The generic parameter lets consumers widen the return type to a
|
|
496
|
-
* concrete `BaseSyncedStore<...>` subclass if they track one:
|
|
497
|
-
*
|
|
498
|
-
* ```ts
|
|
499
|
-
* type AppStore = BaseSyncedStore<AppEvents, typeof schema>;
|
|
500
|
-
* const store = useSyncStore<AppStore>(); // no cast needed at call site
|
|
501
|
-
* ```
|
|
502
|
-
*
|
|
503
|
-
* The runtime value is always the concrete store the SDK constructed,
|
|
504
|
-
* so widening the type is safe. The bounded generic (`T extends
|
|
505
|
-
* SyncStoreContract`) keeps the widening honest.
|
|
506
|
-
*/
|
|
507
|
-
export function useSyncStore<T extends SyncStoreContract = SyncStoreContract>(): T {
|
|
508
|
-
const sync = useContext(SyncContext);
|
|
509
|
-
if (!sync?.store) {
|
|
510
|
-
throw new AbloValidationError(
|
|
511
|
-
'useSyncStore: the sync engine has not yet initialized. Wrap ' +
|
|
512
|
-
'consumers in <ClientSideSuspense> or guard on useSyncStatus().',
|
|
513
|
-
{ code: 'sync_not_ready' },
|
|
514
|
-
);
|
|
240
|
+
/** Props for wrappers around the provider, using the same schema parameter. */
|
|
241
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
242
|
+
export namespace AbloProvider {
|
|
243
|
+
export interface Props<R extends SchemaRecord = SchemaRecord> {
|
|
244
|
+
/**
|
|
245
|
+
* A prebuilt {@link Ablo} client — **the only way to configure the engine.**
|
|
246
|
+
* Construct it yourself with `Ablo({ schema, apiKey, ... })` and pass the
|
|
247
|
+
* instance: the CLIENT owns auth, the credential lifecycle, transport, and
|
|
248
|
+
* connection; this provider is the thin REACTIVE binding over it (context,
|
|
249
|
+
* the bootstrap gate, error/session forwarding).
|
|
250
|
+
*
|
|
251
|
+
* Memoize it (build it once, e.g. with `useMemo` or module scope) — a new
|
|
252
|
+
* instance each render re-keys the bootstrap gate and tears down the socket.
|
|
253
|
+
*/
|
|
254
|
+
client: Ablo<R>;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Block tab close while there are unsynced local writes (the standard
|
|
258
|
+
* `beforeunload` prompt). Browsers ignore custom messages — don't pass one.
|
|
259
|
+
*/
|
|
260
|
+
preventUnsavedChanges?: boolean;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Fired after the client has completed its terminal authentication cleanup
|
|
264
|
+
* (or surfaced a cleanup failure). Use it for app side effects such as a
|
|
265
|
+
* redirect to sign-in or clearing analytics identity.
|
|
266
|
+
*/
|
|
267
|
+
onSessionExpired?: () => void | Promise<void>;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Fired on any error the provider surfaces (engine/WebSocket/bootstrap). For
|
|
271
|
+
* Sentry/Datadog or application error UI.
|
|
272
|
+
*/
|
|
273
|
+
onError?: (error: Error) => void;
|
|
274
|
+
|
|
275
|
+
/** @internal placeholder so the old WS-URL prop shape doesn't silently leak in. */
|
|
276
|
+
url?: never;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Rendered in place of `children` during the *first* bootstrap pass —
|
|
280
|
+
* while the engine is actively transitioning from `initial` →
|
|
281
|
+
* `connected` and has never successfully connected before. Once the
|
|
282
|
+
* engine reaches `connected` the gate latches open for the lifetime
|
|
283
|
+
* of this provider instance; transient `reconnecting` / `needs-auth`
|
|
284
|
+
* states do NOT re-show the fallback (the app's own UI handles those
|
|
285
|
+
* by then).
|
|
286
|
+
*
|
|
287
|
+
* Defaults to `<DefaultFallback />` — a neutral theme-adaptive
|
|
288
|
+
* spinner that uses `currentColor`, ships with zero design-system
|
|
289
|
+
* dependencies, and self-centers in a full-parent container. Pass
|
|
290
|
+
* your own `<Skeleton />` for a branded loading UX. Pass `null` to
|
|
291
|
+
* render nothing during bootstrap. Pass the string literal
|
|
292
|
+
* `"passthrough"` to opt out of the gate entirely — children render
|
|
293
|
+
* immediately and consumers are responsible for their own gating
|
|
294
|
+
* (for example, `useAblo(ablo => ablo.status)` checks).
|
|
295
|
+
* Useful for pages that mount debug helpers, error boundaries, or
|
|
296
|
+
* analytics that must run pre-ready.
|
|
297
|
+
*/
|
|
298
|
+
fallback?: ReactNode | 'passthrough';
|
|
299
|
+
|
|
300
|
+
children: ReactNode;
|
|
515
301
|
}
|
|
516
|
-
return sync.store as T;
|
|
517
302
|
}
|