wenay-common2 1.0.66 → 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 +110 -94
- package/doc/changes/1.0.67.md +10 -0
- package/doc/changes/1.0.68.md +8 -0
- package/doc/wenay-common2-rare.md +56 -5
- 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 +3 -0
- package/lib/Common/events/replay-index.js +3 -0
- package/lib/Common/events/route-coordinator.d.ts +274 -0
- package/lib/Common/events/route-coordinator.js +316 -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
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { Listener } from './Listen';
|
|
2
|
+
import { ReplayRemote } from './replay-wire';
|
|
3
|
+
import { ReplayRouteSubscribeOpts } from './replay-route';
|
|
4
|
+
export type tRouteKind = 'relay' | 'direct';
|
|
5
|
+
export type tConnectorState = 'idle' | 'opening' | 'open' | 'closed' | 'failed';
|
|
6
|
+
export type RouteConnectorInfo = {
|
|
7
|
+
label: string;
|
|
8
|
+
kind: tRouteKind;
|
|
9
|
+
binary?: boolean;
|
|
10
|
+
ordered?: boolean;
|
|
11
|
+
reliable?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type RouteConnectorMetrics = {
|
|
14
|
+
rtt?: number;
|
|
15
|
+
pending?: number;
|
|
16
|
+
};
|
|
17
|
+
export type RouteConnector<Z extends any[] = any[]> = {
|
|
18
|
+
info: RouteConnectorInfo;
|
|
19
|
+
open: () => Promise<ReplayRemote<Z>> | ReplayRemote<Z>;
|
|
20
|
+
close: () => void;
|
|
21
|
+
state: () => tConnectorState;
|
|
22
|
+
metrics?: () => RouteConnectorMetrics;
|
|
23
|
+
onFail?: {
|
|
24
|
+
on: (cb: (reason?: unknown) => void) => any;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export type RoutePairRef = {
|
|
28
|
+
a: string;
|
|
29
|
+
b: string;
|
|
30
|
+
key: string;
|
|
31
|
+
};
|
|
32
|
+
export type RoutePolicyCtx = RoutePairRef & {
|
|
33
|
+
state: tRouteState;
|
|
34
|
+
reason?: unknown;
|
|
35
|
+
};
|
|
36
|
+
type tPolicyHook = (ctx: RoutePolicyCtx) => boolean | Promise<boolean>;
|
|
37
|
+
export type RoutePolicy = {
|
|
38
|
+
canDirect?: tPolicyHook;
|
|
39
|
+
mustRelay?: tPolicyHook;
|
|
40
|
+
mustShadowRelay?: tPolicyHook;
|
|
41
|
+
canExposeEndpoint?: tPolicyHook;
|
|
42
|
+
canReinterpose?: tPolicyHook;
|
|
43
|
+
};
|
|
44
|
+
export type tRouteState = 'relay' | 'direct:connecting' | 'direct' | 'direct+shadowRelay' | 'relay:reinterposing' | 'fallback' | 'blocked' | 'closed';
|
|
45
|
+
export type RouteChangeEvent = RoutePairRef & {
|
|
46
|
+
from: tRouteState;
|
|
47
|
+
to: tRouteState;
|
|
48
|
+
reason?: unknown;
|
|
49
|
+
};
|
|
50
|
+
export type RouteOpResult = {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
state: tRouteState;
|
|
53
|
+
reason?: unknown;
|
|
54
|
+
};
|
|
55
|
+
export type PromoteDirectOpts = {
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
reason?: unknown;
|
|
58
|
+
};
|
|
59
|
+
export type RouteCoordinatorDeps<Z extends any[] = any[]> = {
|
|
60
|
+
policy?: RoutePolicy;
|
|
61
|
+
connect: (ref: RoutePairRef, kind: tRouteKind) => RouteConnector<Z>;
|
|
62
|
+
shadow?: (ref: RoutePairRef, ...ev: Z) => void;
|
|
63
|
+
catchUpTimeoutMs?: number;
|
|
64
|
+
};
|
|
65
|
+
export declare function createRouteCoordinator<Z extends any[] = any[]>(deps: RouteCoordinatorDeps<Z>): {
|
|
66
|
+
pair(a: string, b: string): {
|
|
67
|
+
ref: RoutePairRef;
|
|
68
|
+
state: () => tRouteState;
|
|
69
|
+
reason: () => unknown;
|
|
70
|
+
label: () => string;
|
|
71
|
+
metrics: () => {
|
|
72
|
+
relay: {
|
|
73
|
+
rtt?: number;
|
|
74
|
+
pending?: number;
|
|
75
|
+
state: tConnectorState;
|
|
76
|
+
} | null;
|
|
77
|
+
direct: {
|
|
78
|
+
rtt?: number;
|
|
79
|
+
pending?: number;
|
|
80
|
+
state: tConnectorState;
|
|
81
|
+
} | null;
|
|
82
|
+
};
|
|
83
|
+
subscribe: (cb: Listener<Z>, opts?: Omit<ReplayRouteSubscribeOpts, "label">) => (() => void) & {
|
|
84
|
+
ready: Promise<void>;
|
|
85
|
+
seq: () => number;
|
|
86
|
+
label: () => string | undefined;
|
|
87
|
+
active: () => boolean;
|
|
88
|
+
};
|
|
89
|
+
promoteDirect: (opts?: PromoteDirectOpts) => Promise<RouteOpResult>;
|
|
90
|
+
reinterposeRelay: (reason?: unknown) => Promise<RouteOpResult>;
|
|
91
|
+
fallback: (reason?: unknown) => Promise<RouteOpResult>;
|
|
92
|
+
block: (reason?: unknown) => Promise<RouteOpResult>;
|
|
93
|
+
close: () => void;
|
|
94
|
+
};
|
|
95
|
+
state: (pairOrKey: {
|
|
96
|
+
ref: RoutePairRef;
|
|
97
|
+
state: () => tRouteState;
|
|
98
|
+
reason: () => unknown;
|
|
99
|
+
label: () => string;
|
|
100
|
+
metrics: () => {
|
|
101
|
+
relay: {
|
|
102
|
+
rtt?: number;
|
|
103
|
+
pending?: number;
|
|
104
|
+
state: tConnectorState;
|
|
105
|
+
} | null;
|
|
106
|
+
direct: {
|
|
107
|
+
rtt?: number;
|
|
108
|
+
pending?: number;
|
|
109
|
+
state: tConnectorState;
|
|
110
|
+
} | null;
|
|
111
|
+
};
|
|
112
|
+
subscribe: (cb: Listener<Z>, opts?: Omit<ReplayRouteSubscribeOpts, "label">) => (() => void) & {
|
|
113
|
+
ready: Promise<void>;
|
|
114
|
+
seq: () => number;
|
|
115
|
+
label: () => string | undefined;
|
|
116
|
+
active: () => boolean;
|
|
117
|
+
};
|
|
118
|
+
promoteDirect: (opts?: PromoteDirectOpts) => Promise<RouteOpResult>;
|
|
119
|
+
reinterposeRelay: (reason?: unknown) => Promise<RouteOpResult>;
|
|
120
|
+
fallback: (reason?: unknown) => Promise<RouteOpResult>;
|
|
121
|
+
block: (reason?: unknown) => Promise<RouteOpResult>;
|
|
122
|
+
close: () => void;
|
|
123
|
+
} | RoutePairRef | string) => tRouteState;
|
|
124
|
+
promoteDirect: (pairOrKey: {
|
|
125
|
+
ref: RoutePairRef;
|
|
126
|
+
state: () => tRouteState;
|
|
127
|
+
reason: () => unknown;
|
|
128
|
+
label: () => string;
|
|
129
|
+
metrics: () => {
|
|
130
|
+
relay: {
|
|
131
|
+
rtt?: number;
|
|
132
|
+
pending?: number;
|
|
133
|
+
state: tConnectorState;
|
|
134
|
+
} | null;
|
|
135
|
+
direct: {
|
|
136
|
+
rtt?: number;
|
|
137
|
+
pending?: number;
|
|
138
|
+
state: tConnectorState;
|
|
139
|
+
} | null;
|
|
140
|
+
};
|
|
141
|
+
subscribe: (cb: Listener<Z>, opts?: Omit<ReplayRouteSubscribeOpts, "label">) => (() => void) & {
|
|
142
|
+
ready: Promise<void>;
|
|
143
|
+
seq: () => number;
|
|
144
|
+
label: () => string | undefined;
|
|
145
|
+
active: () => boolean;
|
|
146
|
+
};
|
|
147
|
+
promoteDirect: (opts?: PromoteDirectOpts) => Promise<RouteOpResult>;
|
|
148
|
+
reinterposeRelay: (reason?: unknown) => Promise<RouteOpResult>;
|
|
149
|
+
fallback: (reason?: unknown) => Promise<RouteOpResult>;
|
|
150
|
+
block: (reason?: unknown) => Promise<RouteOpResult>;
|
|
151
|
+
close: () => void;
|
|
152
|
+
} | RoutePairRef | string, opts?: PromoteDirectOpts) => Promise<RouteOpResult>;
|
|
153
|
+
reinterposeRelay: (pairOrKey: {
|
|
154
|
+
ref: RoutePairRef;
|
|
155
|
+
state: () => tRouteState;
|
|
156
|
+
reason: () => unknown;
|
|
157
|
+
label: () => string;
|
|
158
|
+
metrics: () => {
|
|
159
|
+
relay: {
|
|
160
|
+
rtt?: number;
|
|
161
|
+
pending?: number;
|
|
162
|
+
state: tConnectorState;
|
|
163
|
+
} | null;
|
|
164
|
+
direct: {
|
|
165
|
+
rtt?: number;
|
|
166
|
+
pending?: number;
|
|
167
|
+
state: tConnectorState;
|
|
168
|
+
} | null;
|
|
169
|
+
};
|
|
170
|
+
subscribe: (cb: Listener<Z>, opts?: Omit<ReplayRouteSubscribeOpts, "label">) => (() => void) & {
|
|
171
|
+
ready: Promise<void>;
|
|
172
|
+
seq: () => number;
|
|
173
|
+
label: () => string | undefined;
|
|
174
|
+
active: () => boolean;
|
|
175
|
+
};
|
|
176
|
+
promoteDirect: (opts?: PromoteDirectOpts) => Promise<RouteOpResult>;
|
|
177
|
+
reinterposeRelay: (reason?: unknown) => Promise<RouteOpResult>;
|
|
178
|
+
fallback: (reason?: unknown) => Promise<RouteOpResult>;
|
|
179
|
+
block: (reason?: unknown) => Promise<RouteOpResult>;
|
|
180
|
+
close: () => void;
|
|
181
|
+
} | RoutePairRef | string, reason?: unknown) => Promise<RouteOpResult>;
|
|
182
|
+
fallback: (pairOrKey: {
|
|
183
|
+
ref: RoutePairRef;
|
|
184
|
+
state: () => tRouteState;
|
|
185
|
+
reason: () => unknown;
|
|
186
|
+
label: () => string;
|
|
187
|
+
metrics: () => {
|
|
188
|
+
relay: {
|
|
189
|
+
rtt?: number;
|
|
190
|
+
pending?: number;
|
|
191
|
+
state: tConnectorState;
|
|
192
|
+
} | null;
|
|
193
|
+
direct: {
|
|
194
|
+
rtt?: number;
|
|
195
|
+
pending?: number;
|
|
196
|
+
state: tConnectorState;
|
|
197
|
+
} | null;
|
|
198
|
+
};
|
|
199
|
+
subscribe: (cb: Listener<Z>, opts?: Omit<ReplayRouteSubscribeOpts, "label">) => (() => void) & {
|
|
200
|
+
ready: Promise<void>;
|
|
201
|
+
seq: () => number;
|
|
202
|
+
label: () => string | undefined;
|
|
203
|
+
active: () => boolean;
|
|
204
|
+
};
|
|
205
|
+
promoteDirect: (opts?: PromoteDirectOpts) => Promise<RouteOpResult>;
|
|
206
|
+
reinterposeRelay: (reason?: unknown) => Promise<RouteOpResult>;
|
|
207
|
+
fallback: (reason?: unknown) => Promise<RouteOpResult>;
|
|
208
|
+
block: (reason?: unknown) => Promise<RouteOpResult>;
|
|
209
|
+
close: () => void;
|
|
210
|
+
} | RoutePairRef | string, reason?: unknown) => Promise<RouteOpResult>;
|
|
211
|
+
block: (pairOrKey: {
|
|
212
|
+
ref: RoutePairRef;
|
|
213
|
+
state: () => tRouteState;
|
|
214
|
+
reason: () => unknown;
|
|
215
|
+
label: () => string;
|
|
216
|
+
metrics: () => {
|
|
217
|
+
relay: {
|
|
218
|
+
rtt?: number;
|
|
219
|
+
pending?: number;
|
|
220
|
+
state: tConnectorState;
|
|
221
|
+
} | null;
|
|
222
|
+
direct: {
|
|
223
|
+
rtt?: number;
|
|
224
|
+
pending?: number;
|
|
225
|
+
state: tConnectorState;
|
|
226
|
+
} | null;
|
|
227
|
+
};
|
|
228
|
+
subscribe: (cb: Listener<Z>, opts?: Omit<ReplayRouteSubscribeOpts, "label">) => (() => void) & {
|
|
229
|
+
ready: Promise<void>;
|
|
230
|
+
seq: () => number;
|
|
231
|
+
label: () => string | undefined;
|
|
232
|
+
active: () => boolean;
|
|
233
|
+
};
|
|
234
|
+
promoteDirect: (opts?: PromoteDirectOpts) => Promise<RouteOpResult>;
|
|
235
|
+
reinterposeRelay: (reason?: unknown) => Promise<RouteOpResult>;
|
|
236
|
+
fallback: (reason?: unknown) => Promise<RouteOpResult>;
|
|
237
|
+
block: (reason?: unknown) => Promise<RouteOpResult>;
|
|
238
|
+
close: () => void;
|
|
239
|
+
} | RoutePairRef | string, reason?: unknown) => Promise<RouteOpResult>;
|
|
240
|
+
onRoute: (cb: (ev: RouteChangeEvent) => void) => import("./Listen").ListenOff;
|
|
241
|
+
pairs: () => {
|
|
242
|
+
ref: RoutePairRef;
|
|
243
|
+
state: () => tRouteState;
|
|
244
|
+
reason: () => unknown;
|
|
245
|
+
label: () => string;
|
|
246
|
+
metrics: () => {
|
|
247
|
+
relay: {
|
|
248
|
+
rtt?: number;
|
|
249
|
+
pending?: number;
|
|
250
|
+
state: tConnectorState;
|
|
251
|
+
} | null;
|
|
252
|
+
direct: {
|
|
253
|
+
rtt?: number;
|
|
254
|
+
pending?: number;
|
|
255
|
+
state: tConnectorState;
|
|
256
|
+
} | null;
|
|
257
|
+
};
|
|
258
|
+
subscribe: (cb: Listener<Z>, opts?: Omit<ReplayRouteSubscribeOpts, "label">) => (() => void) & {
|
|
259
|
+
ready: Promise<void>;
|
|
260
|
+
seq: () => number;
|
|
261
|
+
label: () => string | undefined;
|
|
262
|
+
active: () => boolean;
|
|
263
|
+
};
|
|
264
|
+
promoteDirect: (opts?: PromoteDirectOpts) => Promise<RouteOpResult>;
|
|
265
|
+
reinterposeRelay: (reason?: unknown) => Promise<RouteOpResult>;
|
|
266
|
+
fallback: (reason?: unknown) => Promise<RouteOpResult>;
|
|
267
|
+
block: (reason?: unknown) => Promise<RouteOpResult>;
|
|
268
|
+
close: () => void;
|
|
269
|
+
}[];
|
|
270
|
+
close(): void;
|
|
271
|
+
};
|
|
272
|
+
export type RouteCoordinator<Z extends any[] = any[]> = ReturnType<typeof createRouteCoordinator<Z>>;
|
|
273
|
+
export type RouteLink<Z extends any[] = any[]> = ReturnType<RouteCoordinator<Z>['pair']>;
|
|
274
|
+
export {};
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRouteCoordinator = createRouteCoordinator;
|
|
4
|
+
const Listen_1 = require("./Listen");
|
|
5
|
+
const replay_route_1 = require("./replay-route");
|
|
6
|
+
function unsubscribeHandle(handle) {
|
|
7
|
+
if (typeof handle == 'function') {
|
|
8
|
+
handle();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (typeof handle?.off == 'function')
|
|
12
|
+
handle.off();
|
|
13
|
+
else if (typeof handle?.unsubscribe == 'function')
|
|
14
|
+
handle.unsubscribe();
|
|
15
|
+
}
|
|
16
|
+
function lazyRemote(connector) {
|
|
17
|
+
let opened = null;
|
|
18
|
+
const get = () => opened ??= Promise.resolve(connector.open());
|
|
19
|
+
function lazyLine(pick) {
|
|
20
|
+
return {
|
|
21
|
+
on(cb) {
|
|
22
|
+
let off = null;
|
|
23
|
+
let dead = false;
|
|
24
|
+
get().then(function attachLazyLine(r) { if (!dead)
|
|
25
|
+
off = pick(r).on(cb); }, function swallowOpenError() { });
|
|
26
|
+
return function offLazyLine() { dead = true; unsubscribeHandle(off); };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
line: lazyLine(r => r.line),
|
|
32
|
+
frameLine: lazyLine(r => r.frameLine ?? r.line),
|
|
33
|
+
since: async (seq) => (await get()).since(seq),
|
|
34
|
+
keyframe: async () => (await get()).keyframe(),
|
|
35
|
+
frame: async (seq, hint) => {
|
|
36
|
+
const r = await get();
|
|
37
|
+
return r.frame ? r.frame(seq, hint) : null;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function createRouteCoordinator(deps) {
|
|
42
|
+
const { policy = {}, connect, shadow, catchUpTimeoutMs } = deps;
|
|
43
|
+
const links = new Map();
|
|
44
|
+
const [emitRoute, routeListen] = (0, Listen_1.listen)();
|
|
45
|
+
async function allowed(hook, ctx, absent = true) {
|
|
46
|
+
return hook ? !!(await hook(ctx)) : absent;
|
|
47
|
+
}
|
|
48
|
+
function pairKey(a, b) {
|
|
49
|
+
return a <= b ? a + '|' + b : b + '|' + a;
|
|
50
|
+
}
|
|
51
|
+
function createLink(a, b) {
|
|
52
|
+
const ref = { a, b, key: pairKey(a, b) };
|
|
53
|
+
let state = 'relay';
|
|
54
|
+
let lastReason;
|
|
55
|
+
let relayConn = null;
|
|
56
|
+
let relayRemote = null;
|
|
57
|
+
let directConn = null;
|
|
58
|
+
let directRemote = null;
|
|
59
|
+
let shadowSub = null;
|
|
60
|
+
const subs = new Set();
|
|
61
|
+
let opChain = Promise.resolve();
|
|
62
|
+
function chained(run) {
|
|
63
|
+
const p = opChain.then(run, run);
|
|
64
|
+
opChain = p.catch(() => { });
|
|
65
|
+
return p;
|
|
66
|
+
}
|
|
67
|
+
function setState(to, reason) {
|
|
68
|
+
if (state == 'closed')
|
|
69
|
+
return;
|
|
70
|
+
const from = state;
|
|
71
|
+
state = to;
|
|
72
|
+
lastReason = reason;
|
|
73
|
+
emitRoute({ ...ref, from, to, reason });
|
|
74
|
+
}
|
|
75
|
+
function ctx(reason) {
|
|
76
|
+
return { ...ref, state, reason };
|
|
77
|
+
}
|
|
78
|
+
function ensureRelay() {
|
|
79
|
+
if (!relayConn || relayConn.state() == 'closed' || relayConn.state() == 'failed') {
|
|
80
|
+
relayConn = connect(ref, 'relay');
|
|
81
|
+
relayRemote = lazyRemote(relayConn);
|
|
82
|
+
watchFail(relayConn, 'relay');
|
|
83
|
+
}
|
|
84
|
+
return relayRemote;
|
|
85
|
+
}
|
|
86
|
+
function watchFail(conn, kind) {
|
|
87
|
+
conn.onFail?.on(function onConnectorFail(reason) {
|
|
88
|
+
if (kind == 'relay') {
|
|
89
|
+
if (conn == relayConn)
|
|
90
|
+
emitRoute({ ...ref, from: state, to: state, reason });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (conn == directConn && (state == 'direct' || state == 'direct+shadowRelay')) {
|
|
94
|
+
demoteToRelay('fallback', reason).catch(() => { });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function currentRemote() {
|
|
99
|
+
const direct = (state == 'direct' || state == 'direct+shadowRelay') && directRemote;
|
|
100
|
+
return direct || ensureRelay();
|
|
101
|
+
}
|
|
102
|
+
function currentLabel() {
|
|
103
|
+
const direct = (state == 'direct' || state == 'direct+shadowRelay') && directConn;
|
|
104
|
+
return direct ? directConn.info.label : (relayConn?.info.label ?? 'relay');
|
|
105
|
+
}
|
|
106
|
+
async function switchSubs(remote, label, timeoutMs) {
|
|
107
|
+
const jobs = Array.from(subs, function switchOne(sub) { return sub.switch(remote, { label }); });
|
|
108
|
+
if (timeoutMs == null)
|
|
109
|
+
return Promise.all(jobs);
|
|
110
|
+
let timer;
|
|
111
|
+
const timeout = new Promise((_, reject) => {
|
|
112
|
+
timer = setTimeout(function catchUpTimeout() {
|
|
113
|
+
reject(new Error('route catch-up timeout: ' + label));
|
|
114
|
+
}, timeoutMs);
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
await Promise.race([Promise.all(jobs), timeout]);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function resyncStraySubs() {
|
|
124
|
+
const remote = currentRemote();
|
|
125
|
+
const label = currentLabel();
|
|
126
|
+
for (const sub of subs) {
|
|
127
|
+
if (sub.label() != label)
|
|
128
|
+
sub.switch(remote, { label }).catch(() => { });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function attachShadow() {
|
|
132
|
+
if (!shadow || shadowSub)
|
|
133
|
+
return;
|
|
134
|
+
const since = subs.size ? Math.min(...Array.from(subs, sub => sub.seq())) : -1;
|
|
135
|
+
shadowSub = (0, replay_route_1.replayRouteSubscribe)(ensureRelay(), function shadowTap(...ev) { shadow(ref, ...ev); }, { label: 'shadowRelay', since });
|
|
136
|
+
}
|
|
137
|
+
function dropShadow() {
|
|
138
|
+
shadowSub?.();
|
|
139
|
+
shadowSub = null;
|
|
140
|
+
}
|
|
141
|
+
function closeDirect() {
|
|
142
|
+
dropShadow();
|
|
143
|
+
directConn?.close();
|
|
144
|
+
directConn = null;
|
|
145
|
+
directRemote = null;
|
|
146
|
+
}
|
|
147
|
+
async function demoteToRelay(finalState, reason) {
|
|
148
|
+
return chained(async function demoteOp() {
|
|
149
|
+
if (state == 'blocked' || state == 'closed')
|
|
150
|
+
return { ok: false, state, reason: state };
|
|
151
|
+
if (state != 'direct' && state != 'direct+shadowRelay') {
|
|
152
|
+
if (finalState == 'fallback')
|
|
153
|
+
setState('fallback', reason);
|
|
154
|
+
return { ok: true, state };
|
|
155
|
+
}
|
|
156
|
+
if (finalState == 'relay' && !(await allowed(policy.canReinterpose, ctx(reason)))) {
|
|
157
|
+
return { ok: false, state, reason: 'policy: canReinterpose' };
|
|
158
|
+
}
|
|
159
|
+
setState('relay:reinterposing', reason);
|
|
160
|
+
const remote = ensureRelay();
|
|
161
|
+
try {
|
|
162
|
+
await switchSubs(remote, relayConn.info.label);
|
|
163
|
+
closeDirect();
|
|
164
|
+
setState(finalState, reason);
|
|
165
|
+
resyncStraySubs();
|
|
166
|
+
return { ok: true, state };
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
setState(shadowSub ? 'direct+shadowRelay' : 'direct', e);
|
|
170
|
+
return { ok: false, state, reason: e };
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function promoteDirect(opts = {}) {
|
|
175
|
+
return chained(async function promoteOp() {
|
|
176
|
+
const { timeoutMs = catchUpTimeoutMs, reason } = opts;
|
|
177
|
+
if (state == 'blocked' || state == 'closed')
|
|
178
|
+
return { ok: false, state, reason: state };
|
|
179
|
+
if (state == 'direct' || state == 'direct+shadowRelay')
|
|
180
|
+
return { ok: true, state };
|
|
181
|
+
if (await allowed(policy.mustRelay, ctx(reason), false)) {
|
|
182
|
+
return { ok: false, state, reason: 'policy: mustRelay' };
|
|
183
|
+
}
|
|
184
|
+
if (!(await allowed(policy.canDirect, ctx(reason)))) {
|
|
185
|
+
return { ok: false, state, reason: 'policy: canDirect' };
|
|
186
|
+
}
|
|
187
|
+
if (!(await allowed(policy.canExposeEndpoint, ctx(reason)))) {
|
|
188
|
+
return { ok: false, state, reason: 'policy: canExposeEndpoint' };
|
|
189
|
+
}
|
|
190
|
+
const wantShadow = await allowed(policy.mustShadowRelay, ctx(reason), false);
|
|
191
|
+
setState('direct:connecting', reason);
|
|
192
|
+
const conn = connect(ref, 'direct');
|
|
193
|
+
const remote = lazyRemote(conn);
|
|
194
|
+
try {
|
|
195
|
+
await switchSubs(remote, conn.info.label, timeoutMs);
|
|
196
|
+
directConn = conn;
|
|
197
|
+
directRemote = remote;
|
|
198
|
+
watchFail(conn, 'direct');
|
|
199
|
+
if (wantShadow) {
|
|
200
|
+
setState('direct+shadowRelay', reason);
|
|
201
|
+
attachShadow();
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
relayConn?.close();
|
|
205
|
+
relayConn = null;
|
|
206
|
+
relayRemote = null;
|
|
207
|
+
setState('direct', reason);
|
|
208
|
+
}
|
|
209
|
+
resyncStraySubs();
|
|
210
|
+
return { ok: true, state };
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
conn.close();
|
|
214
|
+
try {
|
|
215
|
+
await switchSubs(ensureRelay(), relayConn.info.label);
|
|
216
|
+
}
|
|
217
|
+
catch { }
|
|
218
|
+
setState('fallback', e);
|
|
219
|
+
return { ok: false, state, reason: e };
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function block(reason) {
|
|
224
|
+
return chained(async function blockOp() {
|
|
225
|
+
if (state == 'closed')
|
|
226
|
+
return { ok: false, state, reason: state };
|
|
227
|
+
closeDirect();
|
|
228
|
+
relayConn?.close();
|
|
229
|
+
relayConn = null;
|
|
230
|
+
relayRemote = null;
|
|
231
|
+
for (const sub of Array.from(subs))
|
|
232
|
+
sub();
|
|
233
|
+
subs.clear();
|
|
234
|
+
setState('blocked', reason);
|
|
235
|
+
return { ok: true, state };
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function subscribe(cb, opts = {}) {
|
|
239
|
+
if (state == 'blocked' || state == 'closed') {
|
|
240
|
+
throw new Error('route coordinator: pair ' + ref.key + ' is ' + state);
|
|
241
|
+
}
|
|
242
|
+
const sub = (0, replay_route_1.replayRouteSubscribe)(currentRemote(), cb, { ...opts, label: currentLabel() });
|
|
243
|
+
subs.add(sub);
|
|
244
|
+
function off() {
|
|
245
|
+
subs.delete(sub);
|
|
246
|
+
sub();
|
|
247
|
+
}
|
|
248
|
+
return Object.assign(off, {
|
|
249
|
+
ready: sub.ready,
|
|
250
|
+
seq: sub.seq,
|
|
251
|
+
label: sub.label,
|
|
252
|
+
active: sub.active,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function close() {
|
|
256
|
+
if (state == 'closed')
|
|
257
|
+
return;
|
|
258
|
+
closeDirect();
|
|
259
|
+
relayConn?.close();
|
|
260
|
+
relayConn = null;
|
|
261
|
+
relayRemote = null;
|
|
262
|
+
for (const sub of Array.from(subs))
|
|
263
|
+
sub();
|
|
264
|
+
subs.clear();
|
|
265
|
+
links.delete(ref.key);
|
|
266
|
+
setState('closed');
|
|
267
|
+
}
|
|
268
|
+
const link = {
|
|
269
|
+
ref,
|
|
270
|
+
state: () => state,
|
|
271
|
+
reason: () => lastReason,
|
|
272
|
+
label: currentLabel,
|
|
273
|
+
metrics: () => ({
|
|
274
|
+
relay: relayConn ? { state: relayConn.state(), ...relayConn.metrics?.() } : null,
|
|
275
|
+
direct: directConn ? { state: directConn.state(), ...directConn.metrics?.() } : null,
|
|
276
|
+
}),
|
|
277
|
+
subscribe,
|
|
278
|
+
promoteDirect,
|
|
279
|
+
reinterposeRelay: (reason) => demoteToRelay('relay', reason),
|
|
280
|
+
fallback: (reason) => demoteToRelay('fallback', reason),
|
|
281
|
+
block,
|
|
282
|
+
close,
|
|
283
|
+
};
|
|
284
|
+
return link;
|
|
285
|
+
}
|
|
286
|
+
function resolve(pairOrKey) {
|
|
287
|
+
const key = typeof pairOrKey == 'string' ? pairOrKey
|
|
288
|
+
: 'ref' in pairOrKey ? pairOrKey.ref.key : pairOrKey.key;
|
|
289
|
+
const link = links.get(key);
|
|
290
|
+
if (!link)
|
|
291
|
+
throw new Error('route coordinator: unknown pair ' + key);
|
|
292
|
+
return link;
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
pair(a, b) {
|
|
296
|
+
const key = pairKey(a, b);
|
|
297
|
+
let link = links.get(key);
|
|
298
|
+
if (!link || link.state() == 'closed') {
|
|
299
|
+
link = createLink(a, b);
|
|
300
|
+
links.set(key, link);
|
|
301
|
+
}
|
|
302
|
+
return link;
|
|
303
|
+
},
|
|
304
|
+
state: (pairOrKey) => resolve(pairOrKey).state(),
|
|
305
|
+
promoteDirect: (pairOrKey, opts) => resolve(pairOrKey).promoteDirect(opts),
|
|
306
|
+
reinterposeRelay: (pairOrKey, reason) => resolve(pairOrKey).reinterposeRelay(reason),
|
|
307
|
+
fallback: (pairOrKey, reason) => resolve(pairOrKey).fallback(reason),
|
|
308
|
+
block: (pairOrKey, reason) => resolve(pairOrKey).block(reason),
|
|
309
|
+
onRoute: (cb) => routeListen.on(cb),
|
|
310
|
+
pairs: () => Array.from(links.values()),
|
|
311
|
+
close() {
|
|
312
|
+
for (const link of Array.from(links.values()))
|
|
313
|
+
link.close();
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
@@ -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;
|