wenay-react2 1.0.44 → 1.0.45

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,7 +2,7 @@
2
2
 
3
3
  Date: 2026-07-10
4
4
 
5
- Status: ready for publish.
5
+ Status: released.
6
6
 
7
7
  - Fixed: `createColumnState` no longer persists `visible=false` for a column
8
8
  hidden solely by runtime `presentGate` when another grid interaction is read
@@ -0,0 +1,9 @@
1
+ # v1.0.45 — Media and Peer React adapters
2
+
3
+ Status: ready for publication.
4
+
5
+ - Updated `wenay-common2` to `^1.0.73`.
6
+ - Added `useMediaSource` for capture lifecycle only; video/audio frames stay outside React state.
7
+ - Added `usePeer` as a thin Peer SDK adapter with mirrored store, route/status and explicit resync controls.
8
+ - Added QA stand cards 38–40 and focused hook tests.
9
+ - Verified with qa-check TypeScript, full Jest, package build, diff-check and the live stand; manual stand verification was confirmed by the user.
@@ -24,5 +24,5 @@ all known columns.
24
24
 
25
25
  ## Result
26
26
 
27
- Version v1.0.44 is prepared for publication; durable details are in
27
+ Published as `wenay-react2@1.0.44`; durable details are in
28
28
  `doc/changes/v1.0.44.md`.
@@ -0,0 +1,30 @@
1
+ # wenay-common2 1.0.73 adoption — progress
2
+
3
+ ## Update
4
+
5
+ - Previous dependency: 1.0.70.
6
+ - npm latest verified: 1.0.73.
7
+ - Updated dependency and lockfile to 1.0.73.
8
+ - Compatibility: `tsc -p tsconfig.qa-check.json --noEmit` and full Jest 21 suites / 68 tests pass.
9
+
10
+ ## Architecture decision
11
+
12
+ | common2 capability | wenay-react2 decision | Reason |
13
+ | --- | --- | --- |
14
+ | Peer gap repair / `resync()` (1.0.71) | Core-only protocol; future `usePeer` may expose status and explicit `resync` | React must not own journal or repair state. |
15
+ | Hidden-tab Media capture (1.0.72) | Adopt core defaults, no adapter | Browser worker/ImageCapture implementation belongs below React. |
16
+ | `Media.attachVideoCanvas` / audio player / publish pipe (1.0.73) | Recipe and QA consumer first; thin ref-lifecycle hooks only if repeated | Frame decode/playback must bypass React renders. |
17
+
18
+ ## Follow-up
19
+
20
+ - Keep the existing `useMediaSource`, `usePeer` and WebRTC recipe items in target.
21
+ - Add a real Media/Peer QA consumer before introducing any new hook.
22
+ - No raw route-coordinator hook: Peer mirrors already survive route changes.
23
+ ## Implementation batch
24
+
25
+ - Added `useMediaSource`: React owns source start/stop/device lifecycle only; frames remain on the common2 Listen path.
26
+ - Added `usePeer`: React exposes an SDK peer's mirrored store, low-frequency route state and explicit controls; it owns no repair or transport state.
27
+ - QA cards 38/39/40 cover camera canvas viewer, microphone player and an in-process Peer host/mirror/resync scenario.
28
+ - Verification: qa-check TypeScript, Jest 22 suites / 71 tests, package build, diff-check and live stand HTTP 200.
29
+ - Manual Verify: run cards 38, 39, 40 together with existing replay cards 23 and 34 before release.
30
+ - Manual verification completed by the user on the stand; release candidate is 1.0.45.
package/doc/target/my.md CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  Правила ведения: [README.md](README.md). Ниже — статус по задачам; **исходные надиктовки сохранены дословно в конце файла** (ничего не удаляю).
4
4
 
5
- ## In Progress
6
-
7
- _Пусто._
5
+ ## In Progress
6
+
7
+ _Пусто._
8
+
8
9
 
9
10
  «Hook extraction — do-now кандидаты (3)» выполнен 2026-07-09: `useCacheMapPersistence` (cache.ts), `useResizeObserver`/`useElementSize` (MyResizeObserver.tsx), `useLogsPageTable` (logs.tsx); старые имена — compatibility wrappers, card 25 не тронута. Проверено: tsc qa-check, jest 45/45, build, стенд 3010 (карточки 21 F5-persistence / 19 resize / 9 logs). Durable-запись: `doc/changes/v1.0.41.md`; parked/отклонённые кандидаты остаются в `doc/progress/hook-extraction-audit.md`.
10
11
 
@@ -18,12 +19,13 @@ _Пусто._
18
19
  - Оценка: эффективность high (самый дешёвый рост юзабельности), простота easy/medium (докосновно + один реэкспорт-модуль), риск low (аддитивно, публичный API не меняется).
19
20
  - Связка: breaking-версия (A3 + снос deprecated `onCLickClose`/`keySave`/`DragArea`/`menuR`, сужение `ListenApi`) идёт ПОСЛЕ витрины — витрина и есть список того, что сносим.
20
21
 
21
- - **common2 1.0.66–1.0.68 adoption: Media + route coordinator/WebRTC React surface** (из надиктовки №4 «посмотри, что нового, создай цели использовать»).
22
+ - **common2 1.0.66–1.0.73 adoption: Media + Peer/route React surface** (из надиктовки №4 «посмотри, что нового, создай цели использовать»).
22
23
  - Контекст: 1.0.66 = `Media` (захват мик/камера как бинарные Listen/replay-источники, `decodeMediaFrame`), 1.0.67 = `Replay.createRouteCoordinator` (policy-gated relay↔direct, link.subscribe без switchRoute), 1.0.68 = WebRTC direct-путь (`createSignalHub` по существующему RPC-сокету, `createWebRtcConnector` с инжектируемой `rtc`-фабрикой — инжекция `() => new RTCPeerConnection(cfg)` живёт на браузерной/React-стороне, `acceptWebRtcDirect`, `serveReplayChannel`). Вместе: media → replay line → policy-маршрутизация relay/direct.
23
- - **1. `useMediaSource` (capture-control hook)** пересмотр прежнего решения «Media-хуков намеренно нет»: consumption по-прежнему через существующие `useReplay*`-хуки, но capture `control` (`start()` → `'idle'|'requesting'|'live'|'denied'|'no-device'|'error'`, `stop()`, `setDevice`, `listDevices`, stats) это permission/device/lifecycle state machine, hook-shaped. Хук: state как React state, `listen` наружу как есть, stop() на unmount. QA card: камера `replay:true` линия → canvas через `useReplaySubscribe`+`decodeMediaFrame`; deny/no-device видимы на стенде.
24
- - **2. Peer SDK adapter (supersedes the raw coordinator-link idea, common2 1.0.69)** — React surface over `Peer.createPeerClient`: `usePeer(client, account)` -> peer's mirrored store (plugs into existing `useStoreNode`/`useStoreKeys`) + route state/controls (`promoteDirect` result-object, `reinterposeRelay`, `route()`, `ready`, `seq()`). Do NOT bend existing route hooks; mirrors survive route changes by design. QA card modeled on oracle `replay/peer-sdk.test.ts` (fake in-proc RTC runtime); common2 1.0.70 ships the oracles + `demo/` stand in the package as living examples to crib from.
24
+ - **1. `useMediaSource` — DONE in 1.0.45.** Capture lifecycle only: state, start/stop, device selection and stats; frame consumption stays on Listen/replay. QA cards 38/39 use core canvas/audio viewers.
25
+ - **2. Peer SDK adapter DONE in 1.0.45.** `usePeer(client, account)` exposes the mirrored store, route/status and explicit controls including resync, without taking over protocol repair or transport. QA card 40 covers an in-process host/mirror/resync scenario.
25
26
  - **3. WebRTC recipe (docs/example, not a hook)** — how the app injects the `rtc` factory (`() => new RTCPeerConnection(cfg)`) and wires the signal port from `createRpcServerAuto`; combine with a Media source (call/stream demo) over Peer. Real-browser ICE serialization is fixed in 1.0.69 (`toJSON()` on the wire), so a live two-tab demo is feasible — see common2 `demo/` shared-cursors stand.
26
- - Оценка: 1 — simple/low-risk (аддитивно); 2 — medium (in-proc fake стенд, по образцу peer-sdk oracle); 3 — docs-first. Приоритет: после architecture-fix очереди A1-A10.
27
+ - Оценка: 1 — simple/low-risk (аддитивно); 2 — medium (in-proc fake стенд, по образцу peer-sdk oracle); 3 — docs-first. Приоритет: после architecture-fix очереди A1-A10.
28
+ - **Аудит 1.0.71–1.0.73 (2026-07-10, dependency обновлена до 1.0.73)**: `Peer` repair/resync (1.0.71) остаётся core protocol — React не должен повторять gap/journal state machine; `usePeer` может лишь отразить `peer(account)` store/route и вызвать `resync()` по app-owned transport reconnect. Media hidden-tab workers/ImageCapture (1.0.72) принимаем автоматически через core defaults, без React-кода. Media viewer helpers (1.0.73: canvas/audio/publish pipe) — core imperative hot path; React-кандидаты только тонкие lifecycle bindings над ref (`useMediaVideoCanvas`, `useMediaAudioPlayer`), без frame-level React state. Сначала docs/recipe + QA card с реальным consumer; хук — лишь если recipe повторяется.
27
29
 
28
30
  - **Context menu / Menu: controller-first split**.
29
31
  - Прогресс/оценка: [../progress/hook-controller-opportunities.md](../progress/hook-controller-opportunities.md)
@@ -81,7 +83,7 @@ _Пусто._
81
83
 
82
84
  ## Verify
83
85
 
84
- - **ColumnState present-gate не должен перезаписывать пользовательскую visibility** реализация и release v1.0.44 готовы; [progress](../progress/column-state-present-gate.md), [changelog](../changes/v1.0.44.md). Проверить публикацию пакета и потребительский repro: gated `b` остаётся `visible:true` после resize другой колонки и становится видимой при открытии gate.
86
+ _Пусто._ «ColumnState present-gate» выпущен как v1.0.44: repro сначала подтвердил утечку `visible.b=false` после чужого resize под закрытым gate; фикс читает visibility только для ключей, проходящих gate; targeted Jest 3/3, полный Jest 64/64, tsc и build прошли; пакет опубликован. Durable-запись: `doc/changes/v1.0.44.md`; рабочая история: `doc/progress/column-state-present-gate.md`.
85
87
 
86
88
  «Система стилей и CSS variables» проверена: `doneInCode=fully`, `.wenayColsMenu*`/`--cols-menu-*` и `--cols-dots-*`/`--cols-card-*` в коде, changelog v1.0.38/v1.0.39/v1.0.41, отгружено в релизах. Удалено; durable-запись — `doc/progress/style-system-normalization.md`.
87
89
 
@@ -472,9 +472,9 @@ const tt = useReplayHistory(history, (frame) => draw(frame), {head: () => replay
472
472
  tt.live; tt.seq; tt.head; tt.pause(); tt.play(); tt.seek({seq})
473
473
  ```
474
474
 
475
- Route hand-off here is the MANUAL surface (`switchRoute`). common2 1.0.67 `Replay.createRouteCoordinator` moves route decisions (policy, promote/fallback/shadow) out of the consumer; a coordinator `link.subscribe(cb)` handle has the same shape (`ready, seq(), label(), active()`) and survives every route change, so components consuming a coordinator link do not need `switchRoute` at all. common2 1.0.68 supplies the direct transport for it: `createWebRtcConnector` with an app-injected `rtc: () => new RTCPeerConnection(cfg)` factory (the browser — i.e. the React app — owns that injection) and signaling over the existing RPC socket (`createSignalHub`). common2 1.0.69 wraps the whole stack into the `Peer` SDK (`wenay-common2/peer`): `createPeerClient(...).peer(account)` returns a live mirrored store (works with `useStoreNode`/`useStoreKeys` as-is) + route control, surviving relay<->direct hand-offs in one seq space. A dedicated `usePeer` adapter is a target-backlog item, not yet a library surface.
475
+ Route hand-off here is the MANUAL surface (`switchRoute`). common2 1.0.67 `Replay.createRouteCoordinator` moves route decisions (policy, promote/fallback/shadow) out of the consumer; a coordinator `link.subscribe(cb)` handle has the same shape (`ready, seq(), label(), active()`) and survives every route change, so components consuming a coordinator link do not need `switchRoute` at all. common2 1.0.68 supplies the direct transport for it: `createWebRtcConnector` with an app-injected `rtc: () => new RTCPeerConnection(cfg)` factory (the browser — i.e. the React app — owns that injection) and signaling over the existing RPC socket (`createSignalHub`). common2 1.0.69 wraps the whole stack into the `Peer` SDK (`wenay-common2/peer`): `createPeerClient(...).peer(account)` returns a live mirrored store (works with `useStoreNode`/`useStoreKeys` as-is) + route control, surviving relay<->direct hand-offs in one seq space. `usePeer(client, account)` is the thin React adapter: it returns that mirror plus low-frequency route/status and explicit route/resync controls; it does not own journal, repair or transport state.
476
476
 
477
- Media lines (common2 1.0.66 `Media.createAudioSource` / `Media.createVideoSource`) are ordinary binary Listen sources; with `replay:true` their `listen` is a replay line, so `useReplaySubscribe` / `useReplayFrame` consume mic/camera frames with no media-specific hook. Each frame is one `Uint8Array` (`Media.decodeMediaFrame`); draw/play it via ref (canvas, AudioContext), never useState — the same rule as any high-frequency line. Without `replay`, the plain `listen` works with the listen hooks above.
477
+ Media lines (common2 1.0.66 `Media.createAudioSource` / `Media.createVideoSource`) are ordinary binary Listen sources; with `replay:true` their `listen` is a replay line, so `useReplaySubscribe` / `useReplayFrame` consume mic/camera frames with no media-specific hook. `useMediaSource(kind, options)` is only the capture lifecycle adapter (`start`, `stop`, device selection, state and stats); it stops a started source on unmount and returns `listen` unchanged. Each frame is one `Uint8Array` (`Media.decodeMediaFrame`); draw/play it via ref (canvas, AudioContext), never useState — the same rule as any high-frequency line. Without `replay`, the plain `listen` works with the listen hooks above.
478
478
 
479
479
  Contract: `off()` on unmount, StrictMode-safe; seq survives resubscribes inside one mount (keepSeq, default on) — a resubscribe reconnects with `{since}` and gets the journal tail, not a keyframe. Across a FULL unmount/remount keep the position outside via `onSeq` and pass it back as `since`. `seq()` is a getter — high-frequency lines (video frames, ticks) do not re-render per event; draw to canvas via ref, bypassing VDOM. Freshness: detection lives in wenay-common2 (`staleMs` watchdog); the non-route hooks mirror its edge-triggered `onStale` into `stale`, so a fresh 100 ev/s line causes zero extra renders. Route hand-off is explicit through `switchRoute(nextRemote, {label?, since?, reset?, policy?, hint?})`: old route stays live while the replacement catches up by `seq`, then closes. `useReplayHistory` is archive playback — staleness does not apply. QA cards 23 (video line + conflation + time travel + freshness), 24 (store sync), 25 (per-key feed), and 26 (route hand-off) are the live examples.
480
480
 
@@ -1,7 +1,9 @@
1
+ export * from './useReplay';
2
+ export * from './useObserveStore';
3
+ export * from './useOutside';
1
4
  export * from './useKeyboard';
2
5
  export * from './useDraggable';
3
6
  export * from './useReorder';
4
7
  export * from './useReorderBoard';
5
- export * from './useObserveStore';
6
- export * from './useReplay';
7
- export * from './useOutside';
8
+ export * from './useMedia';
9
+ export * from './usePeer';
@@ -1,7 +1,9 @@
1
+ export * from './useReplay';
2
+ export * from './useObserveStore';
3
+ export * from './useOutside';
1
4
  export * from './useKeyboard';
2
5
  export * from './useDraggable';
3
6
  export * from './useReorder';
4
7
  export * from './useReorderBoard';
5
- export * from './useObserveStore';
6
- export * from './useReplay';
7
- export * from './useOutside';
8
+ export * from './useMedia';
9
+ export * from './usePeer';
@@ -0,0 +1,14 @@
1
+ import { Media } from "wenay-common2";
2
+ export type UseMediaSourceController = {
3
+ source: Media.MediaSource;
4
+ listen: Media.MediaAnyListen;
5
+ state: Media.MediaSourceState;
6
+ start(): Promise<Media.MediaSourceState>;
7
+ stop(): void;
8
+ setDevice(id: string): Promise<Media.MediaSourceState>;
9
+ listDevices(): Promise<Media.MediaSourceDevice[]>;
10
+ stats(): Media.MediaStats;
11
+ };
12
+ /** React lifecycle adapter for a common2 Media source. Frames stay on `listen`:
13
+ * draw/play them through a canvas or AudioContext, never through React state. */
14
+ export declare function useMediaSource(kind: "audio" | "video", options?: Media.AudioSourceOpts | Media.VideoSourceOpts): UseMediaSourceController;
@@ -0,0 +1,39 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Media } from "wenay-common2";
3
+ /** React lifecycle adapter for a common2 Media source. Frames stay on `listen`:
4
+ * draw/play them through a canvas or AudioContext, never through React state. */
5
+ export function useMediaSource(kind, options = {}) {
6
+ const sourceRef = useRef(null);
7
+ if (!sourceRef.current)
8
+ sourceRef.current = kind == "video"
9
+ ? Media.createVideoSource(options)
10
+ : Media.createAudioSource(options);
11
+ const source = sourceRef.current;
12
+ const [state, setState] = useState(source.state);
13
+ const sync = useCallback(() => setState(source.state), [source]);
14
+ const start = useCallback(async () => {
15
+ const next = await source.start();
16
+ setState(next);
17
+ return next;
18
+ }, [source]);
19
+ const stop = useCallback(() => {
20
+ source.stop();
21
+ sync();
22
+ }, [source, sync]);
23
+ const setDevice = useCallback(async (id) => {
24
+ const next = await source.setDevice(id);
25
+ setState(next);
26
+ return next;
27
+ }, [source]);
28
+ useEffect(() => () => source.stop(), [source]);
29
+ return useMemo(() => ({
30
+ source,
31
+ listen: source[1],
32
+ state,
33
+ start,
34
+ stop,
35
+ setDevice,
36
+ listDevices: () => source.listDevices(),
37
+ stats: () => source.getStats(),
38
+ }), [source, state, start, stop, setDevice]);
39
+ }
@@ -0,0 +1,16 @@
1
+ import { Peer } from "wenay-common2";
2
+ /** Thin React view over common2 Peer SDK. The SDK keeps route/repair ownership;
3
+ * consumers use `store` with useStoreNode/useStoreKeys as usual. */
4
+ export declare function usePeer<T extends object>(client: Peer.PeerClient<T>, account: string): {
5
+ store: import("wenay-common2/lib/Common/Observe").Store<T>;
6
+ ready: boolean;
7
+ route: string;
8
+ state: import("wenay-common2/lib/Common/events/route-coordinator").tRouteState;
9
+ seq: () => number;
10
+ promoteDirect: (opts?: import("wenay-common2/lib/Common/events/route-coordinator").PromoteDirectOpts) => Promise<import("wenay-common2/lib/Common/events/route-coordinator").RouteOpResult>;
11
+ reinterposeRelay: (reason?: unknown) => Promise<import("wenay-common2/lib/Common/events/route-coordinator").RouteOpResult>;
12
+ fallback: (reason?: unknown) => Promise<import("wenay-common2/lib/Common/events/route-coordinator").RouteOpResult>;
13
+ block: (reason?: unknown) => Promise<import("wenay-common2/lib/Common/events/route-coordinator").RouteOpResult>;
14
+ resync: () => Promise<void>;
15
+ close: () => void;
16
+ };
@@ -0,0 +1,29 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ /** Thin React view over common2 Peer SDK. The SDK keeps route/repair ownership;
3
+ * consumers use `store` with useStoreNode/useStoreKeys as usual. */
4
+ export function usePeer(client, account) {
5
+ const peer = useMemo(() => client.peer(account), [client, account]);
6
+ const [epoch, setEpoch] = useState(0);
7
+ const [ready, setReady] = useState(false);
8
+ useEffect(() => {
9
+ let alive = true;
10
+ setReady(false);
11
+ peer.ready.then(() => { if (alive)
12
+ setReady(true); });
13
+ const off = client.onRoute(() => setEpoch(v => v + 1));
14
+ return () => { alive = false; off(); };
15
+ }, [client, peer]);
16
+ return useMemo(() => ({
17
+ store: peer.store,
18
+ ready,
19
+ route: peer.route(),
20
+ state: peer.state(),
21
+ seq: peer.seq,
22
+ promoteDirect: peer.promoteDirect,
23
+ reinterposeRelay: peer.reinterposeRelay,
24
+ fallback: peer.fallback,
25
+ block: peer.block,
26
+ resync: client.resync,
27
+ close: peer.close,
28
+ }), [client, peer, ready, epoch]);
29
+ }
package/package.json CHANGED
@@ -1,50 +1,50 @@
1
- {
2
- "name": "wenay-react2",
3
- "version": "1.0.44",
4
- "description": "Common react",
5
- "strict": true,
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "files": [
9
- "lib/**/*",
10
- "doc/**/*",
11
- "!**/*.tsbuildinfo"
12
- ],
13
- "author": "wenay",
14
- "license": "ISC",
15
- "peerDependenciesMeta": {
16
- "react-dom": {
17
- "optional": true
18
- }
19
- },
20
- "dependencies": {
21
- "ag-grid-community": "^35.1.0",
22
- "ag-grid-react": "^35.1.0",
23
- "re-resizable": "^6.11.2",
24
- "react": "^19.2.0",
25
- "react-dom": "^19.2.0",
26
- "react-rnd": "^10.5.3",
27
- "wenay-common2": "^1.0.70"
28
- },
29
- "peerDependencies": {
30
- "react": "^19.2.0",
31
- "react-dom": "^19.2.0"
32
- },
33
- "engines": {
34
- "node": ">=18",
35
- "vscode": "^1.22.0"
36
- },
37
- "browserslist": {
38
- "production": [
39
- ">0.2%",
40
- "not dead"
41
- ],
42
- "development": [
43
- ">1%"
44
- ]
45
- },
46
- "exports": {
47
- ".": "./lib/index.js",
48
- "./package.json": "./package.json"
49
- }
50
- }
1
+ {
2
+ "name": "wenay-react2",
3
+ "version": "1.0.45",
4
+ "description": "Common react",
5
+ "strict": true,
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "lib/**/*",
10
+ "doc/**/*",
11
+ "!**/*.tsbuildinfo"
12
+ ],
13
+ "author": "wenay",
14
+ "license": "ISC",
15
+ "peerDependenciesMeta": {
16
+ "react-dom": {
17
+ "optional": true
18
+ }
19
+ },
20
+ "dependencies": {
21
+ "ag-grid-community": "^35.1.0",
22
+ "ag-grid-react": "^35.1.0",
23
+ "re-resizable": "^6.11.2",
24
+ "react": "^19.2.0",
25
+ "react-dom": "^19.2.0",
26
+ "react-rnd": "^10.5.3",
27
+ "wenay-common2": "^1.0.73"
28
+ },
29
+ "peerDependencies": {
30
+ "react": "^19.2.0",
31
+ "react-dom": "^19.2.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=18",
35
+ "vscode": "^1.22.0"
36
+ },
37
+ "browserslist": {
38
+ "production": [
39
+ ">0.2%",
40
+ "not dead"
41
+ ],
42
+ "development": [
43
+ ">1%"
44
+ ]
45
+ },
46
+ "exports": {
47
+ ".": "./lib/index.js",
48
+ "./package.json": "./package.json"
49
+ }
50
+ }