wenay-common2 1.0.70 → 1.0.74

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.
Files changed (39) hide show
  1. package/README.md +5 -3
  2. package/demo/client.ts +230 -3
  3. package/demo/index.html +37 -0
  4. package/demo/server.ts +91 -4
  5. package/doc/INTENT.md +31 -0
  6. package/doc/ROADMAP.md +233 -212
  7. package/doc/changes/1.0.71.md +9 -0
  8. package/doc/changes/1.0.72.md +6 -0
  9. package/doc/changes/1.0.73.md +6 -0
  10. package/doc/changes/1.0.74.md +12 -0
  11. package/doc/wenay-common2-rare.md +684 -627
  12. package/doc/wenay-common2.md +50 -5
  13. package/lib/Common/events/route-signal-webrtc.d.ts +1 -1
  14. package/lib/Common/events/route-signal-webrtc.js +15 -5
  15. package/lib/Common/media/media-index.d.ts +1 -0
  16. package/lib/Common/media/media-index.js +1 -0
  17. package/lib/Common/media/media-source.d.ts +3 -0
  18. package/lib/Common/media/media-source.js +136 -27
  19. package/lib/Common/media/media-view.d.ts +42 -0
  20. package/lib/Common/media/media-view.js +210 -0
  21. package/lib/Common/peer/peer-call.d.ts +65 -0
  22. package/lib/Common/peer/peer-call.js +173 -0
  23. package/lib/Common/peer/peer-client.d.ts +21 -5
  24. package/lib/Common/peer/peer-client.js +53 -3
  25. package/lib/Common/peer/peer-host.d.ts +22 -5
  26. package/lib/Common/peer/peer-host.js +49 -5
  27. package/lib/Common/peer/peer-index.d.ts +2 -0
  28. package/lib/Common/peer/peer-index.js +2 -0
  29. package/lib/Common/peer/peer-media-relay.d.ts +22 -0
  30. package/lib/Common/peer/peer-media-relay.js +155 -0
  31. package/lib/Common/peer/peer-relay.d.ts +10 -2
  32. package/lib/Common/peer/peer-relay.js +40 -18
  33. package/observe/listen-core.test.ts +1 -2
  34. package/package.json +1 -4
  35. package/replay/media-view.test.ts +164 -0
  36. package/replay/peer-call.test.ts +226 -0
  37. package/replay/peer-repair.test.ts +184 -0
  38. package/doc/changes/1.0.63.md +0 -8
  39. package/doc/changes/1.0.64.md +0 -10
package/doc/ROADMAP.md CHANGED
@@ -1,212 +1,233 @@
1
- # wenay-common2 — Roadmap (open / deferred)
2
-
3
- > Forward-looking backlog of distributed-state / transport features that are **not fully built**.
4
- > Everything here sits on top of the existing seams — `{emit,on}` transport, `exposeStore` /
5
- > `createStoreMirror`, and the replay `seq` / `keyframe` / `frame` contract. None of it requires
6
- > changing the store core; the store stays single-authority (one `seq` sequencer, last-writer-wins
7
- > per path), and these features are layers or adapters above that.
8
- > Status: 🔴 not started · 🟡 partial / ongoing · 🧊 deferred (super-low priority, not forbidden).
9
- >
10
- > Current focus is NOT more transport: see `doc/target/library-uplift-tasks.md` (showcase, SDK facade,
11
- > vertical demo app). Transport items below stay available but rank below everything in that plan.
12
-
13
- ## 0. Distributed Runtime Model
14
-
15
- The strategic direction is not "RPC plus store helpers"; it is a small distributed-state runtime:
16
- application code keeps a stable typed API while lower layers can choose the transport, route, replay
17
- source, and authority model.
18
-
19
- Four concerns must stay explicit:
20
-
21
- - **Transport** - how two endpoints exchange `{emit,on}` messages: socket.io, in-process loopback,
22
- relay, WebRTC/direct channel, or a future adapter. Transport is replaceable infrastructure.
23
- - **Routing** - where a logical call or stream goes: server, peer through relay, peer directly, or a
24
- promoted relay <-> direct path. Routing must be allowed to change without changing the facade API.
25
- - **Authority** - who is allowed to write truth: one server, partitioned peer-owned slices, server
26
- authority with client prediction, or true multi-writer conflict resolution. This is a semantic
27
- decision, not a transport decision.
28
- - **Replay** - how live state survives reconnect, lag, transport swap, and history playback:
29
- `seq` + keyframe/frame + deltas. Replay is the continuity layer that makes route changes boring.
30
-
31
- Design rule: direct peer links and relay hand-offs are optimizations of transport/routing. They must
32
- not silently change auth, ownership, validation, or conflict semantics. If a method is exposed as
33
- `api.hit(playerId)`, it may travel through the server or a direct channel, but the authority rules
34
- behind that method must remain the same.
35
-
36
- This implies a useful split:
37
-
38
- - **API surface** remains facade-shaped and typed.
39
- - **Transport/routing layer** may substitute sockets and promote paths.
40
- - **Replay layer** resumes streams and mirrors from the last known `seq`.
41
- - **Authority layer** decides whether an incoming write is accepted, predicted, reconciled, or merged.
42
-
43
- Critical ordering rule: do **not** start with WebRTC/NAT plumbing or CRDTs. The next useful layer is a
44
- small route/account/policy coordinator with a fake/in-process transport adapter. Direct transport and
45
- multi-writer merge are expensive adapters; they only become safe after the coordinator state machine is
46
- boring and well-tested.
47
-
48
- ### 0.1 Account route coordinator ("wrapper over wrappers") 🟡
49
-
50
- Some deployments need separate client/account identities to communicate directly when policy and
51
- network conditions allow it, while still allowing the server/relay to step back into the path later.
52
- This is not just a socket trick; it is an account-aware routing shell above the existing facade,
53
- mirror, and replay primitives.
54
-
55
- - **Account model:** every participant has its own account/session identity and a scoped facade/store
56
- set. Runtime-account maps are a dynamic keyspace, so they should look like `noStrict(accountMap)`
57
- rather than a fixed schema. Access checks stay in the facade/policy layer; `noStrict` is not an ACL.
58
- - **Wrapper over wrappers:** an app-level coordinator owns the set of per-account clients, exposed
59
- facades, mirrors, replay subscriptions, and route state. "State of other accounts" is represented as
60
- selected `Observe` mirrors/replay/offline resources, ideally started and stopped through
61
- `createStoreManager`, not as a new global store core.
62
- - **Route states:** a pair of accounts may be `relay`, `direct`, `direct+shadowRelay` (audit/observe
63
- copy), `blocked`, or `fallback`. Direct links are optional optimizations, useful for latency or
64
- traffic cost, not a semantic change.
65
- - **Re-interposition:** if NDA/privacy policy, audit, moderation, throttling, reauth, direct-link
66
- failure, or group topology changes require it, the relay must be able to re-enter the data path.
67
- This is the reverse of direct promotion: open the replacement route, resume from the last `seq`,
68
- switch consumers after catch-up, then close or demote the old route.
69
- - **Privacy rule:** direct account links are opt-in and policy-gated. Peers only receive the endpoint
70
- and session material needed for that specific relationship; no implicit broad account discovery.
71
-
72
- Concrete next API shape, not final names:
73
-
74
- ```ts
75
- createRouteCoordinator({
76
- policy,
77
- routes,
78
- resources,
79
- }) -> {
80
- pair(a, b),
81
- state(pair),
82
- promoteDirect(pair, opts?),
83
- reinterposeRelay(pair, reason?),
84
- block(pair, reason?),
85
- fallback(pair, reason?),
86
- onRoute(cb),
87
- }
88
- ```
89
-
90
- Policy must run **before** transport promotion:
91
-
92
- - `canDirect(pair, ctx)` - may these accounts attempt direct at all?
93
- - `mustRelay(pair, ctx)` - force relay path because of NDA/audit/moderation/reauth.
94
- - `mustShadowRelay(pair, ctx)` - allow direct payload path, but keep audit/observe copy.
95
- - `canExposeEndpoint(pair, ctx)` - whether signaling may reveal endpoint/session material.
96
- - `canReinterpose(pair, ctx)` - whether/when relay is allowed or required to step back in.
97
-
98
- Minimum state machine:
99
-
100
- - `relay` -> `direct:connecting` -> `direct` -> `relay:reinterposing` -> `relay`
101
- - `relay` -> `direct:connecting` -> `fallback` -> `relay`
102
- - `direct` -> `direct+shadowRelay`
103
- - any state -> `blocked`
104
-
105
- Required failure modes:
106
-
107
- - direct setup never completes: keep relay active and mark fallback;
108
- - replacement route catches up too slowly: keep old route active and fail the switch;
109
- - policy changes mid-stream: re-interpose relay through replay hand-off;
110
- - account reauth changes facade/ACL: rebuild route policy before accepting new writes;
111
- - endpoint/session material is revoked: close direct and resume relay from `seq`;
112
- - audit/shadow route lags: decide whether to throttle, fallback, or block by policy.
113
-
114
- Acceptance tests for 0.1:
115
-
116
- - policy denial: direct is never attempted;
117
- - direct promotion: old relay stays live until replacement catches up;
118
- - failed direct: old relay continues with no data gap;
119
- - re-interposition: direct -> relay resumes from `seq`;
120
- - `direct+shadowRelay`: direct path is active while relay/audit mirror observes;
121
- - revocation: direct closes and relay resumes without changing facade API;
122
- - account map uses `noStrict`, but all access checks live in policy/facade code;
123
- - `createStoreManager` starts/stops selected per-account mirrors without store-core changes.
124
-
125
- Open questions: whether the relay sees payloads or only coordinates encrypted direct streams;
126
- backpressure across multi-hop and direct paths; group topology beyond a pair of accounts;
127
- `noStrict(accountMap)` / `createStoreManager` lifecycle integration for dynamic peer maps.
128
-
129
- Status: 🟡 partial (2026-07-09, v1.0.67). Core implemented as `Replay.createRouteCoordinator`
130
- (`src/Common/events/route-coordinator.ts`): `RouteConnector` contract (pure transport: open/close/state/
131
- metrics/onFail/capabilities), all five policy hooks, the full state machine above (including
132
- `direct+shadowRelay` audit copy, catch-up timeout, revocation auto-fallback, terminal `blocked`), data
133
- continuity through `replayRouteSubscribe`. Acceptance oracle: `replay/route-coordinator.test.ts` over
134
- fake in-process connectors — policy denial never touches transport, promotion keeps the old relay live,
135
- failed/slow direct falls back gap-free, re-interposition resumes from `seq`, shadow relay observes the
136
- switch window, revocation closes direct without facade changes, block is terminal.
137
- v1.0.68 added step 9: `createSignalHub` (offer/answer/ICE/session/revoke over the EXISTING socket/RPC
138
- control channel; `authorize` = server-side `canExposeEndpoint`), `createWebRtcConnector` /
139
- `acceptWebRtcDirect` (RTCPeerConnection injected as a runtime factory, structural types, no lib.dom),
140
- and `serveReplayChannel`/`channelReplayRemote` (replay wire over any ordered channel — the datachannel
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).
145
-
146
- ## 1. Connection hand-off relay direct promotion ("port forwarding") 🟡
147
-
148
- A relay/intermediary bootstraps a connection between two parties, then — on a signal — **steps out
149
- of the middle**: both ends open a second, direct socket and migrate the live stream onto it,
150
- bypassing the relay. The same mechanism must work in reverse: the relay can deliberately
151
- **re-interpose** and become the middleman again. Under the hood the socket substitutes itself; data
152
- starts flowing on the new path.
153
-
154
- - Family: NAT hole-punching / WebRTC TURN→direct promotion, expressed over the existing `{emit,on}`
155
- abstraction rather than a specific transport.
156
- - **Design lead (reuse what exists):** the socket swap is a *stream resume*, which the replay layer
157
- already solves. Migrate with `replaySubscribe(..., {since: prev.seq()})` /
158
- `syncStoreReplay(..., {since})` on the new socket — the consumer fold is gap-free by contract, so a
159
- mid-stream transport change is not a special case. The transport carries only `seq`; the semantics
160
- never learn the socket changed.
161
- - **Implemented foundation (2026-07-08):** `Replay.replayRouteSubscribe(...)` and
162
- `Observe.syncStoreReplayRoute(...)` keep the old route alive, subscribe the replacement route,
163
- catch it up from the last delivered `seq`, then close the old route. This covers relay → direct
164
- promotion and direct relay re-interposition for any ordered `ReplayRemote`; overlap is deduped by
165
- `seq`, and a failed replacement leaves the old route active.
166
- - Open questions: auth continuity across the swap; per-socketKey vs whole-connection hand-off;
167
- policy trigger rules for direct relay re-interposition beyond explicit calls and `onFail`.
168
- - Status: 🟡 mostly done. Route hand-off/resume: `replay/route-handoff.test.ts`. Route decisions +
169
- state machine: `createRouteCoordinator` (v1.0.67). Signaling + direct endpoint negotiation over the
170
- existing control channel and fallback-if-never-establishes: `createSignalHub` /
171
- `createWebRtcConnector` / `acceptWebRtcDirect` (v1.0.68, `replay/route-webrtc.test.ts`). Remaining:
172
- real NAT/WebRTC runtime glue (injected `rtc` factory) and auth-continuity policy.
173
-
174
- ## 2. Coordinated fan-out send to a large group 🧊
175
-
176
- Synchronized/batched send to a very large audience "at once", as opposed to the current model where
177
- **each connection is paced independently** (per-connection lag gate + `frame` policy).
178
-
179
- - Current reality: pacing is per-recipient by design every client's link is unique. Even in video
180
- fan-out no two receivers drain at the same rate, so the per-connection gate is usually the *correct*
181
- answer. That is why this is shelved.
182
- - When it would matter: true lockstep broadcast (all-or-nothing / same-instant delivery) — rare.
183
- - Status: 🧊 deliberately shelved. Revisit only against a concrete lockstep requirement.
184
-
185
- ## 3. Multi-authority stores two truths, one reconciliation (games) 🔴
186
-
187
- Two stores, each **dynamically filled by its own authority ("truth")**, that must agree on a shared
188
- source of truth. Decompose by write topology:
189
-
190
- - **Partitioned authority fits today.** Each peer authoritative over its own slice → two
191
- `exposeStore` / `createStoreMirror` pairs crossed over one duplex `{emit,on}`. No new primitive.
192
- "Confirmation" = the per-direction `seq` ack.
193
- - **Authoritative server + client prediction small layer on top.** Server store is the truth; the
194
- client applies optimistically, then reconciles when the authoritative patch arrives. Build a
195
- `predictedStore` = confirmed mirror + a pending-input list, rebased on each `mirror.each()` /
196
- `changed` tick. Helper factory, not a core change.
197
- - **Symmetric co-write of the same path needs a different model.** LWW-per-path converges but can
198
- silently drop a concurrent write. This is the one case that genuinely needs CRDT/OT. Design lead:
199
- wrap a Yjs/Automerge doc as a `RemoteStore`-shaped source and mirror from it — the store↔transport
200
- decoupling makes this a small adapter, not a rewrite.
201
- - Status: 🧊 deferred, super-low priority. Prediction layer waits for the demo app to demand and shape
202
- it (`doc/target/library-uplift-tasks.md` task 4); CRDT adapter reopens only on a real co-write need.
203
- Partitioned-authority already expressible today.
204
-
205
- ## 4. Data-transfer optimization backlog (ongoing) 🟡
206
-
207
- Open-ended transfer/perf work, especially for backend-heavy models. Never "done".
208
-
209
- - Candidates: tighter `frame` condensation per line; delta/patch minimization; binary framing for hot
210
- paths; batching heuristics beyond the current `pipe` / `space` modes.
211
- - Status: 🧊 deferred, super-low priority; pick items only as real bottlenecks surface in the SDK/demo
212
- consumers, not speculatively.
1
+ # wenay-common2 — Roadmap (open / deferred)
2
+
3
+ > Forward-looking backlog of distributed-state / transport features that are **not fully built**.
4
+ > Everything here sits on top of the existing seams — `{emit,on}` transport, `exposeStore` /
5
+ > `createStoreMirror`, and the replay `seq` / `keyframe` / `frame` contract. None of it requires
6
+ > changing the store core; the store stays single-authority (one `seq` sequencer, last-writer-wins
7
+ > per path), and these features are layers or adapters above that.
8
+ > Status: 🔴 not started · 🟡 partial / ongoing · 🧊 deferred (super-low priority, not forbidden).
9
+ >
10
+ > Current focus is NOT more transport: see `doc/target/library-uplift-tasks.md` (showcase, SDK facade,
11
+ > vertical demo app). Transport items below stay available but rank below everything in that plan.
12
+
13
+ ## 0. Distributed Runtime Model
14
+
15
+ The strategic direction is not "RPC plus store helpers"; it is a small distributed-state runtime:
16
+ application code keeps a stable typed API while lower layers can choose the transport, route, replay
17
+ source, and authority model.
18
+
19
+ Four concerns must stay explicit:
20
+
21
+ - **Transport** - how two endpoints exchange `{emit,on}` messages: socket.io, in-process loopback,
22
+ relay, WebRTC/direct channel, or a future adapter. Transport is replaceable infrastructure.
23
+ - **Routing** - where a logical call or stream goes: server, peer through relay, peer directly, or a
24
+ promoted relay <-> direct path. Routing must be allowed to change without changing the facade API.
25
+ - **Authority** - who is allowed to write truth: one server, partitioned peer-owned slices, server
26
+ authority with client prediction, or true multi-writer conflict resolution. This is a semantic
27
+ decision, not a transport decision.
28
+ - **Replay** - how live state survives reconnect, lag, transport swap, and history playback:
29
+ `seq` + keyframe/frame + deltas. Replay is the continuity layer that makes route changes boring.
30
+
31
+ Design rule: direct peer links and relay hand-offs are optimizations of transport/routing. They must
32
+ not silently change auth, ownership, validation, or conflict semantics. If a method is exposed as
33
+ `api.hit(playerId)`, it may travel through the server or a direct channel, but the authority rules
34
+ behind that method must remain the same.
35
+
36
+ This implies a useful split:
37
+
38
+ - **API surface** remains facade-shaped and typed.
39
+ - **Transport/routing layer** may substitute sockets and promote paths.
40
+ - **Replay layer** resumes streams and mirrors from the last known `seq`.
41
+ - **Authority layer** decides whether an incoming write is accepted, predicted, reconciled, or merged.
42
+
43
+ Critical ordering rule: do **not** start with WebRTC/NAT plumbing or CRDTs. The next useful layer is a
44
+ small route/account/policy coordinator with a fake/in-process transport adapter. Direct transport and
45
+ multi-writer merge are expensive adapters; they only become safe after the coordinator state machine is
46
+ boring and well-tested.
47
+
48
+ ### 0.1 Account route coordinator ("wrapper over wrappers") 🟡
49
+
50
+ Some deployments need separate client/account identities to communicate directly when policy and
51
+ network conditions allow it, while still allowing the server/relay to step back into the path later.
52
+ This is not just a socket trick; it is an account-aware routing shell above the existing facade,
53
+ mirror, and replay primitives.
54
+
55
+ - **Account model:** every participant has its own account/session identity and a scoped facade/store
56
+ set. Runtime-account maps are a dynamic keyspace, so they should look like `noStrict(accountMap)`
57
+ rather than a fixed schema. Access checks stay in the facade/policy layer; `noStrict` is not an ACL.
58
+ - **Wrapper over wrappers:** an app-level coordinator owns the set of per-account clients, exposed
59
+ facades, mirrors, replay subscriptions, and route state. "State of other accounts" is represented as
60
+ selected `Observe` mirrors/replay/offline resources, ideally started and stopped through
61
+ `createStoreManager`, not as a new global store core.
62
+ - **Route states:** a pair of accounts may be `relay`, `direct`, `direct+shadowRelay` (audit/observe
63
+ copy), `blocked`, or `fallback`. Direct links are optional optimizations, useful for latency or
64
+ traffic cost, not a semantic change.
65
+ - **Re-interposition:** if NDA/privacy policy, audit, moderation, throttling, reauth, direct-link
66
+ failure, or group topology changes require it, the relay must be able to re-enter the data path.
67
+ This is the reverse of direct promotion: open the replacement route, resume from the last `seq`,
68
+ switch consumers after catch-up, then close or demote the old route.
69
+ - **Privacy rule:** direct account links are opt-in and policy-gated. Peers only receive the endpoint
70
+ and session material needed for that specific relationship; no implicit broad account discovery.
71
+
72
+ Concrete next API shape, not final names:
73
+
74
+ ```ts
75
+ createRouteCoordinator({
76
+ policy,
77
+ routes,
78
+ resources,
79
+ }) -> {
80
+ pair(a, b),
81
+ state(pair),
82
+ promoteDirect(pair, opts?),
83
+ reinterposeRelay(pair, reason?),
84
+ block(pair, reason?),
85
+ fallback(pair, reason?),
86
+ onRoute(cb),
87
+ }
88
+ ```
89
+
90
+ Policy must run **before** transport promotion:
91
+
92
+ - `canDirect(pair, ctx)` - may these accounts attempt direct at all?
93
+ - `mustRelay(pair, ctx)` - force relay path because of NDA/audit/moderation/reauth.
94
+ - `mustShadowRelay(pair, ctx)` - allow direct payload path, but keep audit/observe copy.
95
+ - `canExposeEndpoint(pair, ctx)` - whether signaling may reveal endpoint/session material.
96
+ - `canReinterpose(pair, ctx)` - whether/when relay is allowed or required to step back in.
97
+
98
+ Minimum state machine:
99
+
100
+ - `relay` -> `direct:connecting` -> `direct` -> `relay:reinterposing` -> `relay`
101
+ - `relay` -> `direct:connecting` -> `fallback` -> `relay`
102
+ - `direct` -> `direct+shadowRelay`
103
+ - any state -> `blocked`
104
+
105
+ Required failure modes:
106
+
107
+ - direct setup never completes: keep relay active and mark fallback;
108
+ - replacement route catches up too slowly: keep old route active and fail the switch;
109
+ - policy changes mid-stream: re-interpose relay through replay hand-off;
110
+ - account reauth changes facade/ACL: rebuild route policy before accepting new writes;
111
+ - endpoint/session material is revoked: close direct and resume relay from `seq`;
112
+ - audit/shadow route lags: decide whether to throttle, fallback, or block by policy.
113
+
114
+ Acceptance tests for 0.1:
115
+
116
+ - policy denial: direct is never attempted;
117
+ - direct promotion: old relay stays live until replacement catches up;
118
+ - failed direct: old relay continues with no data gap;
119
+ - re-interposition: direct -> relay resumes from `seq`;
120
+ - `direct+shadowRelay`: direct path is active while relay/audit mirror observes;
121
+ - revocation: direct closes and relay resumes without changing facade API;
122
+ - account map uses `noStrict`, but all access checks live in policy/facade code;
123
+ - `createStoreManager` starts/stops selected per-account mirrors without store-core changes.
124
+
125
+ Open questions: whether the relay sees payloads or only coordinates encrypted direct streams;
126
+ backpressure across multi-hop and direct paths; group topology beyond a pair of accounts;
127
+ `noStrict(accountMap)` / `createStoreManager` lifecycle integration for dynamic peer maps.
128
+
129
+ Status: 🟡 partial (2026-07-09, v1.0.67). Core implemented as `Replay.createRouteCoordinator`
130
+ (`src/Common/events/route-coordinator.ts`): `RouteConnector` contract (pure transport: open/close/state/
131
+ metrics/onFail/capabilities), all five policy hooks, the full state machine above (including
132
+ `direct+shadowRelay` audit copy, catch-up timeout, revocation auto-fallback, terminal `blocked`), data
133
+ continuity through `replayRouteSubscribe`. Acceptance oracle: `replay/route-coordinator.test.ts` over
134
+ fake in-process connectors — policy denial never touches transport, promotion keeps the old relay live,
135
+ failed/slow direct falls back gap-free, re-interposition resumes from `seq`, shadow relay observes the
136
+ switch window, revocation closes direct without facade changes, block is terminal.
137
+ v1.0.68 added step 9: `createSignalHub` (offer/answer/ICE/session/revoke over the EXISTING socket/RPC
138
+ control channel; `authorize` = server-side `canExposeEndpoint`), `createWebRtcConnector` /
139
+ `acceptWebRtcDirect` (RTCPeerConnection injected as a runtime factory, structural types, no lib.dom),
140
+ and `serveReplayChannel`/`channelReplayRemote` (replay wire over any ordered channel — the datachannel
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).
148
+
149
+ Consumption-layer candidates (2026-07-10, author's ask):
150
+ - **Call system** SHIPPED in v1.0.74: `Peer.createCallManager` (ring/accept/decline/hangup as
151
+ envelopes over the existing signal hub zero new server routes, the host `authorize` hook is the
152
+ single server-side policy point), host `presence` (fragment key, refcounted edges), and
153
+ `Peer.createMediaRelay` (the demo media hub promoted into the library; relay-first stays the
154
+ privacy default, `promoteDirect` the opt-in). Oracle: `replay/peer-call.test.ts`. Consumer step
155
+ shipped too: the demo stand rings/accepts/declines with presence, media attaches only while the
156
+ call is active, and the watch-ACL grant set lives in the host `authorize` hook (verified headless,
157
+ two Chromium tabs, zero page errors).
158
+ - **Store-descriptor media bridge** ("streaming center") — the store carries a small descriptor
159
+ (`{kind: 'video', sourceId, state}`), a bridge watches the mirror and auto-attaches/detaches
160
+ `Media.attachVideoCanvas`/`attachAudioPlayer` to the matching binary line. Bytes flow only while
161
+ subscribed (native Listen-over-RPC semantics). Pairs with the React-hooks direction.
162
+ - 🧊 **Zero-parse patch format** (super-far future) Cap'n Proto / FlatBuffers-style layout for
163
+ store patches: fields read directly from the received buffer, no parse stage. Only if a real
164
+ consumer hits a serialization wall; JSON envelopes are nowhere near the bottleneck today
165
+ (measured 2-3ms e2e on the stand).
166
+
167
+ ## 1. Connection hand-off relay direct promotion ("port forwarding") 🟡
168
+
169
+ A relay/intermediary bootstraps a connection between two parties, then on a signal — **steps out
170
+ of the middle**: both ends open a second, direct socket and migrate the live stream onto it,
171
+ bypassing the relay. The same mechanism must work in reverse: the relay can deliberately
172
+ **re-interpose** and become the middleman again. Under the hood the socket substitutes itself; data
173
+ starts flowing on the new path.
174
+
175
+ - Family: NAT hole-punching / WebRTC TURN→direct promotion, expressed over the existing `{emit,on}`
176
+ abstraction rather than a specific transport.
177
+ - **Design lead (reuse what exists):** the socket swap is a *stream resume*, which the replay layer
178
+ already solves. Migrate with `replaySubscribe(..., {since: prev.seq()})` /
179
+ `syncStoreReplay(..., {since})` on the new socketthe consumer fold is gap-free by contract, so a
180
+ mid-stream transport change is not a special case. The transport carries only `seq`; the semantics
181
+ never learn the socket changed.
182
+ - **Implemented foundation (2026-07-08):** `Replay.replayRouteSubscribe(...)` and
183
+ `Observe.syncStoreReplayRoute(...)` keep the old route alive, subscribe the replacement route,
184
+ catch it up from the last delivered `seq`, then close the old route. This covers relay → direct
185
+ promotion and direct → relay re-interposition for any ordered `ReplayRemote`; overlap is deduped by
186
+ `seq`, and a failed replacement leaves the old route active.
187
+ - Open questions: auth continuity across the swap; per-socketKey vs whole-connection hand-off;
188
+ policy trigger rules for direct relay re-interposition beyond explicit calls and `onFail`.
189
+ - Status: 🟡 mostly done. Route hand-off/resume: `replay/route-handoff.test.ts`. Route decisions +
190
+ state machine: `createRouteCoordinator` (v1.0.67). Signaling + direct endpoint negotiation over the
191
+ existing control channel and fallback-if-never-establishes: `createSignalHub` /
192
+ `createWebRtcConnector` / `acceptWebRtcDirect` (v1.0.68, `replay/route-webrtc.test.ts`). Remaining:
193
+ real NAT/WebRTC runtime glue (injected `rtc` factory) and auth-continuity policy.
194
+
195
+ ## 2. Coordinated fan-out send to a large group 🧊
196
+
197
+ Synchronized/batched send to a very large audience "at once", as opposed to the current model where
198
+ **each connection is paced independently** (per-connection lag gate + `frame` policy).
199
+
200
+ - Current reality: pacing is per-recipient by design — every client's link is unique. Even in video
201
+ fan-out no two receivers drain at the same rate, so the per-connection gate is usually the *correct*
202
+ answer. That is why this is shelved.
203
+ - When it would matter: true lockstep broadcast (all-or-nothing / same-instant delivery) — rare.
204
+ - Status: 🧊 deliberately shelved. Revisit only against a concrete lockstep requirement.
205
+
206
+ ## 3. Multi-authority stores — two truths, one reconciliation (games) 🔴
207
+
208
+ Two stores, each **dynamically filled by its own authority ("truth")**, that must agree on a shared
209
+ source of truth. Decompose by write topology:
210
+
211
+ - **Partitioned authority fits today.** Each peer authoritative over its own slice two
212
+ `exposeStore` / `createStoreMirror` pairs crossed over one duplex `{emit,on}`. No new primitive.
213
+ "Confirmation" = the per-direction `seq` ack.
214
+ - **Authoritative server + client prediction — small layer on top.** Server store is the truth; the
215
+ client applies optimistically, then reconciles when the authoritative patch arrives. Build a
216
+ `predictedStore` = confirmed mirror + a pending-input list, rebased on each `mirror.each()` /
217
+ `changed` tick. Helper factory, not a core change.
218
+ - **Symmetric co-write of the same path — needs a different model.** LWW-per-path converges but can
219
+ silently drop a concurrent write. This is the one case that genuinely needs CRDT/OT. Design lead:
220
+ wrap a Yjs/Automerge doc as a `RemoteStore`-shaped source and mirror from it — the store↔transport
221
+ decoupling makes this a small adapter, not a rewrite.
222
+ - Status: 🧊 deferred, super-low priority. Prediction layer waits for the demo app to demand and shape
223
+ it (`doc/target/library-uplift-tasks.md` task 4); CRDT adapter reopens only on a real co-write need.
224
+ Partitioned-authority already expressible today.
225
+
226
+ ## 4. Data-transfer optimization backlog (ongoing) 🟡
227
+
228
+ Open-ended transfer/perf work, especially for backend-heavy models. Never "done".
229
+
230
+ - Candidates: tighter `frame` condensation per line; delta/patch minimization; binary framing for hot
231
+ paths; batching heuristics beyond the current `pipe` / `space` modes.
232
+ - Status: 🧊 deferred, super-low priority; pick items only as real bottlenecks surface in the SDK/demo
233
+ consumers, not speculatively.
@@ -0,0 +1,9 @@
1
+ # 1.0.71
2
+
3
+ - Publish-path reconnect correctness for the Peer SDK: the relay journal never lies after a publisher gap.
4
+ - `Peer.createPatchRelayJournal({gap: 'resume' | 'sacred'})` — the server declares journal semantics: `'resume'` (default) folds a keyframe and takes a ROOT patch as the one legitimate reset point; `'sacred'` never invents (no folded keyframe, no root reset, strict contiguity, `frame()` throws on an evicted tail).
5
+ - `push()` now returns `true | false | {seq}` — a rejected gap carries the relay's last seq, and that coordinate IS the repair request (self-healing protocol, no separate handshake); duplicates are idempotent no-ops; a non-root FIRST envelope is rejected (a partial-state fold would lie to late joiners); rejection never corrupts the fold.
6
+ - `Peer.createPeerClient`: rejection-driven repair — `repair: 'tail'` (missed envelopes verbatim, keyframe fallback on local eviction) or `'keyframe'` (one cheap root reset for ephemeral state); `journal: 'sacred'` narrows `repair` to `'tail'` at the TYPE level (`tPublishRepair<J>`) — incompatible asks are forbidden by typing, not runtime checks, and a lying client just keeps getting rejections, loudly via `onPublishError`.
7
+ - `client.resync()` — call after a transport reconnect: compares the relay's `seq()` (additive on the wire) with the local line and repairs the gap without waiting for the next write.
8
+ - Add oracle `replay/peer-repair.test.ts`: full gap matrix (folding + sacred), both repair modes over a lossy transport, sacred local-eviction fault surfacing, `resync()`.
9
+ - Media sources: `replay` now also accepts custom `ReplayListenUseOptions` (`replay: {history, current, frame}`), not just `true`; defaults stay per-kind (audio — sacred queue `history:1024`, video — keep-latest `history:256`).
@@ -0,0 +1,6 @@
1
+ # 1.0.72
2
+
3
+ - Media video sources survive hidden/occluded tabs (Chrome throttling). Three measured stalls, three escape hatches, all on by default (`worker: false` opts out): capture tick moves from `setInterval` (~1/s hidden) to a Blob-worker timer; frames come from `ImageCapture.grabFrame()` (a hidden `<video>` stops painting); JPEG encode moves into a worker with a transferred `ImageBitmap` (main-thread `convertToBlob` is gated to ~1000ms hidden, ~5-10ms in the worker). Measured result: a hidden tab streams a stable ~16fps instead of decaying to 1fps after ~30s.
4
+ - Demo stand: live media — camera, microphone and screen share, captured with the public `Media` sources and streamed between the two tabs. Screen share uses the documented `stream` injection point (`createVideoSource({stream: () => getDisplayMedia(...)})`); the demo server grows a tiny relay of per-account replay lines next to `peer.fragment` (video keep-latest, audio short queue, watched account declared via `auth.watch`, frames ride with a `sentAt` wall-clock stamp).
5
+ - Demo client: capture toggles + per-second tx/rx rates and publish->receive latency in the stats line (`rx: cam Nf/Nd 16/s ~3ms`), peer camera/screen on canvases, peer mic through a sequential-playhead PCM player that drops >350ms backlog (live beats lossless for a call-like demo). Mic capture uses `worker:false, bufferSize:4096` — big PCM chunks instead of ~375 socket messages/s.
6
+ - Docs: screen-share recipe + hidden-tab capture note in the Media section; README demo line mentions the media part.
@@ -0,0 +1,6 @@
1
+ # 1.0.73
2
+
3
+ - Media viewer helpers — the consumer side of a media line in one call (`media-view`, additive): `Media.attachVideoCanvas(line, canvas)` (header-driven codec/size, busy-skip keep-latest, `stats()` with frames/drawn/perSec/ageMs), `Media.attachAudioPlayer(line, {maxBacklogSec})` (sequential-playhead PCM, live backlog drop, gesture-gated `enable()`), `Media.pipeMediaPublish(line, publish, {stamp})` (fire-and-forget publish pipe; the `Date.now()` stamp feeds viewer latency stats). DI for tests: `createBitmap` / `audioContext` injection points.
4
+ - Demo stand client rewired onto the helpers — the hand-written viewer plumbing (~90 lines of JPEG decode, PCM playhead, counters) collapses into one call per channel; behavior verified live (cam 30/s ~3ms, mic 12/s ~2ms, zero drops).
5
+ - Route hand-off under media load verified on the live stand: relay -> direct (real ICE/SDP) -> relay while camera + mic stream; zero errors, media rates/latency untouched through both switches (media rides the socket relay; peer-store routes are per-direction by design).
6
+ - Export `Media.toBytes` (binary view normalizer used by the helpers).
@@ -0,0 +1,12 @@
1
+ # 1.0.74
2
+
3
+ - Call system (`peer-call`, additive) — one account calls another, messenger-style, with ZERO new server routes: `Peer.createCallManager({port: callPortOf(remote), self})` drives ring/accept/decline/hangup envelopes over the EXISTING signal hub (`tSignalType` widened additively; the host `authorize` hook sees call envelopes too — one server-side policy point). Call handle: `state()/reason()/changed/ended/accept()/decline()/hangup()`; offline callee fails FAST (send verdict, no timeout wait); ring timeout expires both sides; default incoming gate auto-declines `'busy'` during a live call (also settles glare); `manager.ready` = subscription registration ack via a self-probe on the same ordered socket.
4
+ - Presence in the peer host (additive fragment key) — `presence: {list(), changes}`: refcounted per account, edges on 0↔1 connection-count transitions only. Subscribe first, then `list()`.
5
+ - Media relay formalized (`Peer.createMediaRelay`) — the demo stand's media hub promoted into the library: per-account named replay lines (`'video'` keep-latest + `current:'last'`, `'audio'` short lossless queue), `[frame, sentAt]` stamps for viewer latency stats, `watch` as a `noStrict` dynamic map (peers-map pattern), eager `publishOf` so watchers resolve the account path the moment the connection is wired. Relay-first is the privacy default; `promoteDirect` stays the opt-in.
6
+ - Media watch ACL (`watchOf(watcher)` + `canWatch(watcher, owner, line)`) — per-connection policy views gate every rpc path resolution and every forwarded live frame/keyframe. Revocation therefore blocks an already-open subscription without trusting the client to detach; `dropAccount(account)` still closes lines loudly and frees the keyspace. The unfiltered `watch` map remains for trusted wiring.
7
+ - Oracle `replay/peer-call.test.ts` — real Socket.IO/RPC wire: presence on/off edges, ring→accept→active, media frames + keep-latest `keyframe()` while active, busy gate, decline, hangup, offline fast-fail, ring timeout with callee cancel.
8
+ - Demo stand: messenger-style calls — presence indicator, call/accept/decline/hangup buttons, peer media viewers attach ONLY while the call is active. A tiny server-owned call lifecycle inside the host `authorize` hook proves ring/accept participants before granting media, so a forged `accept` cannot mint access; decline/hangup/offline revoke it. Verified headless (two Chromium tabs, fake camera): ring→accept→frames→hangup→decline, zero page errors.
9
+ - Peer host: `peers` map auto-creates an EMPTY relay journal on first touch (Proxy over the dynamic keyspace) — a mirror subscribed BEFORE its owner ever connected now waits for the first publish instead of failing loudly (the demo A-tab-loads-first race). Optional `accounts(account)` host hook gates junk keys on public servers.
10
+ - Media viewers: `viewer.off()` now detaches RPC-projected lines too (the deep proxy returns a promise of the subscription handle; local lines keep detaching synchronously — order-sensitive: a callable handle is also thenable).
11
+ - Lifecycle hardening: back-to-back incoming rings atomically reserve the default busy gate; same-account signaling registrations fall back to the previous live port when the newest tab closes; call ids stay distinct across manager restarts/tabs. Regression coverage lives in `replay/peer-call.test.ts`.
12
+ - Docs: brief SDK section "Calls, presence and the media relay"; rare reference for `createCallManager` / `createMediaRelay` / presence / widened `SignalEnvelope`.