@syncular/react 0.4.0 → 0.4.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/use-window.js +34 -13
- package/package.json +4 -4
- package/src/use-window.ts +27 -5
package/dist/use-window.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { windowComplete, } from '@syncular/client';
|
|
23
23
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
24
|
+
import { FrameScheduler } from './query-churn.js';
|
|
24
25
|
import { useSyncClient } from './use-client.js';
|
|
25
26
|
const EMPTY = { units: [], pending: [] };
|
|
26
27
|
export function useWindow(base) {
|
|
@@ -38,25 +39,45 @@ export function useWindow(base) {
|
|
|
38
39
|
// biome-ignore lint/correctness/useExhaustiveDependencies: baseKey re-keys the effect for a fresh base object
|
|
39
40
|
useEffect(() => {
|
|
40
41
|
let cancelled = false;
|
|
41
|
-
const read = () =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
51
|
-
read();
|
|
42
|
+
const read = () => Promise.resolve(client.windowState(baseRef.current))
|
|
43
|
+
.then((next) => {
|
|
44
|
+
if (!cancelled)
|
|
45
|
+
setState(next);
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {
|
|
48
|
+
/* transient — the next invalidation re-reads */
|
|
49
|
+
});
|
|
50
|
+
void read();
|
|
52
51
|
// Re-read when the base's table changes locally: a deferred eviction
|
|
53
|
-
// (E1) completing
|
|
52
|
+
// (E1) completing, a re-entry bootstrapping, or a bootstrap completing
|
|
53
|
+
// all invalidate it. The re-read is frame-coalesced like the query
|
|
54
|
+
// hooks' re-runs, then deferred ONE MORE boundary before issuing: every
|
|
55
|
+
// query re-run for the same event has issued its read by then, so on
|
|
56
|
+
// the in-order client channel the pendency verdict resolves AFTER the
|
|
57
|
+
// rows it vouches for. Bootstrap-completion commit order is therefore
|
|
58
|
+
// rows first, pending→complete second — a consumer gating "empty" on
|
|
59
|
+
// `isComplete` never paints a false empty over a stale result (§4.8
|
|
60
|
+
// honesty at the render boundary, not just in the oracle). Both
|
|
61
|
+
// boundaries run INSIDE the scheduler (a two-phase callback re-arming
|
|
62
|
+
// itself once) so a fire parked in a suspended rAF stays covered by the
|
|
63
|
+
// hidden-document rescue — a bare second rAF would not be.
|
|
64
|
+
let armed = false;
|
|
65
|
+
const scheduler = new FrameScheduler(() => {
|
|
66
|
+
if (!armed) {
|
|
67
|
+
armed = true;
|
|
68
|
+
scheduler.schedule(); // boundary two via the dirty/re-run contract
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
armed = false;
|
|
72
|
+
return read();
|
|
73
|
+
});
|
|
54
74
|
const unsubscribe = client.onInvalidate((event) => {
|
|
55
75
|
if (event.tables.has(baseRef.current.table))
|
|
56
|
-
|
|
76
|
+
scheduler.schedule();
|
|
57
77
|
});
|
|
58
78
|
return () => {
|
|
59
79
|
cancelled = true;
|
|
80
|
+
scheduler.dispose();
|
|
60
81
|
unsubscribe();
|
|
61
82
|
};
|
|
62
83
|
}, [client, baseKey]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/react",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "React hooks for Syncular offline-first sync",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"test": "bun test --preload ./test/setup.ts"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@syncular/client": "0.
|
|
51
|
+
"@syncular/client": "0.4.1"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"react": ">=18.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@happy-dom/global-registrator": "^15.11.0",
|
|
58
|
-
"@syncular/core": "0.
|
|
59
|
-
"@syncular/server": "0.
|
|
58
|
+
"@syncular/core": "0.4.1",
|
|
59
|
+
"@syncular/server": "0.4.1",
|
|
60
60
|
"@testing-library/react": "^16.1.0",
|
|
61
61
|
"@types/react": "^18.3.0",
|
|
62
62
|
"react": "^18.3.1",
|
package/src/use-window.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
windowComplete,
|
|
26
26
|
} from '@syncular/client';
|
|
27
27
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
28
|
+
import { FrameScheduler } from './query-churn';
|
|
28
29
|
import { useSyncClient } from './use-client';
|
|
29
30
|
|
|
30
31
|
export interface UseWindowResult {
|
|
@@ -59,7 +60,7 @@ export function useWindow(base: WindowBase): UseWindowResult {
|
|
|
59
60
|
// biome-ignore lint/correctness/useExhaustiveDependencies: baseKey re-keys the effect for a fresh base object
|
|
60
61
|
useEffect(() => {
|
|
61
62
|
let cancelled = false;
|
|
62
|
-
const read = () =>
|
|
63
|
+
const read = (): Promise<void> =>
|
|
63
64
|
Promise.resolve(client.windowState(baseRef.current))
|
|
64
65
|
.then((next) => {
|
|
65
66
|
if (!cancelled) setState(next);
|
|
@@ -67,15 +68,36 @@ export function useWindow(base: WindowBase): UseWindowResult {
|
|
|
67
68
|
.catch(() => {
|
|
68
69
|
/* transient — the next invalidation re-reads */
|
|
69
70
|
});
|
|
70
|
-
|
|
71
|
-
read();
|
|
71
|
+
void read();
|
|
72
72
|
// Re-read when the base's table changes locally: a deferred eviction
|
|
73
|
-
// (E1) completing
|
|
73
|
+
// (E1) completing, a re-entry bootstrapping, or a bootstrap completing
|
|
74
|
+
// all invalidate it. The re-read is frame-coalesced like the query
|
|
75
|
+
// hooks' re-runs, then deferred ONE MORE boundary before issuing: every
|
|
76
|
+
// query re-run for the same event has issued its read by then, so on
|
|
77
|
+
// the in-order client channel the pendency verdict resolves AFTER the
|
|
78
|
+
// rows it vouches for. Bootstrap-completion commit order is therefore
|
|
79
|
+
// rows first, pending→complete second — a consumer gating "empty" on
|
|
80
|
+
// `isComplete` never paints a false empty over a stale result (§4.8
|
|
81
|
+
// honesty at the render boundary, not just in the oracle). Both
|
|
82
|
+
// boundaries run INSIDE the scheduler (a two-phase callback re-arming
|
|
83
|
+
// itself once) so a fire parked in a suspended rAF stays covered by the
|
|
84
|
+
// hidden-document rescue — a bare second rAF would not be.
|
|
85
|
+
let armed = false;
|
|
86
|
+
const scheduler = new FrameScheduler(() => {
|
|
87
|
+
if (!armed) {
|
|
88
|
+
armed = true;
|
|
89
|
+
scheduler.schedule(); // boundary two via the dirty/re-run contract
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
armed = false;
|
|
93
|
+
return read();
|
|
94
|
+
});
|
|
74
95
|
const unsubscribe = client.onInvalidate((event) => {
|
|
75
|
-
if (event.tables.has(baseRef.current.table))
|
|
96
|
+
if (event.tables.has(baseRef.current.table)) scheduler.schedule();
|
|
76
97
|
});
|
|
77
98
|
return () => {
|
|
78
99
|
cancelled = true;
|
|
100
|
+
scheduler.dispose();
|
|
79
101
|
unsubscribe();
|
|
80
102
|
};
|
|
81
103
|
}, [client, baseKey]);
|