wenay-common2 1.0.70 → 1.0.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/demo/client.ts +230 -3
- package/demo/index.html +37 -0
- package/demo/server.ts +91 -4
- package/doc/INTENT.md +31 -0
- package/doc/ROADMAP.md +233 -212
- package/doc/changes/1.0.71.md +9 -0
- package/doc/changes/1.0.72.md +6 -0
- package/doc/changes/1.0.73.md +6 -0
- package/doc/changes/1.0.74.md +12 -0
- package/doc/wenay-common2-rare.md +684 -627
- package/doc/wenay-common2.md +50 -5
- 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-index.d.ts +1 -0
- package/lib/Common/media/media-index.js +1 -0
- package/lib/Common/media/media-source.d.ts +3 -0
- package/lib/Common/media/media-source.js +136 -27
- package/lib/Common/media/media-view.d.ts +42 -0
- package/lib/Common/media/media-view.js +210 -0
- 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 +21 -5
- package/lib/Common/peer/peer-client.js +53 -3
- package/lib/Common/peer/peer-host.d.ts +22 -5
- package/lib/Common/peer/peer-host.js +49 -5
- 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/lib/Common/peer/peer-relay.d.ts +10 -2
- package/lib/Common/peer/peer-relay.js +40 -18
- package/observe/listen-core.test.ts +1 -2
- package/package.json +1 -4
- package/replay/media-view.test.ts +164 -0
- package/replay/peer-call.test.ts +226 -0
- package/replay/peer-repair.test.ts +184 -0
- package/doc/changes/1.0.63.md +0 -8
- package/doc/changes/1.0.64.md +0 -10
|
@@ -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
|
+
}
|
|
@@ -2,7 +2,7 @@ import { StoreDrain, StorePatch } from '../Observe/store';
|
|
|
2
2
|
import { ReplayRemote } from '../events/replay-wire';
|
|
3
3
|
import { RoutePolicy } from '../events/route-coordinator';
|
|
4
4
|
import { RtcPeerConnection, SignalEnvelope } from '../events/route-signal-webrtc';
|
|
5
|
-
import { PatchEnvelope } from './peer-relay';
|
|
5
|
+
import { PatchEnvelope, RelayPushResult, tRelayGap } from './peer-relay';
|
|
6
6
|
export type PeerRemote = {
|
|
7
7
|
signal: {
|
|
8
8
|
send: (env: SignalEnvelope) => Promise<boolean | void>;
|
|
@@ -10,10 +10,22 @@ export type PeerRemote = {
|
|
|
10
10
|
on: (cb: (env: SignalEnvelope) => void) => any;
|
|
11
11
|
};
|
|
12
12
|
};
|
|
13
|
-
publish: (env: PatchEnvelope) => Promise<
|
|
14
|
-
peers: Record<string, ReplayRemote<[StorePatch]
|
|
13
|
+
publish: (env: PatchEnvelope) => Promise<RelayPushResult | void> | RelayPushResult | void;
|
|
14
|
+
peers: Record<string, ReplayRemote<[StorePatch]> & {
|
|
15
|
+
seq?: () => number | Promise<number>;
|
|
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
|
+
};
|
|
15
26
|
};
|
|
16
|
-
export type
|
|
27
|
+
export type tPublishRepair<J extends tRelayGap = 'resume'> = J extends 'sacred' ? 'tail' : 'tail' | 'keyframe';
|
|
28
|
+
export type PeerClientDeps<T extends object, J extends tRelayGap = 'resume'> = {
|
|
17
29
|
remote: PeerRemote;
|
|
18
30
|
account: string;
|
|
19
31
|
initial: T;
|
|
@@ -22,10 +34,13 @@ export type PeerClientDeps<T extends object> = {
|
|
|
22
34
|
accept?: (env: SignalEnvelope) => boolean | Promise<boolean>;
|
|
23
35
|
policy?: RoutePolicy;
|
|
24
36
|
peerInitial?: () => T;
|
|
37
|
+
journal?: J;
|
|
38
|
+
repair?: tPublishRepair<J>;
|
|
39
|
+
onPublishError?: (e: unknown) => void;
|
|
25
40
|
history?: number;
|
|
26
41
|
drain?: StoreDrain;
|
|
27
42
|
};
|
|
28
|
-
export declare function createPeerClient<T extends object>(deps: PeerClientDeps<T>): {
|
|
43
|
+
export declare function createPeerClient<T extends object, J extends tRelayGap = 'resume'>(deps: PeerClientDeps<T, J>): {
|
|
29
44
|
store: import("../Observe/store").Store<T>;
|
|
30
45
|
peer: (other: string) => {
|
|
31
46
|
account: string;
|
|
@@ -41,6 +56,7 @@ export declare function createPeerClient<T extends object>(deps: PeerClientDeps<
|
|
|
41
56
|
close(): void;
|
|
42
57
|
};
|
|
43
58
|
onRoute: (cb: (ev: import("../events/route-coordinator").RouteChangeEvent) => void) => import("../..").ListenOff;
|
|
59
|
+
resync: () => Promise<void>;
|
|
44
60
|
close(): void;
|
|
45
61
|
};
|
|
46
62
|
export type PeerClient<T extends object> = ReturnType<typeof createPeerClient<T>>;
|
|
@@ -7,15 +7,64 @@ const replay_wire_1 = require("../events/replay-wire");
|
|
|
7
7
|
const route_coordinator_1 = require("../events/route-coordinator");
|
|
8
8
|
const route_signal_webrtc_1 = require("../events/route-signal-webrtc");
|
|
9
9
|
function createPeerClient(deps) {
|
|
10
|
-
const { remote, account, initial, rtc, session, accept, policy, peerInitial = () => ({}), history, drain, } = deps;
|
|
10
|
+
const { remote, account, initial, rtc, session, accept, policy, peerInitial = () => ({}), history, drain, journal = 'resume', onPublishError, } = deps;
|
|
11
|
+
const repair = deps.repair ?? 'tail';
|
|
11
12
|
const store = (0, store_1.createStore)(initial, drain !== undefined ? { drain } : {});
|
|
12
13
|
const exposed = (0, store_replay_1.exposeStoreReplay)(store, history !== undefined ? { history } : {});
|
|
14
|
+
let repairing = null;
|
|
15
|
+
function repairEnvelopes(from) {
|
|
16
|
+
const line = exposed.replay;
|
|
17
|
+
if (repair == 'tail' && from >= 0) {
|
|
18
|
+
const tail = line.getSince(from);
|
|
19
|
+
if (tail)
|
|
20
|
+
return tail;
|
|
21
|
+
if (journal == 'sacred') {
|
|
22
|
+
throw new Error('peer publish: local journal evicted seq ' + from + ', sacred relay is unrepairable — raise {history}');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const kf = exposed.replay.keyframe();
|
|
26
|
+
return kf ? [kf] : [];
|
|
27
|
+
}
|
|
28
|
+
async function runRepair(from) {
|
|
29
|
+
for (const env of repairEnvelopes(from)) {
|
|
30
|
+
const res = await remote.publish(env);
|
|
31
|
+
if (res != null && typeof res == 'object') {
|
|
32
|
+
throw new Error('peer publish: repair rejected at relay seq ' + res.seq);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function queueRepair(from) {
|
|
37
|
+
if (repairing)
|
|
38
|
+
return;
|
|
39
|
+
repairing = runRepair(from)
|
|
40
|
+
.catch(function reportRepairError(e) {
|
|
41
|
+
if (onPublishError)
|
|
42
|
+
onPublishError(e);
|
|
43
|
+
else
|
|
44
|
+
setTimeout(function rethrowRepairError() { throw e; }, 0);
|
|
45
|
+
})
|
|
46
|
+
.finally(() => { repairing = null; });
|
|
47
|
+
}
|
|
48
|
+
function handleVerdict(res) {
|
|
49
|
+
if (res != null && typeof res == 'object' && typeof res.seq == 'number')
|
|
50
|
+
queueRepair(res.seq);
|
|
51
|
+
}
|
|
13
52
|
const offPublish = exposed.replay.line.on(function publishEnvelope(env) {
|
|
14
|
-
|
|
53
|
+
Promise.resolve(remote.publish(env)).then(handleVerdict, function onPublishReject(e) {
|
|
54
|
+
onPublishError?.(e);
|
|
55
|
+
});
|
|
15
56
|
});
|
|
16
57
|
const warmup = exposed.replay.keyframe();
|
|
17
58
|
if (warmup)
|
|
18
|
-
|
|
59
|
+
Promise.resolve(remote.publish(warmup)).then(handleVerdict, e => onPublishError?.(e));
|
|
60
|
+
async function resync() {
|
|
61
|
+
const node = remote.peers[account];
|
|
62
|
+
const relaySeq = Number(await node?.seq?.() ?? -1);
|
|
63
|
+
const localSeq = exposed.replay.keyframe()?.seq ?? -1;
|
|
64
|
+
if (relaySeq >= localSeq)
|
|
65
|
+
return;
|
|
66
|
+
await runRepair(relaySeq);
|
|
67
|
+
}
|
|
19
68
|
const port = {
|
|
20
69
|
send: env => remote.signal.send(env),
|
|
21
70
|
signals: { on: cb => remote.signal.signals.on(cb) },
|
|
@@ -96,6 +145,7 @@ function createPeerClient(deps) {
|
|
|
96
145
|
store,
|
|
97
146
|
peer,
|
|
98
147
|
onRoute: coord.onRoute,
|
|
148
|
+
resync,
|
|
99
149
|
close() {
|
|
100
150
|
for (const view of Array.from(views.values()))
|
|
101
151
|
view.close();
|
|
@@ -1,31 +1,48 @@
|
|
|
1
1
|
import { StorePatch } from '../Observe/store';
|
|
2
2
|
import { ReplayRemote } from '../events/replay-wire';
|
|
3
3
|
import { SignalEnvelope } from '../events/route-signal-webrtc';
|
|
4
|
-
import { PatchEnvelope } from './peer-relay';
|
|
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;
|
|
12
|
+
gap?: tRelayGap;
|
|
13
|
+
accounts?: (account: string) => boolean;
|
|
8
14
|
};
|
|
9
15
|
export declare function createPeerHost(deps?: PeerHostDeps): {
|
|
10
16
|
connection: (account: string) => {
|
|
11
17
|
fragment: {
|
|
12
18
|
signal: {
|
|
13
19
|
send: (env: SignalEnvelope) => Promise<boolean>;
|
|
14
|
-
signals: import("
|
|
20
|
+
signals: import("../events/Listen").ListenApi<[SignalEnvelope]>;
|
|
15
21
|
};
|
|
16
|
-
publish: (env: PatchEnvelope) =>
|
|
22
|
+
publish: (env: PatchEnvelope) => import("./peer-relay").RelayPushResult;
|
|
17
23
|
peers: Record<string, ReplayRemote<[StorePatch]>>;
|
|
24
|
+
presence: {
|
|
25
|
+
list: () => string[];
|
|
26
|
+
changes: import("../events/Listen").ListenApi<[PresenceChange]>;
|
|
27
|
+
};
|
|
18
28
|
};
|
|
19
29
|
close: () => void;
|
|
20
30
|
};
|
|
21
31
|
relay: (account: string) => {
|
|
22
|
-
push: (env: PatchEnvelope) =>
|
|
23
|
-
remote: ReplayRemote<[StorePatch]
|
|
32
|
+
push: (env: PatchEnvelope) => import("./peer-relay").RelayPushResult;
|
|
33
|
+
remote: ReplayRemote<[StorePatch]> & {
|
|
34
|
+
seq: () => number;
|
|
35
|
+
};
|
|
36
|
+
gap: tRelayGap;
|
|
24
37
|
seq: () => number;
|
|
25
38
|
snapshot: () => any;
|
|
26
39
|
close: () => void;
|
|
27
40
|
};
|
|
28
41
|
accounts: () => string[];
|
|
42
|
+
presence: {
|
|
43
|
+
list: () => string[];
|
|
44
|
+
changes: import("../events/Listen").ListenApi<[PresenceChange]>;
|
|
45
|
+
};
|
|
29
46
|
revoke: (pair: string, accounts: string[], reason?: string) => void;
|
|
30
47
|
close(): void;
|
|
31
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 } = 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
|
-
relay = (0, peer_relay_1.createPatchRelayJournal)({ history });
|
|
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 {};
|