wenay-react2 1.0.45 → 1.0.46
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/README.md +4 -0
- package/doc/changes/v1.0.46.md +16 -0
- package/doc/examples/README.md +6 -0
- package/doc/examples/peer-call-media.tsx +8 -0
- package/doc/examples/stand.tsx +6 -0
- package/doc/progress/common2-adoption-1.0.74.md +24 -0
- package/doc/progress/stand-as-examples-audit.md +22 -0
- package/doc/target/my.md +12 -0
- package/doc/wenay-react2.md +1 -1
- package/lib/common/demo/peerMedia.d.ts +6 -0
- package/lib/common/demo/peerMedia.js +128 -0
- package/lib/common/src/components/Toolbar/Toolbar.d.ts +4 -0
- package/lib/common/src/components/Toolbar/Toolbar.js +16 -7
- package/lib/common/src/components/index.d.ts +12 -0
- package/lib/common/src/components/index.js +12 -0
- package/lib/common/src/grid/columnState/CardList.d.ts +2 -0
- package/lib/common/src/grid/columnState/CardList.js +1 -1
- package/lib/common/src/grid/columnState/ColumnsMenu.d.ts +2 -0
- package/lib/common/src/grid/columnState/ColumnsMenu.js +3 -2
- package/lib/common/src/grid/columnState/columnState.d.ts +15 -0
- package/lib/common/src/grid/columnState/columnState.js +53 -5
- package/lib/common/src/hooks/index.d.ts +1 -0
- package/lib/common/src/hooks/index.js +1 -0
- package/lib/common/src/hooks/usePeerCall.d.ts +68 -0
- package/lib/common/src/hooks/usePeerCall.js +79 -0
- package/lib/common/src/hooks/useReorder.d.ts +2 -0
- package/lib/common/src/hooks/useReorder.js +3 -1
- package/lib/common/src/utils/memoryStore.d.ts +2 -2
- package/lib/common/testUseReact/qa.d.ts +1 -0
- package/lib/common/testUseReact/qa.js +1148 -0
- package/lib/common/testUseReact/replayVideo.d.ts +14 -0
- package/lib/common/testUseReact/replayVideo.js +311 -0
- package/lib/common/testUseReact/testParams.d.ts +1 -0
- package/lib/common/testUseReact/testParams.js +15 -0
- package/lib/common/testUseReact/useGrid.d.ts +2 -0
- package/lib/common/testUseReact/useGrid.js +62 -0
- package/lib/style/style.css +24 -0
- package/package.json +5 -3
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const videoResolutions: readonly [{
|
|
2
|
+
readonly w: 160;
|
|
3
|
+
readonly h: 90;
|
|
4
|
+
}, {
|
|
5
|
+
readonly w: 320;
|
|
6
|
+
readonly h: 180;
|
|
7
|
+
}, {
|
|
8
|
+
readonly w: 640;
|
|
9
|
+
readonly h: 360;
|
|
10
|
+
}];
|
|
11
|
+
export declare const ReplayRouteDemo: () => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export declare const ReplayVideoDemo: () => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare const ReplayStoreDemo: () => import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export declare const ReplayStoreEachDemo: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* replayVideo.tsx - QA demos for the Replay stack hooks (useReplaySubscribe / useReplayHistory / useStoreReplayMirror).
|
|
3
|
+
*
|
|
4
|
+
* Everything is in-proc: a synthetic "video" producer emits jpeg frames on a replay line
|
|
5
|
+
* (keyframe = last frame), a simulated slow wire + conflateReplay gate shows per-client
|
|
6
|
+
* frame dropping, archiveReplay + openHistory power the time-travel scrubber.
|
|
7
|
+
* The transport itself is already proven in wenay-common2 (replay/video-socket.demo);
|
|
8
|
+
* these cards exercise the React lifecycle side.
|
|
9
|
+
*/
|
|
10
|
+
import React, { StrictMode, useEffect, useMemo, useRef, useState } from "react";
|
|
11
|
+
import { Observe, Replay } from "wenay-common2";
|
|
12
|
+
import { useReplaySubscribe, useReplayRouteSubscribe, useReplayFrame, useReplayHistory, useStoreReplayMirror, useStoreReplayEach, useStoreNode, useStoreKeys } from "../src/hooks";
|
|
13
|
+
const FPS = 10;
|
|
14
|
+
export const videoResolutions = [{ w: 160, h: 90 }, { w: 320, h: 180 }, { w: 640, h: 360 }];
|
|
15
|
+
/* ---------- producer + line + wire + archive (module singleton, starts on first use) ---------- */
|
|
16
|
+
function createVideoDemo() {
|
|
17
|
+
let res = videoResolutions[1];
|
|
18
|
+
let frameN = 0;
|
|
19
|
+
let last = null;
|
|
20
|
+
let stalled = false;
|
|
21
|
+
const canvas = document.createElement("canvas");
|
|
22
|
+
const ctx = canvas.getContext("2d");
|
|
23
|
+
const [emit, replay] = Replay.replayListen({
|
|
24
|
+
history: 256,
|
|
25
|
+
current: () => last ? [last] : undefined, // keyframe source: every frame fully defines the picture
|
|
26
|
+
});
|
|
27
|
+
function drawScene(t) {
|
|
28
|
+
if (canvas.width != res.w)
|
|
29
|
+
canvas.width = res.w;
|
|
30
|
+
if (canvas.height != res.h)
|
|
31
|
+
canvas.height = res.h;
|
|
32
|
+
const g = ctx.createLinearGradient(0, 0, res.w, res.h);
|
|
33
|
+
g.addColorStop(0, `hsl(${Math.round(t / 40) % 360}, 65%, 42%)`);
|
|
34
|
+
g.addColorStop(1, "#16213e");
|
|
35
|
+
ctx.fillStyle = g;
|
|
36
|
+
ctx.fillRect(0, 0, res.w, res.h);
|
|
37
|
+
const bx = res.w / 2 + Math.sin(t / 400) * res.w * 0.35;
|
|
38
|
+
const by = res.h / 2 + Math.cos(t / 300) * res.h * 0.3;
|
|
39
|
+
ctx.beginPath();
|
|
40
|
+
ctx.arc(bx, by, Math.min(res.w, res.h) * 0.09, 0, Math.PI * 2);
|
|
41
|
+
ctx.fillStyle = "#ffd33d";
|
|
42
|
+
ctx.fill();
|
|
43
|
+
ctx.fillStyle = "#fff";
|
|
44
|
+
ctx.font = `bold ${Math.max(11, Math.round(res.h / 9))}px monospace`;
|
|
45
|
+
ctx.fillText(`#${frameN} ${res.w}x${res.h}`, 8, Math.max(14, Math.round(res.h / 7)));
|
|
46
|
+
}
|
|
47
|
+
setInterval(() => {
|
|
48
|
+
if (stalled)
|
|
49
|
+
return; // freshness QA: producer stops emitting, line stays subscribed
|
|
50
|
+
frameN++;
|
|
51
|
+
drawScene(performance.now());
|
|
52
|
+
const frame = { n: frameN, w: res.w, h: res.h, ts: Date.now(), jpeg: canvas.toDataURL("image/jpeg", 0.55) };
|
|
53
|
+
last = frame;
|
|
54
|
+
emit(frame);
|
|
55
|
+
}, 1000 / FPS);
|
|
56
|
+
// fast client path: direct in-proc "wire"
|
|
57
|
+
const remoteDirect = Replay.exposeReplay(replay);
|
|
58
|
+
// slow client path: artificial outgoing buffer + per-connection conflation gate
|
|
59
|
+
const wire = (() => {
|
|
60
|
+
const buf = [];
|
|
61
|
+
const listeners = new Set();
|
|
62
|
+
let rateMs = 400; // one envelope per rateMs = the "bad link"
|
|
63
|
+
const gate = Replay.conflateReplay(replay, {
|
|
64
|
+
pending: () => buf.length, // this client's outgoing buffer
|
|
65
|
+
highWater: 4,
|
|
66
|
+
lowWater: 1,
|
|
67
|
+
keyOf: () => "frame", // frames are absolute -> keep only the last while lagged
|
|
68
|
+
});
|
|
69
|
+
gate.api.line.on(ev => { buf.push(ev); });
|
|
70
|
+
(function pump() {
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
const ev = buf.shift();
|
|
73
|
+
if (ev)
|
|
74
|
+
listeners.forEach(cb => cb(ev));
|
|
75
|
+
pump();
|
|
76
|
+
}, rateMs);
|
|
77
|
+
})();
|
|
78
|
+
const remote = {
|
|
79
|
+
line: { on: (cb) => {
|
|
80
|
+
listeners.add(cb);
|
|
81
|
+
return () => listeners.delete(cb);
|
|
82
|
+
} },
|
|
83
|
+
since: seq => gate.api.since(seq),
|
|
84
|
+
keyframe: () => gate.api.keyframe(),
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
remote,
|
|
88
|
+
stats: () => gate.stats(),
|
|
89
|
+
buffered: () => buf.length,
|
|
90
|
+
setRateMs: (ms) => { rateMs = ms; },
|
|
91
|
+
getRateMs: () => rateMs,
|
|
92
|
+
};
|
|
93
|
+
})();
|
|
94
|
+
// archive + time machine
|
|
95
|
+
const storage = Replay.createMemoryReplayStorage({ maxEvents: 600 });
|
|
96
|
+
Replay.archiveReplay(replay, { storage, everyEvents: 25 });
|
|
97
|
+
const history = Replay.openHistory(storage, replay); // archive -> live handover
|
|
98
|
+
return {
|
|
99
|
+
replay,
|
|
100
|
+
remoteDirect,
|
|
101
|
+
wire,
|
|
102
|
+
history,
|
|
103
|
+
setResolution: (r) => { res = r; },
|
|
104
|
+
getResolution: () => res,
|
|
105
|
+
setStalled: (v) => { stalled = v; },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
let videoDemo = null;
|
|
109
|
+
const getVideoDemo = () => videoDemo ??= (window.__replayVideoDemo = createVideoDemo()); // window exposure: QA debugging
|
|
110
|
+
/* ---------- frame sink: async jpeg decode, latest requested frame wins ---------- */
|
|
111
|
+
function makeFrameSink(canvasRef) {
|
|
112
|
+
let want = -1;
|
|
113
|
+
return (frame) => {
|
|
114
|
+
want = frame.n;
|
|
115
|
+
const img = new Image();
|
|
116
|
+
img.onload = () => {
|
|
117
|
+
if (frame.n != want)
|
|
118
|
+
return; // a newer frame superseded this decode (or we seeked back)
|
|
119
|
+
const canvas = canvasRef.current;
|
|
120
|
+
if (!canvas)
|
|
121
|
+
return;
|
|
122
|
+
if (canvas.width != frame.w)
|
|
123
|
+
canvas.width = frame.w;
|
|
124
|
+
if (canvas.height != frame.h)
|
|
125
|
+
canvas.height = frame.h;
|
|
126
|
+
canvas.getContext("2d")?.drawImage(img, 0, 0);
|
|
127
|
+
};
|
|
128
|
+
img.src = frame.jpeg;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function useTick(ms) {
|
|
132
|
+
const [, setTick] = useState(0);
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const t = setInterval(() => setTick(v => v + 1), ms);
|
|
135
|
+
return () => clearInterval(t);
|
|
136
|
+
}, [ms]);
|
|
137
|
+
}
|
|
138
|
+
const canvasStyle = { width: 320, background: "#000", borderRadius: 6, display: "block", border: "1px solid #d0d7de" };
|
|
139
|
+
const statLine = { fontSize: 12, marginTop: 4, fontFamily: "monospace" };
|
|
140
|
+
function LiveClient(p) {
|
|
141
|
+
const canvasRef = useRef(null);
|
|
142
|
+
const sink = useMemo(() => makeFrameSink(canvasRef), []);
|
|
143
|
+
const framesRef = useRef(0);
|
|
144
|
+
const sub = useReplaySubscribe(p.remote, (frame) => { framesRef.current++; sink(frame); }, { since: p.since, onSeq: p.onSeq });
|
|
145
|
+
useTick(500);
|
|
146
|
+
return _jsxs("div", { children: [_jsx("canvas", { ref: canvasRef, style: canvasStyle }), _jsxs("div", { style: statLine, children: ["ready ", String(sub.ready), " \u00B7 seq ", sub.seq(), " \u00B7 frames ", framesRef.current, sub.error != null ? ` · error ${String(sub.error)}` : ""] })] });
|
|
147
|
+
}
|
|
148
|
+
/* freshness client: no useTick — the ONLY re-render sources are ready/stale transitions.
|
|
149
|
+
* React.memo shields it from the parent's 500ms tick, so the render counter is the proof
|
|
150
|
+
* that a fresh 10 fps line causes zero per-event renders. Wrapped in StrictMode by the card:
|
|
151
|
+
* double-effect must not leak a second watchdog or flicker the badge. */
|
|
152
|
+
const STALE_MS = 2000;
|
|
153
|
+
const staleBadge = { padding: "1px 8px", borderRadius: 4, color: "#fff", fontWeight: 700 };
|
|
154
|
+
const StaleClient = React.memo(function StaleClient(p) {
|
|
155
|
+
const canvasRef = useRef(null);
|
|
156
|
+
const sink = useMemo(() => makeFrameSink(canvasRef), []);
|
|
157
|
+
const framesRef = useRef(0);
|
|
158
|
+
const rendersRef = useRef(0);
|
|
159
|
+
rendersRef.current++;
|
|
160
|
+
const sub = useReplaySubscribe(p.remote, (frame) => { framesRef.current++; sink(frame); }, { staleMs: p.staleMs });
|
|
161
|
+
return _jsxs("div", { children: [_jsx("canvas", { ref: canvasRef, style: canvasStyle }), _jsxs("div", { style: statLine, children: [sub.stale
|
|
162
|
+
? _jsx("span", { style: { ...staleBadge, background: "#cf222e" }, children: "STALE" })
|
|
163
|
+
: _jsx("span", { style: { ...staleBadge, background: "#2da44e" }, children: "fresh" }), " ", "\u00B7 renders ", rendersRef.current, " \u00B7 frames ", framesRef.current, " ", "\u00B7 lastTs ", sub.lastTs() > 0 ? new Date(sub.lastTs()).toLocaleTimeString() : "-"] })] });
|
|
164
|
+
});
|
|
165
|
+
/* pull client (useReplayFrame): NO live subscription — a timer around remote.frame(seq).
|
|
166
|
+
* The canvas advances at the PULL cadence while the producer runs at 10 fps: each pull folds
|
|
167
|
+
* the whole journal tail since our seq (frames counter jumps by ~pace×fps), the sink's
|
|
168
|
+
* latest-wins decode draws only the last one. The remote is wrapped to count frame() calls —
|
|
169
|
+
* also QA-proves a hand-wrapped remote works. Pace switches resubscribe but keep the seq
|
|
170
|
+
* (keepSeq), so no keyframe restart. */
|
|
171
|
+
function PullClient(p) {
|
|
172
|
+
const canvasRef = useRef(null);
|
|
173
|
+
const sink = useMemo(() => makeFrameSink(canvasRef), []);
|
|
174
|
+
const framesRef = useRef(0);
|
|
175
|
+
const pullsRef = useRef(0);
|
|
176
|
+
const [intervalMs, setIntervalMs] = useState(1000);
|
|
177
|
+
const remote = useMemo(() => ({
|
|
178
|
+
...p.remote,
|
|
179
|
+
frame: (seq, hint) => { pullsRef.current++; return p.remote.frame(seq, hint); },
|
|
180
|
+
}), [p.remote]);
|
|
181
|
+
const pf = useReplayFrame(remote, (frame) => { framesRef.current++; sink(frame); }, { intervalMs });
|
|
182
|
+
useTick(500);
|
|
183
|
+
return _jsxs("div", { children: [_jsx("canvas", { ref: canvasRef, style: canvasStyle }), _jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center", marginTop: 4, flexWrap: "wrap" }, children: [_jsx("span", { style: { fontSize: 12 }, children: "pull every:" }), [250, 1000, 3000].map(ms => _jsxs("button", { style: { fontWeight: ms == intervalMs ? 700 : 400 }, onClick: () => setIntervalMs(ms), children: [ms, "ms"] }, ms)), _jsx("button", { onClick: () => void pf.pull(), children: "pull now" })] }), _jsxs("div", { style: statLine, children: ["ready ", String(pf.ready), " \u00B7 seq ", pf.seq(), " \u00B7 pulls ", pullsRef.current, " \u00B7 frames ", framesRef.current, pf.error != null ? ` · error ${String(pf.error)}` : ""] })] });
|
|
184
|
+
}
|
|
185
|
+
function failingFrameRemote() {
|
|
186
|
+
const fail = () => { throw new Error("route failed"); };
|
|
187
|
+
return {
|
|
188
|
+
line: { on: () => () => { } },
|
|
189
|
+
since: async () => fail(),
|
|
190
|
+
keyframe: async () => fail(),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function RouteHandoffClient(p) {
|
|
194
|
+
const canvasRef = useRef(null);
|
|
195
|
+
const sink = useMemo(() => makeFrameSink(canvasRef), []);
|
|
196
|
+
const framesRef = useRef(0);
|
|
197
|
+
const eventsRef = useRef([]);
|
|
198
|
+
const [, setEventTick] = useState(0);
|
|
199
|
+
const badRemote = useMemo(() => failingFrameRemote(), []);
|
|
200
|
+
const route = useReplayRouteSubscribe(p.relay, frame => {
|
|
201
|
+
framesRef.current++;
|
|
202
|
+
sink(frame);
|
|
203
|
+
}, {
|
|
204
|
+
label: "relay",
|
|
205
|
+
onRoute: ev => {
|
|
206
|
+
eventsRef.current = [`${ev.phase}:${ev.from ?? "-"}->${ev.to ?? "-"}@${ev.seq}`, ...eventsRef.current].slice(0, 4);
|
|
207
|
+
setEventTick(v => v + 1);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
useTick(500);
|
|
211
|
+
const err = route.error instanceof Error ? route.error.message : String(route.error ?? "");
|
|
212
|
+
return _jsxs("div", { style: { display: "grid", gap: 6, maxWidth: 360 }, children: [_jsx("canvas", { ref: canvasRef, style: canvasStyle }), _jsxs("div", { style: { display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }, children: [_jsx("button", { onClick: () => { void route.switchRoute(p.direct, { label: "direct" }).catch(() => { }); }, children: "switch direct" }), _jsx("button", { onClick: () => { void route.switchRoute(p.relay, { label: "relay" }).catch(() => { }); }, children: "switch relay" }), _jsx("button", { onClick: () => { void route.switchRoute(badRemote, { label: "bad" }).catch(() => { }); }, children: "fail route" })] }), _jsxs("div", { style: statLine, children: ["ready ", String(route.ready), " \u00B7 switching ", String(route.switching), " \u00B7 active ", String(route.active()), " \u00B7 label ", route.label() ?? "-"] }), _jsxs("div", { style: statLine, children: ["seq ", route.seq(), " \u00B7 frames ", framesRef.current, err ? ` · error ${err}` : ""] }), _jsxs("div", { style: { ...statLine, whiteSpace: "normal" }, children: ["route events: ", eventsRef.current.join(" | ") || "-"] })] });
|
|
213
|
+
}
|
|
214
|
+
export const ReplayRouteDemo = () => {
|
|
215
|
+
const demo = useMemo(() => getVideoDemo(), []);
|
|
216
|
+
return _jsxs("div", { style: { display: "grid", gap: 8 }, children: [_jsx("div", { style: { fontSize: 13, color: "#57606a" }, children: "One canvas, one logical fold. The old route remains live while the replacement catches up by seq." }), _jsx(RouteHandoffClient, { direct: demo.remoteDirect, relay: demo.wire.remote })] });
|
|
217
|
+
};
|
|
218
|
+
/* ---------- card 23: video line - direct client, conflated slow client, time travel ---------- */
|
|
219
|
+
export const ReplayVideoDemo = () => {
|
|
220
|
+
const demo = useMemo(() => getVideoDemo(), []);
|
|
221
|
+
const [resIdx, setResIdx] = useState(1);
|
|
222
|
+
const [slow, setSlow] = useState(true);
|
|
223
|
+
const [stalled, setStalled] = useState(false);
|
|
224
|
+
const [mountedA, setMountedA] = useState(true);
|
|
225
|
+
const [genD, setGenD] = useState(0);
|
|
226
|
+
const lastSeqA = useRef(undefined);
|
|
227
|
+
useTick(500);
|
|
228
|
+
const ttCanvasRef = useRef(null);
|
|
229
|
+
const ttSink = useMemo(() => makeFrameSink(ttCanvasRef), []);
|
|
230
|
+
const tt = useReplayHistory(demo.history, ttSink, { head: () => demo.replay.head() });
|
|
231
|
+
const wireStats = demo.wire.stats();
|
|
232
|
+
return _jsxs("div", { style: { display: "grid", gap: 10 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }, children: [_jsx("span", { style: { fontSize: 13 }, children: "resolution:" }), videoResolutions.map((r, i) => _jsxs("button", { style: { fontWeight: i == resIdx ? 700 : 400 }, onClick: () => { setResIdx(i); demo.setResolution(r); }, children: [r.w, "x", r.h] }, r.w)), _jsxs("label", { style: { fontSize: 13, marginLeft: 12 }, children: [_jsx("input", { type: "checkbox", checked: slow, onChange: e => { setSlow(e.target.checked); demo.wire.setRateMs(e.target.checked ? 400 : 66); } }), "slow network for client B (1 envelope / ", demo.wire.getRateMs(), "ms)"] }), _jsxs("label", { style: { fontSize: 13, marginLeft: 12 }, children: [_jsx("input", { type: "checkbox", checked: stalled, onChange: e => { setStalled(e.target.checked); demo.setStalled(e.target.checked); } }), "stall producer (freshness for D)"] }), _jsxs("span", { style: { fontSize: 12, color: "#57606a" }, children: ["producer: ", FPS, " fps \u00B7 head seq ", demo.replay.head()] })] }), _jsxs("div", { style: { display: "flex", gap: 16, flexWrap: "wrap" }, children: [_jsxs("div", { children: [_jsxs("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 4 }, children: ["A: direct client", " ", _jsx("button", { onClick: () => setMountedA(v => !v), children: mountedA ? "unmount" : "remount (tail via since)" })] }), mountedA
|
|
233
|
+
? _jsx(LiveClient, { remote: demo.remoteDirect, since: lastSeqA.current, onSeq: seq => { lastSeqA.current = seq; } })
|
|
234
|
+
: _jsxs("div", { style: { ...canvasStyle, height: 180, display: "flex", alignItems: "center", justifyContent: "center", color: "#8b949e" }, children: ["unmounted \u00B7 kept seq ", lastSeqA.current ?? "-"] })] }), _jsxs("div", { children: [_jsx("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 4 }, children: "B: slow wire + conflateReplay gate" }), _jsx(LiveClient, { remote: demo.wire.remote }), _jsxs("div", { style: statLine, children: ["wire buffer ", demo.wire.buffered(), " \u00B7 conflating ", String(wireStats.conflating), _jsx("br", {}), "dropped ", wireStats.dropped, " \u00B7 coalesced ", wireStats.coalesced, " \u00B7 recoveries ", wireStats.flushes, " (keyframes ", wireStats.keyframes, ")"] })] }), _jsxs("div", { children: [_jsx("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 4 }, children: "C: time travel (archive + openHistory)" }), _jsx("canvas", { ref: ttCanvasRef, style: canvasStyle }), _jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", marginTop: 4 }, children: [_jsx("button", { onClick: () => tt.live ? tt.pause() : tt.play(), children: tt.live ? "⏸ pause" : "▶ live" }), _jsx("input", { type: "range", style: { width: 200 }, min: Math.max(0, tt.head - 550), max: Math.max(0, tt.head), value: tt.seq < 0 ? 0 : tt.seq, onChange: e => tt.seek({ seq: Number(e.target.value) }) }), _jsxs("span", { style: { fontSize: 12, fontFamily: "monospace" }, children: [tt.seq, "/", tt.head, " ", tt.live ? "live" : "paused"] })] })] }), _jsxs("div", { children: [_jsxs("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 4 }, children: ["D: freshness (staleMs=", STALE_MS, ", StrictMode)", " ", _jsx("button", { onClick: () => setGenD(v => v + 1), children: "new client (keyframe)" })] }), _jsx(StrictMode, { children: _jsx(StaleClient, { remote: demo.remoteDirect, staleMs: STALE_MS }, genD) }), _jsx("div", { style: { fontSize: 12, color: "#57606a", maxWidth: 320, marginTop: 4 }, children: "counters refresh only on fresh\u2194stale flips: a flat \"renders\" while frames grow is the no-per-event-render proof" })] }), _jsxs("div", { children: [_jsx("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 4 }, children: "E: pull at own pace (useReplayFrame)" }), _jsx(PullClient, { remote: demo.remoteDirect })] })] })] });
|
|
235
|
+
};
|
|
236
|
+
function createStoreReplayDemo() {
|
|
237
|
+
let stalled = false;
|
|
238
|
+
const store = Observe.createStore({ ticks: 0, price: 100, note: "start", bag: { a: 1 } });
|
|
239
|
+
const exposed = Observe.exposeStoreReplay(store, { history: 128 });
|
|
240
|
+
setInterval(() => {
|
|
241
|
+
if (stalled)
|
|
242
|
+
return;
|
|
243
|
+
store.state.ticks++;
|
|
244
|
+
store.state.price = Math.round((store.state.price + (Math.random() - 0.5) * 2) * 100) / 100;
|
|
245
|
+
}, 800);
|
|
246
|
+
return { store, remote: exposed.api.replay, setStalled: (v) => { stalled = v; } };
|
|
247
|
+
}
|
|
248
|
+
let storeDemo = null;
|
|
249
|
+
const getStoreReplayDemo = () => storeDemo ??= createStoreReplayDemo();
|
|
250
|
+
export const ReplayStoreDemo = () => {
|
|
251
|
+
const demo = useMemo(() => getStoreReplayDemo(), []);
|
|
252
|
+
const [enabled, setEnabled] = useState(true);
|
|
253
|
+
const [stalled, setStalled] = useState(false);
|
|
254
|
+
const mirror = useStoreReplayMirror(demo.remote, { ticks: 0, price: 0, note: "", bag: {} }, { enabled, staleMs: 2500 });
|
|
255
|
+
const ticks = useStoreNode(mirror.store.node.ticks);
|
|
256
|
+
const price = useStoreNode(mirror.store.node.price);
|
|
257
|
+
const note = useStoreNode(mirror.store.node.note);
|
|
258
|
+
const bagKeys = useStoreKeys(mirror.store.node.bag);
|
|
259
|
+
useTick(500);
|
|
260
|
+
return _jsxs("div", { style: { display: "grid", gap: 8 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }, children: [_jsx("button", { onClick: () => { demo.store.state.note = "srv " + new Date().toLocaleTimeString(); }, children: "server note" }), _jsx("button", { onClick: () => { demo.store.state.bag["k" + (Object.keys(demo.store.state.bag).length + 1)] = Date.now() % 1000; }, children: "server add key" }), _jsx("button", { onClick: () => {
|
|
261
|
+
const keys = Object.keys(demo.store.state.bag);
|
|
262
|
+
if (keys.length)
|
|
263
|
+
delete demo.store.state.bag[keys[keys.length - 1]];
|
|
264
|
+
}, children: "server delete key" }), _jsxs("label", { style: { fontSize: 13 }, children: [_jsx("input", { type: "checkbox", checked: enabled, onChange: e => setEnabled(e.target.checked) }), "sync enabled (uncheck, mutate, recheck -> catches up via journal tail)"] }), _jsx("button", { onClick: () => mirror.restart(), children: "restart (tail)" }), _jsxs("label", { style: { fontSize: 13 }, children: [_jsx("input", { type: "checkbox", checked: stalled, onChange: e => { setStalled(e.target.checked); demo.setStalled(e.target.checked); } }), "stall producer (stale after 2.5s)"] })] }), _jsxs("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", fontSize: 13 }, children: [_jsxs("span", { children: ["ready: ", _jsx("b", { children: String(mirror.ready) })] }), _jsxs("span", { children: ["stale: ", _jsx("b", { style: { color: mirror.stale ? "#cf222e" : "#2da44e" }, children: String(mirror.stale) })] }), _jsxs("span", { children: ["lastTs: ", _jsx("b", { children: mirror.lastTs() > 0 ? new Date(mirror.lastTs()).toLocaleTimeString() : "-" })] }), _jsxs("span", { children: ["seq: ", _jsx("b", { children: mirror.seq() })] }), _jsxs("span", { children: ["mirror ticks: ", _jsx("b", { children: ticks.value })] }), _jsxs("span", { children: ["mirror price: ", _jsx("b", { children: price.value })] }), _jsxs("span", { children: ["mirror note: ", _jsx("b", { children: note.value })] }), _jsxs("span", { children: ["bag keys: ", _jsx("b", { children: bagKeys.stringKeys.join(",") || "-" })] })] }), mirror.error != null && _jsxs("div", { style: { color: "#cf222e" }, children: ["error: ", String(mirror.error)] })] });
|
|
265
|
+
};
|
|
266
|
+
function createStoreEachDemo() {
|
|
267
|
+
let n = 2;
|
|
268
|
+
const store = Observe.createStore({ r1: { qty: 5, px: 101 }, r2: { qty: 3, px: 202 } });
|
|
269
|
+
const exposed = Observe.exposeStoreReplay(store, { history: 256 });
|
|
270
|
+
setInterval(() => {
|
|
271
|
+
const keys = Object.keys(store.state);
|
|
272
|
+
if (!keys.length)
|
|
273
|
+
return;
|
|
274
|
+
const k = keys[Math.floor(Math.random() * keys.length)]; // ONE random row per tick
|
|
275
|
+
store.state[k].px = Math.round(store.state[k].px * (1 + (Math.random() - 0.5) / 40) * 100) / 100;
|
|
276
|
+
}, 700);
|
|
277
|
+
return {
|
|
278
|
+
store,
|
|
279
|
+
remote: exposed.api.replay,
|
|
280
|
+
add: () => { n++; store.state["r" + n] = { qty: 1 + n % 7, px: 100 + n }; },
|
|
281
|
+
del: () => { const keys = Object.keys(store.state); if (keys.length)
|
|
282
|
+
delete store.state[keys[keys.length - 1]]; },
|
|
283
|
+
replaceAll: () => {
|
|
284
|
+
n += 2;
|
|
285
|
+
store.replace({ ["r" + (n - 1)]: { qty: 2, px: 100 + n }, ["r" + n]: { qty: 4, px: 200 + n } });
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
let eachDemo = null;
|
|
290
|
+
const getStoreEachDemo = () => eachDemo ??= createStoreEachDemo();
|
|
291
|
+
const EachClient = ({ remote }) => {
|
|
292
|
+
// the fold target lives OUTSIDE React state: a plain Map of rows, exactly like a grid api would
|
|
293
|
+
const rowsRef = useRef(new Map());
|
|
294
|
+
const callsRef = useRef(0);
|
|
295
|
+
const [, setTick] = useState(0);
|
|
296
|
+
const feed = useStoreReplayEach(remote, (key, row) => {
|
|
297
|
+
callsRef.current++;
|
|
298
|
+
if (row === undefined)
|
|
299
|
+
rowsRef.current.delete(key);
|
|
300
|
+
else
|
|
301
|
+
rowsRef.current.set(key, { ...row, calls: (rowsRef.current.get(key)?.calls ?? 0) + 1 });
|
|
302
|
+
setTick(t => t + 1);
|
|
303
|
+
}, { drain: 100, staleMs: 2500 });
|
|
304
|
+
const rows = [...rowsRef.current.entries()].sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }));
|
|
305
|
+
return _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsxs("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", fontSize: 13 }, children: [_jsxs("span", { children: ["ready: ", _jsx("b", { children: String(feed.ready) })] }), _jsxs("span", { children: ["stale: ", _jsx("b", { style: { color: feed.stale ? "#cf222e" : "#2da44e" }, children: String(feed.stale) })] }), _jsxs("span", { children: ["seq: ", _jsx("b", { children: feed.seq() })] }), _jsxs("span", { children: ["cb calls: ", _jsx("b", { children: callsRef.current })] }), _jsxs("span", { children: ["store keys: ", _jsx("b", { children: Object.keys(feed.store.state).length })] })] }), _jsxs("table", { style: { fontSize: 13, fontFamily: "monospace", borderCollapse: "collapse", width: "fit-content" }, children: [_jsx("thead", { children: _jsx("tr", { children: ["row", "qty", "px", "cb calls"].map(h => _jsx("th", { style: { border: "1px solid #d0d7de", padding: "2px 10px", background: "#f6f8fa" }, children: h }, h)) }) }), _jsx("tbody", { children: rows.map(([key, r]) => _jsxs("tr", { children: [_jsx("td", { style: { border: "1px solid #d0d7de", padding: "2px 10px" }, children: key }), _jsx("td", { style: { border: "1px solid #d0d7de", padding: "2px 10px" }, children: r.qty }), _jsx("td", { style: { border: "1px solid #d0d7de", padding: "2px 10px" }, children: r.px }), _jsx("td", { style: { border: "1px solid #d0d7de", padding: "2px 10px" }, children: r.calls })] }, key)) })] }), feed.error != null && _jsxs("div", { style: { color: "#cf222e" }, children: ["error: ", String(feed.error)] })] });
|
|
306
|
+
};
|
|
307
|
+
export const ReplayStoreEachDemo = () => {
|
|
308
|
+
const demo = useMemo(() => getStoreEachDemo(), []);
|
|
309
|
+
const [gen, setGen] = useState(0);
|
|
310
|
+
return _jsxs("div", { style: { display: "grid", gap: 8 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }, children: [_jsx("button", { onClick: demo.add, children: "server add row" }), _jsx("button", { onClick: demo.del, children: "server delete row" }), _jsx("button", { onClick: demo.replaceAll, children: "server replace ALL (root replace)" }), _jsx("button", { onClick: () => setGen(v => v + 1), children: "remount client (fresh keyframe)" })] }), _jsx(StrictMode, { children: _jsx(EachClient, { remote: demo.remote }, gen) }), _jsx("div", { style: { fontSize: 12, color: "#57606a", maxWidth: 560 }, children: "per-key contract: the producer touches ONE random row per tick, so between your clicks only that row's \"cb calls\" counter grows \u2014 the whole dict is never re-delivered. keyframe / replace ALL are the only events that touch every row (expansion), delete arrives as (key, undefined)." })] });
|
|
311
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function TestParams(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Params } from "wenay-common2";
|
|
3
|
+
import { ParamsEditor } from "../src/components";
|
|
4
|
+
const getParams = () => {
|
|
5
|
+
return new class testParams extends Params.CParams {
|
|
6
|
+
test = { value: 1, range: { min: 1, max: 10, step: 1 } };
|
|
7
|
+
test2 = { value: 1, commentary: ["this test tttt 222"], range: { min: 1, max: 10, step: 1 } };
|
|
8
|
+
test3 = { value: 1, name: "t33", commentary: ["this test tttt 333"], range: { min: 1, max: 10, step: 1 } };
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
const paramsDef = getParams();
|
|
12
|
+
let params = Params.GetSimpleParams(paramsDef);
|
|
13
|
+
export function TestParams() {
|
|
14
|
+
return _jsx(ParamsEditor, { params: Params.mergeParamValuesToInfos(paramsDef, params), onChange: e => { } });
|
|
15
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
|
|
4
|
+
import { AgGridReact } from "ag-grid-react";
|
|
5
|
+
import { sleepAsync } from "wenay-common2";
|
|
6
|
+
import { updateBy } from "../updateBy";
|
|
7
|
+
import { contextMenu } from "../src/menu/menuMouse";
|
|
8
|
+
import { applyGridRows } from "../src/utils";
|
|
9
|
+
ModuleRegistry.registerModules([AllCommunityModule]);
|
|
10
|
+
export const tt = {};
|
|
11
|
+
export const GridExample = () => {
|
|
12
|
+
const gridApi = useRef(null);
|
|
13
|
+
const rowBuffer = useRef({});
|
|
14
|
+
updateBy(tt, () => {
|
|
15
|
+
const price = Math.round(Math.random() * 90000) + 10000;
|
|
16
|
+
applyGridRows({
|
|
17
|
+
gridRef: gridApi,
|
|
18
|
+
getId: e => String(e.make),
|
|
19
|
+
bufTable: rowBuffer.current,
|
|
20
|
+
newData: [{ make: "Tesla", model: "Model Y", price, electric: true }]
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
const [rowData] = useState([
|
|
24
|
+
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
|
|
25
|
+
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
|
|
26
|
+
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
|
|
27
|
+
{ make: "Mercedes", model: "EQA", price: 48890, electric: true },
|
|
28
|
+
{ make: "Fiat", model: "500", price: 15774, electric: false },
|
|
29
|
+
{ make: "Nissan", model: "Juke", price: 20675, electric: false },
|
|
30
|
+
]);
|
|
31
|
+
const [colDefs] = useState([
|
|
32
|
+
{ field: "make" },
|
|
33
|
+
{ field: "model" },
|
|
34
|
+
{ field: "price" },
|
|
35
|
+
{ field: "electric" },
|
|
36
|
+
]);
|
|
37
|
+
const defaultColDef = {
|
|
38
|
+
flex: 1,
|
|
39
|
+
};
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
sleepAsync(1000)
|
|
42
|
+
.then(() => {
|
|
43
|
+
applyGridRows({
|
|
44
|
+
gridRef: gridApi,
|
|
45
|
+
getId: e => String(e.make),
|
|
46
|
+
bufTable: rowBuffer.current,
|
|
47
|
+
newData: [{ make: "Tesla", price: 55555 }]
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}, []);
|
|
51
|
+
return (_jsxs("div", { style: { width: "100%", height: "100%" }, children: [_jsx("button", { onClick: () => applyGridRows({ gridRef: gridApi, getId: e => String(e.make), bufTable: rowBuffer.current, removeData: [{ make: "Tesla" }] }), children: "remove Tesla" }), _jsx(AgGridReact, { onGridReady: e => {
|
|
52
|
+
gridApi.current = e;
|
|
53
|
+
}, onCellMouseDown: (e) => {
|
|
54
|
+
const event = e.event;
|
|
55
|
+
if (event instanceof MouseEvent)
|
|
56
|
+
contextMenu.openAt(event, [
|
|
57
|
+
{
|
|
58
|
+
name: "test", onClick: () => { console.log("test"); }
|
|
59
|
+
}
|
|
60
|
+
]);
|
|
61
|
+
}, getRowId: e => e.data.make, rowData: rowData, columnDefs: colDefs, defaultColDef: defaultColDef })] }));
|
|
62
|
+
};
|
package/lib/style/style.css
CHANGED
|
@@ -1058,6 +1058,30 @@ body {
|
|
|
1058
1058
|
color: var(--cols-card-accent-color, #0969da);
|
|
1059
1059
|
white-space: nowrap;
|
|
1060
1060
|
}
|
|
1061
|
+
.wenayCardListFields {
|
|
1062
|
+
display: block;
|
|
1063
|
+
}
|
|
1064
|
+
.wenayCardListItem_compact .wenayCardListFields {
|
|
1065
|
+
display: grid;
|
|
1066
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1067
|
+
gap: 4px 8px;
|
|
1068
|
+
}
|
|
1069
|
+
.wenayCardListItem_compact .wenayCardListField {
|
|
1070
|
+
min-width: 0;
|
|
1071
|
+
padding: 2px 6px;
|
|
1072
|
+
border-radius: 5px;
|
|
1073
|
+
background: var(--cols-card-field-bg, #f6f8fa);
|
|
1074
|
+
line-height: 1.45;
|
|
1075
|
+
}
|
|
1076
|
+
.wenayCardListItem_compact .wenayCardListLabel {
|
|
1077
|
+
min-width: 0;
|
|
1078
|
+
font-size: 10px;
|
|
1079
|
+
text-transform: uppercase;
|
|
1080
|
+
letter-spacing: .03em;
|
|
1081
|
+
}
|
|
1082
|
+
@media (max-width: 320px) {
|
|
1083
|
+
.wenayCardListItem_compact .wenayCardListFields { grid-template-columns: 1fr; }
|
|
1084
|
+
}
|
|
1061
1085
|
.wenayCardListField {
|
|
1062
1086
|
display: flex;
|
|
1063
1087
|
gap: var(--cols-card-field-gap, 10px);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wenay-react2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.46",
|
|
4
4
|
"description": "Common react",
|
|
5
5
|
"strict": true,
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"react": "^19.2.0",
|
|
25
25
|
"react-dom": "^19.2.0",
|
|
26
26
|
"react-rnd": "^10.5.3",
|
|
27
|
-
"wenay-common2": "^1.0.
|
|
27
|
+
"wenay-common2": "^1.0.74"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"react": "^19.2.0",
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
},
|
|
46
46
|
"exports": {
|
|
47
47
|
".": "./lib/index.js",
|
|
48
|
-
"./package.json": "./package.json"
|
|
48
|
+
"./package.json": "./package.json",
|
|
49
|
+
"./demo/peer-media": "./lib/common/demo/peerMedia.js",
|
|
50
|
+
"./demo/stand": "./lib/common/testUseReact/qa.js"
|
|
49
51
|
}
|
|
50
52
|
}
|