wenay-common2 1.0.74 → 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 +7 -6
- package/doc/changes/1.0.75.md +10 -0
- package/doc/changes/1.0.76.md +4 -0
- package/doc/wenay-common2-rare.md +38 -16
- package/doc/wenay-common2.md +438 -427
- package/lib/Common/events/replay-channel.js +55 -6
- package/lib/Common/events/replay-route.js +5 -2
- package/lib/Common/events/replay-wire.js +137 -47
- package/lib/Common/events/transport-lifecycle.d.ts +23 -0
- package/lib/Common/events/transport-lifecycle.js +94 -0
- package/lib/Common/rcp/rpc-client.js +346 -111
- package/lib/Common/rcp/rpc-clientHub.d.ts +2 -0
- package/lib/Common/rcp/rpc-clientHub.js +120 -25
- package/lib/Common/rcp/rpc-server.js +33 -9
- package/observe/store-manager.test.ts +43 -1
- package/oracle/realsocket/lifecycle.spec.ts +3 -3
- package/oracle/realsocket/replay-reconnect-auth.spec.ts +111 -0
- package/oracle/realsocket/replay-reconnect.spec.ts +592 -0
- package/package.json +1 -1
- package/replay/route-webrtc.test.ts +15 -2
- package/rpc.md +19 -4
- package/doc/changes/1.0.65.md +0 -8
- package/doc/changes/1.0.66.md +0 -8
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,
|
|
143
|
-
|
|
144
|
-
the
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
for true 30fps (`grabFrame`'s ~50ms serial latency caps snapshot
|
|
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
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# 1.0.75
|
|
2
|
+
|
|
3
|
+
- Stable RPC path identity: `func`, `space`, and `strict` cache child proxies per client instance, semantic surface, and collision-safe path; `all === func`, dotted keys stay distinct, and the same client keeps identity through schema refresh, reauth, and transient reconnect.
|
|
4
|
+
- Additive hub lifecycle listeners: `connectListen(cb)` / `disconnectListen(cb)` return individual off functions while legacy `onConnect(fn | null)` / `onDisconnect(fn | null)` retain their single-slot behavior. Internal recovery is independent from every public callback slot.
|
|
5
|
+
- Server-side HELLO barrier serializes concurrent STRICT/CALL/PIPE behind async auth resolution, so reconnect cannot resubscribe against a pre-auth facade; compatibility with older servers and clients is unchanged.
|
|
6
|
+
- Transient Socket.IO reconnect through `createRpcClientHub` now preserves logical deduped RPC Listen subscriptions and their local consumers, retires the dead physical generation, and creates exactly one replacement wire subscription after the new handshake. Unsubscribing offline prevents resurrection.
|
|
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
|
+
- 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
|
+
- 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.
|
|
@@ -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.
|
|
@@ -150,16 +150,27 @@ createRpcServerAuto(opts) // canonical: nested object
|
|
|
150
150
|
// path stays byte-for-byte (Pkt.MAP only grows additively), plus line/frameLine/since/keyframe/frame — so
|
|
151
151
|
// upgrading listen -> replayListen is a declaration-site-only change; replaySubscribe(client.func.key) works as is.
|
|
152
152
|
// opts.replayOpts {pending?, highWater?, lowWater?, pollMs?} arms the per-connection lag gate on frameLine
|
|
153
|
-
// (consumer picks policy 'queue'|'frame' at subscribe time); the replay `line`
|
|
153
|
+
// (consumer picks policy 'queue'|'frame' at subscribe time); the replay `line` stays ungated for connected queue-policy live delivery.
|
|
154
154
|
createRpcServer(opts) // lower-level core
|
|
155
155
|
createRpcServerAutoDetect(opts) // + legacy/v2 protocol auto-detection (createRpcServerAutoWithProtocolDetection)
|
|
156
156
|
createRpcServerInProc(...) // in-process fast path (no socket)
|
|
157
157
|
// clients
|
|
158
|
-
createRpcClientHub(opts) + rpc // multiplexing client hub: connect(token)/reauth(token)/onConnect
|
|
159
|
-
// alias: hub.setToken->connect
|
|
158
|
+
createRpcClientHub(opts) + rpc // multiplexing client hub: connect(token)/reauth(token)/onConnect/onDisconnect + connectListen/disconnectListen
|
|
159
|
+
// alias: hub.setToken->connect. onConnect/onDisconnect are legacy single-slot setters; the additive
|
|
160
|
+
// *Listen registries return per-listener off functions and cannot overwrite each other or internal recovery.
|
|
161
|
+
// A transient disconnect/reconnect of the SAME Socket.IO object keeps this client generation: after
|
|
162
|
+
// the new route/auth handshake, each active deduped logical Listen gets exactly one new physical attempt.
|
|
163
|
+
// Local consumers and their off handles survive; a consumer removed offline is not resurrected.
|
|
164
|
+
// connect()/setToken() hard-rotates the socket/client generation; close()/dispose() is terminal.
|
|
160
165
|
client members: func (proxy) · strict (schema-safe) · schema() · auth() · reauth() · onDisconnect()
|
|
161
166
|
close(reason?, {socketAlive?}) · ready() · init(obj?) · api.subscriptions()
|
|
162
167
|
// alias: dispose->close · readyStrict->ready · initStrict->init
|
|
168
|
+
// func/space/strict child proxies are cached per createRpcClient instance + surface + lossless RPC path:
|
|
169
|
+
// c.func.a.b === c.func.a.b and c.all === c.func; dotted keys remain distinct from segmented paths.
|
|
170
|
+
// Identity survives strict-schema refresh, reauth and transient reconnect. Hard token rotation creates
|
|
171
|
+
// a fresh client/cache; incompatible wait/surface semantics never share a proxy.
|
|
172
|
+
// Only Listen subscriptions are transport-resumed. Pending/attempted ordinary RPC and pipe calls reject
|
|
173
|
+
// on disconnect and are NEVER replayed: retrying an arbitrary command could repeat side effects.
|
|
163
174
|
noStrict(obj) / isNoStrict(obj) // dynamic (no-schema) subtree
|
|
164
175
|
endCallback(fn) // alias: rpcEndCallback
|
|
165
176
|
// subscription primitives (rare/manual; createRpcServerAuto/createRpcClientHub are the normal path)
|
|
@@ -458,28 +469,36 @@ withReplayListen(base, {current?, frame?, history?, getSince?, onJournal?, now?,
|
|
|
458
469
|
// sugar `current: 'last'` — single-entity lines: keyframe = last journaled envelope, no hand-kept state.
|
|
459
470
|
// Source 2 (mini-frame): `frame` lambda — gets the raw tail, returns a state-equivalent compact
|
|
460
471
|
// (last-per-entity, gap aggregate...); cost ~ touched entities, wins over keyframe while the journal covers.
|
|
461
|
-
// Line classes FOLLOW from declared lambdas (no mode flags): current+frame = condensable
|
|
462
|
-
//
|
|
472
|
+
// Line classes FOLLOW from declared lambdas (no mode flags): current+frame = condensable with keyframe fallback;
|
|
473
|
+
// current only = exact retained tail + keyframe fallback; frame without current condenses only while the tail
|
|
474
|
+
// is retained and still fails on eviction; neither = sacred exact queue — eviction THROWS terminally.
|
|
463
475
|
// Triggers: reconnect (`since`), client pull (own timer — replaces any server-side interval mode), server gate drain.
|
|
464
476
|
// The transport sees ONLY seq; entity keys/skip rules live in producer lambdas (hint = opaque per-subscriber pass-through).
|
|
465
477
|
exposeReplay(replay) <-> replaySubscribe(remote, cb, {since?, onSeq?, staleMs?, onStale?, skewMs?, now?, policy?, hint?}) -> off // wire pair over the EXISTING rpc: line = plain Listen, since/keyframe/frame = plain methods
|
|
466
478
|
// NORMAL PATH: createRpcServerAuto exposes replay listens automatically (see rpc section) — exposeReplay stays
|
|
467
479
|
// as the manual/custom-transport path. replaySubscribe prefers `frame` when the server has it (one round trip,
|
|
468
480
|
// server picks tail/mini-frame/keyframe; sacred throw -> onError), falls back to since/keyframe on old servers.
|
|
469
|
-
// policy: 'queue' (default —
|
|
470
|
-
//
|
|
481
|
+
// policy: 'queue' (default — ungated connected live line) | 'frame' (subscribes frameLine when present: on lag
|
|
482
|
+
// the server may drop and recover via a state-equivalent frame(lastSent)). Catch-up is shared by both policies
|
|
483
|
+
// and may use a producer mini-frame/keyframe, so a covered raw seq jump is not a logical data gap. A non-envelope
|
|
471
484
|
// on the line (RPC_STOP — e.g. the gate's loud sacred failure) surfaces via onError + off, never silence.
|
|
472
485
|
// hint reaches the frame lambda on catch-up and on every explicit frame(seq, hint) call (pull); the push-gate's
|
|
473
486
|
// drain recovery uses the line's DEFAULT condensation — client-specific rules/pace = the pull path.
|
|
474
|
-
// off.ready (
|
|
487
|
+
// off.ready (first handover done, or terminal error/teardown) · off.seq() (last honestly delivered coordinate) · off.isStale()/off.lastTs().
|
|
488
|
+
// Hub-managed RPC remotes resume automatically after transient reconnect from off.seq(): live is restored first,
|
|
489
|
+
// racing envelopes queue, then frame/since catch-up is sorted+deduped. Transport-agnostic remotes without RPC
|
|
490
|
+
// lifecycle metadata still reconnect by creating a new subscriber with {since: prev.seq()}.
|
|
475
491
|
replayRouteSubscribe(remote, cb, {label?, since?, onSeq?, onError?, onRoute?}) -> off & {ready, switch(nextRemote, {label?, since?, reset?, policy?, hint?}), seq(), label(), active()}
|
|
476
492
|
// transport hand-off helper: old route remains live, replacement subscribes+catches up from seq, then old closes; overlap is seq-deduped. Use for relay -> direct and direct -> relay over any ordered ReplayRemote.
|
|
477
493
|
// DELIVERY CONTRACT (guaranteed, not best-effort): the subscriber's cb sees ONE uniform stream —
|
|
478
494
|
// first delivery = the snapshot (keyframe as an event of the SAME type; store: root patch),
|
|
479
|
-
// then only strictly-newer events, seq-ascending
|
|
495
|
+
// then only strictly-newer events, seq-ascending and deduped. A raw seq jump is valid only when a
|
|
496
|
+
// producer-declared mini-frame/keyframe is state-equivalent to the omitted range; sacred retained tails
|
|
497
|
+
// are exact. Live events racing ahead of the
|
|
480
498
|
// keyframe over the wire are queued during catch-up and seq-deduped — they can NEVER arrive first.
|
|
481
499
|
// With {since: K}: same fold, journal tail after K instead of a keyframe (evicted -> keyframe fallback,
|
|
482
|
-
// visible
|
|
500
|
+
// visible as a covered seq jump > +1). A sacred eviction instead calls onError, closes the subscriber and
|
|
501
|
+
// never drains post-gap live events as a false continuation. Requires an ORDERED transport (socket.io / TCP / in-proc).
|
|
483
502
|
// Net effect: one client fold `state = apply(state, event)` handles cold start, reconnect,
|
|
484
503
|
// conflation recovery and archive playback identically — snapshot is not a special case.
|
|
485
504
|
// FRESHNESS (staleMs/onStale — an option, not consumer boilerplate): delivery is consistent but silent
|
|
@@ -496,7 +515,8 @@ createRouteCoordinator({connect, policy?, shadow?, catchUpTimeoutMs?}) -> coordi
|
|
|
496
515
|
// LAYERING: a CONNECTOR is a pure transport (no route decisions) — {info: {label, kind: 'relay'|'direct',
|
|
497
516
|
// binary?, ordered?, reliable?}, open() -> ReplayRemote, close(), state(), metrics?() -> {rtt?, pending?},
|
|
498
517
|
// onFail?}; the COORDINATOR alone owns promotion/fallback (state machine + policy); data continuity is
|
|
499
|
-
// replayRouteSubscribe underneath, so ANY route change is
|
|
518
|
+
// replayRouteSubscribe underneath, so ANY route change is deduped and has no uncovered logical gap;
|
|
519
|
+
// a producer-authorized frame/keyframe may still jump raw seq.
|
|
500
520
|
// WebRTC/NAT is deliberately absent here: a future datachannel is just another RouteConnector.
|
|
501
521
|
// connect(ref, kind) -> RouteConnector — transport factory per pair and kind (called per activation).
|
|
502
522
|
// policy hooks run BEFORE any transport action; absent hook = allowed, present hook must return true:
|
|
@@ -531,9 +551,11 @@ createSignalHub({authorize?}) -> {register(account) -> {send, signals, close}, r
|
|
|
531
551
|
// one server-side policy point for endpoint material AND calls.
|
|
532
552
|
createWebRtcConnector({port, rtc, self, peer, pair, session?, label?, openTimeoutMs?}) -> RouteConnector
|
|
533
553
|
// the direct connector for createRouteCoordinator: open() drives offer/answer/ICE through the signal
|
|
534
|
-
// port, waits for the datachannel, returns a replay wire over it. RTCPeerConnection is NOT bundled:
|
|
535
|
-
// rtc is a runtime factory — browser `() => new RTCPeerConnection(cfg)`, Node werift/node-datachannel,
|
|
536
|
-
// 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.
|
|
537
559
|
// revoke/close signals and channel death (incl. DURING open) fail loudly -> coordinator auto-fallback.
|
|
538
560
|
acceptWebRtcDirect({port, rtc, self, serve, accept?}) -> close()
|
|
539
561
|
// responder side: on offer, negotiates answer/ICE and serves serve(env) (exposeReplay(...) as is) into
|
|
@@ -630,7 +652,7 @@ openHistory(storage, live?) -> {at({seq?|ts?}?), subscribe(cb, {since?|ts?, onSe
|
|
|
630
652
|
// seamless rewind->live: create the line with getSince reading the same storage («memory outside»); else the gap closes with a keyframe jump (still consistent)
|
|
631
653
|
storeReplayAt(storage, {seq?|ts?}?) -> snapshot | undefined // store time machine: bit-exact state at any archived moment (same applyStorePatch mechanism)
|
|
632
654
|
```
|
|
633
|
-
> Killer property: a lagging/late/stalled consumer
|
|
655
|
+
> Killer property for state/frame lines: a lagging/late/stalled consumer can replace backlog with a state-equivalent frame/keyframe. A sacred queue deliberately does not: its retained tail is exact, and eviction is a terminal error rather than silent loss.
|
|
634
656
|
> Files: `src/Common/events/replay-{listen,wire,conflate,history,index}.ts` + `src/Common/Observe/store-{replay,offline}.ts`;
|
|
635
657
|
> everything is additive (the canonical Listen surface gained only `registerListenOn`/`ListenOnBrand`; exposeStore/mirror untouched).
|
|
636
658
|
> Oracles: `npx ts-node replay/<f>.ts` — replay-listen / store-replay / offline-store / socket-replay / offline-store-socket / conflate / conflate-socket / coalesce / history / staleness / canvas-socket (raw bytes) / video-socket.demo;
|
|
@@ -682,4 +704,4 @@ const facade = {
|
|
|
682
704
|
// createRpcServerAuto({ object: facade, ... }) exposes accountChanged/accountChangedPaths/btcChanged
|
|
683
705
|
// as normal RPC Listen subscriptions. This is a notification stream, not a full
|
|
684
706
|
// automatic snapshot mirror; send/read snapshots explicitly via facade methods.
|
|
685
|
-
```
|
|
707
|
+
```
|