wenay-common2 1.0.75 → 1.0.76

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/doc/ROADMAP.md CHANGED
@@ -139,12 +139,13 @@ control channel; `authorize` = server-side `canExposeEndpoint`), `createWebRtcCo
139
139
  `acceptWebRtcDirect` (RTCPeerConnection injected as a runtime factory, structural types, no lib.dom),
140
140
  and `serveReplayChannel`/`channelReplayRemote` (replay wire over any ordered channel — the datachannel
141
141
  path bypasses the RPC core by design). Oracle `replay/route-webrtc.test.ts` drives promotion, endpoint
142
- denial, session rejection, and server revoke over both an in-proc hub and a real Socket.IO/RPC wire.
143
- Still open: browser/Node WebRTC glue (step 10 now a one-line `rtc` factory plus media re-emit) and
144
- the account-map lifecycle integration (step 7). Media-side candidates (2026-07-10, after the demo
145
- stand stress test): a real `transport:'webrtc'` media track SDP over the existing signal hub,
146
- media bypassing the socket relay for call-grade smoothness; and `MediaStreamTrackProcessor` capture
147
- for true 30fps (`grabFrame`'s ~50ms serial latency caps snapshot capture at ~15-20fps).
142
+ denial, session rejection, server revoke, and an encoded `Media` `Uint8Array` frame over both the
143
+ direct datachannel and relay fallback. v1.0.76 completes step 10 with a portable binary replay codec
144
+ and the browser/Node `rtc` factory seam; it also completes step 7 with the app-level
145
+ `noStrict(accountMap)` + `createStoreManager` selected-mirror lifecycle oracle. Media-side candidates
146
+ remain a real `transport:'webrtc'` native media track/SFU for call-grade smoothness and
147
+ `MediaStreamTrackProcessor` capture for true 30fps (`grabFrame`'s ~50ms serial latency caps snapshot
148
+ capture at ~15-20fps); they are performance adapters, not route-coordinator prerequisites.
148
149
 
149
150
  Consumption-layer candidates (2026-07-10, author's ask):
150
151
  - ✅ **Call system** — SHIPPED in v1.0.74: `Peer.createCallManager` (ring/accept/decline/hangup as
@@ -7,4 +7,4 @@
7
7
  - Replay subscribers on RPC remotes recover automatically from their own last delivered `seq`: live is restored first, racing envelopes queue, then frame/since catch-up is ordered and deduplicated. Queue policy keeps an ungated live line; producer-declared mini-frames/keyframes may cover a raw seq jump without a logical data gap.
8
8
  - Sacred queues remain exact: a retained journal replays every missing envelope, while eviction/no-keyframe is a terminal `onError`; queued events after the hole are never emitted as a false continuation.
9
9
  - Hard boundaries stay hard: `close()` / `dispose()` and hub `connect()` / `setToken()` terminate the old generation. Pending or attempted ordinary RPC/pipe calls fail on disconnect and are never retried, avoiding duplicated side effects.
10
- - Add real Socket.IO reconnect coverage for stable proxy identity, deduped multi-consumer recovery, repeated reconnect generations, sacred eviction, offline dispose, token rotation, and multicast lifecycle listeners.
10
+ - Add real Socket.IO reconnect coverage for stable proxy identity, deduped multi-consumer recovery, repeated reconnect generations, sacred eviction, offline dispose, token rotation, and multicast lifecycle listeners.
@@ -0,0 +1,4 @@
1
+ # 1.0.76
2
+
3
+ - Complete the route-coordinator account lifecycle target: `noStrict` runtime account maps compose with `Observe.createStoreManager` to start/stop selected replay mirrors independently; add the focused oracle.
4
+ - Preserve `Uint8Array` payloads in replay-over-datachannel with a portable binary codec, so encoded `Media` frames survive relay → direct WebRTC → relay hand-offs byte-for-byte; extend the real signaling oracle.
@@ -551,9 +551,11 @@ createSignalHub({authorize?}) -> {register(account) -> {send, signals, close}, r
551
551
  // one server-side policy point for endpoint material AND calls.
552
552
  createWebRtcConnector({port, rtc, self, peer, pair, session?, label?, openTimeoutMs?}) -> RouteConnector
553
553
  // the direct connector for createRouteCoordinator: open() drives offer/answer/ICE through the signal
554
- // port, waits for the datachannel, returns a replay wire over it. RTCPeerConnection is NOT bundled:
555
- // rtc is a runtime factory — browser `() => new RTCPeerConnection(cfg)`, Node werift/node-datachannel,
556
- // tests an in-proc fake (RtcPeerConnection/RtcDataChannel are structural types, no lib.dom).
554
+ // port, waits for the datachannel, returns a replay wire over it. RTCPeerConnection is NOT bundled:
555
+ // rtc is a runtime factory — browser `() => new RTCPeerConnection(cfg)`, Node werift/node-datachannel,
556
+ // tests an in-proc fake (RtcPeerConnection/RtcDataChannel are structural types, no lib.dom).
557
+ // The replay channel encodes Uint8Array explicitly, preserving Media frames byte-for-byte instead
558
+ // of JSON's numeric-key object conversion; native tracks/SFU are optional performance adapters.
557
559
  // revoke/close signals and channel death (incl. DURING open) fail loudly -> coordinator auto-fallback.
558
560
  acceptWebRtcDirect({port, rtc, self, serve, accept?}) -> close()
559
561
  // responder side: on offer, negotiates answer/ICE and serves serve(env) (exposeReplay(...) as is) into
@@ -195,8 +195,11 @@ me.onRoute(ev => {}) // route transitions for metrics/UI
195
195
  ```
196
196
  Key property: the relay journal stores the owner's envelopes VERBATIM (owner seq space), so a
197
197
  relay <-> direct hand-off is a plain seq resume — no uncovered loss or duplicate delivery. Late joiners
198
- get a keyframe folded server-side even while the owner is offline.
199
- Reconnect correctness is self-healing: a publisher gap makes the relay reject the push WITH its last
198
+ get a keyframe folded server-side even while the owner is offline.
199
+ The same replay datachannel preserves `Media` `Uint8Array` frames byte-for-byte, so a direct route
200
+ can feed existing `Media` Listen/replay consumers; native WebRTC tracks/SFU are optional future
201
+ performance adapters, not a second media semantic.
202
+ Reconnect correctness is self-healing: a publisher gap makes the relay reject the push WITH its last
200
203
  seq, and the client repairs from that coordinate automatically (`repair: 'tail'` lossless (default)
201
204
  | `'keyframe'` cheap reset for ephemeral state). Server declares journal semantics
202
205
  (`createPeerHost({gap: 'resume' | 'sacred'})`); a declared `journal: 'sacred'` TYPE-forbids the cheap
@@ -2,6 +2,55 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.serveReplayChannel = serveReplayChannel;
4
4
  exports.channelReplayRemote = channelReplayRemote;
5
+ const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
6
+ const REPLAY_BYTES = '__wenayReplayBytes';
7
+ function bytesToBase64(bytes) {
8
+ let out = '';
9
+ for (let i = 0; i < bytes.length; i += 3) {
10
+ const a = bytes[i];
11
+ const b = bytes[i + 1];
12
+ const c = bytes[i + 2];
13
+ out += BASE64[a >> 2];
14
+ out += BASE64[((a & 3) << 4) | ((b ?? 0) >> 4)];
15
+ out += b == null ? '=' : BASE64[((b & 15) << 2) | ((c ?? 0) >> 6)];
16
+ out += c == null ? '=' : BASE64[c & 63];
17
+ }
18
+ return out;
19
+ }
20
+ function base64ToBytes(text) {
21
+ const clean = text.replace(/=+$/, '');
22
+ const out = new Uint8Array(Math.floor(clean.length * 3 / 4));
23
+ let bits = 0;
24
+ let nBits = 0;
25
+ let at = 0;
26
+ for (const char of clean) {
27
+ const n = BASE64.indexOf(char);
28
+ if (n < 0)
29
+ throw new Error('replay channel: invalid binary payload');
30
+ bits = (bits << 6) | n;
31
+ nBits += 6;
32
+ if (nBits < 8)
33
+ continue;
34
+ nBits -= 8;
35
+ out[at++] = (bits >> nBits) & 255;
36
+ }
37
+ return out;
38
+ }
39
+ function stringifyMessage(value) {
40
+ return JSON.stringify(value, function encodeReplayBytes(_key, item) {
41
+ if (item instanceof Uint8Array)
42
+ return { [REPLAY_BYTES]: bytesToBase64(item) };
43
+ return item;
44
+ });
45
+ }
46
+ function parseMessage(raw) {
47
+ return JSON.parse(raw, function decodeReplayBytes(_key, item) {
48
+ if (item != null && typeof item == 'object' && Object.keys(item).length == 1 && typeof item[REPLAY_BYTES] == 'string') {
49
+ return base64ToBytes(item[REPLAY_BYTES]);
50
+ }
51
+ return item;
52
+ });
53
+ }
5
54
  function unsubscribeHandle(handle) {
6
55
  if (typeof handle == 'function') {
7
56
  handle();
@@ -22,11 +71,11 @@ function serveReplayChannel(source, channel) {
22
71
  : msg.m == 'frame' ? (source.frame ? await source.frame(msg.a[0], msg.a[1]) : null)
23
72
  : undefined;
24
73
  if (!closed)
25
- channel.send(JSON.stringify({ t: 'res', id: msg.id, ok: true, v: v ?? null }));
74
+ channel.send(stringifyMessage({ t: 'res', id: msg.id, ok: true, v: v ?? null }));
26
75
  }
27
76
  catch (e) {
28
77
  if (!closed)
29
- channel.send(JSON.stringify({ t: 'res', id: msg.id, ok: false, e: String(e) }));
78
+ channel.send(stringifyMessage({ t: 'res', id: msg.id, ok: false, e: String(e) }));
30
79
  }
31
80
  }
32
81
  const offMsg = channel.onMessage(function onReplayChannelMessage(raw) {
@@ -34,7 +83,7 @@ function serveReplayChannel(source, channel) {
34
83
  return;
35
84
  let msg;
36
85
  try {
37
- msg = JSON.parse(raw);
86
+ msg = parseMessage(raw);
38
87
  }
39
88
  catch {
40
89
  return;
@@ -42,7 +91,7 @@ function serveReplayChannel(source, channel) {
42
91
  if (msg?.t == 'sub' && !lineOff) {
43
92
  lineOff = source.line.on(function forwardEnvelope(ev) {
44
93
  if (!closed)
45
- channel.send(JSON.stringify({ t: 'ev', ev }));
94
+ channel.send(stringifyMessage({ t: 'ev', ev }));
46
95
  });
47
96
  return;
48
97
  }
@@ -70,7 +119,7 @@ function channelReplayRemote(channel) {
70
119
  channel.onMessage(function onRemoteChannelMessage(raw) {
71
120
  let msg;
72
121
  try {
73
- msg = JSON.parse(raw);
122
+ msg = parseMessage(raw);
74
123
  }
75
124
  catch {
76
125
  return;
@@ -108,7 +157,7 @@ function channelReplayRemote(channel) {
108
157
  return new Promise((resolve, reject) => {
109
158
  const id = nextId++;
110
159
  pending.set(id, { resolve, reject });
111
- channel.send(JSON.stringify({ t: 'req', id, m, a }));
160
+ channel.send(stringifyMessage({ t: 'req', id, m, a }));
112
161
  });
113
162
  }
114
163
  return {
@@ -7,6 +7,7 @@ import {
7
7
  flushReactive,
8
8
  managedStore,
9
9
  } from '../src/Common/Observe'
10
+ import {isNoStrict, noStrict} from '../src/Common/rcp/rpc-dynamic'
10
11
 
11
12
  let fails = 0
12
13
  const ok = (condition: any, message: string) => {
@@ -111,8 +112,49 @@ async function main() {
111
112
  ok(manager.handles.rows.status().state == 'stopped', 'stopAll closes offline resource')
112
113
  }
113
114
 
115
+ console.log('\n[store-manager] dynamic account-map lifecycle')
116
+ {
117
+ const sources = new Map<string, ReturnType<typeof createStore<Market>>>()
118
+ const remotes = noStrict(new Proxy({} as Record<string, any>, {
119
+ get(_target, key) {
120
+ if (typeof key != 'string') return undefined
121
+ let source = sources.get(key)
122
+ if (!source) {
123
+ source = createStore<Market>({data: {BTC: key.length}, meta: {status: 'relay'}}, {drain: 'micro'})
124
+ sources.set(key, source)
125
+ }
126
+ return exposeStoreReplay(source, {history: 20}).api.replay
127
+ },
128
+ }))
129
+ function accountResource(account: string) {
130
+ return managedStore.replay<Market>({
131
+ remote: remotes[account],
132
+ initial: {data: {}, meta: {}},
133
+ storeOpts: {drain: 'micro'},
134
+ })
135
+ }
136
+ const manager = createStoreManager({
137
+ alice: accountResource('alice'),
138
+ bob: accountResource('bob'),
139
+ })
140
+
141
+ ok(isNoStrict(remotes), 'runtime account map is explicitly noStrict, not a fixed schema')
142
+ const alice = await manager.start('alice')
143
+ ok(alice.state.data.BTC == 5 && manager.get('bob') == null, 'start selects one account mirror without materializing its peers')
144
+
145
+ const source = sources.get('alice')!
146
+ source.state.meta.status = 'direct'
147
+ await settle(source.state)
148
+ ok(alice.state.meta.status == 'direct', 'selected account mirror follows its replay route')
149
+
150
+ manager.stop('alice')
151
+ ok(manager.handles.alice.status().state == 'stopped' && manager.get('alice') != null,
152
+ 'stopping a selected account detaches its route while retaining its local mirror for reuse')
153
+ manager.stopAll()
154
+ }
155
+
114
156
  console.log(`\n${fails == 0 ? 'ALL GREEN' : fails + ' FAILURE(S)'}`)
115
157
  process.exit(fails == 0 ? 0 : 1)
116
158
  }
117
159
 
118
- main().catch(e => { console.error(e); process.exit(1) })
160
+ main().catch(e => { console.error(e); process.exit(1) })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wenay-common2",
3
- "version": "1.0.75",
3
+ "version": "1.0.76",
4
4
  "description": "Common library",
5
5
  "strict": true,
6
6
  "main": "lib/index.js",
@@ -16,6 +16,7 @@ import {
16
16
  } from '../src/Common/events/replay-index'
17
17
  import {createRpcClientHub} from '../src/Common/rcp/rpc-clientHub'
18
18
  import {createRpcServerAuto} from '../src/Common/rcp/rpc-server-auto'
19
+ import {decodeMediaFrame, encodeMediaFrame} from '../src/Common/media/media-source'
19
20
 
20
21
  let fails = 0
21
22
  const ok = (condition: any, message: string) => {
@@ -199,12 +200,24 @@ async function main() {
199
200
  await waitFor('datachannel live events', () => got.length >= 4)
200
201
  ok(json(got) == json([0, 1, 2, 3]), 'replay wire over the datachannel is gap-free and dup-free')
201
202
 
203
+ // A direct media route uses the same replay wire. Binary frames must stay
204
+ // Uint8Array end-to-end — JSON's default numeric-key object is unusable by Media.
205
+ const mediaFrame = encodeMediaFrame({
206
+ kind: 'video-frame', codec: 'jpeg', seq: 4, tMono: 40, width: 2, height: 2,
207
+ }, new Uint8Array([77, 101, 100, 105, 97]))
208
+ emit(mediaFrame as any)
209
+ await waitFor('binary frame over datachannel', () => got.length >= 5)
210
+ const deliveredFrame = got[4] as any
211
+ const decodedFrame = deliveredFrame instanceof Uint8Array ? decodeMediaFrame(deliveredFrame) : null
212
+ ok(decodedFrame?.kind == 'video-frame' && json([...decodedFrame.payload]) == json([77, 101, 100, 105, 97]),
213
+ 'direct replay preserves an encoded Media Uint8Array frame byte-for-byte')
214
+
202
215
  // server-side revoke: policy изменилась — direct закрывается, relay продолжает с seq
203
216
  hub.revoke(link.ref.key, ['a', 'b'], 'policy change')
204
217
  await waitFor('revoke fallback', () => link.state() == 'fallback')
205
218
  state = 4; emit(state)
206
219
  await waitFor('relay after revoke', () => got.includes(4))
207
- ok(json(got) == json([0, 1, 2, 3, 4]), 'server revoke: relay resumes from seq, no gap')
220
+ ok(json(got.filter(v => typeof v == 'number')) == json([0, 1, 2, 3, 4]), 'server revoke: relay resumes from seq, no gap')
208
221
 
209
222
  // сервер не раскрывает endpoint: offer отклонён ДО транспорта
210
223
  allowDirect = false
@@ -223,7 +236,7 @@ async function main() {
223
236
  const retry = await link.promoteDirect()
224
237
  state = 5; emit(state)
225
238
  await waitFor('direct again', () => got.includes(5))
226
- ok(retry.ok && link.state() == 'direct' && json(got) == json([0, 1, 2, 3, 4, 5]), 'pair recovers into direct after denials, still gap-free')
239
+ ok(retry.ok && link.state() == 'direct' && json(got.filter(v => typeof v == 'number')) == json([0, 1, 2, 3, 4, 5]), 'pair recovers into direct after denials, still gap-free')
227
240
 
228
241
  coord.close()
229
242
  stopAccept()
@@ -1,8 +0,0 @@
1
- # 1.0.66
2
-
3
- - Add `Media.createAudioSource` and `Media.createVideoSource` browser capture helpers that expose media as ordinary binary `Listen` sources.
4
- - Add fixed-header media frame helpers: `Media.encodeMediaFrame` and `Media.decodeMediaFrame` (`Uint8Array` header + raw payload, no JSON envelope).
5
- - Add optional `replay:true` media source mode so `createRpcServerAuto` can expose media listeners with legacy + replay surfaces under the same key.
6
- - Add `wenay-common2/media` package export and root `Media` namespace export.
7
- - Add `replay/media-socket.test.ts` covering media binary frame headers, no-device typed state, replay media source shape, and real Socket.IO/RPC binary delivery.
8
- - Document media-over-socket backpressure defaults and reserve WebRTC as a future explicit SFU/signaling opt-in, not the default transport.