@abloatai/humans 0.63.1 → 0.64.1
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 +6 -0
- package/dist/client.d.ts +12 -32
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/local/client/createModelOperations.d.ts +3 -3
- package/dist/local/client/createModelOperations.js +1 -1
- package/dist/local/client/reactiveEngine.js +7 -8
- 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/mutators/defineMutators.d.ts +3 -48
- package/dist/local/mutators/defineMutators.js +0 -15
- package/dist/local/storeAccess.d.ts +7 -0
- package/dist/local/storeAccess.js +6 -0
- package/dist/local/storeContract.d.ts +3 -4
- package/dist/presence/index.d.ts +2 -2
- package/dist/presence/index.js +8 -4
- package/dist/react/AbloProvider.d.ts +57 -121
- package/dist/react/AbloProvider.js +55 -160
- package/dist/react/context.d.ts +6 -45
- package/dist/react/context.js +8 -20
- package/dist/react/createAbloReact.d.ts +13 -48
- package/dist/react/createAbloReact.js +8 -54
- package/dist/react/internalContext.d.ts +1 -27
- package/dist/react/snapshot.d.ts +4 -0
- package/dist/react/snapshot.js +75 -0
- package/dist/react/useAblo.d.ts +28 -92
- package/dist/react/useAblo.js +36 -93
- package/dist/react/useAbloClient.d.ts +5 -0
- package/dist/react/useAbloClient.js +9 -0
- package/dist/react/useMutationFailure.d.ts +6 -0
- package/dist/react/useMutationFailure.js +11 -0
- package/dist/react/useMutators.d.ts +22 -19
- package/dist/react/useMutators.js +3 -3
- package/dist/react/usePresence.d.ts +9 -9
- package/dist/react/usePresence.js +8 -11
- package/dist/react/useReactive.d.ts +12 -0
- package/dist/react/useReactive.js +57 -0
- package/dist/react/useUndoScope.d.ts +16 -30
- package/dist/react/useUndoScope.js +21 -4
- package/dist/react.d.ts +9 -19
- package/dist/react.js +9 -15
- package/dist/reactRuntime.d.ts +1 -1
- package/dist/reactRuntime.js +1 -1
- package/package.json +4 -3
- package/src/Ablo.ts +7 -0
- package/src/client.ts +13 -32
- package/src/index.ts +2 -0
- package/src/local/client/createModelOperations.ts +4 -4
- package/src/local/client/reactiveEngine.ts +7 -8
- package/src/local/client/status.ts +20 -0
- package/src/local/client/storeLifecycle.ts +2 -2
- package/src/local/mutators/defineMutators.ts +3 -51
- package/src/local/storeAccess.ts +10 -0
- package/src/local/storeContract.ts +3 -4
- package/src/presence/index.ts +10 -5
- package/src/react/AbloProvider.tsx +88 -263
- package/src/react/context.ts +10 -61
- package/src/react/createAbloReact.ts +16 -125
- package/src/react/internalContext.ts +1 -27
- package/src/react/snapshot.ts +67 -0
- package/src/react/useAblo.ts +73 -209
- package/src/react/useAbloClient.ts +14 -0
- package/src/react/useMutationFailure.ts +17 -0
- package/src/react/useMutators.ts +36 -32
- package/src/react/usePresence.ts +22 -27
- package/src/react/useReactive.ts +56 -0
- package/src/react/useUndoScope.ts +24 -20
- package/src/react.ts +9 -69
- package/src/reactRuntime.ts +2 -2
- 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 -48
- package/dist/useReactive.d.ts +0 -6
- package/dist/useReactive.js +0 -41
- 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 -53
- package/src/useReactive.ts +0 -49
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
3
3
|
import type { AbloClient as Ablo } from '../client.js';
|
|
4
|
-
import type { PresenceSession } from '@abloatai/transaction/presence';
|
|
5
|
-
import type { GroupScope } from '../local/sync/scopeGroups.js';
|
|
6
|
-
import { type SyncStoreContract } from './context.js';
|
|
7
4
|
/** Reactive binding over an application-owned client. Starts readiness,
|
|
8
5
|
* forwards errors and gates bootstrap; the application owns client disposal.
|
|
9
6
|
*/
|
|
@@ -25,127 +22,66 @@ import { type SyncStoreContract } from './context.js';
|
|
|
25
22
|
* </AbloProvider>
|
|
26
23
|
* ```
|
|
27
24
|
*
|
|
28
|
-
* That's it for most apps.
|
|
25
|
+
* That's it for most apps. The `fallback`,
|
|
29
26
|
* `preventUnsavedChanges`, and `on*` props are opt-in app glue; and the
|
|
30
27
|
* block tagged "Optional DI (advanced)" below is escape-hatch wiring for
|
|
31
28
|
* tests and platform builders — if you don't recognize a prop there, you
|
|
32
29
|
* don't need it.
|
|
33
30
|
*/
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
fallback?: ReactNode | 'passthrough';
|
|
91
|
-
children: ReactNode;
|
|
31
|
+
export declare function AbloProvider<R extends SchemaRecord = SchemaRecord>(props: AbloProvider.Props<R>): React.ReactElement;
|
|
32
|
+
/** Props for wrappers around the provider, using the same schema parameter. */
|
|
33
|
+
export declare namespace AbloProvider {
|
|
34
|
+
interface Props<R extends SchemaRecord = SchemaRecord> {
|
|
35
|
+
/**
|
|
36
|
+
* A prebuilt {@link Ablo} client — **the only way to configure the engine.**
|
|
37
|
+
* Construct it yourself with `Ablo({ schema, apiKey, ... })` and pass the
|
|
38
|
+
* instance: the CLIENT owns auth, the credential lifecycle, transport, and
|
|
39
|
+
* connection; this provider is the thin REACTIVE binding over it (context,
|
|
40
|
+
* the bootstrap gate, error/session forwarding).
|
|
41
|
+
*
|
|
42
|
+
* Memoize it (build it once, e.g. with `useMemo` or module scope) — a new
|
|
43
|
+
* instance each render re-keys the bootstrap gate and tears down the socket.
|
|
44
|
+
*/
|
|
45
|
+
client: Ablo<R>;
|
|
46
|
+
/**
|
|
47
|
+
* Block tab close while there are unsynced local writes (the standard
|
|
48
|
+
* `beforeunload` prompt). Browsers ignore custom messages — don't pass one.
|
|
49
|
+
*/
|
|
50
|
+
preventUnsavedChanges?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Fired after the client has completed its terminal authentication cleanup
|
|
53
|
+
* (or surfaced a cleanup failure). Use it for app side effects such as a
|
|
54
|
+
* redirect to sign-in or clearing analytics identity.
|
|
55
|
+
*/
|
|
56
|
+
onSessionExpired?: () => void | Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Fired on any error the provider surfaces (engine/WebSocket/bootstrap). For
|
|
59
|
+
* Sentry/Datadog or application error UI.
|
|
60
|
+
*/
|
|
61
|
+
onError?: (error: Error) => void;
|
|
62
|
+
/** @internal placeholder so the old WS-URL prop shape doesn't silently leak in. */
|
|
63
|
+
url?: never;
|
|
64
|
+
/**
|
|
65
|
+
* Rendered in place of `children` during the *first* bootstrap pass —
|
|
66
|
+
* while the engine is actively transitioning from `initial` →
|
|
67
|
+
* `connected` and has never successfully connected before. Once the
|
|
68
|
+
* engine reaches `connected` the gate latches open for the lifetime
|
|
69
|
+
* of this provider instance; transient `reconnecting` / `needs-auth`
|
|
70
|
+
* states do NOT re-show the fallback (the app's own UI handles those
|
|
71
|
+
* by then).
|
|
72
|
+
*
|
|
73
|
+
* Defaults to `<DefaultFallback />` — a neutral theme-adaptive
|
|
74
|
+
* spinner that uses `currentColor`, ships with zero design-system
|
|
75
|
+
* dependencies, and self-centers in a full-parent container. Pass
|
|
76
|
+
* your own `<Skeleton />` for a branded loading UX. Pass `null` to
|
|
77
|
+
* render nothing during bootstrap. Pass the string literal
|
|
78
|
+
* `"passthrough"` to opt out of the gate entirely — children render
|
|
79
|
+
* immediately and consumers are responsible for their own gating
|
|
80
|
+
* (for example, `useAblo(ablo => ablo.status)` checks).
|
|
81
|
+
* Useful for pages that mount debug helpers, error boundaries, or
|
|
82
|
+
* analytics that must run pre-ready.
|
|
83
|
+
*/
|
|
84
|
+
fallback?: ReactNode | 'passthrough';
|
|
85
|
+
children: ReactNode;
|
|
86
|
+
}
|
|
92
87
|
}
|
|
93
|
-
export declare function AbloProvider<R extends SchemaRecord = SchemaRecord>(props: AbloProviderProps<R>): React.ReactElement;
|
|
94
|
-
export type { GroupScope };
|
|
95
|
-
/**
|
|
96
|
-
* Read-only presence: the other sessions currently visible to this
|
|
97
|
-
* connection, bridged to React. This is a pure reader of the engine's
|
|
98
|
-
* already-flowing presence stream; it does not mutate connection groups.
|
|
99
|
-
*
|
|
100
|
-
* Pass `scope` to narrow to the peers on that scope's sync group(s); omit
|
|
101
|
-
* it to get everyone on the engine's groups. Membership is driven entirely
|
|
102
|
-
* by the presence channel (set server-side on connect, independent of any
|
|
103
|
-
* cursor/collaboration traffic), so reading it never affects what the
|
|
104
|
-
* connection is subscribed to and can't deadlock against a gated channel.
|
|
105
|
-
*
|
|
106
|
-
* Use this to answer "is anyone else here?", for example to suppress
|
|
107
|
-
* live-cursor broadcasts while alone.
|
|
108
|
-
*
|
|
109
|
-
* ```ts
|
|
110
|
-
* const peers = usePeers({ reports: reportId });
|
|
111
|
-
* const alone = !peers.some((p) => p.participantKind === 'user');
|
|
112
|
-
* ```
|
|
113
|
-
*/
|
|
114
|
-
export declare function usePeers(scope?: GroupScope): readonly PresenceSession[];
|
|
115
|
-
/**
|
|
116
|
-
* Returns the raw `SyncEngine` proxy. Typically you want the typed
|
|
117
|
-
* hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
|
|
118
|
-
* where you need direct access (e.g., `sync.items.onChange(cb)`).
|
|
119
|
-
*
|
|
120
|
-
* The generic parameter narrows the return type to your schema's
|
|
121
|
-
* model record so call sites get typed `sync.items.findMany()` /
|
|
122
|
-
* `sync.sections.create(...)` without a cast at the call site:
|
|
123
|
-
*
|
|
124
|
-
* ```ts
|
|
125
|
-
* const sync = useSync<(typeof schema)['models']>();
|
|
126
|
-
* ```
|
|
127
|
-
*
|
|
128
|
-
* The runtime value is the exact engine the provider constructed;
|
|
129
|
-
* the generic just widens the compile-time type.
|
|
130
|
-
*/
|
|
131
|
-
export declare function useSync<R extends SchemaRecord = SchemaRecord>(): Ablo<R>;
|
|
132
|
-
/**
|
|
133
|
-
* Returns the underlying `SyncStoreContract` (the BaseSyncedStore).
|
|
134
|
-
* Most consumers should prefer the typed hooks (`useQuery` etc.); this
|
|
135
|
-
* is for advanced cases like direct InstanceCache access or custom
|
|
136
|
-
* reactive bridges. Throws if the provider hasn't mounted the store
|
|
137
|
-
* yet — wrap consumers in `<ClientSideSuspense>` to gate correctly.
|
|
138
|
-
*
|
|
139
|
-
* The generic parameter lets consumers widen the return type to a
|
|
140
|
-
* concrete `BaseSyncedStore<...>` subclass if they track one:
|
|
141
|
-
*
|
|
142
|
-
* ```ts
|
|
143
|
-
* type AppStore = BaseSyncedStore<AppEvents, typeof schema>;
|
|
144
|
-
* const store = useSyncStore<AppStore>(); // no cast needed at call site
|
|
145
|
-
* ```
|
|
146
|
-
*
|
|
147
|
-
* The runtime value is always the concrete store the SDK constructed,
|
|
148
|
-
* so widening the type is safe. The bounded generic (`T extends
|
|
149
|
-
* SyncStoreContract`) keeps the widening honest.
|
|
150
|
-
*/
|
|
151
|
-
export declare function useSyncStore<T extends SyncStoreContract = SyncStoreContract>(): T;
|
|
@@ -1,38 +1,42 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback,
|
|
4
|
-
import {
|
|
5
|
-
import { SyncContext } from './context.js';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState, createContext, } from 'react';
|
|
4
|
+
import { AbloStoreContext } from './context.js';
|
|
6
5
|
import { AbloInternalContext } from './internalContext.js';
|
|
7
6
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
8
|
-
import {
|
|
7
|
+
import { useAblo } from './useAblo.js';
|
|
9
8
|
import { DefaultFallback } from './DefaultFallback.js';
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/** Reactive binding over an application-owned client. Starts readiness,
|
|
10
|
+
* forwards errors and gates bootstrap; the application owns client disposal.
|
|
11
|
+
*/
|
|
12
|
+
// ── Props ────────────────────────────────────────────────────────────
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Props for `<AbloProvider>`.
|
|
15
|
+
*
|
|
16
|
+
* The one required prop is a prebuilt {@link Ablo} client — the client
|
|
17
|
+
* owns auth and the credential lifecycle; this provider is the reactive
|
|
18
|
+
* binding over it:
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* // Build once at module scope — a new instance per render tears down the socket.
|
|
22
|
+
* // The endpoint string points at your session-mint route (`ablo init`
|
|
23
|
+
* // scaffolds it); the SDK fetches it and keeps the token fresh.
|
|
24
|
+
* const ablo = Ablo({ schema, session: { endpoint: '/api/ablo-session' } });
|
|
25
|
+
*
|
|
26
|
+
* <AbloProvider client={ablo}>
|
|
27
|
+
* <App />
|
|
28
|
+
* </AbloProvider>
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* That's it for most apps. The `fallback`,
|
|
32
|
+
* `preventUnsavedChanges`, and `on*` props are opt-in app glue; and the
|
|
33
|
+
* block tagged "Optional DI (advanced)" below is escape-hatch wiring for
|
|
34
|
+
* tests and platform builders — if you don't recognize a prop there, you
|
|
35
|
+
* don't need it.
|
|
16
36
|
*/
|
|
17
|
-
|
|
18
|
-
const listeners = new Set();
|
|
19
|
-
return {
|
|
20
|
-
subscribe(fn) {
|
|
21
|
-
listeners.add(fn);
|
|
22
|
-
return () => { listeners.delete(fn); };
|
|
23
|
-
},
|
|
24
|
-
emit(err) {
|
|
25
|
-
for (const fn of listeners) {
|
|
26
|
-
try {
|
|
27
|
-
fn(err);
|
|
28
|
-
}
|
|
29
|
-
catch { }
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
37
|
+
// ── Implementation ───────────────────────────────────────────────────
|
|
34
38
|
export function AbloProvider(props) {
|
|
35
|
-
const { client,
|
|
39
|
+
const { client, preventUnsavedChanges, onSessionExpired, onError, fallback = _jsx(DefaultFallback, {}), children, } = props;
|
|
36
40
|
// The client IS the engine — synchronous, never null. This provider is a
|
|
37
41
|
// REACTIVE binding over it (context + bootstrap gate + error/session
|
|
38
42
|
// forwarding); it does NOT construct, configure, or own the connection. The
|
|
@@ -45,19 +49,16 @@ export function AbloProvider(props) {
|
|
|
45
49
|
// Account scope isn't a prop — read it from `_store.orgId` once `ready()`
|
|
46
50
|
// resolves the identity from the client's auth.
|
|
47
51
|
const [resolvedScope, setResolvedScope] = useState(null);
|
|
48
|
-
// ── Error emitter (provider-instance scoped) ─────────────────────
|
|
49
|
-
const errorEmitterRef = useRef(null);
|
|
50
|
-
if (!errorEmitterRef.current) {
|
|
51
|
-
errorEmitterRef.current = createErrorEmitter();
|
|
52
|
-
}
|
|
53
|
-
const errorEmitter = errorEmitterRef.current;
|
|
54
52
|
// Stash callbacks in refs so a new identity each render doesn't re-run the
|
|
55
53
|
// start effect (the `useEventCallback` idiom).
|
|
56
54
|
const onErrorRef = useRef(onError);
|
|
57
55
|
onErrorRef.current = onError;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
const reportError = useCallback((error) => {
|
|
57
|
+
try {
|
|
58
|
+
onErrorRef.current?.(error);
|
|
59
|
+
}
|
|
60
|
+
catch { /* Error reporting must not interrupt session cleanup. */ }
|
|
61
|
+
}, []);
|
|
61
62
|
const onSessionExpiredRef = useRef(onSessionExpired);
|
|
62
63
|
onSessionExpiredRef.current = onSessionExpired;
|
|
63
64
|
// Re-key the bootstrap gate when the client INSTANCE changes — a genuinely new
|
|
@@ -74,21 +75,21 @@ export function AbloProvider(props) {
|
|
|
74
75
|
// onSessionExpired. Credential cleanup lives in the CLIENT, so direct
|
|
75
76
|
// consumers and React consumers have the same security boundary.
|
|
76
77
|
// 2. Drive `ready()` (idempotent) so bootstrap starts on mount, then read the
|
|
77
|
-
// resolved org scope for
|
|
78
|
+
// resolved org scope for the Ablo store context.
|
|
78
79
|
// It does NOT dispose the client (consumer-owned) and does NOT touch auth.
|
|
79
80
|
useEffect(() => {
|
|
80
81
|
let stale = false;
|
|
81
82
|
const unsubscribeSession = engine.onSessionError((err) => {
|
|
82
|
-
|
|
83
|
+
reportError(err);
|
|
83
84
|
void (async () => {
|
|
84
85
|
try {
|
|
85
86
|
await onSessionExpiredRef.current?.();
|
|
86
87
|
}
|
|
87
88
|
catch (hookErr) {
|
|
88
|
-
|
|
89
|
+
reportError(hookErr);
|
|
89
90
|
}
|
|
90
91
|
})().catch(() => {
|
|
91
|
-
//
|
|
92
|
+
// This was
|
|
92
93
|
// already the error-reporting path, so swallow rather than surface
|
|
93
94
|
// an unhandled rejection loop.
|
|
94
95
|
});
|
|
@@ -106,13 +107,13 @@ export function AbloProvider(props) {
|
|
|
106
107
|
.catch((err) => {
|
|
107
108
|
if (stale)
|
|
108
109
|
return;
|
|
109
|
-
|
|
110
|
+
reportError(err);
|
|
110
111
|
});
|
|
111
112
|
return () => {
|
|
112
113
|
stale = true;
|
|
113
114
|
unsubscribeSession();
|
|
114
115
|
};
|
|
115
|
-
}, [engine,
|
|
116
|
+
}, [engine, reportError]);
|
|
116
117
|
// ── beforeunload + preventUnsavedChanges ─────────────────────────
|
|
117
118
|
useEffect(() => {
|
|
118
119
|
if (typeof window === 'undefined')
|
|
@@ -130,12 +131,12 @@ export function AbloProvider(props) {
|
|
|
130
131
|
window.addEventListener('beforeunload', handler);
|
|
131
132
|
return () => { window.removeEventListener('beforeunload', handler); };
|
|
132
133
|
}, [engine, preventUnsavedChanges]);
|
|
133
|
-
// ──
|
|
134
|
+
// ── Store context value (for Ablo data hooks) ────────────────────
|
|
134
135
|
//
|
|
135
136
|
// The engine is always present (it's the `client` prop), but its org scope is
|
|
136
|
-
// unknown until `ready()` resolves identity — so
|
|
137
|
+
// unknown until `ready()` resolves identity — so the store context is null until
|
|
137
138
|
// then, which drives the initial fallback below.
|
|
138
|
-
const
|
|
139
|
+
const storeContextValue = useMemo(() => {
|
|
139
140
|
const currentAccountScope = (resolvedScope?.engine === engine ? resolvedScope.account : null) ??
|
|
140
141
|
engine._store.orgId;
|
|
141
142
|
if (!currentAccountScope)
|
|
@@ -146,19 +147,16 @@ export function AbloProvider(props) {
|
|
|
146
147
|
schema,
|
|
147
148
|
};
|
|
148
149
|
}, [engine, resolvedScope, schema]);
|
|
149
|
-
//
|
|
150
|
+
// The React tree holds the same client used by core code.
|
|
150
151
|
const internalValue = useMemo(() => ({
|
|
151
|
-
currentUserId: userId ?? null,
|
|
152
|
-
subscribeError: errorEmitter.subscribe,
|
|
153
|
-
emitError: errorEmitter.emit,
|
|
154
152
|
engine: engine,
|
|
155
|
-
}), [
|
|
153
|
+
}), [engine]);
|
|
156
154
|
// ── Render ───────────────────────────────────────────────────────
|
|
157
155
|
//
|
|
158
156
|
// Keep the context tree stable during startup so passthrough children retain
|
|
159
157
|
// their component state when authenticated row scope becomes available.
|
|
160
158
|
const passthrough = fallback === 'passthrough';
|
|
161
|
-
return (_jsx(AbloInternalContext.Provider, { value: internalValue, children: _jsx(
|
|
159
|
+
return (_jsx(AbloInternalContext.Provider, { value: internalValue, children: _jsx(AbloStoreContext.Provider, { value: storeContextValue, children: passthrough ? (children) : storeContextValue ? (_jsx(BootstrapGate, { fallback: fallback, children: children }, engineKey)) : fallback }) }));
|
|
162
160
|
}
|
|
163
161
|
/**
|
|
164
162
|
* Internal gate that renders `fallback` only during the very first
|
|
@@ -172,118 +170,15 @@ export function AbloProvider(props) {
|
|
|
172
170
|
* a new "first bootstrap" cycle.
|
|
173
171
|
*/
|
|
174
172
|
function BootstrapGate({ fallback, children, }) {
|
|
175
|
-
const status =
|
|
173
|
+
const status = useAblo(ablo => ablo.status);
|
|
176
174
|
const [everConnected, setEverConnected] = useState(false);
|
|
177
175
|
useEffect(() => {
|
|
178
|
-
if (status
|
|
179
|
-
status
|
|
180
|
-
status
|
|
176
|
+
if (status?.name === 'connected' ||
|
|
177
|
+
status?.name === 'reconnecting' ||
|
|
178
|
+
status?.name === 'disconnected') {
|
|
181
179
|
setEverConnected(true);
|
|
182
180
|
}
|
|
183
|
-
}, [status
|
|
184
|
-
const showFallback = !everConnected && status
|
|
181
|
+
}, [status?.name]);
|
|
182
|
+
const showFallback = !everConnected && status?.name === 'connecting';
|
|
185
183
|
return _jsx(_Fragment, { children: showFallback ? fallback : children });
|
|
186
184
|
}
|
|
187
|
-
const EMPTY_PRESENCE = Object.freeze([]);
|
|
188
|
-
/**
|
|
189
|
-
* Read-only presence: the other sessions currently visible to this
|
|
190
|
-
* connection, bridged to React. This is a pure reader of the engine's
|
|
191
|
-
* already-flowing presence stream; it does not mutate connection groups.
|
|
192
|
-
*
|
|
193
|
-
* Pass `scope` to narrow to the peers on that scope's sync group(s); omit
|
|
194
|
-
* it to get everyone on the engine's groups. Membership is driven entirely
|
|
195
|
-
* by the presence channel (set server-side on connect, independent of any
|
|
196
|
-
* cursor/collaboration traffic), so reading it never affects what the
|
|
197
|
-
* connection is subscribed to and can't deadlock against a gated channel.
|
|
198
|
-
*
|
|
199
|
-
* Use this to answer "is anyone else here?", for example to suppress
|
|
200
|
-
* live-cursor broadcasts while alone.
|
|
201
|
-
*
|
|
202
|
-
* ```ts
|
|
203
|
-
* const peers = usePeers({ reports: reportId });
|
|
204
|
-
* const alone = !peers.some((p) => p.participantKind === 'user');
|
|
205
|
-
* ```
|
|
206
|
-
*/
|
|
207
|
-
export function usePeers(scope) {
|
|
208
|
-
const ctx = useContext(AbloInternalContext);
|
|
209
|
-
const engine = ctx?.engine ?? null;
|
|
210
|
-
// Resolve scope → groups through the schema.
|
|
211
|
-
// The stringified, sorted key is the stable effect dependency.
|
|
212
|
-
const scopeKey = JSON.stringify(resolveScopeGroups(scope, engine?.schema).sort());
|
|
213
|
-
const groups = useMemo(() => JSON.parse(scopeKey), [scopeKey]);
|
|
214
|
-
const [peers, setPeers] = useState(EMPTY_PRESENCE);
|
|
215
|
-
useEffect(() => {
|
|
216
|
-
if (!engine) {
|
|
217
|
-
setPeers(EMPTY_PRESENCE);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const presence = presenceOfClient(engine);
|
|
221
|
-
const compute = () => groups.length === 0
|
|
222
|
-
? presence.others
|
|
223
|
-
: presence.others.filter((session) => session.activities.some(({ target }) => target.id !== undefined && groups.includes(`${target.model.toLowerCase()}:${target.id}`)));
|
|
224
|
-
// Plain useState + onChange — presence changes on connect/disconnect/activity
|
|
225
|
-
// only (never on cursor traffic, a separate channel), so this fires
|
|
226
|
-
// rarely; a frame of stale presence is harmless.
|
|
227
|
-
setPeers(compute());
|
|
228
|
-
return presence.onChange(() => { setPeers(compute()); });
|
|
229
|
-
}, [engine, groups, scopeKey]);
|
|
230
|
-
return peers;
|
|
231
|
-
}
|
|
232
|
-
// ── Escape-hatches: raw engine/store access ──────────────────────────
|
|
233
|
-
/**
|
|
234
|
-
* Returns the raw `SyncEngine` proxy. Typically you want the typed
|
|
235
|
-
* hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
|
|
236
|
-
* where you need direct access (e.g., `sync.items.onChange(cb)`).
|
|
237
|
-
*
|
|
238
|
-
* The generic parameter narrows the return type to your schema's
|
|
239
|
-
* model record so call sites get typed `sync.items.findMany()` /
|
|
240
|
-
* `sync.sections.create(...)` without a cast at the call site:
|
|
241
|
-
*
|
|
242
|
-
* ```ts
|
|
243
|
-
* const sync = useSync<(typeof schema)['models']>();
|
|
244
|
-
* ```
|
|
245
|
-
*
|
|
246
|
-
* The runtime value is the exact engine the provider constructed;
|
|
247
|
-
* the generic just widens the compile-time type.
|
|
248
|
-
*/
|
|
249
|
-
export function useSync() {
|
|
250
|
-
const ctx = useContext(AbloInternalContext);
|
|
251
|
-
if (!ctx) {
|
|
252
|
-
throw new AbloValidationError('useSync: no <AbloProvider> mounted above this component.', { code: 'no_ablo_provider' });
|
|
253
|
-
}
|
|
254
|
-
if (!ctx.engine) {
|
|
255
|
-
throw new AbloValidationError('useSync: the sync engine has not yet initialized. Wrap your ' +
|
|
256
|
-
'consumer in <ClientSideSuspense> or guard on useSyncStatus().', { code: 'sync_not_ready' });
|
|
257
|
-
}
|
|
258
|
-
return rebindProviderEngine(ctx.engine);
|
|
259
|
-
}
|
|
260
|
-
function rebindProviderEngine(engine) {
|
|
261
|
-
return engine;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Returns the underlying `SyncStoreContract` (the BaseSyncedStore).
|
|
265
|
-
* Most consumers should prefer the typed hooks (`useQuery` etc.); this
|
|
266
|
-
* is for advanced cases like direct InstanceCache access or custom
|
|
267
|
-
* reactive bridges. Throws if the provider hasn't mounted the store
|
|
268
|
-
* yet — wrap consumers in `<ClientSideSuspense>` to gate correctly.
|
|
269
|
-
*
|
|
270
|
-
* The generic parameter lets consumers widen the return type to a
|
|
271
|
-
* concrete `BaseSyncedStore<...>` subclass if they track one:
|
|
272
|
-
*
|
|
273
|
-
* ```ts
|
|
274
|
-
* type AppStore = BaseSyncedStore<AppEvents, typeof schema>;
|
|
275
|
-
* const store = useSyncStore<AppStore>(); // no cast needed at call site
|
|
276
|
-
* ```
|
|
277
|
-
*
|
|
278
|
-
* The runtime value is always the concrete store the SDK constructed,
|
|
279
|
-
* so widening the type is safe. The bounded generic (`T extends
|
|
280
|
-
* SyncStoreContract`) keeps the widening honest.
|
|
281
|
-
*/
|
|
282
|
-
export function useSyncStore() {
|
|
283
|
-
const sync = useContext(SyncContext);
|
|
284
|
-
if (!sync?.store) {
|
|
285
|
-
throw new AbloValidationError('useSyncStore: the sync engine has not yet initialized. Wrap ' +
|
|
286
|
-
'consumers in <ClientSideSuspense> or guard on useSyncStatus().', { code: 'sync_not_ready' });
|
|
287
|
-
}
|
|
288
|
-
return sync.store;
|
|
289
|
-
}
|
package/dist/react/context.d.ts
CHANGED
|
@@ -1,55 +1,16 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
1
|
import type { Schema } from '@abloatai/transaction/schema/schema';
|
|
3
2
|
import type { SyncStoreContract } from '../local/storeContract.js';
|
|
4
3
|
export type { SyncStoreContract, LocalMutation, } from '../local/storeContract.js';
|
|
5
|
-
export interface
|
|
4
|
+
export interface AbloStoreContextValue {
|
|
6
5
|
store: SyncStoreContract;
|
|
7
6
|
/** The organization id used as the default scope for reads and writes. */
|
|
8
7
|
organizationId: string;
|
|
9
|
-
/**
|
|
10
|
-
* An optional schema. When provided, hooks that take a model by name (such as
|
|
11
|
-
* `useQuery('items')`) read that model's metadata from this schema, so
|
|
12
|
-
* callers don't pass a schema at every call site. When omitted, those hooks
|
|
13
|
-
* require the schema as an argument instead.
|
|
14
|
-
*
|
|
15
|
-
* The field is loosely typed here because a single runtime context value is
|
|
16
|
-
* shared by every hook. Precise per-model types come from your `Register`
|
|
17
|
-
* module augmentation
|
|
18
|
-
* (`declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`),
|
|
19
|
-
* not from this reference.
|
|
20
|
-
*/
|
|
8
|
+
/** Runtime schema used by ambient mutator overloads. */
|
|
21
9
|
schema?: Schema;
|
|
22
10
|
}
|
|
23
|
-
export declare const
|
|
11
|
+
export declare const AbloStoreContext: import("react").Context<AbloStoreContextValue | null>;
|
|
24
12
|
/**
|
|
25
|
-
* Reads the
|
|
26
|
-
*
|
|
27
|
-
* context by rendering the internal {@link SyncProvider}; you wire
|
|
28
|
-
* `<AbloProvider client={ablo}>` rather than touching this directly.
|
|
13
|
+
* Reads the store scope owned by `<AbloProvider>`, throwing a clear error when
|
|
14
|
+
* no provider is mounted above it.
|
|
29
15
|
*/
|
|
30
|
-
export declare function
|
|
31
|
-
/**
|
|
32
|
-
* Props for SyncProvider.
|
|
33
|
-
*/
|
|
34
|
-
export interface SyncProviderProps {
|
|
35
|
-
/** The sync store, which must implement {@link SyncStoreContract}. */
|
|
36
|
-
store: SyncStoreContract;
|
|
37
|
-
/** The organization id used as the default scope for reads and writes. */
|
|
38
|
-
organizationId: string;
|
|
39
|
-
/**
|
|
40
|
-
* An optional schema. Provide it to enable hooks that take a model by name
|
|
41
|
-
* (such as `useQuery('items')`); the model types also narrow through your
|
|
42
|
-
* `Register` augmentation. Omit it to pass the schema to those hooks directly
|
|
43
|
-
* instead.
|
|
44
|
-
*/
|
|
45
|
-
schema?: Schema;
|
|
46
|
-
children?: ReactNode;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* A low-level provider that places a built sync store on React context so the
|
|
50
|
-
* data hooks can reach it. This is an internal building block: it is not part
|
|
51
|
-
* of the package's public entry point. Reach for `<AbloProvider>` instead,
|
|
52
|
-
* which builds the store from your `Ablo({ schema, apiKey })` client and
|
|
53
|
-
* renders this provider underneath.
|
|
54
|
-
*/
|
|
55
|
-
export declare function SyncProvider({ store, organizationId, schema, children, }: SyncProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SyncReactContext | null>>;
|
|
16
|
+
export declare function useAbloStoreContext(): AbloStoreContextValue;
|
package/dist/react/context.js
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { createContext,
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
3
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
4
|
-
export const
|
|
4
|
+
export const AbloStoreContext = createContext(null);
|
|
5
5
|
/**
|
|
6
|
-
* Reads the
|
|
7
|
-
*
|
|
8
|
-
* context by rendering the internal {@link SyncProvider}; you wire
|
|
9
|
-
* `<AbloProvider client={ablo}>` rather than touching this directly.
|
|
6
|
+
* Reads the store scope owned by `<AbloProvider>`, throwing a clear error when
|
|
7
|
+
* no provider is mounted above it.
|
|
10
8
|
*/
|
|
11
|
-
export function
|
|
12
|
-
const ctx = useContext(
|
|
9
|
+
export function useAbloStoreContext() {
|
|
10
|
+
const ctx = useContext(AbloStoreContext);
|
|
13
11
|
if (!ctx) {
|
|
14
|
-
throw new AbloValidationError('
|
|
15
|
-
code: '
|
|
12
|
+
throw new AbloValidationError('Ablo hooks must be used within an <AbloProvider>.', {
|
|
13
|
+
code: 'ablo_context_missing_provider',
|
|
16
14
|
});
|
|
17
15
|
}
|
|
18
16
|
return ctx;
|
|
19
17
|
}
|
|
20
|
-
/**
|
|
21
|
-
* A low-level provider that places a built sync store on React context so the
|
|
22
|
-
* data hooks can reach it. This is an internal building block: it is not part
|
|
23
|
-
* of the package's public entry point. Reach for `<AbloProvider>` instead,
|
|
24
|
-
* which builds the store from your `Ablo({ schema, apiKey })` client and
|
|
25
|
-
* renders this provider underneath.
|
|
26
|
-
*/
|
|
27
|
-
export function SyncProvider({ store, organizationId, schema, children, }) {
|
|
28
|
-
return createElement(SyncContext.Provider, { value: { store, organizationId, schema } }, children);
|
|
29
|
-
}
|