wenay-react2 1.0.32 → 1.0.33
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.
|
@@ -2,6 +2,7 @@ import { ObserveAll2, Replay } from "wenay-common2";
|
|
|
2
2
|
type StorePatch = ObserveAll2.StorePatch;
|
|
3
3
|
type ReplayEvent<Z extends any[]> = Replay.ReplayEvent<Z>;
|
|
4
4
|
type ReplayRemote<Z extends any[]> = Replay.ReplayRemote<Z>;
|
|
5
|
+
type StaleInfo = Replay.StaleInfo;
|
|
5
6
|
export type UseReplaySubscribeOptions = {
|
|
6
7
|
/** Start position for the first subscription: journal tail after this seq; omit = keyframe. */
|
|
7
8
|
since?: number;
|
|
@@ -11,13 +12,30 @@ export type UseReplaySubscribeOptions = {
|
|
|
11
12
|
enabled?: boolean;
|
|
12
13
|
onSeq?: (seq: number) => void;
|
|
13
14
|
onError?: (e: unknown) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Producer-freshness watchdog (detection lives in wenay-common2): no event with a fresh
|
|
17
|
+
* producer ts for staleMs -> `stale` flips true, flips back on resume. Omitted = zero cost
|
|
18
|
+
* (no watchdog, no extra state updates). Changing staleMs resubscribes (reconnects by tail
|
|
19
|
+
* under keepSeq, so it is cheap).
|
|
20
|
+
*/
|
|
21
|
+
staleMs?: number;
|
|
22
|
+
/** Edge-triggered mirror of common2's onStale (fresh<->stale transitions only). Goes through a ref — a new identity does not resubscribe. */
|
|
23
|
+
onStale?: (info: StaleInfo) => void;
|
|
14
24
|
};
|
|
15
25
|
export type ReplaySubscribeController = {
|
|
16
26
|
/** Keyframe/tail catch-up finished, events are live from here. */
|
|
17
27
|
readonly ready: boolean;
|
|
18
28
|
readonly error: unknown;
|
|
29
|
+
/**
|
|
30
|
+
* Line is stale by the staleMs watchdog. React state, but it updates ONLY on fresh<->stale
|
|
31
|
+
* transitions — a high-frequency line causes zero extra renders while fresh.
|
|
32
|
+
* Always false without staleMs.
|
|
33
|
+
*/
|
|
34
|
+
readonly stale: boolean;
|
|
19
35
|
/** Last seen seq (reconnect point). Getter — reading it does not re-render. */
|
|
20
36
|
seq(): number;
|
|
37
|
+
/** Producer ts of the last delivered event (0 before the first delivery). Getter — reading it does not re-render. */
|
|
38
|
+
lastTs(): number;
|
|
21
39
|
/** Drop and re-create the subscription. since omitted = continue by keepSeq rules; a number = explicit position. */
|
|
22
40
|
restart(since?: number): void;
|
|
23
41
|
};
|
|
@@ -11,7 +11,9 @@ import { ObserveAll2, Replay } from "wenay-common2";
|
|
|
11
11
|
* (ref/store/canvas). If the consumer state resets on resubscribe, pass {keepSeq: false};
|
|
12
12
|
* - a FULL unmount loses the hook's refs: to reconnect by tail after a remount, keep the position
|
|
13
13
|
* outside via onSeq and pass it back as `since` (see the QA card for the pattern);
|
|
14
|
-
* - a different `remote` identity always starts from scratch (seq from another line is meaningless)
|
|
14
|
+
* - a different `remote` identity always starts from scratch (seq from another line is meaningless);
|
|
15
|
+
* - freshness (staleMs/onStale, controller.stale/lastTs()) is detected by wenay-common2;
|
|
16
|
+
* the hooks only mirror the fresh<->stale edges into React state.
|
|
15
17
|
*
|
|
16
18
|
* Server-side parts of the stack (conflateReplay, archiveReplay) are per-connection/per-process
|
|
17
19
|
* and intentionally have no hooks here.
|
|
@@ -28,14 +30,15 @@ function useLatestRef(value) {
|
|
|
28
30
|
* cb goes through a ref — a new cb identity does not resubscribe.
|
|
29
31
|
*/
|
|
30
32
|
export function useReplaySubscribe(remote, cb, options = {}) {
|
|
31
|
-
const { since, keepSeq = true, enabled = true, onSeq, onError } = options;
|
|
33
|
+
const { since, keepSeq = true, enabled = true, onSeq, onError, staleMs, onStale } = options;
|
|
32
34
|
const cbRef = useLatestRef(cb);
|
|
33
|
-
const hooksRef = useLatestRef({ onSeq, onError });
|
|
35
|
+
const hooksRef = useLatestRef({ onSeq, onError, onStale });
|
|
34
36
|
const seqRef = useRef(since);
|
|
35
37
|
const subRef = useRef(null);
|
|
36
38
|
const lastRemoteRef = useRef(undefined);
|
|
37
39
|
const [ready, setReady] = useState(false);
|
|
38
40
|
const [error, setError] = useState(null);
|
|
41
|
+
const [stale, setStale] = useState(false);
|
|
39
42
|
const [epoch, setEpoch] = useState(0);
|
|
40
43
|
useEffect(() => {
|
|
41
44
|
if (!remote || !enabled)
|
|
@@ -46,6 +49,10 @@ export function useReplaySubscribe(remote, cb, options = {}) {
|
|
|
46
49
|
let alive = true;
|
|
47
50
|
setReady(false);
|
|
48
51
|
setError(null);
|
|
52
|
+
// stale is NOT reset here: it re-syncs from common2 after the first delivery (a stale
|
|
53
|
+
// keyframe must show stale from the start, not flicker through false on resubscribe)
|
|
54
|
+
if (staleMs === undefined)
|
|
55
|
+
setStale(false);
|
|
49
56
|
const off = Replay.replaySubscribe(remote, (...event) => cbRef.current(...event), {
|
|
50
57
|
since: seqRef.current,
|
|
51
58
|
onSeq: seq => {
|
|
@@ -57,10 +64,23 @@ export function useReplaySubscribe(remote, cb, options = {}) {
|
|
|
57
64
|
setError(e);
|
|
58
65
|
hooksRef.current.onError?.(e);
|
|
59
66
|
},
|
|
67
|
+
...(staleMs !== undefined ? {
|
|
68
|
+
staleMs,
|
|
69
|
+
onStale: (info) => {
|
|
70
|
+
if (alive)
|
|
71
|
+
setStale(info.stale);
|
|
72
|
+
hooksRef.current.onStale?.(info);
|
|
73
|
+
},
|
|
74
|
+
} : null),
|
|
60
75
|
});
|
|
61
76
|
subRef.current = off;
|
|
62
|
-
off.ready.then(() => {
|
|
63
|
-
|
|
77
|
+
off.ready.then(() => {
|
|
78
|
+
if (!alive)
|
|
79
|
+
return;
|
|
80
|
+
setReady(true);
|
|
81
|
+
if (staleMs !== undefined)
|
|
82
|
+
setStale(off.isStale()); // fresh line after a stale one: no edge from common2, sync by hand
|
|
83
|
+
}, e => { if (alive)
|
|
64
84
|
setError(e); });
|
|
65
85
|
return () => {
|
|
66
86
|
alive = false;
|
|
@@ -69,16 +89,18 @@ export function useReplaySubscribe(remote, cb, options = {}) {
|
|
|
69
89
|
if (!keepSeq)
|
|
70
90
|
seqRef.current = since;
|
|
71
91
|
};
|
|
72
|
-
// keepSeq/since are start-position config, not subscription identity — no resubscribe on change
|
|
92
|
+
// keepSeq/since are start-position config, not subscription identity — no resubscribe on change;
|
|
93
|
+
// staleMs is subscribe-time config in common2, so changing it resubscribes
|
|
73
94
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
|
-
}, [remote, enabled, epoch]);
|
|
95
|
+
}, [remote, enabled, epoch, staleMs]);
|
|
75
96
|
const seq = useCallback(() => subRef.current?.seq() ?? seqRef.current ?? -1, []);
|
|
97
|
+
const lastTs = useCallback(() => subRef.current?.lastTs() ?? 0, []);
|
|
76
98
|
const restart = useCallback((at) => {
|
|
77
99
|
if (at !== undefined)
|
|
78
100
|
seqRef.current = at;
|
|
79
101
|
setEpoch(v => v + 1);
|
|
80
102
|
}, []);
|
|
81
|
-
return useMemo(() => ({ ready, error, seq, restart }), [ready, error, seq, restart]);
|
|
103
|
+
return useMemo(() => ({ ready, error, stale, seq, lastTs, restart }), [ready, error, stale, seq, lastTs, restart]);
|
|
82
104
|
}
|
|
83
105
|
/**
|
|
84
106
|
* Sync an existing mirror store from a store replay line (`ObserveAll2.exposeStoreReplay(...).api.replay`).
|
|
@@ -87,13 +109,14 @@ export function useReplaySubscribe(remote, cb, options = {}) {
|
|
|
87
109
|
* (useStoreNode/useStoreSelect).
|
|
88
110
|
*/
|
|
89
111
|
export function useStoreReplaySync(store, remote, options = {}) {
|
|
90
|
-
const { since, keepSeq = true, enabled = true, onSeq, onError } = options;
|
|
91
|
-
const hooksRef = useLatestRef({ onSeq, onError });
|
|
112
|
+
const { since, keepSeq = true, enabled = true, onSeq, onError, staleMs, onStale } = options;
|
|
113
|
+
const hooksRef = useLatestRef({ onSeq, onError, onStale });
|
|
92
114
|
const seqRef = useRef(since);
|
|
93
115
|
const subRef = useRef(null);
|
|
94
116
|
const lastRemoteRef = useRef(undefined);
|
|
95
117
|
const [ready, setReady] = useState(false);
|
|
96
118
|
const [error, setError] = useState(null);
|
|
119
|
+
const [stale, setStale] = useState(false);
|
|
97
120
|
const [epoch, setEpoch] = useState(0);
|
|
98
121
|
useEffect(() => {
|
|
99
122
|
if (!store || !remote || !enabled)
|
|
@@ -104,6 +127,8 @@ export function useStoreReplaySync(store, remote, options = {}) {
|
|
|
104
127
|
let alive = true;
|
|
105
128
|
setReady(false);
|
|
106
129
|
setError(null);
|
|
130
|
+
if (staleMs === undefined)
|
|
131
|
+
setStale(false); // see useReplaySubscribe: stale re-syncs from common2, no reset-to-false flicker
|
|
107
132
|
const off = ObserveAll2.syncStoreReplay(store, remote, {
|
|
108
133
|
since: seqRef.current,
|
|
109
134
|
onSeq: seq => {
|
|
@@ -115,10 +140,23 @@ export function useStoreReplaySync(store, remote, options = {}) {
|
|
|
115
140
|
setError(e);
|
|
116
141
|
hooksRef.current.onError?.(e);
|
|
117
142
|
},
|
|
143
|
+
...(staleMs !== undefined ? {
|
|
144
|
+
staleMs,
|
|
145
|
+
onStale: (info) => {
|
|
146
|
+
if (alive)
|
|
147
|
+
setStale(info.stale);
|
|
148
|
+
hooksRef.current.onStale?.(info);
|
|
149
|
+
},
|
|
150
|
+
} : null),
|
|
118
151
|
});
|
|
119
152
|
subRef.current = off;
|
|
120
|
-
off.ready.then(() => {
|
|
121
|
-
|
|
153
|
+
off.ready.then(() => {
|
|
154
|
+
if (!alive)
|
|
155
|
+
return;
|
|
156
|
+
setReady(true);
|
|
157
|
+
if (staleMs !== undefined)
|
|
158
|
+
setStale(off.isStale());
|
|
159
|
+
}, e => { if (alive)
|
|
122
160
|
setError(e); });
|
|
123
161
|
return () => {
|
|
124
162
|
alive = false;
|
|
@@ -128,14 +166,15 @@ export function useStoreReplaySync(store, remote, options = {}) {
|
|
|
128
166
|
seqRef.current = since;
|
|
129
167
|
};
|
|
130
168
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
131
|
-
}, [store, remote, enabled, epoch]);
|
|
169
|
+
}, [store, remote, enabled, epoch, staleMs]);
|
|
132
170
|
const seq = useCallback(() => subRef.current?.seq() ?? seqRef.current ?? -1, []);
|
|
171
|
+
const lastTs = useCallback(() => subRef.current?.lastTs() ?? 0, []);
|
|
133
172
|
const restart = useCallback((at) => {
|
|
134
173
|
if (at !== undefined)
|
|
135
174
|
seqRef.current = at;
|
|
136
175
|
setEpoch(v => v + 1);
|
|
137
176
|
}, []);
|
|
138
|
-
return useMemo(() => ({ ready, error, seq, restart }), [ready, error, seq, restart]);
|
|
177
|
+
return useMemo(() => ({ ready, error, stale, seq, lastTs, restart }), [ready, error, stale, seq, lastTs, restart]);
|
|
139
178
|
}
|
|
140
179
|
/**
|
|
141
180
|
* Convenience: create a local mirror store and keep it synced from a store replay line.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wenay-react2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.33",
|
|
4
4
|
"description": "Common react",
|
|
5
5
|
"strict": true,
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"react": "^19.2.0",
|
|
24
24
|
"react-dom": "^19.2.0",
|
|
25
25
|
"react-rnd": "^10.5.3",
|
|
26
|
-
"wenay-common2": "^1.0.
|
|
26
|
+
"wenay-common2": "^1.0.57"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"react": "^19.2.0",
|