@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.
Files changed (60) hide show
  1. package/dist/Ablo.d.ts +4 -0
  2. package/dist/client.d.ts +11 -31
  3. package/dist/local/client/reactiveEngine.js +4 -5
  4. package/dist/{react/useSyncStatus.d.ts → local/client/status.d.ts} +4 -3
  5. package/dist/local/client/status.js +14 -0
  6. package/dist/local/client/storeLifecycle.js +2 -2
  7. package/dist/local/storeContract.d.ts +3 -4
  8. package/dist/presence/index.js +6 -2
  9. package/dist/react/AbloProvider.d.ts +59 -139
  10. package/dist/react/AbloProvider.js +60 -181
  11. package/dist/react/createAbloReact.d.ts +10 -32
  12. package/dist/react/createAbloReact.js +10 -54
  13. package/dist/react/internalContext.d.ts +3 -20
  14. package/dist/react/snapshot.d.ts +4 -0
  15. package/dist/react/snapshot.js +75 -0
  16. package/dist/react/useAblo.d.ts +28 -45
  17. package/dist/react/useAblo.js +39 -77
  18. package/dist/react/useMutators.d.ts +20 -17
  19. package/dist/react/usePresence.js +7 -6
  20. package/dist/react/useReactive.d.ts +12 -0
  21. package/dist/react/useReactive.js +57 -0
  22. package/dist/react/useUndoScope.d.ts +15 -29
  23. package/dist/react/useUndoScope.js +17 -0
  24. package/dist/react.d.ts +7 -19
  25. package/dist/react.js +7 -15
  26. package/package.json +4 -3
  27. package/src/Ablo.ts +5 -0
  28. package/src/client.ts +12 -31
  29. package/src/local/client/reactiveEngine.ts +4 -5
  30. package/src/local/client/status.ts +20 -0
  31. package/src/local/client/storeLifecycle.ts +2 -2
  32. package/src/local/storeContract.ts +3 -4
  33. package/src/presence/index.ts +6 -2
  34. package/src/react/AbloProvider.tsx +94 -309
  35. package/src/react/createAbloReact.ts +18 -99
  36. package/src/react/internalContext.ts +3 -20
  37. package/src/react/snapshot.ts +67 -0
  38. package/src/react/useAblo.ts +71 -140
  39. package/src/react/useMutators.ts +30 -26
  40. package/src/react/usePresence.ts +7 -12
  41. package/src/react/useReactive.ts +56 -0
  42. package/src/react/useUndoScope.ts +18 -14
  43. package/src/react.ts +7 -69
  44. package/dist/react/ClientSideSuspense.d.ts +0 -36
  45. package/dist/react/ClientSideSuspense.js +0 -17
  46. package/dist/react/useCurrentUserId.d.ts +0 -2
  47. package/dist/react/useCurrentUserId.js +0 -12
  48. package/dist/react/useErrorListener.d.ts +0 -2
  49. package/dist/react/useErrorListener.js +0 -14
  50. package/dist/react/useMutationFailureListener.d.ts +0 -8
  51. package/dist/react/useMutationFailureListener.js +0 -19
  52. package/dist/react/useSyncStatus.js +0 -37
  53. package/dist/useReactive.d.ts +0 -6
  54. package/dist/useReactive.js +0 -43
  55. package/src/react/ClientSideSuspense.tsx +0 -57
  56. package/src/react/useCurrentUserId.ts +0 -17
  57. package/src/react/useErrorListener.ts +0 -22
  58. package/src/react/useMutationFailureListener.ts +0 -34
  59. package/src/react/useSyncStatus.ts +0 -42
  60. 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 { useSyncStatus } from './useSyncStatus.js';
17
+ import { useAblo } from './useAblo.js';
22
18
  import { DefaultFallback } from './DefaultFallback.js';
23
- import { presenceOfClient } from '../presence/index.js';
24
19
 
25
- /**
26
- * Ablo umbrella provider — owns the sync engine, multiplayer, and
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. `userId` is informational; the `fallback`,
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: AbloProviderProps<R>,
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 [resolvedAccountScope, setResolvedAccountScope] = useState<string | null>(null);
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
- useEffect(() => {
199
- return errorEmitter.subscribe((err) => onErrorRef.current?.(err));
200
- }, [errorEmitter]);
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
- errorEmitter.emit(err);
109
+ reportError(err);
226
110
  void (async () => {
227
111
  try {
228
112
  await onSessionExpiredRef.current?.();
229
113
  } catch (hookErr) {
230
- errorEmitter.emit(hookErr as Error);
114
+ reportError(hookErr as Error);
231
115
  }
232
116
  })().catch(() => {
233
- // Only a throwing errorEmitter subscriber can land here — it was
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
- setResolvedAccountScope(
244
- (engine._store as SyncStoreContract & { orgId?: string }).orgId ?? null,
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
- errorEmitter.emit(err as Error);
134
+ reportError(err as Error);
250
135
  });
251
136
 
252
137
  return () => {
253
138
  stale = true;
254
139
  unsubscribeSession();
255
140
  };
256
- }, [engine, errorEmitter]);
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
- resolvedAccountScope ??
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, resolvedAccountScope, schema]);
176
+ }, [engine, resolvedScope, schema]);
292
177
 
293
- // ── Internal context (currentUserId + error subscription) ────────
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
- }), [userId, errorEmitter, engine]);
182
+ }), [engine]);
301
183
 
302
184
  // ── Render ───────────────────────────────────────────────────────
303
185
  //
304
- // Two-phase gate (see `BootstrapGate` below for the latch logic):
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 on `engineState.key` in the parent so engine rotations
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 = useSyncStatus();
223
+ const status = useAblo(ablo => ablo.status);
367
224
  const [everConnected, setEverConnected] = useState(false);
368
225
 
369
226
  useEffect(() => {
370
227
  if (
371
- status.name === 'connected' ||
372
- status.name === 'reconnecting' ||
373
- status.name === 'disconnected'
228
+ status?.name === 'connected' ||
229
+ status?.name === 'reconnecting' ||
230
+ status?.name === 'disconnected'
374
231
  ) {
375
232
  setEverConnected(true);
376
233
  }
377
- }, [status.name]);
234
+ }, [status?.name]);
378
235
 
379
- const showFallback = !everConnected && status.name === 'connecting';
236
+ const showFallback = !everConnected && status?.name === 'connecting';
380
237
  return <>{showFallback ? fallback : children}</>;
381
238
  }
382
239
 
383
-
384
- const EMPTY_PRESENCE: readonly PresenceSession[] = Object.freeze([]);
385
-
386
- export type { GroupScope };
387
-
388
- /**
389
- * Read-only presence: the other sessions currently visible to this
390
- * connection, bridged to React. This is a pure reader of the engine's
391
- * already-flowing presence stream; it does not mutate connection groups.
392
- *
393
- * Pass `scope` to narrow to the peers on that scope's sync group(s); omit
394
- * it to get everyone on the engine's groups. Membership is driven entirely
395
- * by the presence channel (set server-side on connect, independent of any
396
- * cursor/collaboration traffic), so reading it never affects what the
397
- * connection is subscribed to and can't deadlock against a gated channel.
398
- *
399
- * Use this to answer "is anyone else here?", for example to suppress
400
- * live-cursor broadcasts while alone.
401
- *
402
- * ```ts
403
- * const peers = usePeers({ reports: reportId });
404
- * const alone = !peers.some((p) => p.participantKind === 'user');
405
- * ```
406
- */
407
- export function usePeers(scope?: GroupScope): readonly PresenceSession[] {
408
- const ctx = useContext(AbloInternalContext);
409
- const engine = ctx?.engine ?? null;
410
-
411
- // Resolve scope → groups through the schema.
412
- // The stringified, sorted key is the stable effect dependency.
413
- const scopeKey = JSON.stringify(
414
- resolveScopeGroups(scope, engine?.schema).sort(),
415
- );
416
- const groups = useMemo(() => JSON.parse(scopeKey) as string[], [scopeKey]);
417
-
418
- const [peers, setPeers] = useState<readonly PresenceSession[]>(EMPTY_PRESENCE);
419
-
420
- useEffect(() => {
421
- if (!engine) {
422
- setPeers(EMPTY_PRESENCE);
423
- return;
424
- }
425
- const presence = presenceOfClient(engine);
426
- const compute = (): readonly PresenceSession[] =>
427
- groups.length === 0
428
- ? presence.others
429
- : presence.others.filter((session) =>
430
- session.activities.some(({ target }) =>
431
- target.id !== undefined && groups.includes(
432
- `${target.model.toLowerCase()}:${target.id}`,
433
- ),
434
- ),
435
- );
436
- // Plain useState + onChange — presence changes on connect/disconnect/activity
437
- // only (never on cursor traffic, a separate channel), so this fires
438
- // rarely; a frame of stale presence is harmless.
439
- setPeers(compute());
440
- return presence.onChange(() => { setPeers(compute()); });
441
- }, [engine, groups, scopeKey]);
442
-
443
- return peers;
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
  }