wenay-common2 1.0.67 → 1.0.68
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 +19 -10
- package/doc/changes/1.0.68.md +8 -0
- package/doc/wenay-common2-rare.md +25 -0
- package/lib/Common/events/replay-channel.d.ts +9 -0
- package/lib/Common/events/replay-channel.js +129 -0
- package/lib/Common/events/replay-index.d.ts +2 -0
- package/lib/Common/events/replay-index.js +2 -0
- package/lib/Common/events/route-signal-webrtc.d.ts +83 -0
- package/lib/Common/events/route-signal-webrtc.js +245 -0
- package/package.json +1 -1
package/doc/ROADMAP.md
CHANGED
|
@@ -119,9 +119,9 @@ Acceptance tests for 0.1:
|
|
|
119
119
|
- account map uses `noStrict`, but all access checks live in policy/facade code;
|
|
120
120
|
- `createStoreManager` starts/stops selected per-account mirrors without store-core changes.
|
|
121
121
|
|
|
122
|
-
Open questions:
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
Open questions: whether the relay sees payloads or only coordinates encrypted direct streams;
|
|
123
|
+
backpressure across multi-hop and direct paths; group topology beyond a pair of accounts;
|
|
124
|
+
`noStrict(accountMap)` / `createStoreManager` lifecycle integration for dynamic peer maps.
|
|
125
125
|
|
|
126
126
|
Status: 🟡 partial (2026-07-09, v1.0.67). Core implemented as `Replay.createRouteCoordinator`
|
|
127
127
|
(`src/Common/events/route-coordinator.ts`): `RouteConnector` contract (pure transport: open/close/state/
|
|
@@ -130,9 +130,15 @@ metrics/onFail/capabilities), all five policy hooks, the full state machine abov
|
|
|
130
130
|
continuity through `replayRouteSubscribe`. Acceptance oracle: `replay/route-coordinator.test.ts` over
|
|
131
131
|
fake in-process connectors — policy denial never touches transport, promotion keeps the old relay live,
|
|
132
132
|
failed/slow direct falls back gap-free, re-interposition resumes from `seq`, shadow relay observes the
|
|
133
|
-
switch window, revocation closes direct without facade changes, block is terminal.
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
switch window, revocation closes direct without facade changes, block is terminal.
|
|
134
|
+
v1.0.68 added step 9: `createSignalHub` (offer/answer/ICE/session/revoke over the EXISTING socket/RPC
|
|
135
|
+
control channel; `authorize` = server-side `canExposeEndpoint`), `createWebRtcConnector` /
|
|
136
|
+
`acceptWebRtcDirect` (RTCPeerConnection injected as a runtime factory, structural types, no lib.dom),
|
|
137
|
+
and `serveReplayChannel`/`channelReplayRemote` (replay wire over any ordered channel — the datachannel
|
|
138
|
+
path bypasses the RPC core by design). Oracle `replay/route-webrtc.test.ts` drives promotion, endpoint
|
|
139
|
+
denial, session rejection, and server revoke over both an in-proc hub and a real Socket.IO/RPC wire.
|
|
140
|
+
Still open: browser/Node WebRTC glue (step 10 — now a one-line `rtc` factory plus media re-emit) and
|
|
141
|
+
the account-map lifecycle integration (step 7).
|
|
136
142
|
|
|
137
143
|
## 1. Connection hand-off — relay ↔ direct promotion ("port forwarding") 🟡
|
|
138
144
|
|
|
@@ -154,10 +160,13 @@ starts flowing on the new path.
|
|
|
154
160
|
catch it up from the last delivered `seq`, then close the old route. This covers relay → direct
|
|
155
161
|
promotion and direct → relay re-interposition for any ordered `ReplayRemote`; overlap is deduped by
|
|
156
162
|
`seq`, and a failed replacement leaves the old route active.
|
|
157
|
-
- Open questions:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
- Open questions: auth continuity across the swap; per-socketKey vs whole-connection hand-off;
|
|
164
|
+
policy trigger rules for direct → relay re-interposition beyond explicit calls and `onFail`.
|
|
165
|
+
- Status: 🟡 mostly done. Route hand-off/resume: `replay/route-handoff.test.ts`. Route decisions +
|
|
166
|
+
state machine: `createRouteCoordinator` (v1.0.67). Signaling + direct endpoint negotiation over the
|
|
167
|
+
existing control channel and fallback-if-never-establishes: `createSignalHub` /
|
|
168
|
+
`createWebRtcConnector` / `acceptWebRtcDirect` (v1.0.68, `replay/route-webrtc.test.ts`). Remaining:
|
|
169
|
+
real NAT/WebRTC runtime glue (injected `rtc` factory) and auth-continuity policy.
|
|
161
170
|
|
|
162
171
|
## 2. Coordinated fan-out send to a large group 🧊
|
|
163
172
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# 1.0.68
|
|
2
|
+
|
|
3
|
+
- Add `Replay.createSignalHub`: WebRTC signaling (offer/answer/ICE/session/revoke envelopes) routed between accounts over the EXISTING socket/RPC control channel — the port shape `{send, signals}` is exposable by `createRpcServerAuto` as is; `authorize(env)` is the server-side `canExposeEndpoint` enforcement point, `from`-spoofing is cut at the port.
|
|
4
|
+
- Add `Replay.createWebRtcConnector`: the direct `RouteConnector` for `createRouteCoordinator` — `open()` negotiates offer/answer/ICE through a signal port and returns a replay wire over the datachannel. `RTCPeerConnection` is NOT bundled: `rtc` is an injected runtime factory (browser `() => new RTCPeerConnection(cfg)`, Node werift/node-datachannel, tests an in-proc fake) over structural `RtcPeerConnection`/`RtcDataChannel` types (no lib.dom).
|
|
5
|
+
- Add `Replay.acceptWebRtcDirect`: responder side — answers offers, serves `exposeReplay(...)` into the incoming datachannel; `accept(env)` validates opaque session material and rejects with a loud `revoke` so the initiator fails fast instead of timing out.
|
|
6
|
+
- Add `Replay.serveReplayChannel` / `channelReplayRemote` / `channelFromDataChannel`: replay wire over any ordered string channel (datachannel/MessagePort/worker/pipe) via a tiny JSON sub/req/res protocol — no RPC core on the direct path; channel close surfaces as a non-envelope (loud `onError`), never silence.
|
|
7
|
+
- Revoke/close signals and channel death during `open()` abort the pending promotion immediately (no `openTimeoutMs` wait); the coordinator auto-falls back to relay from the last `seq`.
|
|
8
|
+
- Add oracle `replay/route-webrtc.test.ts`: fake in-proc RTC runtime + signaling hub (promotion, server-side endpoint denial, bad session revoke, server revoke fallback, recovery into direct) and the same signaling driven over a real Socket.IO/RPC wire.
|
|
@@ -511,6 +511,31 @@ createRouteCoordinator({connect, policy?, shadow?, catchUpTimeoutMs?}) -> coordi
|
|
|
511
511
|
// direct+shadowRelay: payload rides direct while deps.shadow(ref, ...ev) receives the relay audit copy,
|
|
512
512
|
// starting from the consumers' seq coordinate — the switch window never escapes the audit.
|
|
513
513
|
// Acceptance oracle: replay/route-coordinator.test.ts (fake in-process relay/direct connectors).
|
|
514
|
+
createSignalHub({authorize?}) -> {register(account) -> {send, signals, close}, revoke(pair, accounts, reason?), accounts(), close()}
|
|
515
|
+
// WebRTC signaling over the EXISTING control channel: the port shape {send, signals} is a function +
|
|
516
|
+
// Listen — exactly what createRpcServerAuto exposes, so the relay socket IS the signaling wire
|
|
517
|
+
// (per connection: const port = hub.register(account); object = {send: port.send, signals: port.signals}).
|
|
518
|
+
// authorize(env) is the SERVER-side canExposeEndpoint point — endpoint/session material is revealed only
|
|
519
|
+
// past it; client-side coordinator policy stays advisory. from-spoofing is cut at the port.
|
|
520
|
+
// SignalEnvelope = {type: 'offer'|'answer'|'ice'|'revoke'|'close', pair, from, to, sdp?, candidate?,
|
|
521
|
+
// session?, reason?} — session is opaque auth material (the wire never looks inside).
|
|
522
|
+
createWebRtcConnector({port, rtc, self, peer, pair, session?, label?, openTimeoutMs?}) -> RouteConnector
|
|
523
|
+
// the direct connector for createRouteCoordinator: open() drives offer/answer/ICE through the signal
|
|
524
|
+
// port, waits for the datachannel, returns a replay wire over it. RTCPeerConnection is NOT bundled:
|
|
525
|
+
// rtc is a runtime factory — browser `() => new RTCPeerConnection(cfg)`, Node werift/node-datachannel,
|
|
526
|
+
// tests an in-proc fake (RtcPeerConnection/RtcDataChannel are structural types, no lib.dom).
|
|
527
|
+
// revoke/close signals and channel death (incl. DURING open) fail loudly -> coordinator auto-fallback.
|
|
528
|
+
acceptWebRtcDirect({port, rtc, self, serve, accept?}) -> close()
|
|
529
|
+
// responder side: on offer, negotiates answer/ICE and serves serve(env) (exposeReplay(...) as is) into
|
|
530
|
+
// the incoming datachannel; accept(env) validates session material and rejects with a loud revoke
|
|
531
|
+
// (the initiator fails fast, not by timeout). Repeated offer for a pair recreates the session.
|
|
532
|
+
serveReplayChannel(source, channel) <-> channelReplayRemote(channel) -> ReplayRemote
|
|
533
|
+
// replay wire over ANY ordered string channel (datachannel/MessagePort/worker/pipe): tiny JSON
|
|
534
|
+
// sub/req/res protocol, no RPC core — a direct channel lives OUTSIDE the main rpc connection.
|
|
535
|
+
// Channel close = non-envelope (null) on the line: replay subscribers report onError, never silence.
|
|
536
|
+
// ReplayMessageChannel = {send, onMessage, onClose?, close?}; channelFromDataChannel(dc) adapts a
|
|
537
|
+
// datachannel (and owns its handlers). Oracle: replay/route-webrtc.test.ts (fake RTC runtime +
|
|
538
|
+
// in-proc hub + the same signaling over a real Socket.IO/RPC wire).
|
|
514
539
|
exposeStoreReplay(store, opts?) <-> syncStoreReplay(mirror, remote, opts?) // layer B: patch line; keyframe = root patch ({path: [], value: snapshot})
|
|
515
540
|
syncStoreReplayRoute(mirror, remote, opts?) -> off & {ready, switch(nextRemote, opts), seq(), label(), active()} // same patch fold, but route-replaceable for relay/direct promotion
|
|
516
541
|
syncStoreReplayEach<T>(remote, cb, opts?) -> off & {store, ready, seq(), isStale(), lastTs()} // one-call per-key fold over the patch line (mirror + syncStoreReplay + store.each()); most-used surface — full contract + example in wenay-common2.md
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ReplayRemote } from './replay-wire';
|
|
2
|
+
export type ReplayMessageChannel = {
|
|
3
|
+
send: (data: string) => void;
|
|
4
|
+
onMessage: (cb: (data: string) => void) => (() => void) | void;
|
|
5
|
+
onClose?: (cb: () => void) => (() => void) | void;
|
|
6
|
+
close?: () => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function serveReplayChannel<Z extends any[]>(source: ReplayRemote<Z>, channel: ReplayMessageChannel): () => void;
|
|
9
|
+
export declare function channelReplayRemote<Z extends any[]>(channel: ReplayMessageChannel): ReplayRemote<Z>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serveReplayChannel = serveReplayChannel;
|
|
4
|
+
exports.channelReplayRemote = channelReplayRemote;
|
|
5
|
+
function unsubscribeHandle(handle) {
|
|
6
|
+
if (typeof handle == 'function') {
|
|
7
|
+
handle();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (typeof handle?.off == 'function')
|
|
11
|
+
handle.off();
|
|
12
|
+
else if (typeof handle?.unsubscribe == 'function')
|
|
13
|
+
handle.unsubscribe();
|
|
14
|
+
}
|
|
15
|
+
function serveReplayChannel(source, channel) {
|
|
16
|
+
let lineOff = null;
|
|
17
|
+
let closed = false;
|
|
18
|
+
async function handleRequest(msg) {
|
|
19
|
+
try {
|
|
20
|
+
const v = msg.m == 'since' ? await source.since(msg.a[0])
|
|
21
|
+
: msg.m == 'keyframe' ? await source.keyframe()
|
|
22
|
+
: msg.m == 'frame' ? (source.frame ? await source.frame(msg.a[0], msg.a[1]) : null)
|
|
23
|
+
: undefined;
|
|
24
|
+
if (!closed)
|
|
25
|
+
channel.send(JSON.stringify({ t: 'res', id: msg.id, ok: true, v: v ?? null }));
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
if (!closed)
|
|
29
|
+
channel.send(JSON.stringify({ t: 'res', id: msg.id, ok: false, e: String(e) }));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const offMsg = channel.onMessage(function onReplayChannelMessage(raw) {
|
|
33
|
+
if (closed)
|
|
34
|
+
return;
|
|
35
|
+
let msg;
|
|
36
|
+
try {
|
|
37
|
+
msg = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (msg?.t == 'sub' && !lineOff) {
|
|
43
|
+
lineOff = source.line.on(function forwardEnvelope(ev) {
|
|
44
|
+
if (!closed)
|
|
45
|
+
channel.send(JSON.stringify({ t: 'ev', ev }));
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (msg?.t == 'req')
|
|
50
|
+
void handleRequest(msg);
|
|
51
|
+
});
|
|
52
|
+
function close() {
|
|
53
|
+
if (closed)
|
|
54
|
+
return;
|
|
55
|
+
closed = true;
|
|
56
|
+
unsubscribeHandle(lineOff);
|
|
57
|
+
lineOff = null;
|
|
58
|
+
if (typeof offMsg == 'function')
|
|
59
|
+
offMsg();
|
|
60
|
+
}
|
|
61
|
+
channel.onClose?.(close);
|
|
62
|
+
return close;
|
|
63
|
+
}
|
|
64
|
+
function channelReplayRemote(channel) {
|
|
65
|
+
let nextId = 1;
|
|
66
|
+
let subscribed = false;
|
|
67
|
+
let closed = false;
|
|
68
|
+
const pending = new Map();
|
|
69
|
+
const lineCbs = new Set();
|
|
70
|
+
channel.onMessage(function onRemoteChannelMessage(raw) {
|
|
71
|
+
let msg;
|
|
72
|
+
try {
|
|
73
|
+
msg = JSON.parse(raw);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (msg?.t == 'ev') {
|
|
79
|
+
for (const cb of Array.from(lineCbs))
|
|
80
|
+
cb(msg.ev);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (msg?.t == 'res') {
|
|
84
|
+
const p = pending.get(msg.id);
|
|
85
|
+
pending.delete(msg.id);
|
|
86
|
+
if (!p)
|
|
87
|
+
return;
|
|
88
|
+
if (msg.ok)
|
|
89
|
+
p.resolve(msg.v);
|
|
90
|
+
else
|
|
91
|
+
p.reject(new Error(msg.e ?? 'replay channel request failed'));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
channel.onClose?.(function onRemoteChannelClosed() {
|
|
95
|
+
if (closed)
|
|
96
|
+
return;
|
|
97
|
+
closed = true;
|
|
98
|
+
for (const [, p] of pending)
|
|
99
|
+
p.reject(new Error('replay channel closed'));
|
|
100
|
+
pending.clear();
|
|
101
|
+
for (const cb of Array.from(lineCbs))
|
|
102
|
+
cb(null);
|
|
103
|
+
lineCbs.clear();
|
|
104
|
+
});
|
|
105
|
+
function req(m, a) {
|
|
106
|
+
if (closed)
|
|
107
|
+
return Promise.reject(new Error('replay channel closed'));
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const id = nextId++;
|
|
110
|
+
pending.set(id, { resolve, reject });
|
|
111
|
+
channel.send(JSON.stringify({ t: 'req', id, m, a }));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
line: {
|
|
116
|
+
on(cb) {
|
|
117
|
+
lineCbs.add(cb);
|
|
118
|
+
if (!subscribed && !closed) {
|
|
119
|
+
subscribed = true;
|
|
120
|
+
channel.send(JSON.stringify({ t: 'sub' }));
|
|
121
|
+
}
|
|
122
|
+
return function offChannelLine() { lineCbs.delete(cb); };
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
since: seq => req('since', [seq]),
|
|
126
|
+
keyframe: () => req('keyframe', []),
|
|
127
|
+
frame: (seq, hint) => req('frame', [seq, hint]),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -20,3 +20,5 @@ __exportStar(require("./replay-conflate"), exports);
|
|
|
20
20
|
__exportStar(require("./replay-history"), exports);
|
|
21
21
|
__exportStar(require("./replay-route"), exports);
|
|
22
22
|
__exportStar(require("./route-coordinator"), exports);
|
|
23
|
+
__exportStar(require("./replay-channel"), exports);
|
|
24
|
+
__exportStar(require("./route-signal-webrtc"), exports);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ReplayRemote } from './replay-wire';
|
|
2
|
+
import { RouteConnector } from './route-coordinator';
|
|
3
|
+
import { ReplayMessageChannel } from './replay-channel';
|
|
4
|
+
export type tSignalType = 'offer' | 'answer' | 'ice' | 'revoke' | 'close';
|
|
5
|
+
export type SignalEnvelope = {
|
|
6
|
+
type: tSignalType;
|
|
7
|
+
pair: string;
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
sdp?: string;
|
|
11
|
+
candidate?: unknown;
|
|
12
|
+
session?: unknown;
|
|
13
|
+
reason?: string;
|
|
14
|
+
};
|
|
15
|
+
export type SignalPort = {
|
|
16
|
+
send: (env: SignalEnvelope) => Promise<boolean | void> | boolean | void;
|
|
17
|
+
signals: {
|
|
18
|
+
on: (cb: (env: SignalEnvelope) => void) => any;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare function createSignalHub(deps?: {
|
|
22
|
+
authorize?: (env: SignalEnvelope) => boolean | Promise<boolean>;
|
|
23
|
+
}): {
|
|
24
|
+
register: (account: string) => {
|
|
25
|
+
account: string;
|
|
26
|
+
send: (env: SignalEnvelope) => Promise<boolean>;
|
|
27
|
+
signals: import("./Listen").ListenApi<[SignalEnvelope]>;
|
|
28
|
+
close: () => void;
|
|
29
|
+
};
|
|
30
|
+
revoke: (pair: string, accounts: string[], reason?: string) => void;
|
|
31
|
+
accounts: () => string[];
|
|
32
|
+
close(): void;
|
|
33
|
+
};
|
|
34
|
+
export type SignalHub = ReturnType<typeof createSignalHub>;
|
|
35
|
+
export type RtcSessionDescription = {
|
|
36
|
+
type: string;
|
|
37
|
+
sdp?: string;
|
|
38
|
+
};
|
|
39
|
+
export type RtcDataChannel = {
|
|
40
|
+
send: (data: string) => void;
|
|
41
|
+
close: () => void;
|
|
42
|
+
onopen?: ((ev?: unknown) => void) | null;
|
|
43
|
+
onmessage?: ((ev: {
|
|
44
|
+
data: unknown;
|
|
45
|
+
}) => void) | null;
|
|
46
|
+
onclose?: ((ev?: unknown) => void) | null;
|
|
47
|
+
onerror?: ((ev?: unknown) => void) | null;
|
|
48
|
+
};
|
|
49
|
+
export type RtcPeerConnection = {
|
|
50
|
+
createDataChannel: (label: string, opts?: unknown) => RtcDataChannel;
|
|
51
|
+
createOffer: () => Promise<RtcSessionDescription>;
|
|
52
|
+
createAnswer: () => Promise<RtcSessionDescription>;
|
|
53
|
+
setLocalDescription: (d: RtcSessionDescription) => Promise<unknown> | unknown;
|
|
54
|
+
setRemoteDescription: (d: RtcSessionDescription) => Promise<unknown> | unknown;
|
|
55
|
+
addIceCandidate: (c: unknown) => Promise<unknown> | unknown;
|
|
56
|
+
close: () => void;
|
|
57
|
+
onicecandidate?: ((ev: {
|
|
58
|
+
candidate?: unknown;
|
|
59
|
+
}) => void) | null;
|
|
60
|
+
ondatachannel?: ((ev: {
|
|
61
|
+
channel: RtcDataChannel;
|
|
62
|
+
}) => void) | null;
|
|
63
|
+
};
|
|
64
|
+
export declare function channelFromDataChannel(dc: RtcDataChannel): ReplayMessageChannel;
|
|
65
|
+
export type WebRtcConnectorDeps = {
|
|
66
|
+
port: SignalPort;
|
|
67
|
+
rtc: () => RtcPeerConnection;
|
|
68
|
+
self: string;
|
|
69
|
+
peer: string;
|
|
70
|
+
pair: string;
|
|
71
|
+
session?: unknown;
|
|
72
|
+
label?: string;
|
|
73
|
+
openTimeoutMs?: number;
|
|
74
|
+
};
|
|
75
|
+
export declare function createWebRtcConnector<Z extends any[] = any[]>(deps: WebRtcConnectorDeps): RouteConnector<Z>;
|
|
76
|
+
export type WebRtcAcceptDeps<Z extends any[]> = {
|
|
77
|
+
port: SignalPort;
|
|
78
|
+
rtc: () => RtcPeerConnection;
|
|
79
|
+
self: string;
|
|
80
|
+
serve: (env: SignalEnvelope) => ReplayRemote<Z> | null | Promise<ReplayRemote<Z> | null>;
|
|
81
|
+
accept?: (env: SignalEnvelope) => boolean | Promise<boolean>;
|
|
82
|
+
};
|
|
83
|
+
export declare function acceptWebRtcDirect<Z extends any[] = any[]>(deps: WebRtcAcceptDeps<Z>): () => void;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSignalHub = createSignalHub;
|
|
4
|
+
exports.channelFromDataChannel = channelFromDataChannel;
|
|
5
|
+
exports.createWebRtcConnector = createWebRtcConnector;
|
|
6
|
+
exports.acceptWebRtcDirect = acceptWebRtcDirect;
|
|
7
|
+
const Listen_1 = require("./Listen");
|
|
8
|
+
const replay_channel_1 = require("./replay-channel");
|
|
9
|
+
function createSignalHub(deps = {}) {
|
|
10
|
+
const { authorize } = deps;
|
|
11
|
+
const ports = new Map();
|
|
12
|
+
function register(account) {
|
|
13
|
+
const [emit, signals] = (0, Listen_1.listen)();
|
|
14
|
+
ports.set(account, emit);
|
|
15
|
+
async function send(env) {
|
|
16
|
+
if (env == null || env.from != account)
|
|
17
|
+
return false;
|
|
18
|
+
if (authorize && !(await authorize(env)))
|
|
19
|
+
return false;
|
|
20
|
+
const target = ports.get(env.to);
|
|
21
|
+
if (!target)
|
|
22
|
+
return false;
|
|
23
|
+
target(env);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
function close() {
|
|
27
|
+
if (ports.get(account) == emit)
|
|
28
|
+
ports.delete(account);
|
|
29
|
+
signals.close();
|
|
30
|
+
}
|
|
31
|
+
return { account, send, signals, close };
|
|
32
|
+
}
|
|
33
|
+
function revoke(pair, accounts, reason) {
|
|
34
|
+
for (const account of accounts) {
|
|
35
|
+
ports.get(account)?.({ type: 'revoke', pair, from: '', to: account, reason });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
register,
|
|
40
|
+
revoke,
|
|
41
|
+
accounts: () => Array.from(ports.keys()),
|
|
42
|
+
close() {
|
|
43
|
+
ports.clear();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function channelFromDataChannel(dc) {
|
|
48
|
+
const msgCbs = new Set();
|
|
49
|
+
const closeCbs = new Set();
|
|
50
|
+
let closed = false;
|
|
51
|
+
function fireClose() {
|
|
52
|
+
if (closed)
|
|
53
|
+
return;
|
|
54
|
+
closed = true;
|
|
55
|
+
for (const cb of Array.from(closeCbs))
|
|
56
|
+
cb();
|
|
57
|
+
}
|
|
58
|
+
dc.onmessage = function onDcMessage(ev) {
|
|
59
|
+
const data = String(ev.data);
|
|
60
|
+
for (const cb of Array.from(msgCbs))
|
|
61
|
+
cb(data);
|
|
62
|
+
};
|
|
63
|
+
dc.onclose = fireClose;
|
|
64
|
+
dc.onerror = fireClose;
|
|
65
|
+
return {
|
|
66
|
+
send: data => dc.send(data),
|
|
67
|
+
onMessage: cb => { msgCbs.add(cb); return () => msgCbs.delete(cb); },
|
|
68
|
+
onClose: cb => { closeCbs.add(cb); return () => closeCbs.delete(cb); },
|
|
69
|
+
close: () => { dc.close(); fireClose(); },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function createWebRtcConnector(deps) {
|
|
73
|
+
const { port, rtc, self, peer, pair, session, label = 'direct', openTimeoutMs = 10_000 } = deps;
|
|
74
|
+
let state = 'idle';
|
|
75
|
+
let pc = null;
|
|
76
|
+
let channel = null;
|
|
77
|
+
let offSignals = null;
|
|
78
|
+
let abortOpen = null;
|
|
79
|
+
const [emitFail, failListen] = (0, Listen_1.listen)();
|
|
80
|
+
function teardown(next) {
|
|
81
|
+
state = next;
|
|
82
|
+
if (typeof offSignals == 'function')
|
|
83
|
+
offSignals();
|
|
84
|
+
else
|
|
85
|
+
offSignals?.off?.();
|
|
86
|
+
offSignals = null;
|
|
87
|
+
channel?.close?.();
|
|
88
|
+
channel = null;
|
|
89
|
+
pc?.close();
|
|
90
|
+
pc = null;
|
|
91
|
+
}
|
|
92
|
+
function fail(reason) {
|
|
93
|
+
if (state == 'closed' || state == 'failed')
|
|
94
|
+
return;
|
|
95
|
+
const abort = abortOpen;
|
|
96
|
+
abortOpen = null;
|
|
97
|
+
teardown('failed');
|
|
98
|
+
abort?.(reason instanceof Error ? reason : new Error(String(reason)));
|
|
99
|
+
emitFail(reason);
|
|
100
|
+
}
|
|
101
|
+
async function open() {
|
|
102
|
+
state = 'opening';
|
|
103
|
+
const me = rtc();
|
|
104
|
+
pc = me;
|
|
105
|
+
const dc = me.createDataChannel('replay');
|
|
106
|
+
let openTimer = null;
|
|
107
|
+
const opened = new Promise((resolve, reject) => {
|
|
108
|
+
abortOpen = reject;
|
|
109
|
+
dc.onopen = () => resolve();
|
|
110
|
+
openTimer = setTimeout(function webRtcOpenTimeout() {
|
|
111
|
+
reject(new Error('webrtc direct open timeout: ' + pair));
|
|
112
|
+
}, openTimeoutMs);
|
|
113
|
+
});
|
|
114
|
+
opened.catch(() => { });
|
|
115
|
+
offSignals = port.signals.on(function onSignal(env) {
|
|
116
|
+
if (env == null || env.pair != pair || env.to != self || pc != me)
|
|
117
|
+
return;
|
|
118
|
+
if (env.type == 'answer' && env.sdp != null) {
|
|
119
|
+
void Promise.resolve(me.setRemoteDescription({ type: 'answer', sdp: env.sdp }))
|
|
120
|
+
.catch(fail);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (env.type == 'ice' && env.candidate != null) {
|
|
124
|
+
void Promise.resolve(me.addIceCandidate(env.candidate)).catch(fail);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (env.type == 'revoke' || env.type == 'close') {
|
|
128
|
+
fail(new Error('direct route ' + env.type + (env.reason ? ': ' + env.reason : '')));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
me.onicecandidate = function onIce(ev) {
|
|
132
|
+
if (ev?.candidate != null) {
|
|
133
|
+
void port.send({ type: 'ice', pair, from: self, to: peer, candidate: ev.candidate });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
try {
|
|
137
|
+
const offer = await me.createOffer();
|
|
138
|
+
await me.setLocalDescription(offer);
|
|
139
|
+
const accepted = await port.send({ type: 'offer', pair, from: self, to: peer, sdp: offer.sdp, session });
|
|
140
|
+
if (accepted == false)
|
|
141
|
+
throw new Error('signaling rejected offer (endpoint not exposed): ' + pair);
|
|
142
|
+
await opened;
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
teardown('failed');
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
clearTimeout(openTimer);
|
|
150
|
+
abortOpen = null;
|
|
151
|
+
}
|
|
152
|
+
state = 'open';
|
|
153
|
+
channel = channelFromDataChannel(dc);
|
|
154
|
+
channel.onClose?.(function onDirectChannelDied() {
|
|
155
|
+
if (state == 'open')
|
|
156
|
+
fail(new Error('direct channel closed: ' + pair));
|
|
157
|
+
});
|
|
158
|
+
return (0, replay_channel_1.channelReplayRemote)(channel);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
info: { label, kind: 'direct', binary: false, ordered: true, reliable: true },
|
|
162
|
+
open,
|
|
163
|
+
close() {
|
|
164
|
+
if (state == 'closed')
|
|
165
|
+
return;
|
|
166
|
+
void port.send({ type: 'close', pair, from: self, to: peer });
|
|
167
|
+
teardown('closed');
|
|
168
|
+
},
|
|
169
|
+
state: () => state,
|
|
170
|
+
onFail: { on: cb => failListen.on(cb) },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function acceptWebRtcDirect(deps) {
|
|
174
|
+
const { port, rtc, self, serve, accept } = deps;
|
|
175
|
+
const sessions = new Map();
|
|
176
|
+
let closed = false;
|
|
177
|
+
function dropSession(key) {
|
|
178
|
+
const s = sessions.get(key);
|
|
179
|
+
if (!s)
|
|
180
|
+
return;
|
|
181
|
+
sessions.delete(key);
|
|
182
|
+
s.stop?.();
|
|
183
|
+
s.pc.close();
|
|
184
|
+
}
|
|
185
|
+
async function onOffer(env) {
|
|
186
|
+
const key = env.pair + '|' + env.from;
|
|
187
|
+
if (accept && !(await accept(env))) {
|
|
188
|
+
void port.send({ type: 'revoke', pair: env.pair, from: self, to: env.from, reason: 'offer rejected' });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const source = await serve(env);
|
|
192
|
+
if (!source) {
|
|
193
|
+
void port.send({ type: 'revoke', pair: env.pair, from: self, to: env.from, reason: 'nothing to serve' });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
dropSession(key);
|
|
197
|
+
const pc = rtc();
|
|
198
|
+
const session = { pc, stop: null };
|
|
199
|
+
sessions.set(key, session);
|
|
200
|
+
pc.ondatachannel = function onIncomingChannel(ev) {
|
|
201
|
+
session.stop = (0, replay_channel_1.serveReplayChannel)(source, channelFromDataChannel(ev.channel));
|
|
202
|
+
};
|
|
203
|
+
pc.onicecandidate = function onIce(ev) {
|
|
204
|
+
if (ev?.candidate != null) {
|
|
205
|
+
void port.send({ type: 'ice', pair: env.pair, from: self, to: env.from, candidate: ev.candidate });
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
try {
|
|
209
|
+
await pc.setRemoteDescription({ type: 'offer', sdp: env.sdp });
|
|
210
|
+
const answer = await pc.createAnswer();
|
|
211
|
+
await pc.setLocalDescription(answer);
|
|
212
|
+
void port.send({ type: 'answer', pair: env.pair, from: self, to: env.from, sdp: answer.sdp });
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
dropSession(key);
|
|
216
|
+
void port.send({ type: 'revoke', pair: env.pair, from: self, to: env.from, reason: 'negotiation failed' });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const offSignals = port.signals.on(function onAcceptSignal(env) {
|
|
220
|
+
if (closed || env == null || env.to != self)
|
|
221
|
+
return;
|
|
222
|
+
if (env.type == 'offer') {
|
|
223
|
+
void onOffer(env);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const key = env.pair + '|' + env.from;
|
|
227
|
+
if (env.type == 'ice' && env.candidate != null) {
|
|
228
|
+
void Promise.resolve(sessions.get(key)?.pc.addIceCandidate(env.candidate)).catch(() => dropSession(key));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (env.type == 'close' || env.type == 'revoke')
|
|
232
|
+
dropSession(key);
|
|
233
|
+
});
|
|
234
|
+
return function closeAccept() {
|
|
235
|
+
if (closed)
|
|
236
|
+
return;
|
|
237
|
+
closed = true;
|
|
238
|
+
if (typeof offSignals == 'function')
|
|
239
|
+
offSignals();
|
|
240
|
+
else
|
|
241
|
+
offSignals?.off?.();
|
|
242
|
+
for (const key of Array.from(sessions.keys()))
|
|
243
|
+
dropSession(key);
|
|
244
|
+
};
|
|
245
|
+
}
|