@abloatai/humans 0.63.0 → 0.63.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/react/AbloProvider.d.ts +2 -18
- package/dist/react/AbloProvider.js +11 -27
- package/dist/react/useAblo.d.ts +1 -1
- package/dist/react/useAblo.js +3 -6
- package/dist/react/useSyncStatus.js +14 -3
- package/dist/useReactive.js +1 -3
- package/package.json +2 -2
- package/src/react/AbloProvider.tsx +14 -54
- package/src/react/useAblo.ts +4 -7
- package/src/react/useSyncStatus.ts +14 -3
- package/src/useReactive.ts +1 -3
|
@@ -4,24 +4,8 @@ import type { AbloClient as Ablo } from '../client.js';
|
|
|
4
4
|
import type { PresenceSession } from '@abloatai/transaction/presence';
|
|
5
5
|
import type { GroupScope } from '../local/sync/scopeGroups.js';
|
|
6
6
|
import { type SyncStoreContract } from './context.js';
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
9
|
-
* the full lifecycle (Strict-Mode-safe singleton, `beforeunload`,
|
|
10
|
-
* session-expiry handling, post-bootstrap hooks).
|
|
11
|
-
*
|
|
12
|
-
* Design goals:
|
|
13
|
-
*
|
|
14
|
-
* - **One component, one import.** Consumers write the provider
|
|
15
|
-
* once at the root; nothing else needs to plumb the engine.
|
|
16
|
-
* - **Multiplayer is default.** React consumers share the client's scoped
|
|
17
|
-
* groups, presence stream, and model surface without another join step.
|
|
18
|
-
* - **Declarative props for app glue.** `preventUnsavedChanges`,
|
|
19
|
-
* `onSessionExpired`, `postBootstrap`, `resolveUsers` — each
|
|
20
|
-
* absorbs a class of integration code that previously lived in
|
|
21
|
-
* userland.
|
|
22
|
-
* - **Singleton safety.** The engine lives in a ref and rotates
|
|
23
|
-
* only when `userId` / account scope / `url` change. React
|
|
24
|
-
* Strict Mode double-mount does not leak a second WebSocket.
|
|
7
|
+
/** Reactive binding over an application-owned client. Starts readiness,
|
|
8
|
+
* forwards errors and gates bootstrap; the application owns client disposal.
|
|
25
9
|
*/
|
|
26
10
|
/**
|
|
27
11
|
* Props for `<AbloProvider>`.
|
|
@@ -44,7 +44,7 @@ export function AbloProvider(props) {
|
|
|
44
44
|
const schema = engine.schema;
|
|
45
45
|
// Account scope isn't a prop — read it from `_store.orgId` once `ready()`
|
|
46
46
|
// resolves the identity from the client's auth.
|
|
47
|
-
const [
|
|
47
|
+
const [resolvedScope, setResolvedScope] = useState(null);
|
|
48
48
|
// ── Error emitter (provider-instance scoped) ─────────────────────
|
|
49
49
|
const errorEmitterRef = useRef(null);
|
|
50
50
|
if (!errorEmitterRef.current) {
|
|
@@ -98,7 +98,10 @@ export function AbloProvider(props) {
|
|
|
98
98
|
.then(() => {
|
|
99
99
|
if (stale)
|
|
100
100
|
return;
|
|
101
|
-
|
|
101
|
+
setResolvedScope({
|
|
102
|
+
engine,
|
|
103
|
+
account: engine._store.orgId ?? null,
|
|
104
|
+
});
|
|
102
105
|
})
|
|
103
106
|
.catch((err) => {
|
|
104
107
|
if (stale)
|
|
@@ -133,7 +136,7 @@ export function AbloProvider(props) {
|
|
|
133
136
|
// unknown until `ready()` resolves identity — so `syncValue` is null until
|
|
134
137
|
// then, which drives the initial fallback below.
|
|
135
138
|
const syncValue = useMemo(() => {
|
|
136
|
-
const currentAccountScope =
|
|
139
|
+
const currentAccountScope = (resolvedScope?.engine === engine ? resolvedScope.account : null) ??
|
|
137
140
|
engine._store.orgId;
|
|
138
141
|
if (!currentAccountScope)
|
|
139
142
|
return null;
|
|
@@ -142,7 +145,7 @@ export function AbloProvider(props) {
|
|
|
142
145
|
organizationId: currentAccountScope,
|
|
143
146
|
schema,
|
|
144
147
|
};
|
|
145
|
-
}, [engine,
|
|
148
|
+
}, [engine, resolvedScope, schema]);
|
|
146
149
|
// ── Internal context (currentUserId + error subscription) ────────
|
|
147
150
|
const internalValue = useMemo(() => ({
|
|
148
151
|
currentUserId: userId ?? null,
|
|
@@ -152,28 +155,10 @@ export function AbloProvider(props) {
|
|
|
152
155
|
}), [userId, errorEmitter, engine]);
|
|
153
156
|
// ── Render ───────────────────────────────────────────────────────
|
|
154
157
|
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// 1. Engine is null on first render (constructed in the effect
|
|
158
|
-
// above, not in render). We render `fallback` directly — there
|
|
159
|
-
// is no SyncContext to read status from, and by definition the
|
|
160
|
-
// engine hasn't started bootstrapping.
|
|
161
|
-
// 2. Engine exists. Mount SyncContext. `BootstrapGate` then reads
|
|
162
|
-
// `useSyncStatus()` and shows `fallback` only during the very
|
|
163
|
-
// first `connecting` transition; children render on every
|
|
164
|
-
// subsequent state change, including reconnects and auth
|
|
165
|
-
// failures (the app's own UI handles those).
|
|
166
|
-
//
|
|
167
|
-
// `fallback === 'passthrough'` short-circuits both branches — children
|
|
168
|
-
// render immediately without any gate, restoring pre-gate behavior
|
|
169
|
-
// for consumers who need debug helpers / error boundaries / analytics
|
|
170
|
-
// to mount before the engine is ready.
|
|
158
|
+
// Keep the context tree stable during startup so passthrough children retain
|
|
159
|
+
// their component state when authenticated row scope becomes available.
|
|
171
160
|
const passthrough = fallback === 'passthrough';
|
|
172
|
-
|
|
173
|
-
if (!syncValue) {
|
|
174
|
-
return (_jsx(AbloInternalContext.Provider, { value: internalValue, children: initialFallback }));
|
|
175
|
-
}
|
|
176
|
-
return (_jsx(AbloInternalContext.Provider, { value: internalValue, children: _jsx(SyncContext.Provider, { value: syncValue, children: passthrough ? (children) : (_jsx(BootstrapGate, { fallback: fallback, children: children }, engineKey)) }) }));
|
|
161
|
+
return (_jsx(AbloInternalContext.Provider, { value: internalValue, children: _jsx(SyncContext.Provider, { value: syncValue, children: passthrough ? (children) : syncValue ? (_jsx(BootstrapGate, { fallback: fallback, children: children }, engineKey)) : fallback }) }));
|
|
177
162
|
}
|
|
178
163
|
/**
|
|
179
164
|
* Internal gate that renders `fallback` only during the very first
|
|
@@ -183,8 +168,7 @@ export function AbloProvider(props) {
|
|
|
183
168
|
* re-show the fallback, because by then the app has already rendered
|
|
184
169
|
* once and its own reconnect UI should take over.
|
|
185
170
|
*
|
|
186
|
-
* Re-keyed
|
|
187
|
-
* (userId/org/url change) reset the latch — a new engine genuinely IS
|
|
171
|
+
* Re-keyed when the client instance changes so account rotations reset the latch — a new engine genuinely IS
|
|
188
172
|
* a new "first bootstrap" cycle.
|
|
189
173
|
*/
|
|
190
174
|
function BootstrapGate({ fallback, children, }) {
|
package/dist/react/useAblo.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export type UseAbloHydratedModelResult<T> = Omit<UseAbloModelResult<T>, 'data'>
|
|
|
60
60
|
* // are typed as snapshot rows — data fields + computeds, no relation
|
|
61
61
|
* // accessors — matching what the hook actually returns:
|
|
62
62
|
* const doc = useAblo((ablo) => ablo.records.local.get(id)) ?? serverDoc;
|
|
63
|
-
* const
|
|
63
|
+
* const { claimed } = useAblo((ablo) => ablo.records, id);
|
|
64
64
|
*
|
|
65
65
|
* // Without the augmentation, pass the schema as a type argument:
|
|
66
66
|
* const ablo = useAblo<(typeof schema)['models']>();
|
package/dist/react/useAblo.js
CHANGED
|
@@ -82,12 +82,9 @@ export function useAbloImpl(boundClient, modelOrSelect, id, options) {
|
|
|
82
82
|
: modelOrSelect;
|
|
83
83
|
// Claims arrive through an event emitter (engine.claims), not through MobX, so
|
|
84
84
|
// the useReactive reactions below cannot track them; we bridge changes with a
|
|
85
|
-
// setState bump instead.
|
|
86
|
-
// claims
|
|
87
|
-
//
|
|
88
|
-
// re-render and recompute it on every claim or presence change anywhere — a
|
|
89
|
-
// real storm during AI editing or live collaboration — for a value that cannot
|
|
90
|
-
// change.
|
|
85
|
+
// setState bump instead. Subscribe the model-row form (`id !== undefined`)
|
|
86
|
+
// to claims. Selector-only reads track MobX model data; callers displaying
|
|
87
|
+
// ownership use the row form's `claims` / `claimed` result.
|
|
91
88
|
const [claimVersion, setClaimVersion] = useState(0);
|
|
92
89
|
useEffect(() => {
|
|
93
90
|
if (!engine || id === undefined)
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useCallback } from 'react';
|
|
3
|
-
import {
|
|
2
|
+
import { useCallback, useContext } from 'react';
|
|
3
|
+
import { SyncContext, } from './context.js';
|
|
4
|
+
import { AbloInternalContext } from './internalContext.js';
|
|
5
|
+
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
4
6
|
import { useReactive } from '../useReactive.js';
|
|
5
7
|
/** Reactively exposes the local store's connection and confirmation status. */
|
|
6
8
|
export function useSyncStatus() {
|
|
7
|
-
const
|
|
9
|
+
const provider = useContext(AbloInternalContext);
|
|
10
|
+
const sync = useContext(SyncContext);
|
|
11
|
+
// Status does not require authenticated row scope. The client exists before
|
|
12
|
+
// ready() resolves, including inside passthrough children and custom fallbacks.
|
|
13
|
+
const store = provider?.engine?._store ?? sync?.store;
|
|
14
|
+
if (!store) {
|
|
15
|
+
throw new AbloValidationError('Sync hooks must be used within an <AbloProvider>.', {
|
|
16
|
+
code: 'sync_context_missing_provider',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
8
19
|
const compute = useCallback(() => deriveStatus(store), [store]);
|
|
9
20
|
return useReactive(compute, sameSnapshot);
|
|
10
21
|
}
|
package/dist/useReactive.js
CHANGED
|
@@ -11,7 +11,6 @@ export function useReactive(compute, equals = defaultEquals) {
|
|
|
11
11
|
const computeRef = useRef(compute);
|
|
12
12
|
const equalsRef = useRef(equals);
|
|
13
13
|
const snapshotRef = useRef(null);
|
|
14
|
-
const versionRef = useRef(0);
|
|
15
14
|
equalsRef.current = equals;
|
|
16
15
|
if (snapshotRef.current === null) {
|
|
17
16
|
snapshotRef.current = { value: compute() };
|
|
@@ -20,7 +19,6 @@ export function useReactive(compute, equals = defaultEquals) {
|
|
|
20
19
|
const next = compute();
|
|
21
20
|
if (!equals(snapshotRef.current.value, next)) {
|
|
22
21
|
snapshotRef.current = { value: next };
|
|
23
|
-
versionRef.current++;
|
|
24
22
|
}
|
|
25
23
|
}
|
|
26
24
|
computeRef.current = compute;
|
|
@@ -30,7 +28,7 @@ export function useReactive(compute, equals = defaultEquals) {
|
|
|
30
28
|
snapshotRef.current = { value: next };
|
|
31
29
|
onChange();
|
|
32
30
|
}
|
|
33
|
-
}), [
|
|
31
|
+
}), [compute]);
|
|
34
32
|
const getSnapshot = useCallback(() => snapshotRef.current.value, []);
|
|
35
33
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
36
34
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/humans",
|
|
3
|
-
"version": "0.63.
|
|
3
|
+
"version": "0.63.1",
|
|
4
4
|
"description": "The optional human-facing local-state package for Ablo: presence, live queries, and React bindings.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"directory": "packages/humans"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@abloatai/transaction": "^0.63.
|
|
87
|
+
"@abloatai/transaction": "^0.63.1",
|
|
88
88
|
"mobx": "^6.13.7",
|
|
89
89
|
"uuid": "^11.1.0",
|
|
90
90
|
"zod": "^4.4.3"
|
|
@@ -22,24 +22,8 @@ import { useSyncStatus } from './useSyncStatus.js';
|
|
|
22
22
|
import { DefaultFallback } from './DefaultFallback.js';
|
|
23
23
|
import { presenceOfClient } from '../presence/index.js';
|
|
24
24
|
|
|
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.
|
|
25
|
+
/** Reactive binding over an application-owned client. Starts readiness,
|
|
26
|
+
* forwards errors and gates bootstrap; the application owns client disposal.
|
|
43
27
|
*/
|
|
44
28
|
|
|
45
29
|
// ── Props ────────────────────────────────────────────────────────────
|
|
@@ -182,7 +166,7 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
182
166
|
|
|
183
167
|
// Account scope isn't a prop — read it from `_store.orgId` once `ready()`
|
|
184
168
|
// resolves the identity from the client's auth.
|
|
185
|
-
const [
|
|
169
|
+
const [resolvedScope, setResolvedScope] = useState<{ engine: typeof engine; account: string | null } | null>(null);
|
|
186
170
|
|
|
187
171
|
// ── Error emitter (provider-instance scoped) ─────────────────────
|
|
188
172
|
const errorEmitterRef = useRef<ReturnType<typeof createErrorEmitter> | null>(null);
|
|
@@ -240,9 +224,10 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
240
224
|
.ready()
|
|
241
225
|
.then(() => {
|
|
242
226
|
if (stale) return;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
227
|
+
setResolvedScope({
|
|
228
|
+
engine,
|
|
229
|
+
account: (engine._store as SyncStoreContract & { orgId?: string }).orgId ?? null,
|
|
230
|
+
});
|
|
246
231
|
})
|
|
247
232
|
.catch((err) => {
|
|
248
233
|
if (stale) return;
|
|
@@ -280,7 +265,7 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
280
265
|
// then, which drives the initial fallback below.
|
|
281
266
|
const syncValue = useMemo(() => {
|
|
282
267
|
const currentAccountScope =
|
|
283
|
-
|
|
268
|
+
(resolvedScope?.engine === engine ? resolvedScope.account : null) ??
|
|
284
269
|
(engine._store as SyncStoreContract & { orgId?: string }).orgId;
|
|
285
270
|
if (!currentAccountScope) return null;
|
|
286
271
|
return {
|
|
@@ -288,7 +273,7 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
288
273
|
organizationId: currentAccountScope,
|
|
289
274
|
schema,
|
|
290
275
|
};
|
|
291
|
-
}, [engine,
|
|
276
|
+
}, [engine, resolvedScope, schema]);
|
|
292
277
|
|
|
293
278
|
// ── Internal context (currentUserId + error subscription) ────────
|
|
294
279
|
|
|
@@ -301,44 +286,20 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
301
286
|
|
|
302
287
|
// ── Render ───────────────────────────────────────────────────────
|
|
303
288
|
//
|
|
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
|
-
|
|
289
|
+
// Keep the context tree stable during startup so passthrough children retain
|
|
290
|
+
// their component state when authenticated row scope becomes available.
|
|
321
291
|
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
292
|
|
|
332
293
|
return (
|
|
333
294
|
<AbloInternalContext.Provider value={internalValue}>
|
|
334
295
|
<SyncContext.Provider value={syncValue}>
|
|
335
296
|
{passthrough ? (
|
|
336
297
|
children
|
|
337
|
-
) : (
|
|
298
|
+
) : syncValue ? (
|
|
338
299
|
<BootstrapGate key={engineKey} fallback={fallback}>
|
|
339
300
|
{children}
|
|
340
301
|
</BootstrapGate>
|
|
341
|
-
)}
|
|
302
|
+
) : fallback}
|
|
342
303
|
</SyncContext.Provider>
|
|
343
304
|
</AbloInternalContext.Provider>
|
|
344
305
|
);
|
|
@@ -352,8 +313,7 @@ export function AbloProvider<R extends SchemaRecord = SchemaRecord>(
|
|
|
352
313
|
* re-show the fallback, because by then the app has already rendered
|
|
353
314
|
* once and its own reconnect UI should take over.
|
|
354
315
|
*
|
|
355
|
-
* Re-keyed
|
|
356
|
-
* (userId/org/url change) reset the latch — a new engine genuinely IS
|
|
316
|
+
* Re-keyed when the client instance changes so account rotations reset the latch — a new engine genuinely IS
|
|
357
317
|
* a new "first bootstrap" cycle.
|
|
358
318
|
*/
|
|
359
319
|
function BootstrapGate({
|
package/src/react/useAblo.ts
CHANGED
|
@@ -144,7 +144,7 @@ function snapshotValue<T>(value: T): T {
|
|
|
144
144
|
* // are typed as snapshot rows — data fields + computeds, no relation
|
|
145
145
|
* // accessors — matching what the hook actually returns:
|
|
146
146
|
* const doc = useAblo((ablo) => ablo.records.local.get(id)) ?? serverDoc;
|
|
147
|
-
* const
|
|
147
|
+
* const { claimed } = useAblo((ablo) => ablo.records, id);
|
|
148
148
|
*
|
|
149
149
|
* // Without the augmentation, pass the schema as a type argument:
|
|
150
150
|
* const ablo = useAblo<(typeof schema)['models']>();
|
|
@@ -237,12 +237,9 @@ export function useAbloImpl<
|
|
|
237
237
|
|
|
238
238
|
// Claims arrive through an event emitter (engine.claims), not through MobX, so
|
|
239
239
|
// the useReactive reactions below cannot track them; we bridge changes with a
|
|
240
|
-
// setState bump instead.
|
|
241
|
-
// claims
|
|
242
|
-
//
|
|
243
|
-
// re-render and recompute it on every claim or presence change anywhere — a
|
|
244
|
-
// real storm during AI editing or live collaboration — for a value that cannot
|
|
245
|
-
// change.
|
|
240
|
+
// setState bump instead. Subscribe the model-row form (`id !== undefined`)
|
|
241
|
+
// to claims. Selector-only reads track MobX model data; callers displaying
|
|
242
|
+
// ownership use the row form's `claims` / `claimed` result.
|
|
246
243
|
const [claimVersion, setClaimVersion] = useState(0);
|
|
247
244
|
useEffect(() => {
|
|
248
245
|
if (!engine || id === undefined) return;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback } from 'react';
|
|
3
|
+
import { useCallback, useContext } from 'react';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
SyncContext,
|
|
6
6
|
type SyncStoreContract,
|
|
7
7
|
} from './context.js';
|
|
8
|
+
import { AbloInternalContext } from './internalContext.js';
|
|
9
|
+
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
8
10
|
import { useReactive } from '../useReactive.js';
|
|
9
11
|
|
|
10
12
|
export type SyncStatusSnapshot =
|
|
@@ -17,7 +19,16 @@ export type SyncStatusSnapshot =
|
|
|
17
19
|
|
|
18
20
|
/** Reactively exposes the local store's connection and confirmation status. */
|
|
19
21
|
export function useSyncStatus(): SyncStatusSnapshot {
|
|
20
|
-
const
|
|
22
|
+
const provider = useContext(AbloInternalContext);
|
|
23
|
+
const sync = useContext(SyncContext);
|
|
24
|
+
// Status does not require authenticated row scope. The client exists before
|
|
25
|
+
// ready() resolves, including inside passthrough children and custom fallbacks.
|
|
26
|
+
const store = provider?.engine?._store ?? sync?.store;
|
|
27
|
+
if (!store) {
|
|
28
|
+
throw new AbloValidationError('Sync hooks must be used within an <AbloProvider>.', {
|
|
29
|
+
code: 'sync_context_missing_provider',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
21
32
|
const compute = useCallback(() => deriveStatus(store), [store]);
|
|
22
33
|
return useReactive(compute, sameSnapshot);
|
|
23
34
|
}
|
package/src/useReactive.ts
CHANGED
|
@@ -16,7 +16,6 @@ export function useReactive<T>(
|
|
|
16
16
|
const computeRef = useRef(compute);
|
|
17
17
|
const equalsRef = useRef(equals);
|
|
18
18
|
const snapshotRef = useRef<{ value: T } | null>(null);
|
|
19
|
-
const versionRef = useRef(0);
|
|
20
19
|
|
|
21
20
|
equalsRef.current = equals;
|
|
22
21
|
if (snapshotRef.current === null) {
|
|
@@ -25,7 +24,6 @@ export function useReactive<T>(
|
|
|
25
24
|
const next = compute();
|
|
26
25
|
if (!equals(snapshotRef.current.value, next)) {
|
|
27
26
|
snapshotRef.current = { value: next };
|
|
28
|
-
versionRef.current++;
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
29
|
computeRef.current = compute;
|
|
@@ -39,7 +37,7 @@ export function useReactive<T>(
|
|
|
39
37
|
onChange();
|
|
40
38
|
}
|
|
41
39
|
},
|
|
42
|
-
), [
|
|
40
|
+
), [compute]);
|
|
43
41
|
const getSnapshot = useCallback(() => snapshotRef.current!.value, []);
|
|
44
42
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
45
43
|
}
|