wenay-common2 1.0.73 → 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.
- package/README.md +3 -2
- package/demo/client.ts +137 -32
- package/demo/index.html +7 -0
- package/demo/server.ts +79 -42
- package/doc/INTENT.md +31 -31
- package/doc/ROADMAP.md +233 -230
- package/doc/changes/1.0.74.md +12 -0
- package/doc/wenay-common2-rare.md +684 -648
- package/doc/wenay-common2.md +37 -3
- package/lib/Common/events/route-signal-webrtc.d.ts +1 -1
- package/lib/Common/events/route-signal-webrtc.js +15 -5
- package/lib/Common/media/media-view.d.ts +2 -2
- package/lib/Common/media/media-view.js +23 -4
- package/lib/Common/peer/peer-call.d.ts +65 -0
- package/lib/Common/peer/peer-call.js +173 -0
- package/lib/Common/peer/peer-client.d.ts +9 -0
- package/lib/Common/peer/peer-host.d.ts +14 -1
- package/lib/Common/peer/peer-host.js +48 -4
- package/lib/Common/peer/peer-index.d.ts +2 -0
- package/lib/Common/peer/peer-index.js +2 -0
- package/lib/Common/peer/peer-media-relay.d.ts +22 -0
- package/lib/Common/peer/peer-media-relay.js +155 -0
- package/observe/listen-core.test.ts +1 -2
- package/package.json +1 -4
- package/replay/media-view.test.ts +4 -4
- package/replay/peer-call.test.ts +226 -0
- package/doc/changes/1.0.64.md +0 -10
package/doc/ROADMAP.md
CHANGED
|
@@ -1,230 +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). 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** —
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
`
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
-
|
|
201
|
-
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
- **
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 socket — the 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,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`.
|