@jgengine/ws 0.7.0 → 0.9.0

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.
@@ -0,0 +1,564 @@
1
+ import { decidePoseSync, spawnPresenceState } from "@jgengine/core/multiplayer/presenceModel";
2
+ import { createPositionHistory } from "@jgengine/core/multiplayer/lagCompensation";
3
+ import { createChatRateLimiter, DEFAULT_CHAT_BODY_LENGTH, DEFAULT_CHAT_HISTORY_LIMIT, DEFAULT_CHAT_RATE_LIMIT, } from "@jgengine/core/game/chat";
4
+ import { decodeWsClientMessage, encodeWsMessage, inspectWsDecodeFailure, subscriptionKey, } from "./protocol.js";
5
+ export const DEFAULT_POSE_RULES = {
6
+ maxSpeed: 12,
7
+ maxVerticalOffset: 3,
8
+ minElapsedSec: 0.05,
9
+ maxElapsedSec: 0.5,
10
+ keepAliveRefreshMs: 10_000,
11
+ };
12
+ function appearanceEqual(a, b) {
13
+ if (a === b)
14
+ return true;
15
+ if (a === undefined || b === undefined)
16
+ return false;
17
+ const aKeys = Object.keys(a);
18
+ const bKeys = Object.keys(b);
19
+ if (aKeys.length !== bKeys.length)
20
+ return false;
21
+ return aKeys.every((key) => a[key] === b[key]);
22
+ }
23
+ export function createHostRouter(options) {
24
+ const host = options.host;
25
+ const now = options.now ?? Date.now;
26
+ const poseRules = options.poseRules ?? DEFAULT_POSE_RULES;
27
+ const allowAnonymous = options.allowAnonymous === true;
28
+ const singleSession = options.singleSession !== false;
29
+ const authenticate = options.authenticate ??
30
+ (allowAnonymous ? ({ userId }) => userId : null);
31
+ const connections = new Set();
32
+ const sessionsByUserId = new Map();
33
+ const presence = new Map();
34
+ const positionHistoryMs = options.positionHistoryMs ?? 1_000;
35
+ const histories = new Map();
36
+ const historyFor = (serverId) => {
37
+ let history = histories.get(serverId);
38
+ if (history === undefined) {
39
+ history = createPositionHistory({ historyMs: positionHistoryMs });
40
+ histories.set(serverId, history);
41
+ }
42
+ return history;
43
+ };
44
+ const send = (connection, message) => {
45
+ connection.transport.send(encodeWsMessage(message));
46
+ };
47
+ const reply = (connection, id, result) => {
48
+ send(connection, { v: 1, t: "reply", id, ok: true, result });
49
+ };
50
+ const replyError = (connection, id, reason) => {
51
+ send(connection, { v: 1, t: "reply", id, ok: false, reason });
52
+ };
53
+ const presenceRows = (serverId) => {
54
+ const rows = presence.get(serverId);
55
+ if (rows === undefined)
56
+ return [];
57
+ return [...rows.entries()].map(([userId, pose]) => ({
58
+ userId,
59
+ position: pose.position,
60
+ rotationY: pose.rotationY,
61
+ rotationPitch: pose.rotationPitch ?? 0,
62
+ lastSeenAt: pose.lastSeenAtMs ?? 0,
63
+ appearance: pose.appearance,
64
+ }));
65
+ };
66
+ const broadcastPresence = (serverId) => {
67
+ const key = subscriptionKey("presence", serverId);
68
+ const data = presenceRows(serverId);
69
+ for (const connection of connections) {
70
+ if (!connection.subscriptions.has(key))
71
+ continue;
72
+ send(connection, { v: 1, t: "update", channel: "presence", serverId, data });
73
+ }
74
+ };
75
+ const chatRings = new Map();
76
+ const chatLimiter = createChatRateLimiter(options.chatRateLimit ?? DEFAULT_CHAT_RATE_LIMIT);
77
+ const chatHistoryLimit = options.chatHistoryLimit ?? DEFAULT_CHAT_HISTORY_LIMIT;
78
+ const chatMaxBodyLength = options.chatMaxBodyLength ?? DEFAULT_CHAT_BODY_LENGTH;
79
+ let chatCounter = 0;
80
+ const chatRing = (serverId, channelId) => {
81
+ const key = `${serverId}|${channelId}`;
82
+ let ring = chatRings.get(key);
83
+ if (ring === undefined) {
84
+ ring = [];
85
+ chatRings.set(key, ring);
86
+ }
87
+ return ring;
88
+ };
89
+ const broadcastChat = (serverId, channelId) => {
90
+ const key = subscriptionKey("chat", serverId, channelId);
91
+ const data = chatRing(serverId, channelId).slice();
92
+ for (const connection of connections) {
93
+ if (!connection.subscriptions.has(key))
94
+ continue;
95
+ send(connection, { v: 1, t: "update", channel: "chat", serverId, action: channelId, data });
96
+ }
97
+ };
98
+ const voiceRosters = new Map();
99
+ const voiceRoster = (serverId, channelId) => {
100
+ const key = `${serverId}|${channelId}`;
101
+ let roster = voiceRosters.get(key);
102
+ if (roster === undefined) {
103
+ roster = new Map();
104
+ voiceRosters.set(key, roster);
105
+ }
106
+ return roster;
107
+ };
108
+ const voiceParticipants = (serverId, channelId) => [...voiceRoster(serverId, channelId).values()].map((participant) => ({ ...participant }));
109
+ const broadcastVoice = (serverId, channelId) => {
110
+ const key = subscriptionKey("voice", serverId, channelId);
111
+ const data = voiceParticipants(serverId, channelId);
112
+ for (const connection of connections) {
113
+ if (!connection.subscriptions.has(key))
114
+ continue;
115
+ send(connection, { v: 1, t: "update", channel: "voice", serverId, action: channelId, data });
116
+ }
117
+ };
118
+ const dropVoice = (connection) => {
119
+ if (connection.userId === null)
120
+ return;
121
+ for (const [key, roster] of voiceRosters) {
122
+ if (!roster.delete(connection.userId))
123
+ continue;
124
+ if (roster.size === 0)
125
+ voiceRosters.delete(key);
126
+ const [serverId, channelId] = key.split("|");
127
+ broadcastVoice(serverId, channelId);
128
+ }
129
+ };
130
+ const dropVoiceForServer = (userId, serverId) => {
131
+ const prefix = `${serverId}|`;
132
+ for (const [key, roster] of voiceRosters) {
133
+ if (!key.startsWith(prefix))
134
+ continue;
135
+ if (!roster.delete(userId))
136
+ continue;
137
+ if (roster.size === 0)
138
+ voiceRosters.delete(key);
139
+ broadcastVoice(serverId, key.slice(prefix.length));
140
+ }
141
+ };
142
+ const requireMembership = async (connection, id, serverId) => {
143
+ const userId = connection.userId;
144
+ if (userId === null) {
145
+ replyError(connection, id, "Not authenticated");
146
+ return null;
147
+ }
148
+ if (!(await host.isMember({ userId, serverId }))) {
149
+ replyError(connection, id, "Not a member of this server");
150
+ return null;
151
+ }
152
+ return userId;
153
+ };
154
+ const handleChatSend = async (connection, message) => {
155
+ const userId = await requireMembership(connection, message.id, message.serverId);
156
+ if (userId === null)
157
+ return;
158
+ const body = message.body.trim();
159
+ if (body.length === 0) {
160
+ replyError(connection, message.id, "empty message");
161
+ return;
162
+ }
163
+ if (body.length > chatMaxBodyLength) {
164
+ replyError(connection, message.id, "message too long");
165
+ return;
166
+ }
167
+ const timestamp = now();
168
+ if (!chatLimiter.allow(`${userId}|${message.serverId}`, timestamp)) {
169
+ replyError(connection, message.id, "rate limited");
170
+ return;
171
+ }
172
+ chatCounter += 1;
173
+ const entry = {
174
+ id: `msg_${chatCounter}`,
175
+ channelId: message.channelId,
176
+ fromUserId: userId,
177
+ body,
178
+ at: timestamp,
179
+ };
180
+ const ring = chatRing(message.serverId, message.channelId);
181
+ ring.push(entry);
182
+ if (ring.length > chatHistoryLimit)
183
+ ring.splice(0, ring.length - chatHistoryLimit);
184
+ reply(connection, message.id, null);
185
+ broadcastChat(message.serverId, message.channelId);
186
+ };
187
+ const pushSubscription = async (connection, channel, serverId, action) => {
188
+ if (connection.userId === null)
189
+ return;
190
+ if (channel === "server") {
191
+ const data = await host.getServerView({ userId: connection.userId, serverId });
192
+ send(connection, { v: 1, t: "update", channel, serverId, data });
193
+ }
194
+ else if (channel === "player") {
195
+ const data = await host.getPlayerView({ userId: connection.userId, serverId });
196
+ send(connection, { v: 1, t: "update", channel, serverId, data });
197
+ }
198
+ else if (channel === "feed") {
199
+ const data = await host.getFeed({ userId: connection.userId, serverId, action: action ?? "" });
200
+ send(connection, { v: 1, t: "update", channel, serverId, action: action ?? "", data });
201
+ }
202
+ else if (channel === "chat") {
203
+ const channelId = action ?? "";
204
+ send(connection, {
205
+ v: 1,
206
+ t: "update",
207
+ channel,
208
+ serverId,
209
+ action: channelId,
210
+ data: chatRing(serverId, channelId).slice(),
211
+ });
212
+ }
213
+ else if (channel === "voice") {
214
+ const channelId = action ?? "";
215
+ send(connection, {
216
+ v: 1,
217
+ t: "update",
218
+ channel,
219
+ serverId,
220
+ action: channelId,
221
+ data: voiceParticipants(serverId, channelId),
222
+ });
223
+ }
224
+ else {
225
+ send(connection, { v: 1, t: "update", channel, serverId, data: presenceRows(serverId) });
226
+ }
227
+ };
228
+ const onHostEvent = (event) => {
229
+ for (const connection of connections) {
230
+ if (connection.userId === null)
231
+ continue;
232
+ if (event.type === "server") {
233
+ if (connection.subscriptions.has(subscriptionKey("server", event.serverId))) {
234
+ void pushSubscription(connection, "server", event.serverId);
235
+ }
236
+ }
237
+ else if (event.type === "player") {
238
+ if (connection.userId === event.userId &&
239
+ connection.subscriptions.has(subscriptionKey("player", event.serverId))) {
240
+ void pushSubscription(connection, "player", event.serverId);
241
+ }
242
+ }
243
+ else {
244
+ if (connection.subscriptions.has(subscriptionKey("feed", event.serverId, event.action))) {
245
+ void pushSubscription(connection, "feed", event.serverId, event.action);
246
+ }
247
+ }
248
+ }
249
+ };
250
+ const unsubscribeHost = host.subscribe(onHostEvent);
251
+ const handlePose = async (connection, serverId, pose) => {
252
+ if (connection.userId === null)
253
+ return;
254
+ if (!(await host.isMember({ userId: connection.userId, serverId })))
255
+ return;
256
+ const rows = presence.get(serverId) ?? new Map();
257
+ presence.set(serverId, rows);
258
+ const timestamp = now();
259
+ const hadRow = rows.has(connection.userId);
260
+ const current = rows.get(connection.userId) ?? spawnPresenceState(undefined, timestamp, poseRules);
261
+ const decision = decidePoseSync(current, { position: { x: pose.x, y: pose.y, z: pose.z }, rotationY: pose.rotationY, rotationPitch: pose.rotationPitch }, poseRules, timestamp);
262
+ const appearanceChanged = pose.appearance !== undefined && !appearanceEqual(pose.appearance, current.appearance);
263
+ if (decision.changed || decision.refreshKeepAlive || appearanceChanged || !hadRow) {
264
+ rows.set(connection.userId, {
265
+ position: decision.position,
266
+ rotationY: decision.rotationY,
267
+ rotationPitch: decision.rotationPitch,
268
+ lastSeenAtMs: timestamp,
269
+ appearance: pose.appearance ?? current.appearance,
270
+ });
271
+ historyFor(serverId).record(connection.userId, timestamp, decision.position);
272
+ }
273
+ if (decision.changed || appearanceChanged || !hadRow) {
274
+ broadcastPresence(serverId);
275
+ }
276
+ };
277
+ const dropPresence = (connection) => {
278
+ if (connection.userId === null)
279
+ return;
280
+ for (const [serverId, rows] of presence) {
281
+ if (rows.delete(connection.userId)) {
282
+ if (rows.size === 0)
283
+ presence.delete(serverId);
284
+ broadcastPresence(serverId);
285
+ }
286
+ }
287
+ };
288
+ const dropPresenceForServer = (userId, serverId) => {
289
+ const rows = presence.get(serverId);
290
+ if (rows === undefined || !rows.delete(userId))
291
+ return;
292
+ if (rows.size === 0)
293
+ presence.delete(serverId);
294
+ broadcastPresence(serverId);
295
+ };
296
+ const handleMessage = async (connection, message) => {
297
+ if (message.t === "hello") {
298
+ if (connection.userId !== null) {
299
+ replyError(connection, message.id, "Already authenticated");
300
+ return;
301
+ }
302
+ if (authenticate === null) {
303
+ replyError(connection, message.id, "Not authenticated");
304
+ connection.transport.close();
305
+ return;
306
+ }
307
+ const userId = await authenticate({ userId: message.userId, token: message.token });
308
+ if (userId === null) {
309
+ replyError(connection, message.id, "Not authenticated");
310
+ connection.transport.close();
311
+ return;
312
+ }
313
+ if (singleSession) {
314
+ const previous = sessionsByUserId.get(userId);
315
+ if (previous !== undefined && previous !== connection) {
316
+ sessionsByUserId.delete(userId);
317
+ previous.userId = null;
318
+ previous.transport.close();
319
+ }
320
+ sessionsByUserId.set(userId, connection);
321
+ }
322
+ connection.userId = userId;
323
+ reply(connection, message.id, { userId });
324
+ return;
325
+ }
326
+ if (message.t === "pose") {
327
+ await handlePose(connection, message.serverId, message.pose);
328
+ return;
329
+ }
330
+ if (connection.userId === null) {
331
+ replyError(connection, message.id, "Not authenticated");
332
+ return;
333
+ }
334
+ const userId = connection.userId;
335
+ try {
336
+ switch (message.t) {
337
+ case "join": {
338
+ const result = await host.joinServer({
339
+ userId,
340
+ gameId: message.gameId,
341
+ serverId: message.serverId,
342
+ attributes: message.attributes,
343
+ });
344
+ connection.joinedServers.add(result.serverId);
345
+ reply(connection, message.id, result);
346
+ return;
347
+ }
348
+ case "joinByCode": {
349
+ const result = await host.joinByCode({
350
+ userId,
351
+ gameId: message.gameId,
352
+ code: message.code,
353
+ });
354
+ if (result !== null)
355
+ connection.joinedServers.add(result.serverId);
356
+ reply(connection, message.id, result);
357
+ return;
358
+ }
359
+ case "browse": {
360
+ const result = await host.browseServers({
361
+ gameId: message.gameId,
362
+ filter: message.filter,
363
+ limit: message.limit,
364
+ });
365
+ reply(connection, message.id, result);
366
+ return;
367
+ }
368
+ case "leave": {
369
+ await host.leaveServer({ userId, serverId: message.serverId });
370
+ connection.joinedServers.delete(message.serverId);
371
+ dropPresenceForServer(userId, message.serverId);
372
+ dropVoiceForServer(userId, message.serverId);
373
+ reply(connection, message.id, null);
374
+ return;
375
+ }
376
+ case "runCommand": {
377
+ const result = await host.runCommand({
378
+ userId,
379
+ serverId: message.serverId,
380
+ command: message.command,
381
+ input: message.input,
382
+ });
383
+ reply(connection, message.id, result);
384
+ return;
385
+ }
386
+ case "pushFeed": {
387
+ await host.pushFeedEntry({
388
+ userId,
389
+ serverId: message.serverId,
390
+ action: message.action,
391
+ entry: message.entry,
392
+ });
393
+ reply(connection, message.id, null);
394
+ return;
395
+ }
396
+ case "chatSend": {
397
+ await handleChatSend(connection, message);
398
+ return;
399
+ }
400
+ case "voiceJoin": {
401
+ if ((await requireMembership(connection, message.id, message.serverId)) === null)
402
+ return;
403
+ const participant = { userId };
404
+ if (message.streamId !== undefined)
405
+ participant.streamId = message.streamId;
406
+ voiceRoster(message.serverId, message.channelId).set(userId, participant);
407
+ reply(connection, message.id, null);
408
+ broadcastVoice(message.serverId, message.channelId);
409
+ return;
410
+ }
411
+ case "voiceLeave": {
412
+ if ((await requireMembership(connection, message.id, message.serverId)) === null)
413
+ return;
414
+ const roster = voiceRoster(message.serverId, message.channelId);
415
+ const removed = roster.delete(userId);
416
+ reply(connection, message.id, null);
417
+ if (removed)
418
+ broadcastVoice(message.serverId, message.channelId);
419
+ return;
420
+ }
421
+ case "voicePublish": {
422
+ if ((await requireMembership(connection, message.id, message.serverId)) === null)
423
+ return;
424
+ const participant = voiceRoster(message.serverId, message.channelId).get(userId);
425
+ if (participant === undefined) {
426
+ replyError(connection, message.id, "not in this voice channel");
427
+ return;
428
+ }
429
+ participant.streamId = message.streamId;
430
+ reply(connection, message.id, null);
431
+ broadcastVoice(message.serverId, message.channelId);
432
+ return;
433
+ }
434
+ case "subscribe": {
435
+ if ((message.channel === "chat" ||
436
+ message.channel === "voice" ||
437
+ message.channel === "presence") &&
438
+ !(await host.isMember({ userId, serverId: message.serverId }))) {
439
+ replyError(connection, message.id, "Not a member of this server");
440
+ return;
441
+ }
442
+ connection.subscriptions.add(subscriptionKey(message.channel, message.serverId, message.action));
443
+ reply(connection, message.id, null);
444
+ await pushSubscription(connection, message.channel, message.serverId, message.action);
445
+ return;
446
+ }
447
+ case "unsubscribe": {
448
+ connection.subscriptions.delete(subscriptionKey(message.channel, message.serverId, message.action));
449
+ reply(connection, message.id, null);
450
+ return;
451
+ }
452
+ }
453
+ }
454
+ catch (error) {
455
+ replyError(connection, message.id, error instanceof Error ? error.message : "Internal error");
456
+ }
457
+ };
458
+ const leaveJoinedServers = (connection) => {
459
+ if (connection.userId === null)
460
+ return;
461
+ const userId = connection.userId;
462
+ const servers = [...connection.joinedServers];
463
+ connection.joinedServers.clear();
464
+ for (const serverId of servers) {
465
+ void host.leaveServer({ userId, serverId }).catch(() => undefined);
466
+ }
467
+ };
468
+ return {
469
+ connect: (transport) => {
470
+ const connection = {
471
+ transport,
472
+ userId: null,
473
+ subscriptions: new Set(),
474
+ joinedServers: new Set(),
475
+ queue: Promise.resolve(),
476
+ };
477
+ connections.add(connection);
478
+ return {
479
+ handleRaw: (raw) => {
480
+ connection.queue = connection.queue
481
+ .then(async () => {
482
+ const text = typeof raw === "string" ? raw : String(raw);
483
+ const message = decodeWsClientMessage(text);
484
+ if (message === null) {
485
+ const failure = inspectWsDecodeFailure(text);
486
+ if (failure.id !== undefined) {
487
+ replyError(connection, failure.id, failure.reason);
488
+ }
489
+ return;
490
+ }
491
+ await handleMessage(connection, message);
492
+ })
493
+ .catch(() => undefined);
494
+ },
495
+ close: () => {
496
+ if (connection.userId !== null && sessionsByUserId.get(connection.userId) === connection) {
497
+ sessionsByUserId.delete(connection.userId);
498
+ }
499
+ connections.delete(connection);
500
+ leaveJoinedServers(connection);
501
+ dropPresence(connection);
502
+ dropVoice(connection);
503
+ },
504
+ };
505
+ },
506
+ rewind: ({ serverId, atMs }) => {
507
+ const history = histories.get(serverId);
508
+ if (history === undefined)
509
+ return [];
510
+ const positions = [];
511
+ for (const userId of history.entities()) {
512
+ const sample = history.sampleAt(userId, atMs);
513
+ if (sample !== null) {
514
+ positions.push({ userId, x: sample.x, y: sample.y, z: sample.z });
515
+ }
516
+ }
517
+ return positions;
518
+ },
519
+ close: () => {
520
+ unsubscribeHost();
521
+ connections.clear();
522
+ },
523
+ };
524
+ }
525
+ export function loopbackPipe(router) {
526
+ return (handlers) => {
527
+ let open = true;
528
+ const connection = router.connect({
529
+ send: (data) => {
530
+ if (!open)
531
+ return;
532
+ queueMicrotask(() => {
533
+ if (open)
534
+ handlers.onMessage(data);
535
+ });
536
+ },
537
+ close: () => {
538
+ if (!open)
539
+ return;
540
+ open = false;
541
+ connection.close();
542
+ queueMicrotask(() => handlers.onClose());
543
+ },
544
+ });
545
+ queueMicrotask(() => {
546
+ if (open)
547
+ handlers.onOpen();
548
+ });
549
+ return {
550
+ send: (data) => {
551
+ if (!open)
552
+ return;
553
+ queueMicrotask(() => connection.handleRaw(data));
554
+ },
555
+ close: () => {
556
+ if (!open)
557
+ return;
558
+ open = false;
559
+ connection.close();
560
+ queueMicrotask(() => handlers.onClose());
561
+ },
562
+ };
563
+ };
564
+ }
@@ -0,0 +1,10 @@
1
+ export * from "./createWsBackend.js";
2
+ export * from "./host.js";
3
+ export * from "./hostRouter.js";
4
+ export * from "./httpReads.js";
5
+ export * from "./peer.js";
6
+ export * from "./pipe.js";
7
+ export * from "./protocol.js";
8
+ export * from "./readsHandler.js";
9
+ export * from "./socketIoPipe.js";
10
+ export * from "./voiceChannel.js";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./createWsBackend.js";
2
+ export * from "./host.js";
3
+ export * from "./hostRouter.js";
4
+ export * from "./httpReads.js";
5
+ export * from "./peer.js";
6
+ export * from "./pipe.js";
7
+ export * from "./protocol.js";
8
+ export * from "./readsHandler.js";
9
+ export * from "./socketIoPipe.js";
10
+ export * from "./voiceChannel.js";
package/dist/peer.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ import type { GameRuntime } from "@jgengine/core/runtime/gameRuntime";
2
+ import type { HostPersistence } from "@jgengine/core/runtime/hostPersistence";
3
+ import { type GameHost } from "./host.js";
4
+ import { type HostRouter, type HostRouterOptions } from "./hostRouter.js";
5
+ import { type WsBackend } from "./createWsBackend.js";
6
+ export type PeerSignalPayload = {
7
+ type: "offer" | "answer";
8
+ sdp: string;
9
+ };
10
+ export declare function encodePeerSignal(payload: PeerSignalPayload): string;
11
+ export declare function decodePeerSignal(code: string): PeerSignalPayload | null;
12
+ export type PeerRtcOptions = {
13
+ configuration?: RTCConfiguration;
14
+ iceGatherTimeoutMs?: number;
15
+ };
16
+ export type PeerHostOptions = {
17
+ userId: string;
18
+ token?: string;
19
+ host?: GameHost;
20
+ runtimes?: GameRuntime[];
21
+ persistence?: HostPersistence;
22
+ tickMs?: number;
23
+ router?: Omit<HostRouterOptions, "host">;
24
+ rtc?: PeerRtcOptions;
25
+ };
26
+ export type PeerHost = {
27
+ backend: WsBackend;
28
+ host: GameHost;
29
+ router: HostRouter;
30
+ accept: (offerCode: string) => Promise<string>;
31
+ close: () => void;
32
+ };
33
+ export declare function createPeerHost(options: PeerHostOptions): PeerHost;
34
+ export type PeerGuestOptions = {
35
+ userId: string;
36
+ token?: string;
37
+ rtc?: PeerRtcOptions;
38
+ };
39
+ export type PeerGuest = {
40
+ backend: WsBackend;
41
+ offer: () => Promise<string>;
42
+ connect: (answerCode: string) => Promise<void>;
43
+ close: () => void;
44
+ };
45
+ export declare function createPeerGuest(options: PeerGuestOptions): PeerGuest;
46
+ export type PeerSignaling = {
47
+ publishOffer: (offerCode: string) => Promise<string>;
48
+ onOffer: (answer: (offerCode: string) => Promise<string>) => () => void;
49
+ close: () => void;
50
+ };
51
+ export declare function broadcastChannelSignaling(room: string): PeerSignaling;
52
+ export declare function announcePeerHost(host: PeerHost, signaling: PeerSignaling): () => void;
53
+ export declare function joinPeerSession(guest: PeerGuest, signaling: PeerSignaling): Promise<WsBackend>;