@robota-sdk/agent-transport-webrtc 3.0.0-beta.100
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/CHANGELOG.md +694 -0
- package/LICENSE +661 -0
- package/README.md +111 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.cts +1650 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1650 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +93 -0
- package/src/__tests__/admission-secure-by-default.test.ts +63 -0
- package/src/__tests__/admission-steps.test.ts +46 -0
- package/src/__tests__/device-mesh-relay.test.ts +441 -0
- package/src/__tests__/device-mesh.test.ts +682 -0
- package/src/__tests__/discovering-mesh-relay.test.ts +234 -0
- package/src/__tests__/enrollment-link.test.ts +103 -0
- package/src/__tests__/fake-datachannel.ts +83 -0
- package/src/__tests__/handoff-grant-gate.test.ts +403 -0
- package/src/__tests__/local-peer-proof-gate.test.ts +215 -0
- package/src/__tests__/mesh-admission-contract.test.ts +38 -0
- package/src/__tests__/mesh-discovery.test.ts +820 -0
- package/src/__tests__/mesh-fixtures.ts +148 -0
- package/src/__tests__/mesh-internet.test.ts +942 -0
- package/src/__tests__/mesh-turn-relay.test.ts +161 -0
- package/src/__tests__/negotiated-certificate.test.ts +76 -0
- package/src/__tests__/nostr-source-condition.test.ts +27 -0
- package/src/__tests__/operator-approval-gate.test.ts +308 -0
- package/src/__tests__/outbound-delivery-carrier.test.ts +155 -0
- package/src/__tests__/pairing-e2e.test.ts +237 -0
- package/src/__tests__/pairing-gate-e3.test.ts +194 -0
- package/src/__tests__/pairing-gate-e4.test.ts +110 -0
- package/src/__tests__/pairing-gate.test.ts +296 -0
- package/src/__tests__/pairing-relay-in-the-middle.test.ts +225 -0
- package/src/__tests__/rtc-channel-close.test.ts +52 -0
- package/src/__tests__/rtc-peer-candidates.test.ts +61 -0
- package/src/__tests__/session-attachment-app-view.test.ts +66 -0
- package/src/__tests__/session-attachment-budget.test.ts +136 -0
- package/src/__tests__/turn-server-bind.test.ts +59 -0
- package/src/__tests__/turn-server.test.ts +335 -0
- package/src/__tests__/turn-test-client.ts +256 -0
- package/src/__tests__/webrtc-implementation.test.ts +83 -0
- package/src/__tests__/webrtc-transport.test.ts +349 -0
- package/src/__tests__/ws-mesh-relay-client.test.ts +95 -0
- package/src/__tests__/ws-signaling-client.test.ts +108 -0
- package/src/admission-steps.ts +55 -0
- package/src/channel-delivery.ts +44 -0
- package/src/datachannel-loader.ts +86 -0
- package/src/device-mesh-node.ts +1019 -0
- package/src/discovering-mesh-relay.ts +675 -0
- package/src/enrollment-link.ts +465 -0
- package/src/handoff-grant-gate.ts +186 -0
- package/src/index.ts +139 -0
- package/src/local-peer-proof.ts +135 -0
- package/src/mainline-dht-store.ts +174 -0
- package/src/mesh-dht.ts +446 -0
- package/src/mesh-discovery.ts +87 -0
- package/src/mesh-item-store.ts +287 -0
- package/src/mesh-lan-listener.ts +193 -0
- package/src/mesh-mdns.ts +406 -0
- package/src/mesh-peer-link.ts +600 -0
- package/src/mesh-public-relays.ts +25 -0
- package/src/mesh-records.ts +357 -0
- package/src/mesh-relay.ts +98 -0
- package/src/mesh-signal.ts +116 -0
- package/src/mesh-turn-relay.ts +238 -0
- package/src/negotiated-certificate.ts +72 -0
- package/src/nostr-mesh-relay.ts +391 -0
- package/src/pairing-channel-lifecycle.ts +31 -0
- package/src/pairing-controllers.ts +81 -0
- package/src/pairing-frames.ts +34 -0
- package/src/pairing-gate-options.ts +133 -0
- package/src/pairing-gate.ts +397 -0
- package/src/rtc-peer.ts +429 -0
- package/src/session-attachment.ts +96 -0
- package/src/signaling.ts +62 -0
- package/src/stun-message.ts +341 -0
- package/src/transport-lifecycle-error.ts +11 -0
- package/src/turn-server.ts +1028 -0
- package/src/webrtc-delivery-lifecycle.ts +54 -0
- package/src/webrtc-transport-options.ts +87 -0
- package/src/webrtc-transport.ts +368 -0
- package/src/ws-mesh-relay-client.ts +117 -0
- package/src/ws-signaling-client.ts +139 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The discovering relay on its own, over scripted endpoints: when a direct path is set aside, and for
|
|
3
|
+
* how long.
|
|
4
|
+
*/
|
|
5
|
+
import type { IPairRendezvous } from '@robota-sdk/agent-remote-pairing';
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { DiscoveringMeshRelay } from '../discovering-mesh-relay.js';
|
|
9
|
+
import type { IMeshCandidate, IMeshPeerRoute } from '../mesh-discovery.js';
|
|
10
|
+
import { lanAddressOf, lanPresenceProof, type IMeshLanListener } from '../mesh-lan-listener.js';
|
|
11
|
+
import type { IMeshRelay } from '../mesh-relay.js';
|
|
12
|
+
import type { IWebSocketLike } from '../ws-signaling-client.js';
|
|
13
|
+
|
|
14
|
+
const TAG = new Uint8Array(32).fill(7);
|
|
15
|
+
const LAN_TOPIC = Buffer.from(TAG).toString('base64url');
|
|
16
|
+
const ENDPOINT: IMeshCandidate = { host: '10.0.0.9', port: 4242 };
|
|
17
|
+
const ADMISSION_MS = 300;
|
|
18
|
+
const RECHECK_MS = 1_000;
|
|
19
|
+
|
|
20
|
+
const cleanups: (() => void)[] = [];
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const cleanup of cleanups.splice(0).reverse()) cleanup();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const route: IMeshPeerRoute = {
|
|
26
|
+
deviceId: 'peer-device',
|
|
27
|
+
inbound: 'in-topic',
|
|
28
|
+
outbound: 'out-topic',
|
|
29
|
+
rendezvous: {
|
|
30
|
+
tag: () => Promise.resolve(TAG),
|
|
31
|
+
lookupTags: () => Promise.resolve([TAG]),
|
|
32
|
+
} as unknown as IPairRendezvous,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const listener: IMeshLanListener = {
|
|
36
|
+
port: 1,
|
|
37
|
+
setPresence: () => undefined,
|
|
38
|
+
onMessage: () => () => undefined,
|
|
39
|
+
close: () => Promise.resolve(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface IFakeEndpoint {
|
|
43
|
+
readonly probes: number;
|
|
44
|
+
readonly signals: unknown[];
|
|
45
|
+
readonly closed: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** An endpoint that proves it holds the pair's topic and records what reaches it. */
|
|
49
|
+
function holdingEndpoint(): {
|
|
50
|
+
endpoint: IFakeEndpoint;
|
|
51
|
+
connect: (candidate: IMeshCandidate) => IWebSocketLike;
|
|
52
|
+
} {
|
|
53
|
+
const endpoint = { probes: 0, signals: [] as unknown[], closed: 0 };
|
|
54
|
+
const connect = (): IWebSocketLike => {
|
|
55
|
+
const handlers = new Map<string, ((arg: unknown) => void)[]>();
|
|
56
|
+
const emit = (event: string, arg?: unknown): void => {
|
|
57
|
+
for (const handler of handlers.get(event) ?? []) handler(arg);
|
|
58
|
+
};
|
|
59
|
+
const socket: IWebSocketLike = {
|
|
60
|
+
readyState: 0,
|
|
61
|
+
send(raw) {
|
|
62
|
+
const frame = JSON.parse(raw) as {
|
|
63
|
+
type: string;
|
|
64
|
+
to: string;
|
|
65
|
+
nonce?: string;
|
|
66
|
+
data?: unknown;
|
|
67
|
+
};
|
|
68
|
+
if (frame.type === 'probe') {
|
|
69
|
+
endpoint.probes += 1;
|
|
70
|
+
queueMicrotask(() =>
|
|
71
|
+
emit(
|
|
72
|
+
'message',
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
type: 'present',
|
|
75
|
+
to: lanAddressOf(LAN_TOPIC),
|
|
76
|
+
proof: lanPresenceProof(LAN_TOPIC, frame.nonce ?? ''),
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
endpoint.signals.push(frame.data);
|
|
83
|
+
},
|
|
84
|
+
close() {
|
|
85
|
+
if (socket.readyState === 3) return;
|
|
86
|
+
socket.readyState = 3;
|
|
87
|
+
endpoint.closed += 1;
|
|
88
|
+
emit('close');
|
|
89
|
+
},
|
|
90
|
+
on(event, handler) {
|
|
91
|
+
handlers.set(event, [...(handlers.get(event) ?? []), handler]);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
queueMicrotask(() => {
|
|
95
|
+
socket.readyState = 1;
|
|
96
|
+
emit('open');
|
|
97
|
+
});
|
|
98
|
+
return socket;
|
|
99
|
+
};
|
|
100
|
+
return { endpoint, connect };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function recordingRelay(): IMeshRelay & { readonly sent: unknown[] } {
|
|
104
|
+
const sent: unknown[] = [];
|
|
105
|
+
return {
|
|
106
|
+
sent,
|
|
107
|
+
declarePresence: () => undefined,
|
|
108
|
+
send: (_topic, data) => sent.push(data),
|
|
109
|
+
onMessage: () => () => undefined,
|
|
110
|
+
onAbsent: () => () => undefined,
|
|
111
|
+
close: () => undefined,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const hello = { v: 1, kind: 'hello', from: 'A'.repeat(22) };
|
|
116
|
+
const offer = {
|
|
117
|
+
v: 1,
|
|
118
|
+
kind: 'offer',
|
|
119
|
+
from: 'A'.repeat(22),
|
|
120
|
+
to: 'B'.repeat(22),
|
|
121
|
+
cid: 'C'.repeat(22),
|
|
122
|
+
sdp: 'x',
|
|
123
|
+
};
|
|
124
|
+
const ice = {
|
|
125
|
+
v: 1,
|
|
126
|
+
kind: 'ice',
|
|
127
|
+
from: 'A'.repeat(22),
|
|
128
|
+
to: 'B'.repeat(22),
|
|
129
|
+
cid: 'C'.repeat(22),
|
|
130
|
+
candidate: { candidate: 'c' },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
function relayOver(connect: (candidate: IMeshCandidate) => IWebSocketLike, now: () => number) {
|
|
134
|
+
const relay = recordingRelay();
|
|
135
|
+
const discovering = new DiscoveringMeshRelay({
|
|
136
|
+
relay,
|
|
137
|
+
listener,
|
|
138
|
+
sources: [{ candidates: () => Promise.resolve([ENDPOINT]) }],
|
|
139
|
+
connect,
|
|
140
|
+
admissionTimeoutMs: ADMISSION_MS,
|
|
141
|
+
relayRecheckMs: RECHECK_MS,
|
|
142
|
+
now,
|
|
143
|
+
});
|
|
144
|
+
cleanups.push(() => discovering.close());
|
|
145
|
+
discovering.declarePeers([route]);
|
|
146
|
+
return { relay, discovering };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const settle = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
150
|
+
|
|
151
|
+
describe('the admission deadline of a direct path', () => {
|
|
152
|
+
it('is not started by signals that trail an admitted attempt', async () => {
|
|
153
|
+
const { endpoint, connect } = holdingEndpoint();
|
|
154
|
+
const { relay, discovering } = relayOver(connect, Date.now);
|
|
155
|
+
discovering.send(route.outbound, hello);
|
|
156
|
+
await vi.waitFor(() => expect(endpoint.signals).toEqual([hello]));
|
|
157
|
+
discovering.send(route.outbound, offer);
|
|
158
|
+
discovering.confirmPeer(route.deviceId);
|
|
159
|
+
|
|
160
|
+
// The admitted attempt's last candidates go out after the admission.
|
|
161
|
+
discovering.send(route.outbound, ice);
|
|
162
|
+
await settle(ADMISSION_MS * 2);
|
|
163
|
+
discovering.send(route.outbound, ice);
|
|
164
|
+
|
|
165
|
+
expect(endpoint.signals).toEqual([hello, offer, ice, ice]);
|
|
166
|
+
expect(endpoint.closed).toBe(0);
|
|
167
|
+
expect(relay.sent).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('is started by a new attempt after an admission, and sets the endpoint aside when it lapses', async () => {
|
|
171
|
+
const { endpoint, connect } = holdingEndpoint();
|
|
172
|
+
const { relay, discovering } = relayOver(connect, Date.now);
|
|
173
|
+
discovering.send(route.outbound, hello);
|
|
174
|
+
await vi.waitFor(() => expect(endpoint.signals).toEqual([hello]));
|
|
175
|
+
discovering.confirmPeer(route.deviceId);
|
|
176
|
+
|
|
177
|
+
discovering.send(route.outbound, offer);
|
|
178
|
+
await vi.waitFor(() => expect(endpoint.closed).toBe(1));
|
|
179
|
+
discovering.send(route.outbound, hello);
|
|
180
|
+
expect(relay.sent).toEqual([hello]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('an endpoint that keeps failing', () => {
|
|
185
|
+
it('is set aside for longer each time, rather than probed again every cycle', async () => {
|
|
186
|
+
let now = Date.now();
|
|
187
|
+
const { endpoint, connect } = holdingEndpoint();
|
|
188
|
+
const { relay, discovering } = relayOver(connect, () => now);
|
|
189
|
+
|
|
190
|
+
const failOnce = async (probes: number): Promise<void> => {
|
|
191
|
+
discovering.send(route.outbound, hello);
|
|
192
|
+
await vi.waitFor(() => expect(endpoint.probes).toBe(probes));
|
|
193
|
+
await vi.waitFor(() => expect(endpoint.closed).toBe(probes));
|
|
194
|
+
};
|
|
195
|
+
await failOnce(1);
|
|
196
|
+
// The relay carries the pair until the recheck; then the endpoint is tried again, and fails again.
|
|
197
|
+
now += RECHECK_MS + 1;
|
|
198
|
+
await failOnce(2);
|
|
199
|
+
|
|
200
|
+
// One recheck later it is still set aside: the relay carries the pair without a probe.
|
|
201
|
+
now += RECHECK_MS + 1;
|
|
202
|
+
relay.sent.length = 0;
|
|
203
|
+
discovering.send(route.outbound, hello);
|
|
204
|
+
await vi.waitFor(() => expect(relay.sent).toEqual([hello]));
|
|
205
|
+
expect(endpoint.probes).toBe(2);
|
|
206
|
+
|
|
207
|
+
// Twice as long, and it is tried again.
|
|
208
|
+
now += RECHECK_MS + 1;
|
|
209
|
+
discovering.send(route.outbound, hello);
|
|
210
|
+
await vi.waitFor(() => expect(endpoint.probes).toBe(3));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('is trusted afresh once it carries an admission', async () => {
|
|
214
|
+
let now = Date.now();
|
|
215
|
+
const { endpoint, connect } = holdingEndpoint();
|
|
216
|
+
const { discovering } = relayOver(connect, () => now);
|
|
217
|
+
|
|
218
|
+
for (let failures = 1; failures <= 3; failures += 1) {
|
|
219
|
+
discovering.send(route.outbound, hello);
|
|
220
|
+
await vi.waitFor(() => expect(endpoint.closed).toBe(failures));
|
|
221
|
+
now += RECHECK_MS * 2 ** failures + 1;
|
|
222
|
+
}
|
|
223
|
+
discovering.send(route.outbound, hello);
|
|
224
|
+
await vi.waitFor(() => expect(endpoint.probes).toBe(4));
|
|
225
|
+
discovering.confirmPeer(route.deviceId);
|
|
226
|
+
endpoint.signals.length = 0;
|
|
227
|
+
// A later failure starts the backoff over: one recheck period, not eight.
|
|
228
|
+
discovering.send(route.outbound, offer);
|
|
229
|
+
await vi.waitFor(() => expect(endpoint.closed).toBe(4));
|
|
230
|
+
now += RECHECK_MS + 1;
|
|
231
|
+
discovering.send(route.outbound, hello);
|
|
232
|
+
await vi.waitFor(() => expect(endpoint.probes).toBe(5));
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EnrollmentLinkError,
|
|
5
|
+
dialEnrollment,
|
|
6
|
+
listenForEnrollment,
|
|
7
|
+
type IEnrollmentChannel,
|
|
8
|
+
} from '../enrollment-link.js';
|
|
9
|
+
import { createInMemoryMeshRelayHub } from '../mesh-relay.js';
|
|
10
|
+
|
|
11
|
+
const EXISTING = 'e'.repeat(43);
|
|
12
|
+
const JOINER = 'j'.repeat(43);
|
|
13
|
+
|
|
14
|
+
function nextFrame(channel: IEnrollmentChannel): Promise<unknown> {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const stop = channel.onFrame((frame) => {
|
|
17
|
+
stop();
|
|
18
|
+
resolve(frame);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('enrollment link — a data channel bound to the negotiated connection', () => {
|
|
24
|
+
it('connects dialer and listener through the relay; each names the other by its verified certificate', async () => {
|
|
25
|
+
const hub = createInMemoryMeshRelayHub();
|
|
26
|
+
const listenerRelay = hub.connect();
|
|
27
|
+
const dialerRelay = hub.connect();
|
|
28
|
+
const accepted: IEnrollmentChannel[] = [];
|
|
29
|
+
const listener = listenForEnrollment({
|
|
30
|
+
relay: listenerRelay,
|
|
31
|
+
inbound: EXISTING,
|
|
32
|
+
outbound: JOINER,
|
|
33
|
+
onChannel: (channel) => accepted.push(channel),
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
const dialed = await dialEnrollment({
|
|
37
|
+
relay: dialerRelay,
|
|
38
|
+
inbound: JOINER,
|
|
39
|
+
outbound: EXISTING,
|
|
40
|
+
connectTimeoutMs: 10_000,
|
|
41
|
+
});
|
|
42
|
+
await expect.poll(() => accepted.length, { timeout: 10_000 }).toBe(1);
|
|
43
|
+
const existing = accepted[0]!;
|
|
44
|
+
expect(dialed.remoteFingerprint).toBe(existing.localFingerprint);
|
|
45
|
+
expect(existing.remoteFingerprint).toBe(dialed.localFingerprint);
|
|
46
|
+
expect(dialed.localFingerprint).not.toBe(existing.localFingerprint);
|
|
47
|
+
|
|
48
|
+
const arrived = nextFrame(existing);
|
|
49
|
+
dialed.send({ t: 'hello' });
|
|
50
|
+
expect(await arrived).toEqual({ t: 'hello' });
|
|
51
|
+
const closed = new Promise<void>((resolve) => existing.onClose(resolve));
|
|
52
|
+
dialed.close();
|
|
53
|
+
await closed;
|
|
54
|
+
} finally {
|
|
55
|
+
listener.close();
|
|
56
|
+
listenerRelay.close();
|
|
57
|
+
dialerRelay.close();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('refuses at once when nobody waits at the topic', async () => {
|
|
62
|
+
const hub = createInMemoryMeshRelayHub();
|
|
63
|
+
const relay = hub.connect();
|
|
64
|
+
try {
|
|
65
|
+
const error = await dialEnrollment({ relay, inbound: JOINER, outbound: EXISTING }).catch(
|
|
66
|
+
(e: unknown) => e,
|
|
67
|
+
);
|
|
68
|
+
expect(error).toBeInstanceOf(EnrollmentLinkError);
|
|
69
|
+
expect((error as EnrollmentLinkError).reason).toBe('absent');
|
|
70
|
+
} finally {
|
|
71
|
+
relay.close();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('serves one attempt at a time: a second dialer is not taken while the first channel is open', async () => {
|
|
76
|
+
const hub = createInMemoryMeshRelayHub();
|
|
77
|
+
const listenerRelay = hub.connect();
|
|
78
|
+
const first = hub.connect();
|
|
79
|
+
const second = hub.connect();
|
|
80
|
+
const accepted: IEnrollmentChannel[] = [];
|
|
81
|
+
const listener = listenForEnrollment({
|
|
82
|
+
relay: listenerRelay,
|
|
83
|
+
inbound: EXISTING,
|
|
84
|
+
outbound: JOINER,
|
|
85
|
+
onChannel: (channel) => accepted.push(channel),
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
const one = await dialEnrollment({ relay: first, inbound: JOINER, outbound: EXISTING });
|
|
89
|
+
const other = await dialEnrollment({
|
|
90
|
+
relay: second,
|
|
91
|
+
inbound: JOINER,
|
|
92
|
+
outbound: EXISTING,
|
|
93
|
+
connectTimeoutMs: 1_500,
|
|
94
|
+
}).catch((e: unknown) => e);
|
|
95
|
+
expect((other as EnrollmentLinkError).reason).toBe('timeout');
|
|
96
|
+
expect(accepted).toHaveLength(1);
|
|
97
|
+
one.close();
|
|
98
|
+
} finally {
|
|
99
|
+
listener.close();
|
|
100
|
+
for (const relay of [listenerRelay, first, second]) relay.close();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scripted stand-in for the `node-datachannel` module: records the configuration a connection is
|
|
3
|
+
* built with and exposes the callbacks the transport registers, so a test can drive them.
|
|
4
|
+
*/
|
|
5
|
+
import type {
|
|
6
|
+
IDataChannelModule,
|
|
7
|
+
INdcConfig,
|
|
8
|
+
INdcDataChannel,
|
|
9
|
+
INdcPeerConnection,
|
|
10
|
+
} from '../datachannel-loader.js';
|
|
11
|
+
|
|
12
|
+
export interface IFakeChannelOptions {
|
|
13
|
+
readonly sendMessage?: (message: string) => boolean;
|
|
14
|
+
readonly close?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface IFakeConnection {
|
|
18
|
+
readonly config: INdcConfig;
|
|
19
|
+
readonly setRemoteDescription: (sdp: string, type: string) => void;
|
|
20
|
+
/** Callbacks the transport registered. */
|
|
21
|
+
localCandidate?: (candidate: string, mid: string) => void;
|
|
22
|
+
channelClosed?: () => void;
|
|
23
|
+
channelMessage?: (message: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface IFakeDataChannel {
|
|
27
|
+
readonly module: IDataChannelModule;
|
|
28
|
+
readonly connections: IFakeConnection[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function fakeDataChannel(
|
|
32
|
+
options: {
|
|
33
|
+
readonly offerSdp?: string;
|
|
34
|
+
readonly channel?: IFakeChannelOptions;
|
|
35
|
+
readonly setRemoteDescription?: (sdp: string, type: string) => void;
|
|
36
|
+
} = {},
|
|
37
|
+
): IFakeDataChannel {
|
|
38
|
+
const connections: IFakeConnection[] = [];
|
|
39
|
+
const offerSdp = options.offerSdp ?? 'a=fingerprint:sha-256 AA';
|
|
40
|
+
const PeerConnection = function (_name: string, config: INdcConfig): INdcPeerConnection {
|
|
41
|
+
const record: IFakeConnection = {
|
|
42
|
+
config,
|
|
43
|
+
setRemoteDescription: options.setRemoteDescription ?? (() => undefined),
|
|
44
|
+
};
|
|
45
|
+
connections.push(record);
|
|
46
|
+
let onLocalDescription: ((sdp: string, type: string) => void) | undefined;
|
|
47
|
+
const channel: INdcDataChannel = {
|
|
48
|
+
close: options.channel?.close ?? (() => undefined),
|
|
49
|
+
sendMessage: options.channel?.sendMessage ?? (() => true),
|
|
50
|
+
isOpen: () => true,
|
|
51
|
+
bufferedAmount: () => 0,
|
|
52
|
+
onOpen: () => undefined,
|
|
53
|
+
onClosed: (callback) => {
|
|
54
|
+
record.channelClosed = callback;
|
|
55
|
+
},
|
|
56
|
+
onError: () => undefined,
|
|
57
|
+
onMessage: (callback) => {
|
|
58
|
+
record.channelMessage = callback;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
close: () => undefined,
|
|
63
|
+
setLocalDescription: (type) => {
|
|
64
|
+
queueMicrotask(() => onLocalDescription?.(offerSdp, type ?? 'offer'));
|
|
65
|
+
},
|
|
66
|
+
setRemoteDescription: (sdp, type) => record.setRemoteDescription(sdp, type),
|
|
67
|
+
localDescription: () => ({ type: 'offer', sdp: offerSdp }),
|
|
68
|
+
remoteFingerprint: () => ({ value: '', algorithm: 'sha-256' }),
|
|
69
|
+
addRemoteCandidate: () => undefined,
|
|
70
|
+
createDataChannel: () => channel,
|
|
71
|
+
state: () => 'new',
|
|
72
|
+
onLocalDescription: (callback) => {
|
|
73
|
+
onLocalDescription = callback;
|
|
74
|
+
},
|
|
75
|
+
onLocalCandidate: (callback) => {
|
|
76
|
+
record.localCandidate = callback;
|
|
77
|
+
},
|
|
78
|
+
onStateChange: () => undefined,
|
|
79
|
+
onDataChannel: () => undefined,
|
|
80
|
+
};
|
|
81
|
+
} as unknown as IDataChannelModule['PeerConnection'];
|
|
82
|
+
return { module: { PeerConnection }, connections };
|
|
83
|
+
}
|