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/wenay-common2.md
CHANGED
|
@@ -201,6 +201,43 @@ failures surface via `onPublishError`, never silently. Policy/session material:
|
|
|
201
201
|
contract and the underlying primitives (`createRouteCoordinator`, `createSignalHub`,
|
|
202
202
|
`createWebRtcConnector`). Oracle: `replay/peer-sdk.test.ts`.
|
|
203
203
|
|
|
204
|
+
### Calls, presence and the media relay (messenger-style, on the same parts)
|
|
205
|
+
```
|
|
206
|
+
// presence rides in the fragment (host >= 1.0.74): subscribe FIRST, then list()
|
|
207
|
+
c.app.func.peer.presence.changes.on(({account, online}) => {})
|
|
208
|
+
await c.app.func.peer.presence.list() // ['a', 'b', ...] — connected right now
|
|
209
|
+
|
|
210
|
+
// SERVER — media relay next to the fragment (per-account named lines, policy-gated watch):
|
|
211
|
+
const media = Peer.createMediaRelay({
|
|
212
|
+
lines: {cam: 'video', mic: 'audio', screen: 'video'},
|
|
213
|
+
canWatch?: (watcher, owner, line) => bool, // ACL, checked on paths + every forwarded frame
|
|
214
|
+
})
|
|
215
|
+
object: {peer: peer.fragment, media: {publish: media.publishOf(account), watch: media.watchOf(account)}}
|
|
216
|
+
media.dropAccount(account) // retire lines + keyspace (e.g. on presence offline); next publish revives
|
|
217
|
+
|
|
218
|
+
// CLIENT — calls are envelopes over the SAME signal port; media attach stays app code:
|
|
219
|
+
const calls = Peer.createCallManager({port: Peer.callPortOf(c.app.func.peer), self: 'a'})
|
|
220
|
+
await calls.ready // signal subscription confirmed server-side
|
|
221
|
+
calls.rings.on(call => { call.accept() /* or call.decline() */ })
|
|
222
|
+
const call = calls.call('b', {kinds: ['cam', 'mic']}) // meta rides opaquely
|
|
223
|
+
call.changed.on(state => {}) // 'active' | 'ended'
|
|
224
|
+
await call.ended // 'declined'|'busy'|'offline'|'timeout'|'hangup'|'canceled'
|
|
225
|
+
call.hangup()
|
|
226
|
+
// while active: publish own frames + attach the peer's lines (Media.attach* viewers)
|
|
227
|
+
media.publish('cam', frame, Date.now()); attachVideoCanvas(c.app.func.media.watch.b.cam, canvas)
|
|
228
|
+
```
|
|
229
|
+
Relay-first is the privacy default (mainstream messengers route calls through their servers too);
|
|
230
|
+
`promoteDirect` stays the policy-gated opt-in for the data path. Ring/accept/decline/hangup ride the
|
|
231
|
+
existing signal hub — the host `authorize` hook sees them too (single server-side policy point), and
|
|
232
|
+
an offline callee fails fast (`'offline'`), no timeout wait. The default incoming gate auto-declines
|
|
233
|
+
with `'busy'` during a live call, which also settles simultaneous cross-calls. `video` relay lines
|
|
234
|
+
are keep-latest (late joiner pulls `keyframe()` instantly), `audio` is a short lossless queue; frames
|
|
235
|
+
ride as `[frame, sentAt]` so viewer latency stats work out of the box. Watch access is an ACL, not a
|
|
236
|
+
convention: `watchOf(account)` views run `canWatch` on every new subscribe/keyframe and every live
|
|
237
|
+
frame. "Media access follows the call" is a few lines of app code (grant on `'active'`, revoke on
|
|
238
|
+
`'ended'`); after revocation an already-open policy view receives no more data. Oracle:
|
|
239
|
+
`replay/peer-call.test.ts`.
|
|
240
|
+
|
|
204
241
|
## 🔁 Observe — reactive state + store/mirror API
|
|
205
242
|
> `import { Observe } from "wenay-common2"` or `import * as Observe from "wenay-common2/observe"`.
|
|
206
243
|
> This is the documented v2 reactive/store surface.
|
|
@@ -391,6 +428,3 @@ facade — details in rare docs. New code should declare `frame` on the line ins
|
|
|
391
428
|
Wire-level proof/oracles: `npx ts-node replay/rpc-auto.test.ts` (real Socket.IO: auto-exposure, legacy
|
|
392
429
|
parity, frame equivalence, gate lag sim), plus `replay/conflate-socket.test.ts`, `replay/conflate.test.ts`,
|
|
393
430
|
`replay/coalesce.test.ts`. Full generic surface (history/time-travel, archive) → 🎞️ in rare docs.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ReplayRemote } from './replay-wire';
|
|
2
2
|
import { RouteConnector } from './route-coordinator';
|
|
3
3
|
import { ReplayMessageChannel } from './replay-channel';
|
|
4
|
-
export type tSignalType = 'offer' | 'answer' | 'ice' | 'revoke' | 'close';
|
|
4
|
+
export type tSignalType = 'offer' | 'answer' | 'ice' | 'revoke' | 'close' | 'ring' | 'accept' | 'decline' | 'hangup';
|
|
5
5
|
export type SignalEnvelope = {
|
|
6
6
|
type: tSignalType;
|
|
7
7
|
pair: string;
|
|
@@ -11,28 +11,38 @@ function createSignalHub(deps = {}) {
|
|
|
11
11
|
const ports = new Map();
|
|
12
12
|
function register(account) {
|
|
13
13
|
const [emit, signals] = (0, Listen_1.listen)();
|
|
14
|
-
ports.
|
|
14
|
+
const accountPorts = ports.get(account) ?? [];
|
|
15
|
+
accountPorts.push(emit);
|
|
16
|
+
ports.set(account, accountPorts);
|
|
15
17
|
async function send(env) {
|
|
16
18
|
if (env == null || env.from != account)
|
|
17
19
|
return false;
|
|
18
20
|
if (authorize && !(await authorize(env)))
|
|
19
21
|
return false;
|
|
20
|
-
const
|
|
22
|
+
const targets = ports.get(env.to);
|
|
23
|
+
const target = targets?.[targets.length - 1];
|
|
21
24
|
if (!target)
|
|
22
25
|
return false;
|
|
23
26
|
target(env);
|
|
24
27
|
return true;
|
|
25
28
|
}
|
|
26
29
|
function close() {
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
const accountPorts = ports.get(account);
|
|
31
|
+
if (accountPorts) {
|
|
32
|
+
const i = accountPorts.indexOf(emit);
|
|
33
|
+
if (i >= 0)
|
|
34
|
+
accountPorts.splice(i, 1);
|
|
35
|
+
if (!accountPorts.length)
|
|
36
|
+
ports.delete(account);
|
|
37
|
+
}
|
|
29
38
|
signals.close();
|
|
30
39
|
}
|
|
31
40
|
return { account, send, signals, close };
|
|
32
41
|
}
|
|
33
42
|
function revoke(pair, accounts, reason) {
|
|
34
43
|
for (const account of accounts) {
|
|
35
|
-
ports.get(account)
|
|
44
|
+
const accountPorts = ports.get(account);
|
|
45
|
+
accountPorts?.[accountPorts.length - 1]?.({ type: 'revoke', pair, from: '', to: account, reason });
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
48
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
type tMediaLine = {
|
|
2
|
-
on(cb: (frame: any, sentAt?: number) => void):
|
|
2
|
+
on(cb: (frame: any, sentAt?: number) => void): any;
|
|
3
3
|
};
|
|
4
4
|
export type AttachVideoCanvasOpts = {
|
|
5
5
|
createBitmap?: (blob: any) => Promise<any>;
|
|
@@ -38,5 +38,5 @@ export type PipeMediaPublishOpts = {
|
|
|
38
38
|
stamp?: boolean;
|
|
39
39
|
onError?: (e: unknown) => void;
|
|
40
40
|
};
|
|
41
|
-
export declare function pipeMediaPublish(line: tMediaLine, publish: (frame: Uint8Array, sentAt?: number) => unknown, opts?: PipeMediaPublishOpts):
|
|
41
|
+
export declare function pipeMediaPublish(line: tMediaLine, publish: (frame: Uint8Array, sentAt?: number) => unknown, opts?: PipeMediaPublishOpts): any;
|
|
42
42
|
export {};
|
|
@@ -4,6 +4,25 @@ exports.attachVideoCanvas = attachVideoCanvas;
|
|
|
4
4
|
exports.attachAudioPlayer = attachAudioPlayer;
|
|
5
5
|
exports.pipeMediaPublish = pipeMediaPublish;
|
|
6
6
|
const media_source_1 = require("./media-source");
|
|
7
|
+
function makeOff(raw) {
|
|
8
|
+
function unsubscribe(h) {
|
|
9
|
+
if (typeof h == 'function')
|
|
10
|
+
h();
|
|
11
|
+
else
|
|
12
|
+
h?.off?.();
|
|
13
|
+
}
|
|
14
|
+
return function off() {
|
|
15
|
+
if (typeof raw == 'function') {
|
|
16
|
+
raw();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (raw != null && typeof raw.then == 'function') {
|
|
20
|
+
void raw.then(unsubscribe);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
unsubscribe(raw);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
7
26
|
function mimeForCodec(codec) {
|
|
8
27
|
if (codec == 'png')
|
|
9
28
|
return 'image/png';
|
|
@@ -57,7 +76,7 @@ function attachVideoCanvas(line, canvas, opts = {}) {
|
|
|
57
76
|
let width = 0;
|
|
58
77
|
let height = 0;
|
|
59
78
|
let busy = false;
|
|
60
|
-
const off = line.on(async function onVideoFrame(raw, sentAt) {
|
|
79
|
+
const off = makeOff(line.on(async function onVideoFrame(raw, sentAt) {
|
|
61
80
|
frames++;
|
|
62
81
|
age.note(sentAt);
|
|
63
82
|
rate.note();
|
|
@@ -85,7 +104,7 @@ function attachVideoCanvas(line, canvas, opts = {}) {
|
|
|
85
104
|
finally {
|
|
86
105
|
busy = false;
|
|
87
106
|
}
|
|
88
|
-
});
|
|
107
|
+
}));
|
|
89
108
|
return {
|
|
90
109
|
stats: () => ({ frames, drawn, perSec: rate.perSec, ageMs: age.ageMs, width, height }),
|
|
91
110
|
off,
|
|
@@ -150,7 +169,7 @@ function attachAudioPlayer(line, opts = {}) {
|
|
|
150
169
|
playhead = at + nFrames / sampleRate;
|
|
151
170
|
played++;
|
|
152
171
|
}
|
|
153
|
-
const off = line.on(function onAudioFrame(raw, sentAt) {
|
|
172
|
+
const off = makeOff(line.on(function onAudioFrame(raw, sentAt) {
|
|
154
173
|
frames++;
|
|
155
174
|
age.note(sentAt);
|
|
156
175
|
rate.note();
|
|
@@ -162,7 +181,7 @@ function attachAudioPlayer(line, opts = {}) {
|
|
|
162
181
|
catch (e) {
|
|
163
182
|
opts.onError?.(e);
|
|
164
183
|
}
|
|
165
|
-
});
|
|
184
|
+
}));
|
|
166
185
|
return {
|
|
167
186
|
enable() {
|
|
168
187
|
audioCtx = audioCtx ?? makeContext();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { SignalEnvelope, SignalPort } from '../events/route-signal-webrtc';
|
|
2
|
+
export type tCallState = 'ringing' | 'active' | 'ended';
|
|
3
|
+
export type tCallEnd = 'declined' | 'busy' | 'offline' | 'timeout' | 'hangup' | 'canceled' | 'closed';
|
|
4
|
+
export type CallManagerDeps = {
|
|
5
|
+
port: SignalPort;
|
|
6
|
+
self: string;
|
|
7
|
+
ringTimeoutMs?: number;
|
|
8
|
+
incoming?: (info: {
|
|
9
|
+
peer: string;
|
|
10
|
+
meta?: unknown;
|
|
11
|
+
}) => boolean | Promise<boolean>;
|
|
12
|
+
};
|
|
13
|
+
export declare function callPortOf(remote: {
|
|
14
|
+
signal: {
|
|
15
|
+
send: (env: SignalEnvelope) => any;
|
|
16
|
+
signals: {
|
|
17
|
+
on: (cb: (env: SignalEnvelope) => void) => any;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}): SignalPort;
|
|
21
|
+
export declare function createCallManager(deps: CallManagerDeps): {
|
|
22
|
+
ready: Promise<void>;
|
|
23
|
+
call: (other: string, meta?: unknown) => {
|
|
24
|
+
id: string;
|
|
25
|
+
peer: string;
|
|
26
|
+
direction: "out" | "in";
|
|
27
|
+
meta: unknown;
|
|
28
|
+
state: () => tCallState;
|
|
29
|
+
reason: () => tCallEnd | null;
|
|
30
|
+
changed: import("../events/Listen").ListenApi<[tCallState]>;
|
|
31
|
+
ended: Promise<tCallEnd>;
|
|
32
|
+
accept(): void;
|
|
33
|
+
decline(why?: tCallEnd): void;
|
|
34
|
+
hangup(): void;
|
|
35
|
+
};
|
|
36
|
+
rings: import("../events/Listen").ListenApi<[{
|
|
37
|
+
id: string;
|
|
38
|
+
peer: string;
|
|
39
|
+
direction: "out" | "in";
|
|
40
|
+
meta: unknown;
|
|
41
|
+
state: () => tCallState;
|
|
42
|
+
reason: () => tCallEnd | null;
|
|
43
|
+
changed: import("../events/Listen").ListenApi<[tCallState]>;
|
|
44
|
+
ended: Promise<tCallEnd>;
|
|
45
|
+
accept(): void;
|
|
46
|
+
decline(why?: tCallEnd): void;
|
|
47
|
+
hangup(): void;
|
|
48
|
+
}]>;
|
|
49
|
+
active: () => {
|
|
50
|
+
id: string;
|
|
51
|
+
peer: string;
|
|
52
|
+
direction: "out" | "in";
|
|
53
|
+
meta: unknown;
|
|
54
|
+
state: () => tCallState;
|
|
55
|
+
reason: () => tCallEnd | null;
|
|
56
|
+
changed: import("../events/Listen").ListenApi<[tCallState]>;
|
|
57
|
+
ended: Promise<tCallEnd>;
|
|
58
|
+
accept(): void;
|
|
59
|
+
decline(why?: tCallEnd): void;
|
|
60
|
+
hangup(): void;
|
|
61
|
+
} | null;
|
|
62
|
+
close(): void;
|
|
63
|
+
};
|
|
64
|
+
export type CallManager = ReturnType<typeof createCallManager>;
|
|
65
|
+
export type CallHandle = ReturnType<CallManager['call']>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.callPortOf = callPortOf;
|
|
4
|
+
exports.createCallManager = createCallManager;
|
|
5
|
+
const Listen_1 = require("../events/Listen");
|
|
6
|
+
function callPortOf(remote) {
|
|
7
|
+
return {
|
|
8
|
+
send: env => remote.signal.send(env),
|
|
9
|
+
signals: { on: cb => remote.signal.signals.on(cb) },
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function createCallManager(deps) {
|
|
13
|
+
const { port, self, ringTimeoutMs = 30_000, incoming } = deps;
|
|
14
|
+
const [emitRing, rings] = (0, Listen_1.listen)();
|
|
15
|
+
const calls = new Map();
|
|
16
|
+
let n = 0;
|
|
17
|
+
const callScope = Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
|
18
|
+
let evaluatingIncoming = 0;
|
|
19
|
+
let closed = false;
|
|
20
|
+
function makeCall(pair, other, direction, meta) {
|
|
21
|
+
let state = 'ringing';
|
|
22
|
+
let reason = null;
|
|
23
|
+
const [emitChange, changed] = (0, Listen_1.listen)();
|
|
24
|
+
let resolveEnded;
|
|
25
|
+
const ended = new Promise(r => { resolveEnded = r; });
|
|
26
|
+
const ringTimer = setTimeout(function onRingTimeout() { finish('timeout', direction == 'out'); }, ringTimeoutMs);
|
|
27
|
+
function send(type, why) {
|
|
28
|
+
void Promise.resolve(port.send({ type, pair, from: self, to: other, reason: why })).catch(swallowSendError);
|
|
29
|
+
}
|
|
30
|
+
function finish(why, notify, notifyAs = 'hangup') {
|
|
31
|
+
if (state == 'ended')
|
|
32
|
+
return;
|
|
33
|
+
state = 'ended';
|
|
34
|
+
reason = why;
|
|
35
|
+
clearTimeout(ringTimer);
|
|
36
|
+
calls.delete(pair);
|
|
37
|
+
if (notify)
|
|
38
|
+
send(notifyAs, why);
|
|
39
|
+
emitChange('ended');
|
|
40
|
+
changed.close();
|
|
41
|
+
resolveEnded(why);
|
|
42
|
+
}
|
|
43
|
+
function activate() {
|
|
44
|
+
if (state != 'ringing')
|
|
45
|
+
return;
|
|
46
|
+
state = 'active';
|
|
47
|
+
clearTimeout(ringTimer);
|
|
48
|
+
emitChange('active');
|
|
49
|
+
}
|
|
50
|
+
const handle = {
|
|
51
|
+
id: pair,
|
|
52
|
+
peer: other,
|
|
53
|
+
direction,
|
|
54
|
+
meta,
|
|
55
|
+
state: () => state,
|
|
56
|
+
reason: () => reason,
|
|
57
|
+
changed,
|
|
58
|
+
ended,
|
|
59
|
+
accept() {
|
|
60
|
+
if (direction != 'in' || state != 'ringing')
|
|
61
|
+
return;
|
|
62
|
+
send('accept');
|
|
63
|
+
activate();
|
|
64
|
+
},
|
|
65
|
+
decline(why = 'declined') {
|
|
66
|
+
if (direction != 'in' || state != 'ringing')
|
|
67
|
+
return;
|
|
68
|
+
finish(why, true, 'decline');
|
|
69
|
+
},
|
|
70
|
+
hangup() {
|
|
71
|
+
finish(state == 'ringing' && direction == 'out' ? 'canceled' : 'hangup', true);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
return { handle, activate, finish };
|
|
75
|
+
}
|
|
76
|
+
function swallowSendError() { }
|
|
77
|
+
function hasLiveCall() {
|
|
78
|
+
for (const c of calls.values())
|
|
79
|
+
if (c.handle.state() != 'ended')
|
|
80
|
+
return true;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function call(other, meta) {
|
|
84
|
+
if (closed)
|
|
85
|
+
throw new Error('call manager is closed');
|
|
86
|
+
const pair = 'call:' + self + ':' + callScope + ':' + (++n);
|
|
87
|
+
const made = makeCall(pair, other, 'out', meta);
|
|
88
|
+
calls.set(pair, made);
|
|
89
|
+
Promise.resolve(port.send({ type: 'ring', pair, from: self, to: other, session: meta }))
|
|
90
|
+
.then(function onRingVerdict(accepted) {
|
|
91
|
+
if (accepted == false)
|
|
92
|
+
made.finish('offline', false);
|
|
93
|
+
}, function onRingSendError() { made.finish('offline', false); });
|
|
94
|
+
return made.handle;
|
|
95
|
+
}
|
|
96
|
+
async function onRing(env) {
|
|
97
|
+
if (calls.has(env.pair))
|
|
98
|
+
return;
|
|
99
|
+
evaluatingIncoming++;
|
|
100
|
+
const gate = incoming ?? function defaultBusyGate() {
|
|
101
|
+
return evaluatingIncoming == 1 && !hasLiveCall();
|
|
102
|
+
};
|
|
103
|
+
let allowed = false;
|
|
104
|
+
try {
|
|
105
|
+
allowed = !!(await gate({ peer: env.from, meta: env.session }));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
allowed = false;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
evaluatingIncoming--;
|
|
112
|
+
}
|
|
113
|
+
if (closed)
|
|
114
|
+
return;
|
|
115
|
+
if (!allowed) {
|
|
116
|
+
void Promise.resolve(port.send({ type: 'decline', pair: env.pair, from: self, to: env.from, reason: 'busy' })).catch(swallowSendError);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const made = makeCall(env.pair, env.from, 'in', env.session);
|
|
120
|
+
calls.set(env.pair, made);
|
|
121
|
+
emitRing(made.handle);
|
|
122
|
+
}
|
|
123
|
+
const readyProbe = 'call-ready:' + self;
|
|
124
|
+
let resolveReady;
|
|
125
|
+
const ready = new Promise(r => { resolveReady = r; });
|
|
126
|
+
const offSignals = port.signals.on(function onCallSignal(env) {
|
|
127
|
+
if (closed || env == null || env.to != self)
|
|
128
|
+
return;
|
|
129
|
+
if (env.pair == readyProbe) {
|
|
130
|
+
resolveReady();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (env.type == 'ring') {
|
|
134
|
+
void onRing(env);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const live = calls.get(env.pair);
|
|
138
|
+
if (!live)
|
|
139
|
+
return;
|
|
140
|
+
if (env.type == 'accept') {
|
|
141
|
+
live.activate();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (env.type == 'decline') {
|
|
145
|
+
live.finish(env.reason ?? 'declined', false);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (env.type == 'hangup') {
|
|
149
|
+
live.finish(live.handle.state() == 'ringing' ? 'canceled' : 'hangup', false);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
Promise.resolve(port.send({ type: 'close', pair: readyProbe, from: self, to: self }))
|
|
153
|
+
.then(function onProbeVerdict(v) { if (v == false)
|
|
154
|
+
resolveReady(); }, function onProbeError() { resolveReady(); });
|
|
155
|
+
return {
|
|
156
|
+
ready,
|
|
157
|
+
call,
|
|
158
|
+
rings,
|
|
159
|
+
active: () => Array.from(calls.values()).map(c => c.handle).find(h => h.state() == 'active') ?? null,
|
|
160
|
+
close() {
|
|
161
|
+
if (closed)
|
|
162
|
+
return;
|
|
163
|
+
closed = true;
|
|
164
|
+
if (typeof offSignals == 'function')
|
|
165
|
+
offSignals();
|
|
166
|
+
else
|
|
167
|
+
offSignals?.off?.();
|
|
168
|
+
for (const c of Array.from(calls.values()))
|
|
169
|
+
c.finish('closed', true);
|
|
170
|
+
rings.close();
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -14,6 +14,15 @@ export type PeerRemote = {
|
|
|
14
14
|
peers: Record<string, ReplayRemote<[StorePatch]> & {
|
|
15
15
|
seq?: () => number | Promise<number>;
|
|
16
16
|
}>;
|
|
17
|
+
presence?: {
|
|
18
|
+
list: () => Promise<string[]> | string[];
|
|
19
|
+
changes: {
|
|
20
|
+
on: (cb: (change: {
|
|
21
|
+
account: string;
|
|
22
|
+
online: boolean;
|
|
23
|
+
}) => void) => any;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
17
26
|
};
|
|
18
27
|
export type tPublishRepair<J extends tRelayGap = 'resume'> = J extends 'sacred' ? 'tail' : 'tail' | 'keyframe';
|
|
19
28
|
export type PeerClientDeps<T extends object, J extends tRelayGap = 'resume'> = {
|
|
@@ -2,20 +2,29 @@ import { StorePatch } from '../Observe/store';
|
|
|
2
2
|
import { ReplayRemote } from '../events/replay-wire';
|
|
3
3
|
import { SignalEnvelope } from '../events/route-signal-webrtc';
|
|
4
4
|
import { PatchEnvelope, tRelayGap } from './peer-relay';
|
|
5
|
+
export type PresenceChange = {
|
|
6
|
+
account: string;
|
|
7
|
+
online: boolean;
|
|
8
|
+
};
|
|
5
9
|
export type PeerHostDeps = {
|
|
6
10
|
authorize?: (env: SignalEnvelope) => boolean | Promise<boolean>;
|
|
7
11
|
history?: number;
|
|
8
12
|
gap?: tRelayGap;
|
|
13
|
+
accounts?: (account: string) => boolean;
|
|
9
14
|
};
|
|
10
15
|
export declare function createPeerHost(deps?: PeerHostDeps): {
|
|
11
16
|
connection: (account: string) => {
|
|
12
17
|
fragment: {
|
|
13
18
|
signal: {
|
|
14
19
|
send: (env: SignalEnvelope) => Promise<boolean>;
|
|
15
|
-
signals: import("
|
|
20
|
+
signals: import("../events/Listen").ListenApi<[SignalEnvelope]>;
|
|
16
21
|
};
|
|
17
22
|
publish: (env: PatchEnvelope) => import("./peer-relay").RelayPushResult;
|
|
18
23
|
peers: Record<string, ReplayRemote<[StorePatch]>>;
|
|
24
|
+
presence: {
|
|
25
|
+
list: () => string[];
|
|
26
|
+
changes: import("../events/Listen").ListenApi<[PresenceChange]>;
|
|
27
|
+
};
|
|
19
28
|
};
|
|
20
29
|
close: () => void;
|
|
21
30
|
};
|
|
@@ -30,6 +39,10 @@ export declare function createPeerHost(deps?: PeerHostDeps): {
|
|
|
30
39
|
close: () => void;
|
|
31
40
|
};
|
|
32
41
|
accounts: () => string[];
|
|
42
|
+
presence: {
|
|
43
|
+
list: () => string[];
|
|
44
|
+
changes: import("../events/Listen").ListenApi<[PresenceChange]>;
|
|
45
|
+
};
|
|
33
46
|
revoke: (pair: string, accounts: string[], reason?: string) => void;
|
|
34
47
|
close(): void;
|
|
35
48
|
};
|
|
@@ -2,41 +2,85 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createPeerHost = createPeerHost;
|
|
4
4
|
const rpc_dynamic_1 = require("../rcp/rpc-dynamic");
|
|
5
|
+
const Listen_1 = require("../events/Listen");
|
|
5
6
|
const route_signal_webrtc_1 = require("../events/route-signal-webrtc");
|
|
6
7
|
const peer_relay_1 = require("./peer-relay");
|
|
7
8
|
function createPeerHost(deps = {}) {
|
|
8
|
-
const { authorize, history, gap } = deps;
|
|
9
|
+
const { authorize, history, gap, accounts: accountAllowed } = deps;
|
|
9
10
|
const hub = (0, route_signal_webrtc_1.createSignalHub)({ authorize });
|
|
10
11
|
const relays = new Map();
|
|
11
|
-
const
|
|
12
|
+
const peersMap = {};
|
|
13
|
+
const peersView = (0, rpc_dynamic_1.noStrict)(new Proxy(peersMap, {
|
|
14
|
+
has(_t, k) { return typeof k == 'string' && (!accountAllowed || accountAllowed(k)); },
|
|
15
|
+
get(t, k) {
|
|
16
|
+
if (typeof k != 'string')
|
|
17
|
+
return t[k];
|
|
18
|
+
if (accountAllowed && !accountAllowed(k))
|
|
19
|
+
return undefined;
|
|
20
|
+
return ensureRelay(k).remote;
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
const online = new Map();
|
|
24
|
+
const [emitPresence, presenceChanges] = (0, Listen_1.listen)();
|
|
25
|
+
const presence = {
|
|
26
|
+
list: () => Array.from(online.keys()),
|
|
27
|
+
changes: presenceChanges,
|
|
28
|
+
};
|
|
29
|
+
function presenceJoin(account) {
|
|
30
|
+
const n = (online.get(account) ?? 0) + 1;
|
|
31
|
+
online.set(account, n);
|
|
32
|
+
if (n == 1)
|
|
33
|
+
emitPresence({ account, online: true });
|
|
34
|
+
}
|
|
35
|
+
function presenceLeave(account) {
|
|
36
|
+
const n = (online.get(account) ?? 0) - 1;
|
|
37
|
+
if (n > 0) {
|
|
38
|
+
online.set(account, n);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
online.delete(account);
|
|
42
|
+
emitPresence({ account, online: false });
|
|
43
|
+
}
|
|
12
44
|
function ensureRelay(account) {
|
|
13
45
|
let relay = relays.get(account);
|
|
14
46
|
if (!relay) {
|
|
15
47
|
relay = (0, peer_relay_1.createPatchRelayJournal)({ history, gap });
|
|
16
48
|
relays.set(account, relay);
|
|
17
|
-
|
|
49
|
+
peersMap[account] = relay.remote;
|
|
18
50
|
}
|
|
19
51
|
return relay;
|
|
20
52
|
}
|
|
21
53
|
function connection(account) {
|
|
22
54
|
const port = hub.register(account);
|
|
23
55
|
const mine = ensureRelay(account);
|
|
56
|
+
presenceJoin(account);
|
|
57
|
+
let closed = false;
|
|
24
58
|
return {
|
|
25
59
|
fragment: {
|
|
26
60
|
signal: { send: port.send, signals: port.signals },
|
|
27
61
|
publish: (env) => mine.push(env),
|
|
28
62
|
peers: peersView,
|
|
63
|
+
presence,
|
|
64
|
+
},
|
|
65
|
+
close: function closeConnection() {
|
|
66
|
+
if (closed)
|
|
67
|
+
return;
|
|
68
|
+
closed = true;
|
|
69
|
+
port.close();
|
|
70
|
+
presenceLeave(account);
|
|
29
71
|
},
|
|
30
|
-
close: () => port.close(),
|
|
31
72
|
};
|
|
32
73
|
}
|
|
33
74
|
return {
|
|
34
75
|
connection,
|
|
35
76
|
relay: ensureRelay,
|
|
36
77
|
accounts: () => Array.from(relays.keys()),
|
|
78
|
+
presence,
|
|
37
79
|
revoke: hub.revoke,
|
|
38
80
|
close() {
|
|
39
81
|
hub.close();
|
|
82
|
+
presenceChanges.close();
|
|
83
|
+
online.clear();
|
|
40
84
|
for (const relay of relays.values())
|
|
41
85
|
relay.close();
|
|
42
86
|
relays.clear();
|
|
@@ -17,3 +17,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./peer-relay"), exports);
|
|
18
18
|
__exportStar(require("./peer-host"), exports);
|
|
19
19
|
__exportStar(require("./peer-client"), exports);
|
|
20
|
+
__exportStar(require("./peer-media-relay"), exports);
|
|
21
|
+
__exportStar(require("./peer-call"), exports);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ListenReplayApi } from '../events/replay-listen';
|
|
2
|
+
export type tMediaRelayKind = 'video' | 'audio';
|
|
3
|
+
export type tCanWatch = (watcher: string, owner: string, line: string) => boolean;
|
|
4
|
+
export type MediaRelayDeps = {
|
|
5
|
+
lines: Record<string, tMediaRelayKind>;
|
|
6
|
+
videoHistory?: number;
|
|
7
|
+
audioHistory?: number;
|
|
8
|
+
canWatch?: tCanWatch;
|
|
9
|
+
};
|
|
10
|
+
type tMediaFrame = [unknown, number];
|
|
11
|
+
type tRelayLine = ListenReplayApi<tMediaFrame>;
|
|
12
|
+
export declare function createMediaRelay(deps: MediaRelayDeps): {
|
|
13
|
+
publishOf: (account: string) => (line: string, frame: unknown, sentAt: number) => void;
|
|
14
|
+
watch: Record<string, Record<string, tRelayLine>>;
|
|
15
|
+
watchOf: (watcher: string) => Record<string, Record<string, tRelayLine>>;
|
|
16
|
+
lines: (account: string) => Record<string, tRelayLine>;
|
|
17
|
+
accounts: () => string[];
|
|
18
|
+
dropAccount: (account: string) => void;
|
|
19
|
+
close(): void;
|
|
20
|
+
};
|
|
21
|
+
export type MediaRelay = ReturnType<typeof createMediaRelay>;
|
|
22
|
+
export {};
|