wenay-common2 1.0.73 → 1.0.75

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.
Files changed (41) hide show
  1. package/README.md +3 -2
  2. package/demo/client.ts +137 -32
  3. package/demo/index.html +7 -0
  4. package/demo/server.ts +79 -42
  5. package/doc/INTENT.md +31 -31
  6. package/doc/ROADMAP.md +233 -230
  7. package/doc/changes/1.0.74.md +12 -0
  8. package/doc/changes/1.0.75.md +10 -0
  9. package/doc/wenay-common2-rare.md +705 -649
  10. package/doc/wenay-common2.md +438 -396
  11. package/lib/Common/events/replay-route.js +5 -2
  12. package/lib/Common/events/replay-wire.js +137 -47
  13. package/lib/Common/events/route-signal-webrtc.d.ts +1 -1
  14. package/lib/Common/events/route-signal-webrtc.js +15 -5
  15. package/lib/Common/events/transport-lifecycle.d.ts +23 -0
  16. package/lib/Common/events/transport-lifecycle.js +94 -0
  17. package/lib/Common/media/media-view.d.ts +2 -2
  18. package/lib/Common/media/media-view.js +23 -4
  19. package/lib/Common/peer/peer-call.d.ts +65 -0
  20. package/lib/Common/peer/peer-call.js +173 -0
  21. package/lib/Common/peer/peer-client.d.ts +9 -0
  22. package/lib/Common/peer/peer-host.d.ts +14 -1
  23. package/lib/Common/peer/peer-host.js +48 -4
  24. package/lib/Common/peer/peer-index.d.ts +2 -0
  25. package/lib/Common/peer/peer-index.js +2 -0
  26. package/lib/Common/peer/peer-media-relay.d.ts +22 -0
  27. package/lib/Common/peer/peer-media-relay.js +155 -0
  28. package/lib/Common/rcp/rpc-client.js +346 -111
  29. package/lib/Common/rcp/rpc-clientHub.d.ts +2 -0
  30. package/lib/Common/rcp/rpc-clientHub.js +120 -25
  31. package/lib/Common/rcp/rpc-server.js +33 -9
  32. package/observe/listen-core.test.ts +1 -2
  33. package/oracle/realsocket/lifecycle.spec.ts +3 -3
  34. package/oracle/realsocket/replay-reconnect-auth.spec.ts +111 -0
  35. package/oracle/realsocket/replay-reconnect.spec.ts +592 -0
  36. package/package.json +1 -4
  37. package/replay/media-view.test.ts +4 -4
  38. package/replay/peer-call.test.ts +226 -0
  39. package/rpc.md +19 -4
  40. package/doc/changes/1.0.64.md +0 -10
  41. package/doc/changes/1.0.65.md +0 -8
@@ -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("../..").ListenApi<[SignalEnvelope]>;
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 peersView = (0, rpc_dynamic_1.noStrict)({});
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
- peersView[account] = relay.remote;
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();
@@ -1,3 +1,5 @@
1
1
  export * from './peer-relay';
2
2
  export * from './peer-host';
3
3
  export * from './peer-client';
4
+ export * from './peer-media-relay';
5
+ export * from './peer-call';
@@ -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 {};
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMediaRelay = createMediaRelay;
4
+ const replay_listen_1 = require("../events/replay-listen");
5
+ const rpc_dynamic_1 = require("../rcp/rpc-dynamic");
6
+ function createMediaRelay(deps) {
7
+ const { lines, videoHistory = 8, audioHistory = 64, canWatch } = deps;
8
+ const accounts = new Map();
9
+ const watch = (0, rpc_dynamic_1.noStrict)({});
10
+ function makeLine(kind) {
11
+ return kind == 'video'
12
+ ? (0, replay_listen_1.replayListen)({
13
+ history: videoHistory, current: 'last',
14
+ frame: function lastFrameOnly(tail) { return tail.length ? [tail[tail.length - 1]] : []; },
15
+ })
16
+ : (0, replay_listen_1.replayListen)({ history: audioHistory });
17
+ }
18
+ function linesOf(account) {
19
+ let entry = accounts.get(account);
20
+ if (!entry) {
21
+ entry = { emits: {}, view: (0, rpc_dynamic_1.noStrict)({}) };
22
+ for (const name of Object.keys(lines)) {
23
+ const [emit, api] = makeLine(lines[name]);
24
+ entry.emits[name] = emit;
25
+ entry.view[name] = api;
26
+ }
27
+ accounts.set(account, entry);
28
+ watch[account] = entry.view;
29
+ }
30
+ return entry;
31
+ }
32
+ function publishOf(account) {
33
+ linesOf(account);
34
+ return function publish(line, frame, sentAt) {
35
+ linesOf(account).emits[line]?.(frame, sentAt);
36
+ };
37
+ }
38
+ const watcherViews = new Map();
39
+ function allowed(watcher, owner, line) {
40
+ return canWatch ? canWatch(watcher, owner, line) : true;
41
+ }
42
+ function filteredLine(watcher, owner, name, source, cache) {
43
+ const [emit, filtered] = (0, replay_listen_1.replayListen)({
44
+ history: lines[name] == 'video' ? videoHistory : audioHistory,
45
+ current: function currentIfAllowed() {
46
+ return allowed(watcher, owner, name) ? source.keyframe()?.event : undefined;
47
+ },
48
+ ...(lines[name] == 'video' ? {
49
+ frame: function lastAllowedFrame(tail) {
50
+ return tail.length ? [tail[tail.length - 1]] : [];
51
+ },
52
+ } : {}),
53
+ });
54
+ const offSource = source.on(function forwardAllowed(frame, sentAt) {
55
+ if (allowed(watcher, owner, name))
56
+ emit(frame, sentAt);
57
+ });
58
+ const closeFiltered = filtered.close;
59
+ filtered.close = function closePolicyLine() {
60
+ offSource();
61
+ closeFiltered();
62
+ };
63
+ cache.filtered.add(filtered);
64
+ return filtered;
65
+ }
66
+ function ownerViewFor(watcher, owner, cache) {
67
+ let view = cache.owners.get(owner);
68
+ if (view)
69
+ return view;
70
+ const entry = accounts.get(owner);
71
+ if (!entry)
72
+ return undefined;
73
+ const policyLines = {};
74
+ for (const name of Object.keys(lines))
75
+ policyLines[name] = filteredLine(watcher, owner, name, entry.view[name], cache);
76
+ view = (0, rpc_dynamic_1.noStrict)(new Proxy(policyLines, {
77
+ has(t, k) { return typeof k == 'string' && k in t && allowed(watcher, owner, k); },
78
+ get(t, k) {
79
+ if (typeof k != 'string')
80
+ return t[k];
81
+ return k in t && allowed(watcher, owner, k) ? t[k] : undefined;
82
+ },
83
+ }));
84
+ cache.owners.set(owner, view);
85
+ return view;
86
+ }
87
+ function watchOf(watcher) {
88
+ let cached = watcherViews.get(watcher);
89
+ if (cached)
90
+ return cached.root;
91
+ const owners = new Map();
92
+ const filtered = new Set();
93
+ function visible(owner) {
94
+ return accounts.has(owner) && Object.keys(lines).some(name => allowed(watcher, owner, name));
95
+ }
96
+ const root = (0, rpc_dynamic_1.noStrict)(new Proxy({}, {
97
+ has(_t, k) { return typeof k == 'string' && visible(k); },
98
+ get(_t, k) {
99
+ if (typeof k != 'string')
100
+ return undefined;
101
+ return visible(k) ? ownerViewFor(watcher, k, cached) : undefined;
102
+ },
103
+ }));
104
+ cached = { root, owners, filtered };
105
+ watcherViews.set(watcher, cached);
106
+ return root;
107
+ }
108
+ function dropLines(entry) {
109
+ for (const name of Object.keys(entry.view))
110
+ entry.view[name].close();
111
+ }
112
+ function closeWatcherCache(cache) {
113
+ for (const line of cache.filtered)
114
+ line.close();
115
+ cache.filtered.clear();
116
+ cache.owners.clear();
117
+ }
118
+ function dropAccount(account) {
119
+ const entry = accounts.get(account);
120
+ const ownCache = watcherViews.get(account);
121
+ if (ownCache)
122
+ closeWatcherCache(ownCache);
123
+ watcherViews.delete(account);
124
+ for (const cache of watcherViews.values()) {
125
+ const ownerView = cache.owners.get(account);
126
+ if (ownerView)
127
+ for (const line of Object.values(ownerView)) {
128
+ line.close();
129
+ cache.filtered.delete(line);
130
+ }
131
+ cache.owners.delete(account);
132
+ }
133
+ if (!entry)
134
+ return;
135
+ accounts.delete(account);
136
+ delete watch[account];
137
+ dropLines(entry);
138
+ }
139
+ return {
140
+ publishOf,
141
+ watch,
142
+ watchOf,
143
+ lines: (account) => linesOf(account).view,
144
+ accounts: () => Array.from(accounts.keys()),
145
+ dropAccount,
146
+ close() {
147
+ for (const cache of watcherViews.values())
148
+ closeWatcherCache(cache);
149
+ for (const entry of accounts.values())
150
+ dropLines(entry);
151
+ accounts.clear();
152
+ watcherViews.clear();
153
+ },
154
+ };
155
+ }