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.
@@ -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.set(account, emit);
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 target = ports.get(env.to);
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
- if (ports.get(account) == emit)
28
- ports.delete(account);
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)?.({ type: 'revoke', pair, from: '', to: account, reason });
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): () => 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): () => void;
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("../..").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 {};