@neta-art/cohub 11.1.0 → 11.3.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.
Files changed (78) hide show
  1. package/dist/board/editor/context.d.ts +1 -0
  2. package/dist/board/editor/context.js +1 -0
  3. package/dist/board/editor/editor.d.ts +1 -0
  4. package/dist/board/editor/editor.js +7 -0
  5. package/dist/board/editor/interaction.d.ts +4 -0
  6. package/dist/board/editor/interaction.js +16 -7
  7. package/dist/board/editor/selection-transform.d.ts +1 -1
  8. package/dist/board/editor/selection-transform.js +13 -2
  9. package/dist/board/export/index.d.ts +2 -0
  10. package/dist/board/export/index.js +1 -0
  11. package/dist/board/export/scene.d.ts +2 -0
  12. package/dist/board/export/scene.js +4 -0
  13. package/dist/board/model/file-preview.d.ts +0 -1
  14. package/dist/board/model/file-preview.js +10 -1
  15. package/dist/board/model/index.d.ts +2 -1
  16. package/dist/board/model/index.js +2 -1
  17. package/dist/board/model/script-api.d.ts +69 -0
  18. package/dist/board/model/script-api.js +77 -0
  19. package/dist/board/render/audio-waveform.d.ts +1 -1
  20. package/dist/board/render/audio-waveform.js +8 -5
  21. package/dist/board/render/card-tag.d.ts +24 -0
  22. package/dist/board/render/card-tag.js +205 -0
  23. package/dist/board/render/index.d.ts +7 -4
  24. package/dist/board/render/index.js +7 -4
  25. package/dist/board/render/media-interaction.d.ts +2 -1
  26. package/dist/board/render/media-interaction.js +14 -6
  27. package/dist/board/render/palette.js +2 -2
  28. package/dist/board/render/renderers/arrow-card-renderer.js +20 -16
  29. package/dist/board/render/renderers/audio-card-renderer.d.ts +8 -1
  30. package/dist/board/render/renderers/audio-card-renderer.js +64 -60
  31. package/dist/board/render/renderers/base-card-renderer.d.ts +1 -26
  32. package/dist/board/render/renderers/base-card-renderer.js +6 -92
  33. package/dist/board/render/renderers/board-renderer-registry.d.ts +8 -0
  34. package/dist/board/render/renderers/board-renderer-registry.js +3 -3
  35. package/dist/board/render/renderers/custom-card-renderer.js +4 -7
  36. package/dist/board/render/renderers/effect-card-renderer.js +0 -5
  37. package/dist/board/render/renderers/file-card-renderer.d.ts +1 -11
  38. package/dist/board/render/renderers/file-card-renderer.js +43 -105
  39. package/dist/board/render/renderers/frame-card-renderer.js +14 -15
  40. package/dist/board/render/renderers/image-card-renderer.js +62 -67
  41. package/dist/board/render/renderers/media-key.d.ts +6 -0
  42. package/dist/board/render/renderers/media-key.js +39 -0
  43. package/dist/board/render/renderers/shape-card-renderer.js +6 -12
  44. package/dist/board/render/renderers/task-card-renderer.d.ts +1 -9
  45. package/dist/board/render/renderers/task-card-renderer.js +82 -56
  46. package/dist/board/render/renderers/unknown-card-renderer.js +17 -16
  47. package/dist/board/render/renderers/video-card-renderer.js +73 -80
  48. package/dist/board/render/stroke.d.ts +1 -1
  49. package/dist/board/render/stroke.js +5 -4
  50. package/dist/board/render/style.d.ts +29 -0
  51. package/dist/board/render/style.js +54 -0
  52. package/dist/board/render/surface.d.ts +34 -0
  53. package/dist/board/render/surface.js +132 -0
  54. package/dist/board/render/text-fit.d.ts +8 -0
  55. package/dist/board/render/text-fit.js +60 -0
  56. package/dist/board/render/text-resolution.d.ts +2 -1
  57. package/dist/board/render/text-resolution.js +4 -1
  58. package/dist/board/stage/index.d.ts +3 -2
  59. package/dist/board/stage/index.js +2 -1
  60. package/dist/board/stage/overlay.d.ts +41 -0
  61. package/dist/board/stage/overlay.js +182 -0
  62. package/dist/board/stage/scene.d.ts +2 -43
  63. package/dist/board/stage/scene.js +23 -131
  64. package/dist/board/stage/stage.d.ts +2 -0
  65. package/dist/board/stage/stage.js +99 -32
  66. package/dist/chunks/app-realtime.js +1697 -0
  67. package/dist/chunks/environment.d.ts +997 -3
  68. package/dist/chunks/http.d.ts +393 -3
  69. package/dist/chunks/http.js +40 -1
  70. package/dist/chunks/websocket.d.ts +4 -0
  71. package/dist/chunks/websocket.js +11 -2
  72. package/dist/http.d.ts +2 -2
  73. package/dist/index.d.ts +4 -323
  74. package/dist/index.js +58 -1660
  75. package/dist/protocol/dist/board-model.js +51 -51
  76. package/dist/protocol/dist/board-schema.d.ts +1 -2
  77. package/dist/protocol/dist/board-schema.js +7 -22
  78. package/package.json +2 -1
@@ -0,0 +1,1697 @@
1
+ import { O as readRtcTransportStats, k as waitForIceGathering } from "./http.js";
2
+ import { o as REALTIME_ROOM_EVENT_NAME_PATTERN, t as HttpError } from "./transport.js";
3
+ import { z } from "zod";
4
+ //#region \0rolldown/runtime.js
5
+ var __defProp = Object.defineProperty;
6
+ var __exportAll = (all, no_symbols) => {
7
+ let target = {};
8
+ for (var name in all) __defProp(target, name, {
9
+ get: all[name],
10
+ enumerable: true
11
+ });
12
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
13
+ return target;
14
+ };
15
+ z.object({
16
+ urls: z.union([z.string().min(1).max(512), z.array(z.string().min(1).max(512)).min(1).max(16)]),
17
+ username: z.string().max(512).optional(),
18
+ credential: z.string().max(512).optional()
19
+ });
20
+ const sessionDescriptionSchema = z.object({
21
+ type: z.enum(["offer", "answer"]),
22
+ sdp: z.string().min(1).max(65536)
23
+ });
24
+ const APP_RTC_DELIVERIES = ["reliable", "latest"];
25
+ const APP_RTC_PUBLICATION_KINDS = [
26
+ "data",
27
+ "audio",
28
+ "video"
29
+ ];
30
+ const APP_RTC_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
31
+ const isAppRtcName = (value) => typeof value === "string" && APP_RTC_NAME_PATTERN.test(value) && !value.toLowerCase().startsWith("cohub.");
32
+ const APP_RTC_LIMITS = {
33
+ channelsPerMember: 16,
34
+ tracksPerMember: 4,
35
+ dataSubscriptions: 512,
36
+ mediaSubscriptions: 16,
37
+ messageBytes: 65536,
38
+ operationsPerSecond: 10,
39
+ opensPerSecond: 2,
40
+ batch: 64
41
+ };
42
+ const APP_RTC_QUALITIES = [
43
+ "high",
44
+ "medium",
45
+ "low"
46
+ ];
47
+ const APP_RTC_SIMULCAST_LAYERS = [
48
+ {
49
+ rid: "f",
50
+ quality: "high",
51
+ scaleResolutionDownBy: 1
52
+ },
53
+ {
54
+ rid: "h",
55
+ quality: "medium",
56
+ scaleResolutionDownBy: 2
57
+ },
58
+ {
59
+ rid: "q",
60
+ quality: "low",
61
+ scaleResolutionDownBy: 4
62
+ }
63
+ ];
64
+ z.object({
65
+ id: z.string().min(1).max(32),
66
+ name: z.string().regex(APP_RTC_NAME_PATTERN),
67
+ kind: z.enum(APP_RTC_PUBLICATION_KINDS),
68
+ delivery: z.enum(APP_RTC_DELIVERIES).optional(),
69
+ simulcast: z.boolean().optional()
70
+ });
71
+ const nameSchema = z.string().refine(isAppRtcName, "invalid name");
72
+ const participantIdSchema = z.string().uuid();
73
+ z.object({
74
+ ticket: z.string().min(1).max(4096),
75
+ participantId: participantIdSchema
76
+ });
77
+ z.object({ answer: sessionDescriptionSchema.refine((value) => value.type === "answer", "expected an answer") });
78
+ z.union([z.object({ channels: z.array(z.object({
79
+ name: nameSchema,
80
+ delivery: z.enum(APP_RTC_DELIVERIES)
81
+ })).min(1).max(APP_RTC_LIMITS.channelsPerMember) }), z.object({
82
+ offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer"),
83
+ tracks: z.array(z.object({
84
+ name: nameSchema,
85
+ kind: z.enum(["audio", "video"]),
86
+ mid: z.string().min(1).max(32),
87
+ simulcast: z.boolean().optional()
88
+ })).min(1).max(APP_RTC_LIMITS.tracksPerMember)
89
+ })]);
90
+ z.object({
91
+ ids: z.array(z.string().min(1).max(32)).min(1).max(APP_RTC_LIMITS.channelsPerMember + APP_RTC_LIMITS.tracksPerMember),
92
+ offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
93
+ });
94
+ const subscriptionRefSchema = z.object({
95
+ participantId: participantIdSchema,
96
+ publicationId: z.string().min(1).max(32)
97
+ });
98
+ const qualitySchema = z.enum(APP_RTC_QUALITIES);
99
+ z.object({
100
+ kind: z.enum(["data", "media"]),
101
+ items: z.array(subscriptionRefSchema.extend({ quality: qualitySchema.optional() })).min(1).max(APP_RTC_LIMITS.batch)
102
+ });
103
+ z.object({ items: z.array(subscriptionRefSchema.extend({
104
+ mid: z.string().min(1).max(32),
105
+ quality: qualitySchema.optional()
106
+ })).min(1).max(APP_RTC_LIMITS.mediaSubscriptions) });
107
+ z.object({
108
+ items: z.array(subscriptionRefSchema).min(1).max(APP_RTC_LIMITS.batch),
109
+ offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
110
+ });
111
+ //#endregion
112
+ //#region src/apis/app-room-rtc.ts
113
+ var AppRtcError = class extends Error {
114
+ code;
115
+ constructor(code, message) {
116
+ super(message);
117
+ this.code = code;
118
+ this.name = "AppRtcError";
119
+ }
120
+ };
121
+ const SERVER_CODES = /* @__PURE__ */ new Set([
122
+ "RTC_UNAVAILABLE",
123
+ "RTC_FORBIDDEN",
124
+ "RTC_LIMIT_EXCEEDED",
125
+ "RTC_RATE_LIMITED",
126
+ "ROOM_NOT_FOUND",
127
+ "ROOM_EXPIRED"
128
+ ]);
129
+ const toAppRtcError = (error) => {
130
+ if (error instanceof AppRtcError) return error;
131
+ if (error instanceof HttpError && SERVER_CODES.has(error.code)) return new AppRtcError(error.code, error.message);
132
+ return new AppRtcError("RTC_FAILED", error instanceof Error ? error.message : String(error));
133
+ };
134
+ const HEADER_BYTES = 5;
135
+ const FLAG_BINARY = 1;
136
+ const encoder = new TextEncoder();
137
+ const decoder = new TextDecoder();
138
+ function encodeAppRtcFrame(data, sequence) {
139
+ let payload;
140
+ let flags = 0;
141
+ if (data instanceof ArrayBuffer) {
142
+ payload = new Uint8Array(data);
143
+ flags = FLAG_BINARY;
144
+ } else if (ArrayBuffer.isView(data)) {
145
+ payload = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
146
+ flags = FLAG_BINARY;
147
+ } else {
148
+ const json = JSON.stringify(data);
149
+ if (json === void 0) throw new TypeError("RTC message is not serializable");
150
+ payload = encoder.encode(json);
151
+ }
152
+ if (payload.byteLength > APP_RTC_LIMITS.messageBytes) throw new RangeError(`RTC message exceeds ${APP_RTC_LIMITS.messageBytes} bytes`);
153
+ const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
154
+ frame[0] = flags;
155
+ new DataView(frame.buffer).setUint32(1, sequence >>> 0);
156
+ frame.set(payload, HEADER_BYTES);
157
+ return frame;
158
+ }
159
+ function decodeAppRtcFrame(frame) {
160
+ if (frame.byteLength < HEADER_BYTES) return null;
161
+ const view = new DataView(frame);
162
+ const flags = view.getUint8(0);
163
+ const sequence = view.getUint32(1);
164
+ const payload = new Uint8Array(frame, HEADER_BYTES);
165
+ if (flags & FLAG_BINARY) return {
166
+ data: payload.slice().buffer,
167
+ sequence
168
+ };
169
+ try {
170
+ return {
171
+ data: JSON.parse(decoder.decode(payload)),
172
+ sequence
173
+ };
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+ const isNewerSequence = (next, last) => last === null || next !== last && next - last >>> 0 < 2147483648;
179
+ const publicationsKey = (members) => members.map((member) => `${member.participantId}:${member.publications?.map((item) => item.id).join(",") ?? ""}`).join(";");
180
+ const GATHER_TIMEOUT_MS = 2e3;
181
+ const CONNECT_TIMEOUT_MS = 15e3;
182
+ const PENDING_RELIABLE_FRAMES = 256;
183
+ const LATEST_BACKLOG_BYTES = 65536;
184
+ const RECONNECT_DELAYS_MS = [
185
+ 500,
186
+ 1e3,
187
+ 2e3,
188
+ 5e3,
189
+ 1e4,
190
+ 3e4
191
+ ];
192
+ const RATE_LIMIT_RETRY_MS = 1e3;
193
+ var ConnectionReplacedError = class extends Error {
194
+ constructor() {
195
+ super("RTC connection was replaced");
196
+ this.name = "ConnectionReplacedError";
197
+ }
198
+ };
199
+ const policyOf = (delivery) => delivery === "latest" ? {
200
+ ordered: false,
201
+ maxRetransmits: 0
202
+ } : { ordered: true };
203
+ const isSessionLost = (error) => error instanceof HttpError && (error.code === "RTC_STALE" || error.code === "RTC_SESSION_NOT_FOUND");
204
+ const isRoomGone = (error) => error instanceof HttpError && (error.code === "ROOM_NOT_FOUND" || error.code === "ROOM_EXPIRED");
205
+ const chunk = (items, size) => {
206
+ const chunks = [];
207
+ for (let index = 0; index < items.length; index += size) chunks.push(items.slice(index, index + size));
208
+ return chunks;
209
+ };
210
+ const mediaKey = (participantId, publicationId) => `${participantId}:${publicationId}`;
211
+ var AppRtcChannel = class {
212
+ rtc;
213
+ name;
214
+ delivery;
215
+ handlers = /* @__PURE__ */ new Set();
216
+ peerHandlers = /* @__PURE__ */ new Set();
217
+ openPeers = /* @__PURE__ */ new Set();
218
+ publication = null;
219
+ pending = [];
220
+ offFlush = null;
221
+ sequence = 0;
222
+ closed = false;
223
+ constructor(rtc, name, delivery) {
224
+ this.rtc = rtc;
225
+ this.name = name;
226
+ this.delivery = delivery;
227
+ }
228
+ get peers() {
229
+ return [...this.openPeers];
230
+ }
231
+ send(data) {
232
+ if (this.closed || this.rtc.state === "closed") return false;
233
+ this.sequence = this.sequence + 1 >>> 0;
234
+ const frame = encodeAppRtcFrame(data, this.sequence);
235
+ const channel = this.publication?.channel;
236
+ if (channel?.readyState === "open" && this.pending.length === 0) return this.write(channel, frame);
237
+ if (this.delivery === "latest") this.pending = [frame];
238
+ else if (this.pending.length >= PENDING_RELIABLE_FRAMES) return false;
239
+ else this.pending.push(frame);
240
+ return true;
241
+ }
242
+ onMessage(handler) {
243
+ this.handlers.add(handler);
244
+ return () => this.handlers.delete(handler);
245
+ }
246
+ /** Reports when this client starts or stops receiving a peer's channel. */
247
+ onPeer(handler) {
248
+ this.peerHandlers.add(handler);
249
+ return () => this.peerHandlers.delete(handler);
250
+ }
251
+ close() {
252
+ if (this.closed) return;
253
+ this.closed = true;
254
+ this.handlers.clear();
255
+ this.peerHandlers.clear();
256
+ this.openPeers.clear();
257
+ this.pending = [];
258
+ this.offFlush?.();
259
+ this.offFlush = null;
260
+ this.rtc.closeChannel(this);
261
+ }
262
+ /** @internal */
263
+ get isOpen() {
264
+ return !this.closed;
265
+ }
266
+ /** @internal */
267
+ get publicationId() {
268
+ return this.publication?.id ?? null;
269
+ }
270
+ /** @internal */
271
+ attach(id, channel) {
272
+ this.offFlush?.();
273
+ this.publication = {
274
+ id,
275
+ channel
276
+ };
277
+ channel.binaryType = "arraybuffer";
278
+ channel.bufferedAmountLowThreshold = LATEST_BACKLOG_BYTES;
279
+ const flush = () => {
280
+ let sent = 0;
281
+ while (sent < this.pending.length) {
282
+ const frame = this.pending[sent];
283
+ if (!frame || !this.write(channel, frame)) break;
284
+ sent++;
285
+ }
286
+ this.pending = this.pending.slice(sent);
287
+ };
288
+ channel.addEventListener("open", flush);
289
+ channel.addEventListener("bufferedamountlow", flush);
290
+ this.offFlush = () => {
291
+ channel.removeEventListener("open", flush);
292
+ channel.removeEventListener("bufferedamountlow", flush);
293
+ };
294
+ if (channel.readyState === "open") flush();
295
+ }
296
+ /** @internal */
297
+ detach() {
298
+ this.offFlush?.();
299
+ this.offFlush = null;
300
+ this.publication = null;
301
+ }
302
+ /** @internal */
303
+ fail() {
304
+ this.close();
305
+ }
306
+ /** @internal */
307
+ deliver(from, data) {
308
+ for (const handler of this.handlers) handler({
309
+ from,
310
+ data
311
+ });
312
+ }
313
+ /** @internal */
314
+ setPeer(participantId, open) {
315
+ if (open ? this.openPeers.has(participantId) : !this.openPeers.delete(participantId)) return;
316
+ if (open) this.openPeers.add(participantId);
317
+ for (const handler of this.peerHandlers) handler({
318
+ participantId,
319
+ open
320
+ });
321
+ }
322
+ write(channel, frame) {
323
+ if (channel.bufferedAmount > (this.delivery === "latest" ? LATEST_BACKLOG_BYTES : 4194304)) return false;
324
+ try {
325
+ channel.send(frame);
326
+ return true;
327
+ } catch {
328
+ return false;
329
+ }
330
+ }
331
+ };
332
+ var AppRoomRtc = class {
333
+ host;
334
+ _state = "connecting";
335
+ pc = null;
336
+ sessionId = null;
337
+ queue = Promise.resolve();
338
+ releasePending = Promise.resolve();
339
+ reconcileQueued = false;
340
+ reconcileFailures = 0;
341
+ reconnectAttempt = 0;
342
+ reconnectTimer = null;
343
+ generation = 0;
344
+ filter = null;
345
+ channels = /* @__PURE__ */ new Map();
346
+ channelsToPublish = /* @__PURE__ */ new Set();
347
+ remoteData = /* @__PURE__ */ new Map();
348
+ localTracks = /* @__PURE__ */ new Map();
349
+ remoteMedia = /* @__PURE__ */ new Map();
350
+ mediaByMid = /* @__PURE__ */ new Map();
351
+ stateHandlers = /* @__PURE__ */ new Set();
352
+ trackHandlers = /* @__PURE__ */ new Set();
353
+ offMembers;
354
+ constructor(host) {
355
+ this.host = host;
356
+ let published = "";
357
+ this.offMembers = host.onMembersChanged((members) => {
358
+ const next = publicationsKey(members);
359
+ if (next === published) return;
360
+ published = next;
361
+ this.scheduleReconcile();
362
+ });
363
+ }
364
+ get state() {
365
+ return this._state;
366
+ }
367
+ get tracks() {
368
+ const tracks = [];
369
+ for (const [key, item] of this.remoteMedia) {
370
+ if (!item.track) continue;
371
+ tracks.push({
372
+ participantId: item.participantId,
373
+ name: item.name,
374
+ kind: item.kind,
375
+ track: item.track,
376
+ quality: item.quality ?? null,
377
+ setQuality: (quality) => this.setQuality(key, quality)
378
+ });
379
+ }
380
+ return tracks;
381
+ }
382
+ get candidates() {
383
+ return this.mediaCandidates().map(({ candidate }) => candidate);
384
+ }
385
+ onStateChange(handler) {
386
+ this.stateHandlers.add(handler);
387
+ return () => this.stateHandlers.delete(handler);
388
+ }
389
+ onTracksChanged(handler) {
390
+ this.trackHandlers.add(handler);
391
+ return () => this.trackHandlers.delete(handler);
392
+ }
393
+ select(filter) {
394
+ this.filter = filter;
395
+ this.scheduleReconcile();
396
+ }
397
+ channel(name, options = {}) {
398
+ if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC channel name: ${name}`);
399
+ const delivery = options.delivery ?? "reliable";
400
+ const existing = this.channels.get(name);
401
+ if (existing?.isOpen) {
402
+ if (existing.delivery !== delivery) throw new AppRtcError("RTC_FAILED", `RTC channel ${name} is already open with ${existing.delivery} delivery`);
403
+ return existing;
404
+ }
405
+ if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
406
+ const channel = new AppRtcChannel(this, name, delivery);
407
+ this.channels.set(name, channel);
408
+ this.publishChannel(channel);
409
+ this.scheduleReconcile();
410
+ return channel;
411
+ }
412
+ async publish(tracks) {
413
+ if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
414
+ const inputs = Object.entries(tracks);
415
+ for (const [name] of inputs) {
416
+ if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC track name: ${name}`);
417
+ if (this.localTracks.has(name)) throw new AppRtcError("RTC_FAILED", `RTC track ${name} is already published`);
418
+ }
419
+ if (this.localTracks.size + inputs.length > APP_RTC_LIMITS.tracksPerMember) throw new AppRtcError("RTC_LIMIT_EXCEEDED", `at most ${APP_RTC_LIMITS.tracksPerMember} tracks per member`);
420
+ const entries = inputs.map(([name, input]) => {
421
+ const { track, simulcast } = "track" in input ? input : {
422
+ track: input,
423
+ simulcast: false
424
+ };
425
+ const kind = track.kind === "audio" ? "audio" : "video";
426
+ return this.localEntry(name, kind, track, simulcast === true && kind === "video");
427
+ });
428
+ for (const entry of entries) this.localTracks.set(entry.name, entry);
429
+ try {
430
+ await this.enqueue(() => this.publishTracks(entries.map((entry) => entry.name)));
431
+ } catch (error) {
432
+ if (!(error instanceof ConnectionReplacedError)) {
433
+ for (const entry of entries) this.dropLocal(entry);
434
+ throw toAppRtcError(error);
435
+ }
436
+ }
437
+ return Object.fromEntries(entries.map((entry) => [entry.name, this.localHandle(entry)]));
438
+ }
439
+ async stats() {
440
+ return this.pc ? readRtcTransportStats(await this.pc.getStats()) : {
441
+ path: null,
442
+ relayProtocol: null,
443
+ rttMs: null
444
+ };
445
+ }
446
+ close() {
447
+ if (this._state === "closed") return;
448
+ this.setState("closed");
449
+ this.offMembers();
450
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
451
+ for (const channel of this.channels.values()) channel.close();
452
+ for (const entry of this.localTracks.values()) entry.track.removeEventListener("ended", entry.onEnded);
453
+ this.teardown(true);
454
+ }
455
+ async shutdown() {
456
+ this.close();
457
+ await this.releasePending;
458
+ }
459
+ /** @internal */
460
+ async start() {
461
+ try {
462
+ await this.connect();
463
+ } catch (error) {
464
+ throw toAppRtcError(error);
465
+ }
466
+ }
467
+ /** @internal */
468
+ async verify() {
469
+ if (this._state === "closed" || !this.sessionId) return;
470
+ try {
471
+ await this.request("GET", `/${this.sessionId}`);
472
+ } catch (error) {
473
+ if (isSessionLost(error)) this.reconnect();
474
+ }
475
+ }
476
+ /** @internal */
477
+ closeChannel(channel) {
478
+ if (this.channels.get(channel.name) === channel) this.channels.delete(channel.name);
479
+ const id = channel.publicationId;
480
+ channel.detach();
481
+ if (id) this.enqueue(() => this.unpublishData([id])).catch(() => void 0);
482
+ this.scheduleReconcile();
483
+ }
484
+ localEntry(name, kind, track, simulcast) {
485
+ const entry = {
486
+ name,
487
+ kind,
488
+ track,
489
+ simulcast,
490
+ transceiver: null,
491
+ id: null,
492
+ ended: /* @__PURE__ */ new Set(),
493
+ onEnded: () => void 0
494
+ };
495
+ entry.onEnded = () => {
496
+ if (this.localTracks.get(name) !== entry) return;
497
+ for (const handler of entry.ended) handler();
498
+ this.stopLocal(entry).catch(() => void 0);
499
+ };
500
+ track.addEventListener("ended", entry.onEnded);
501
+ return entry;
502
+ }
503
+ localHandle(entry) {
504
+ return {
505
+ name: entry.name,
506
+ kind: entry.kind,
507
+ simulcast: entry.simulcast,
508
+ get track() {
509
+ return entry.track;
510
+ },
511
+ replace: async (track) => {
512
+ if (track.kind !== entry.kind) throw new AppRtcError("RTC_FAILED", `replace ${entry.name} with another ${entry.kind} track`);
513
+ if (this.localTracks.get(entry.name) !== entry) throw new AppRtcError("RTC_CLOSED", `RTC track ${entry.name} has stopped`);
514
+ entry.track.removeEventListener("ended", entry.onEnded);
515
+ entry.track = track;
516
+ track.addEventListener("ended", entry.onEnded);
517
+ await entry.transceiver?.sender.replaceTrack(track);
518
+ },
519
+ onEnded: (handler) => {
520
+ entry.ended.add(handler);
521
+ return () => entry.ended.delete(handler);
522
+ },
523
+ stop: () => this.stopLocal(entry)
524
+ };
525
+ }
526
+ dropLocal(entry) {
527
+ entry.track.removeEventListener("ended", entry.onEnded);
528
+ if (this.localTracks.get(entry.name) === entry) this.localTracks.delete(entry.name);
529
+ }
530
+ stopLocal(entry) {
531
+ if (this.localTracks.get(entry.name) !== entry) return Promise.resolve();
532
+ this.dropLocal(entry);
533
+ return this.enqueue(() => this.unpublishTrack(entry)).catch((error) => {
534
+ if (error instanceof ConnectionReplacedError) return;
535
+ throw toAppRtcError(error);
536
+ });
537
+ }
538
+ async publishTracks(names) {
539
+ const pc = this.requirePc();
540
+ const entries = names.flatMap((name) => {
541
+ const entry = this.localTracks.get(name);
542
+ return entry && !entry.id ? [entry] : [];
543
+ });
544
+ if (entries.length === 0) return;
545
+ for (const entry of entries) entry.transceiver = pc.addTransceiver(entry.track, {
546
+ direction: "sendonly",
547
+ ...entry.simulcast ? { sendEncodings: APP_RTC_SIMULCAST_LAYERS.map(({ rid, scaleResolutionDownBy }) => ({
548
+ rid,
549
+ scaleResolutionDownBy
550
+ })) } : {}
551
+ });
552
+ const offer = await this.localOffer(pc);
553
+ let result;
554
+ try {
555
+ result = await this.request("POST", `/${this.sessionId}/publish`, {
556
+ offer,
557
+ tracks: entries.map((entry) => ({
558
+ name: entry.name,
559
+ kind: entry.kind,
560
+ mid: entry.transceiver?.mid ?? "",
561
+ ...entry.simulcast ? { simulcast: true } : {}
562
+ }))
563
+ });
564
+ } catch (error) {
565
+ if (error instanceof ConnectionReplacedError) throw error;
566
+ await this.settleOffer(pc, void 0).catch(() => void 0);
567
+ for (const entry of entries) {
568
+ entry.transceiver?.stop();
569
+ entry.transceiver = null;
570
+ }
571
+ throw error;
572
+ }
573
+ await this.settleOffer(pc, result.answer);
574
+ for (const publication of result.publications) {
575
+ const entry = entries.find((candidate) => candidate.name === publication.name);
576
+ if (entry) entry.id = publication.id;
577
+ }
578
+ }
579
+ async unpublishTrack(entry) {
580
+ if (!entry.id || !entry.transceiver || !this.pc) return;
581
+ const id = entry.id;
582
+ entry.transceiver.stop();
583
+ await this.offerExchange(this.pc, (offer) => this.request("POST", `/${this.sessionId}/unpublish`, {
584
+ ids: [id],
585
+ offer
586
+ }));
587
+ entry.id = null;
588
+ }
589
+ publishChannel(channel) {
590
+ this.channelsToPublish.add(channel);
591
+ if (this.channelsToPublish.size > 1 || !this.pc) return;
592
+ this.enqueue(async () => {
593
+ const batch = [...this.channelsToPublish];
594
+ this.channelsToPublish.clear();
595
+ try {
596
+ await this.publishChannels(batch);
597
+ } catch (error) {
598
+ if (isSessionLost(error) || error instanceof ConnectionReplacedError) throw error;
599
+ if (error instanceof HttpError && error.code === "RTC_RATE_LIMITED") {
600
+ setTimeout(() => {
601
+ for (const item of batch) if (item.isOpen) this.publishChannel(item);
602
+ }, RATE_LIMIT_RETRY_MS);
603
+ return;
604
+ }
605
+ for (const item of batch) item.fail();
606
+ console.warn("[Cohub AppRtc] failed to publish channels", error);
607
+ }
608
+ }).catch(() => void 0);
609
+ }
610
+ async connect() {
611
+ const generation = ++this.generation;
612
+ const opened = await this.send("POST", "", {
613
+ ticket: this.host.ticket,
614
+ participantId: this.host.participantId()
615
+ });
616
+ if (generation !== this.generation || this._state === "closed") {
617
+ this.send("DELETE", `/${opened.sessionId}`).catch(() => void 0);
618
+ return;
619
+ }
620
+ this.sessionId = opened.sessionId;
621
+ const configuration = {
622
+ iceServers: opened.iceServers,
623
+ bundlePolicy: "max-bundle"
624
+ };
625
+ let pc;
626
+ try {
627
+ pc = this.host.createPeerConnection?.(configuration) ?? new RTCPeerConnection(configuration);
628
+ } catch (error) {
629
+ await this.shutdown();
630
+ throw error;
631
+ }
632
+ this.pc = pc;
633
+ pc.addEventListener("connectionstatechange", () => {
634
+ if (this.pc !== pc) return;
635
+ if (pc.connectionState === "connected") {
636
+ this.reconnectAttempt = 0;
637
+ this.setState("connected");
638
+ } else if (pc.connectionState === "failed") this.reconnect();
639
+ });
640
+ pc.addEventListener("track", (event) => this.onTrack(event));
641
+ const ready = (async () => {
642
+ await this.answerOffer(opened.offer);
643
+ await this.connected(pc);
644
+ })();
645
+ this.queue = ready.catch(() => void 0);
646
+ try {
647
+ await ready;
648
+ } catch (error) {
649
+ if (generation === this.generation) this.reconnect();
650
+ throw error;
651
+ }
652
+ this.channelsToPublish.clear();
653
+ const republish = [...this.channels.values()].filter((channel) => channel.isOpen);
654
+ const tracks = [...this.localTracks.keys()];
655
+ await Promise.all([republish.length ? this.enqueue(() => this.publishChannels(republish)).catch((error) => {
656
+ if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
657
+ for (const channel of republish) channel.fail();
658
+ console.warn("[Cohub AppRtc] failed to publish channels on the new connection", error);
659
+ }) : null, tracks.length ? this.enqueue(() => this.publishTracks(tracks)).catch((error) => {
660
+ if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
661
+ console.warn("[Cohub AppRtc] failed to publish tracks on the new connection", error);
662
+ }) : null]);
663
+ this.scheduleReconcile();
664
+ }
665
+ connected(pc) {
666
+ if (pc.connectionState === "connected") return Promise.resolve();
667
+ return new Promise((resolve, reject) => {
668
+ const timer = setTimeout(() => done(/* @__PURE__ */ new Error("RTC connection timed out")), CONNECT_TIMEOUT_MS);
669
+ const done = (error) => {
670
+ clearTimeout(timer);
671
+ pc.removeEventListener("connectionstatechange", check);
672
+ if (error) reject(error);
673
+ else resolve();
674
+ };
675
+ const check = () => {
676
+ if (pc.connectionState === "connected") done();
677
+ else if (pc.connectionState === "failed" || pc.connectionState === "closed") done(/* @__PURE__ */ new Error("RTC connection failed"));
678
+ };
679
+ pc.addEventListener("connectionstatechange", check);
680
+ });
681
+ }
682
+ reconnect() {
683
+ if (this._state === "closed" || this.reconnectTimer) return;
684
+ this.setState(this._state === "connecting" ? "connecting" : "reconnecting");
685
+ this.teardown(true);
686
+ const delay = RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
687
+ this.reconnectAttempt += 1;
688
+ this.reconnectTimer = setTimeout(() => {
689
+ this.reconnectTimer = null;
690
+ this.connect().catch((error) => {
691
+ if (isRoomGone(error) || error instanceof HttpError && (error.status === 403 || error.status === 503)) this.close();
692
+ else this.reconnect();
693
+ });
694
+ }, delay);
695
+ }
696
+ teardown(release) {
697
+ this.generation += 1;
698
+ const sessionId = this.sessionId;
699
+ this.sessionId = null;
700
+ this.queue = Promise.resolve();
701
+ for (const channel of this.channels.values()) channel.detach();
702
+ for (const item of this.remoteData.values()) this.dropRemoteData(item);
703
+ this.remoteData.clear();
704
+ for (const item of this.localTracks.values()) {
705
+ item.transceiver = null;
706
+ item.id = null;
707
+ }
708
+ const hadTracks = this.remoteMedia.size > 0;
709
+ this.remoteMedia.clear();
710
+ this.mediaByMid.clear();
711
+ this.pc?.close();
712
+ this.pc = null;
713
+ if (hadTracks) this.notifyTracks();
714
+ if (release && sessionId) {
715
+ this.releasePending = Promise.allSettled([this.releasePending, this.send("DELETE", `/${sessionId}`)]).then((results) => {
716
+ const failed = results.find((result) => result.status === "rejected");
717
+ if (failed?.status === "rejected") throw failed.reason;
718
+ });
719
+ this.releasePending.catch(() => void 0);
720
+ }
721
+ }
722
+ enqueue(operation) {
723
+ const generation = this.generation;
724
+ const run = async () => {
725
+ if (generation !== this.generation || !this.pc || !this.sessionId) throw new ConnectionReplacedError();
726
+ try {
727
+ return await operation();
728
+ } catch (error) {
729
+ if (isSessionLost(error)) this.reconnect();
730
+ throw error;
731
+ }
732
+ };
733
+ const next = this.queue.then(run, run);
734
+ this.queue = next.catch(() => void 0);
735
+ return next;
736
+ }
737
+ async publishChannels(channels) {
738
+ const pc = this.requirePc();
739
+ const open = channels.filter((channel) => channel.isOpen && !channel.publicationId);
740
+ if (open.length === 0) return;
741
+ const result = await this.request("POST", `/${this.sessionId}/publish`, { channels: open.map((channel) => ({
742
+ name: channel.name,
743
+ delivery: channel.delivery
744
+ })) });
745
+ for (const publication of result.publications) {
746
+ const channel = open.find((item) => item.name === publication.name);
747
+ const id = result.channelIds?.[publication.id];
748
+ if (!channel || id === void 0) continue;
749
+ const dataChannel = pc.createDataChannel(publication.name, {
750
+ negotiated: true,
751
+ id,
752
+ ...policyOf(channel.delivery)
753
+ });
754
+ channel.attach(publication.id, dataChannel);
755
+ if (!channel.isOpen) await this.unpublishData([publication.id]);
756
+ }
757
+ }
758
+ async unpublishData(ids) {
759
+ await this.request("POST", `/${this.sessionId}/unpublish`, { ids });
760
+ }
761
+ async settleOffer(pc, answer) {
762
+ if (answer) await pc.setRemoteDescription(answer);
763
+ else if (pc.signalingState === "have-local-offer") await pc.setLocalDescription({ type: "rollback" });
764
+ }
765
+ async offerExchange(pc, send) {
766
+ const offer = await this.localOffer(pc);
767
+ let answer;
768
+ try {
769
+ ({answer} = await send(offer));
770
+ } catch (error) {
771
+ if (!(error instanceof ConnectionReplacedError)) await this.settleOffer(pc, void 0).catch(() => void 0);
772
+ throw error;
773
+ }
774
+ await this.settleOffer(pc, answer);
775
+ }
776
+ scheduleReconcile() {
777
+ if (this.reconcileQueued || !this.pc || this._state === "closed") return;
778
+ this.reconcileQueued = true;
779
+ this.enqueue(async () => {
780
+ this.reconcileQueued = false;
781
+ await this.reconcile();
782
+ this.reconcileFailures = 0;
783
+ }).catch((error) => {
784
+ this.reconcileQueued = false;
785
+ if (isSessionLost(error) || error instanceof ConnectionReplacedError) return;
786
+ console.warn("[Cohub AppRtc] failed to update subscriptions", error);
787
+ if (error instanceof HttpError && error.code === "RTC_LIMIT_EXCEEDED") return;
788
+ const delay = RECONNECT_DELAYS_MS[Math.min(this.reconcileFailures, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
789
+ this.reconcileFailures += 1;
790
+ const generation = this.generation;
791
+ setTimeout(() => {
792
+ if (generation === this.generation) this.scheduleReconcile();
793
+ }, delay);
794
+ });
795
+ }
796
+ mediaCandidates() {
797
+ const self = this.host.participantId();
798
+ const candidates = [];
799
+ for (const member of this.host.members()) {
800
+ if (member.participantId === self) continue;
801
+ for (const publication of member.publications ?? []) {
802
+ if (publication.kind === "data") continue;
803
+ candidates.push({
804
+ publication,
805
+ candidate: {
806
+ participantId: member.participantId,
807
+ name: publication.name,
808
+ kind: publication.kind,
809
+ simulcast: publication.simulcast === true,
810
+ subscribed: this.remoteMedia.has(mediaKey(member.participantId, publication.id))
811
+ }
812
+ });
813
+ }
814
+ }
815
+ return candidates;
816
+ }
817
+ async reconcile() {
818
+ const self = this.host.participantId();
819
+ const wantedData = /* @__PURE__ */ new Map();
820
+ for (const member of this.host.members()) {
821
+ if (member.participantId === self) continue;
822
+ for (const publication of member.publications ?? []) {
823
+ if (publication.kind !== "data") continue;
824
+ const channel = this.channels.get(publication.name);
825
+ if (channel?.isOpen && channel.delivery === (publication.delivery ?? "reliable")) wantedData.set(mediaKey(member.participantId, publication.id), {
826
+ participantId: member.participantId,
827
+ publicationId: publication.id,
828
+ name: publication.name,
829
+ delivery: channel.delivery
830
+ });
831
+ }
832
+ }
833
+ const wantedMedia = /* @__PURE__ */ new Map();
834
+ for (const { candidate, publication } of this.mediaCandidates()) {
835
+ if (wantedMedia.size >= APP_RTC_LIMITS.mediaSubscriptions) break;
836
+ if (this.filter && !this.filter(candidate)) continue;
837
+ const key = mediaKey(candidate.participantId, publication.id);
838
+ const quality = this.remoteMedia.get(key)?.quality ?? (candidate.simulcast ? "high" : void 0);
839
+ wantedMedia.set(key, {
840
+ participantId: candidate.participantId,
841
+ publicationId: publication.id,
842
+ name: candidate.name,
843
+ kind: candidate.kind,
844
+ ...quality ? { quality } : {}
845
+ });
846
+ }
847
+ await this.reconcileData(wantedData);
848
+ await this.reconcileMedia(wantedMedia);
849
+ if ([...wantedData.keys()].some((key) => !this.remoteData.has(key)) || [...wantedMedia.keys()].some((key) => !this.remoteMedia.has(key))) throw new AppRtcError("RTC_FAILED", "Some RTC subscriptions could not be established");
850
+ }
851
+ dropRemoteData(item) {
852
+ item.channel.close();
853
+ this.channels.get(item.name)?.setPeer(item.participantId, false);
854
+ }
855
+ async reconcileData(wanted) {
856
+ const pc = this.requirePc();
857
+ const stale = [...this.remoteData].filter(([key]) => !wanted.has(key));
858
+ for (const batch of chunk(stale, APP_RTC_LIMITS.batch)) {
859
+ await this.request("POST", `/${this.sessionId}/unsubscribe`, { items: batch.map(([, item]) => ({
860
+ participantId: item.participantId,
861
+ publicationId: item.publicationId
862
+ })) });
863
+ for (const [key, item] of batch) {
864
+ this.dropRemoteData(item);
865
+ this.remoteData.delete(key);
866
+ }
867
+ }
868
+ const missing = [...wanted].filter(([key]) => !this.remoteData.has(key)).map(([, item]) => item);
869
+ for (const batch of chunk(missing, APP_RTC_LIMITS.batch)) {
870
+ const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
871
+ kind: "data",
872
+ items: batch.map(({ participantId, publicationId }) => ({
873
+ participantId,
874
+ publicationId
875
+ }))
876
+ });
877
+ for (const subscription of result.subscriptions) {
878
+ const item = batch.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
879
+ if (!item || subscription.channelId === void 0) continue;
880
+ const channel = pc.createDataChannel(item.name, {
881
+ negotiated: true,
882
+ id: subscription.channelId,
883
+ ...policyOf(item.delivery)
884
+ });
885
+ channel.binaryType = "arraybuffer";
886
+ const remote = {
887
+ name: item.name,
888
+ participantId: item.participantId,
889
+ publicationId: item.publicationId,
890
+ channel,
891
+ last: null
892
+ };
893
+ channel.addEventListener("message", (event) => this.onData(remote, event.data));
894
+ channel.addEventListener("open", () => {
895
+ if (this.remoteData.get(mediaKey(remote.participantId, remote.publicationId)) === remote) this.channels.get(remote.name)?.setPeer(remote.participantId, true);
896
+ });
897
+ this.remoteData.set(mediaKey(item.participantId, item.publicationId), remote);
898
+ }
899
+ }
900
+ }
901
+ async reconcileMedia(wanted) {
902
+ const stale = [...this.remoteMedia].filter(([key]) => !wanted.has(key));
903
+ const missing = [...wanted].filter(([key]) => !this.remoteMedia.has(key)).map(([, item]) => item);
904
+ const moves = [];
905
+ for (const to of [...missing]) {
906
+ const index = stale.findIndex(([, receiver]) => receiver.kind === to.kind);
907
+ const found = index >= 0 ? stale.splice(index, 1)[0] : void 0;
908
+ if (!found) continue;
909
+ moves.push({
910
+ from: found[0],
911
+ receiver: found[1],
912
+ to
913
+ });
914
+ missing.splice(missing.indexOf(to), 1);
915
+ }
916
+ if (moves.length) {
917
+ const moved = await this.retarget(moves.map(({ receiver, to }) => ({
918
+ ...to,
919
+ mid: receiver.mid
920
+ })));
921
+ for (const { from, receiver, to } of moves) if (moved.has(receiver.mid)) {
922
+ this.remoteMedia.delete(from);
923
+ this.remoteMedia.set(mediaKey(to.participantId, to.publicationId), {
924
+ ...to,
925
+ mid: receiver.mid,
926
+ track: receiver.track
927
+ });
928
+ this.mediaByMid.set(receiver.mid, mediaKey(to.participantId, to.publicationId));
929
+ } else {
930
+ stale.push([from, receiver]);
931
+ missing.push(to);
932
+ }
933
+ if (moved.size) this.notifyTracks();
934
+ }
935
+ await this.unsubscribeMedia(stale);
936
+ await this.subscribeMedia(missing);
937
+ }
938
+ async retarget(items) {
939
+ try {
940
+ const result = await this.request("PATCH", `/${this.sessionId}/media`, { items: items.map(({ participantId, publicationId, mid, quality }) => ({
941
+ participantId,
942
+ publicationId,
943
+ mid,
944
+ ...quality ? { quality } : {}
945
+ })) });
946
+ if (result.offer) await this.answerOffer(result.offer);
947
+ return new Set(result.updated.map((item) => item.mid));
948
+ } catch (error) {
949
+ if (error instanceof ConnectionReplacedError || isSessionLost(error)) throw error;
950
+ return /* @__PURE__ */ new Set();
951
+ }
952
+ }
953
+ async setQuality(key, quality) {
954
+ await this.enqueue(async () => {
955
+ const item = this.remoteMedia.get(key);
956
+ if (!item || item.quality === void 0 || item.quality === quality) return;
957
+ if ((await this.retarget([{
958
+ ...item,
959
+ quality
960
+ }])).has(item.mid)) {
961
+ item.quality = quality;
962
+ this.notifyTracks();
963
+ }
964
+ }).catch((error) => {
965
+ if (error instanceof ConnectionReplacedError) return;
966
+ throw toAppRtcError(error);
967
+ });
968
+ }
969
+ async unsubscribeMedia(stale) {
970
+ if (stale.length === 0) return;
971
+ const pc = this.requirePc();
972
+ for (const [, item] of stale) {
973
+ pc.getTransceivers().find((transceiver) => transceiver.mid === item.mid)?.stop();
974
+ item.track = null;
975
+ }
976
+ this.notifyTracks();
977
+ await this.offerExchange(pc, (offer) => this.request("POST", `/${this.sessionId}/unsubscribe`, {
978
+ items: stale.map(([, item]) => ({
979
+ participantId: item.participantId,
980
+ publicationId: item.publicationId
981
+ })),
982
+ offer
983
+ }));
984
+ for (const [key, item] of stale) {
985
+ this.remoteMedia.delete(key);
986
+ this.mediaByMid.delete(item.mid);
987
+ }
988
+ }
989
+ async subscribeMedia(missing) {
990
+ if (missing.length === 0) return;
991
+ const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
992
+ kind: "media",
993
+ items: missing.map(({ participantId, publicationId, quality }) => ({
994
+ participantId,
995
+ publicationId,
996
+ ...quality ? { quality } : {}
997
+ }))
998
+ });
999
+ for (const subscription of result.subscriptions) {
1000
+ const item = missing.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
1001
+ if (!item || !subscription.mid) continue;
1002
+ const key = mediaKey(item.participantId, item.publicationId);
1003
+ this.remoteMedia.set(key, {
1004
+ ...item,
1005
+ mid: subscription.mid,
1006
+ track: null
1007
+ });
1008
+ this.mediaByMid.set(subscription.mid, key);
1009
+ }
1010
+ if (!result.offer) return;
1011
+ try {
1012
+ await this.answerOffer(result.offer);
1013
+ } catch (error) {
1014
+ if (!(error instanceof ConnectionReplacedError)) this.reconnect();
1015
+ throw error;
1016
+ }
1017
+ }
1018
+ async answerOffer(offer) {
1019
+ const pc = this.requirePc();
1020
+ await pc.setRemoteDescription(offer);
1021
+ await pc.setLocalDescription(await pc.createAnswer());
1022
+ await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
1023
+ const answer = pc.localDescription;
1024
+ if (!answer) throw new Error("the browser produced no answer");
1025
+ await this.request("PUT", `/${this.sessionId}`, { answer: {
1026
+ type: "answer",
1027
+ sdp: answer.sdp
1028
+ } });
1029
+ }
1030
+ async localOffer(pc) {
1031
+ await pc.setLocalDescription(await pc.createOffer());
1032
+ await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
1033
+ const offer = pc.localDescription;
1034
+ if (!offer) throw new Error("the browser produced no offer");
1035
+ return {
1036
+ type: "offer",
1037
+ sdp: offer.sdp
1038
+ };
1039
+ }
1040
+ async request(method, path, body) {
1041
+ const generation = this.generation;
1042
+ const settled = await this.send(method, path, body).then((value) => ({ value }), (error) => ({ error }));
1043
+ if (generation !== this.generation) throw new ConnectionReplacedError();
1044
+ if ("error" in settled) throw settled.error;
1045
+ return settled.value;
1046
+ }
1047
+ send(method, path, body) {
1048
+ const url = `${this.host.hosted ? `/api/rooms/${encodeURIComponent(this.host.roomId)}/rtc` : `/api/apps/${encodeURIComponent(this.host.appId)}/realtime/rooms/${encodeURIComponent(this.host.roomId)}/rtc`}${path}`;
1049
+ return this.host.transport.request(url, {
1050
+ method,
1051
+ ...body === void 0 ? {} : {
1052
+ headers: { "Content-Type": "application/json" },
1053
+ body: JSON.stringify(body)
1054
+ }
1055
+ });
1056
+ }
1057
+ requirePc() {
1058
+ if (!this.pc || !this.sessionId) throw new ConnectionReplacedError();
1059
+ return this.pc;
1060
+ }
1061
+ onData(remote, raw) {
1062
+ if (!(raw instanceof ArrayBuffer)) return;
1063
+ const frame = decodeAppRtcFrame(raw);
1064
+ if (!frame) return;
1065
+ const channel = this.channels.get(remote.name);
1066
+ if (!channel?.isOpen) return;
1067
+ if (channel.delivery === "latest") {
1068
+ if (!isNewerSequence(frame.sequence, remote.last)) return;
1069
+ remote.last = frame.sequence;
1070
+ }
1071
+ channel.deliver(remote.participantId, frame.data);
1072
+ }
1073
+ onTrack(event) {
1074
+ const mid = event.transceiver.mid;
1075
+ const key = mid ? this.mediaByMid.get(mid) : void 0;
1076
+ const item = key ? this.remoteMedia.get(key) : void 0;
1077
+ if (!item || !mid) return;
1078
+ item.track = event.track;
1079
+ event.track.addEventListener("ended", () => {
1080
+ const current = this.mediaByMid.get(mid);
1081
+ const owner = current ? this.remoteMedia.get(current) : void 0;
1082
+ if (owner?.track === event.track) {
1083
+ owner.track = null;
1084
+ this.notifyTracks();
1085
+ }
1086
+ });
1087
+ this.notifyTracks();
1088
+ }
1089
+ notifyTracks() {
1090
+ const tracks = this.tracks;
1091
+ for (const handler of this.trackHandlers) handler(tracks);
1092
+ }
1093
+ setState(state) {
1094
+ if (this._state === state) return;
1095
+ this._state = state;
1096
+ for (const handler of this.stateHandlers) handler(state);
1097
+ }
1098
+ };
1099
+ //#endregion
1100
+ //#region src/apis/app-realtime.ts
1101
+ var app_realtime_exports = /* @__PURE__ */ __exportAll({
1102
+ AppRealtimeApi: () => AppRealtimeApi,
1103
+ AppRoom: () => AppRoom,
1104
+ WorkRealtimeApi: () => WorkRealtimeApi,
1105
+ WorkRoom: () => WorkRoom
1106
+ });
1107
+ const randomId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1108
+ const textEncoder = new TextEncoder();
1109
+ const byteLength = (text) => textEncoder.encode(text).byteLength;
1110
+ /**
1111
+ * Ceiling on deltas held during a join, so a stalled handshake cannot buffer without
1112
+ * limit. Dropping the tail needs no extra signal: the gap detection below reports it.
1113
+ */
1114
+ const APP_ROOM_MAX_JOIN_BUFFER = 512;
1115
+ /** Room events that mutate state, so they must not be applied before the join snapshot. */
1116
+ const APP_ROOM_DELTA_EVENTS = /* @__PURE__ */ new Set([
1117
+ "realtime.room.event",
1118
+ "realtime.room.member.joined",
1119
+ "realtime.room.member.left",
1120
+ "realtime.room.presence.updated",
1121
+ "realtime.room.publications.updated"
1122
+ ]);
1123
+ const APP_ROOM_MEMBER_EVENTS = /* @__PURE__ */ new Set([
1124
+ "realtime.room.member.joined",
1125
+ "realtime.room.member.left",
1126
+ "realtime.room.presence.updated",
1127
+ "realtime.room.publications.updated"
1128
+ ]);
1129
+ const requestError = (event) => {
1130
+ const payload = event.payload;
1131
+ return new Error(typeof payload.message === "string" ? payload.message : typeof payload.code === "string" ? payload.code : "room request failed");
1132
+ };
1133
+ const isRoomEvent = (event, roomId) => {
1134
+ if (event.domain !== "room") return false;
1135
+ return event.payload.roomId === roomId || event.rooms?.includes(`room:${roomId}`);
1136
+ };
1137
+ var AppRoom = class {
1138
+ websocket;
1139
+ admission;
1140
+ appContext;
1141
+ id;
1142
+ code;
1143
+ createdAt;
1144
+ expiresAt;
1145
+ maxParticipants;
1146
+ seatPerUser;
1147
+ /** Opaque, stable identity of this viewer inside the room. */
1148
+ userKey;
1149
+ _participantId;
1150
+ _state = "connecting";
1151
+ _members = [];
1152
+ joinBuffer = null;
1153
+ lastSequence = 0;
1154
+ hasJoined = false;
1155
+ explicitLeave = false;
1156
+ expiryTimer = null;
1157
+ pending = /* @__PURE__ */ new Map();
1158
+ handlers = /* @__PURE__ */ new Map();
1159
+ allHandlers = /* @__PURE__ */ new Set();
1160
+ stateHandlers = /* @__PURE__ */ new Set();
1161
+ membersHandlers = /* @__PURE__ */ new Set();
1162
+ outOfSyncHandlers = /* @__PURE__ */ new Set();
1163
+ sendErrorHandlers = /* @__PURE__ */ new Set();
1164
+ offEvent;
1165
+ offOpen;
1166
+ offClose;
1167
+ offReconnecting;
1168
+ instanceId = randomId().replace(/[^A-Za-z0-9_-]/g, "");
1169
+ rtcConnection = null;
1170
+ constructor(websocket, admission, appContext) {
1171
+ this.websocket = websocket;
1172
+ this.admission = admission;
1173
+ this.appContext = appContext;
1174
+ this.id = admission.room.id;
1175
+ this.code = admission.room.code;
1176
+ this.createdAt = admission.room.createdAt;
1177
+ this.expiresAt = admission.room.expiresAt;
1178
+ this.maxParticipants = admission.room.maxParticipants;
1179
+ this.seatPerUser = admission.room.seatPerUser === true;
1180
+ this.userKey = admission.userKey;
1181
+ this._participantId = admission.participantId;
1182
+ this.offEvent = websocket.on("event", (event) => this.handleEvent(event));
1183
+ this.offOpen = websocket.on("open", () => {
1184
+ if (!this.hasJoined || this.explicitLeave || this._state === "expired" || this._state === "closed") return;
1185
+ this.rejoin();
1186
+ });
1187
+ this.offReconnecting = websocket.on("reconnecting", () => {
1188
+ if (this.hasJoined && !this.explicitLeave) this.setState("reconnecting");
1189
+ });
1190
+ this.offClose = websocket.on("close", ({ willReconnect }) => {
1191
+ this.rejectPending(/* @__PURE__ */ new Error("room connection closed"));
1192
+ if (this.explicitLeave || !willReconnect) {
1193
+ if (this._state !== "expired") this.setState("closed");
1194
+ this.hasJoined = false;
1195
+ this.dispose();
1196
+ } else if (this.hasJoined) this.setState("reconnecting");
1197
+ });
1198
+ const delay = Math.max(0, Date.parse(this.expiresAt) - Date.now());
1199
+ this.expiryTimer = setTimeout(() => this.expire(), Math.min(delay, 2147e6));
1200
+ }
1201
+ get state() {
1202
+ return this._state;
1203
+ }
1204
+ /**
1205
+ * Identity of this connection inside the room. A `seatPerUser` join can take over a
1206
+ * seat the viewer already holds, so this may differ from the id issued at admission.
1207
+ */
1208
+ get participantId() {
1209
+ return this._participantId;
1210
+ }
1211
+ get members() {
1212
+ return this._members.slice();
1213
+ }
1214
+ get sequence() {
1215
+ return this.lastSequence;
1216
+ }
1217
+ async connect(options = {}) {
1218
+ if (this.explicitLeave || this.state === "closed" || this.state === "expired") throw new Error("room is closed");
1219
+ if (this.hasJoined) return this;
1220
+ if (options.after !== void 0 && (!Number.isSafeInteger(options.after) || options.after < 0)) throw new Error("after must be a non-negative safe integer");
1221
+ this.lastSequence = options.after ?? 0;
1222
+ await this.joinOverWebsocket(false, options.after);
1223
+ return this;
1224
+ }
1225
+ subscribe(type, handler) {
1226
+ const handlers = this.handlers.get(type) ?? /* @__PURE__ */ new Set();
1227
+ handlers.add(handler);
1228
+ this.handlers.set(type, handlers);
1229
+ return () => handlers.delete(handler);
1230
+ }
1231
+ subscribeAll(handler) {
1232
+ this.allHandlers.add(handler);
1233
+ return () => this.allHandlers.delete(handler);
1234
+ }
1235
+ onStateChange(handler) {
1236
+ this.stateHandlers.add(handler);
1237
+ return () => this.stateHandlers.delete(handler);
1238
+ }
1239
+ onMembersChanged(handler) {
1240
+ this.membersHandlers.add(handler);
1241
+ return () => this.membersHandlers.delete(handler);
1242
+ }
1243
+ onOutOfSync(handler) {
1244
+ this.outOfSyncHandlers.add(handler);
1245
+ return () => this.outOfSyncHandlers.delete(handler);
1246
+ }
1247
+ /** Reports failures of {@link send}, which has no ack to reject. */
1248
+ onSendError(handler) {
1249
+ this.sendErrorHandlers.add(handler);
1250
+ return () => this.sendErrorHandlers.delete(handler);
1251
+ }
1252
+ /**
1253
+ * Publishes without waiting for the server ack, for high-frequency traffic such as
1254
+ * input frames. Awaiting {@link publish} instead caps a loop at `1000 / rtt` events
1255
+ * per second. Ordering still holds, but failures surface through
1256
+ * {@link onSendError} rather than a rejected promise, and calls are ignored while
1257
+ * the room is not joined; watch {@link onStateChange} to know when it resumes.
1258
+ */
1259
+ send(type, data) {
1260
+ if (this._state !== "joined" || !this.hasJoined) return;
1261
+ const failure = this.validateSend(type, data);
1262
+ if (failure) {
1263
+ for (const handler of this.sendErrorHandlers) handler(failure);
1264
+ return;
1265
+ }
1266
+ this.websocket.publishRealtimeRoom({
1267
+ roomId: this.id,
1268
+ event: type,
1269
+ data
1270
+ }).catch((error) => {
1271
+ const reason = error instanceof Error ? error : new Error(String(error));
1272
+ for (const handler of this.sendErrorHandlers) handler(reason);
1273
+ });
1274
+ }
1275
+ validateSend(type, data) {
1276
+ if (!REALTIME_ROOM_EVENT_NAME_PATTERN.test(type)) return /* @__PURE__ */ new Error(`invalid room event name: ${type}`);
1277
+ let encoded;
1278
+ try {
1279
+ encoded = JSON.stringify(data);
1280
+ } catch {
1281
+ return /* @__PURE__ */ new Error("room event data is not serializable");
1282
+ }
1283
+ if (encoded === void 0) return /* @__PURE__ */ new Error("room event data is not serializable");
1284
+ if (byteLength(encoded) > 16384) return /* @__PURE__ */ new Error("room event payload is too large");
1285
+ return null;
1286
+ }
1287
+ async publish(type, data, options) {
1288
+ this.assertJoined();
1289
+ const failure = this.validateSend(type, data);
1290
+ if (failure) throw failure;
1291
+ const requestId = randomId();
1292
+ const response = await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.request.ok"]), () => this.websocket.publishRealtimeRoom({
1293
+ roomId: this.id,
1294
+ event: type,
1295
+ data,
1296
+ clientEventId: options?.clientEventId,
1297
+ requestId
1298
+ }));
1299
+ const payload = response.payload;
1300
+ return {
1301
+ eventId: typeof payload.eventId === "string" ? payload.eventId : response.id,
1302
+ sequence: typeof payload.sequence === "number" ? payload.sequence : 0,
1303
+ clientEventId: typeof payload.clientEventId === "string" ? payload.clientEventId : null
1304
+ };
1305
+ }
1306
+ /** Published events after `after`, oldest first; without it, the newest ones. */
1307
+ async history(input = {}) {
1308
+ this.assertJoined();
1309
+ if (!this.websocket.supportsCapability("realtime.room.history.v1")) throw new Error("Room history is not supported by the Gateway");
1310
+ const requestId = randomId();
1311
+ const response = await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
1312
+ roomId: this.id,
1313
+ ...input,
1314
+ requestId
1315
+ }));
1316
+ return this.toHistory(response.payload);
1317
+ }
1318
+ /** An invite for someone outside the App, e.g. an Agent: `cohub rooms join <invite>`. */
1319
+ async invite(input = {}) {
1320
+ this.assertJoined();
1321
+ const context = this.appContext;
1322
+ if (!context) throw new Error("Invites are only available inside a published App or hosted Room");
1323
+ if (context.hosted) return context.transport.request(`/api/rooms/${encodeURIComponent(this.id)}/invites`, {
1324
+ method: "POST",
1325
+ headers: { "Content-Type": "application/json" },
1326
+ body: JSON.stringify(input)
1327
+ });
1328
+ return context.transport.request(`/api/apps/${encodeURIComponent(context.appId)}/realtime/rooms/${encodeURIComponent(this.id)}/invites`, {
1329
+ method: "POST",
1330
+ headers: { "Content-Type": "application/json" },
1331
+ body: JSON.stringify({
1332
+ ticket: this.admission.ticket,
1333
+ ...input
1334
+ })
1335
+ });
1336
+ }
1337
+ rtcFactory;
1338
+ rtc(options) {
1339
+ if (options?.createPeerConnection) this.rtcFactory = options.createPeerConnection;
1340
+ this.assertJoined();
1341
+ if (this.rtcConnection) {
1342
+ const current = this.rtcConnection;
1343
+ return current.then((rtc) => {
1344
+ if (rtc.state !== "closed") return rtc;
1345
+ return (this.rtcConnection === current ? this.openRtc() : this.rtcConnection) ?? this.openRtc();
1346
+ });
1347
+ }
1348
+ return this.openRtc();
1349
+ }
1350
+ openRtc() {
1351
+ this.assertJoined();
1352
+ const context = this.appContext;
1353
+ if (!context) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC requires room context"));
1354
+ if (!this.rtcFactory && typeof RTCPeerConnection === "undefined") return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC needs a WebRTC adapter"));
1355
+ if (!this.websocket.supportsCapability("realtime.room.rtc.v1")) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC is not supported by the Gateway"));
1356
+ const rtc = new AppRoomRtc({
1357
+ roomId: this.id,
1358
+ appId: context.appId,
1359
+ ticket: this.admission.ticket,
1360
+ transport: context.transport,
1361
+ hosted: context.hosted,
1362
+ createPeerConnection: this.rtcFactory,
1363
+ participantId: () => this._participantId,
1364
+ members: () => this._members,
1365
+ onMembersChanged: (handler) => this.onMembersChanged(handler)
1366
+ });
1367
+ const opening = rtc.start().then(() => rtc, (error) => {
1368
+ rtc.close();
1369
+ if (this.rtcConnection === opening) this.rtcConnection = null;
1370
+ throw error;
1371
+ });
1372
+ this.rtcConnection = opening;
1373
+ return opening;
1374
+ }
1375
+ async setPresence(presence) {
1376
+ this.assertJoined();
1377
+ const requestId = randomId();
1378
+ await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.request.ok"]), () => this.websocket.updateRealtimeRoomPresence({
1379
+ roomId: this.id,
1380
+ presence,
1381
+ requestId
1382
+ }));
1383
+ }
1384
+ async disconnect() {
1385
+ if (this.explicitLeave || this._state === "closed" || this._state === "expired") return;
1386
+ this.explicitLeave = true;
1387
+ try {
1388
+ if (this.hasJoined && this.websocket.state === "open") {
1389
+ const requestId = randomId();
1390
+ await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.request.ok"]), () => this.appContext?.hosted ? this.websocket.detachRealtimeRoom({
1391
+ roomId: this.id,
1392
+ requestId
1393
+ }) : this.websocket.leaveRealtimeRoom({
1394
+ roomId: this.id,
1395
+ requestId
1396
+ }));
1397
+ }
1398
+ } finally {
1399
+ this.hasJoined = false;
1400
+ this.setState("closed");
1401
+ this.dispose();
1402
+ }
1403
+ }
1404
+ async leave() {
1405
+ if (!this.appContext?.hosted) return this.disconnect();
1406
+ if (this.explicitLeave || this.state === "closed" || this.state === "expired") return;
1407
+ await this.appContext.transport.request(`/api/rooms/${encodeURIComponent(this.id)}/seat`, { method: "DELETE" });
1408
+ this.explicitLeave = true;
1409
+ this.hasJoined = false;
1410
+ this.rejectPending(/* @__PURE__ */ new Error("room left"));
1411
+ this.setState("closed");
1412
+ this.dispose();
1413
+ }
1414
+ async rejoin() {
1415
+ if (this.explicitLeave || this._state === "expired" || this._state === "closed") return;
1416
+ try {
1417
+ await this.joinOverWebsocket(true);
1418
+ } catch (error) {
1419
+ if (this.state === "expired") return;
1420
+ this.setState("closed");
1421
+ this.dispose();
1422
+ console.warn("[Cohub AppRoom] failed to rejoin room", error);
1423
+ }
1424
+ }
1425
+ async joinOverWebsocket(isReconnect, initialAfter) {
1426
+ if (!this.websocket.supportsCapability("realtime.room.v1") && this.websocket.state === "open") throw new Error("Realtime rooms are not supported by the Gateway");
1427
+ this.setState(isReconnect ? "reconnecting" : "connecting");
1428
+ const requestId = randomId();
1429
+ const after = isReconnect && this.websocket.supportsCapability("realtime.room.history.v1") ? this.lastSequence : initialAfter;
1430
+ let missed = null;
1431
+ this.joinBuffer = [];
1432
+ try {
1433
+ if (this.appContext?.hosted && isReconnect) {
1434
+ const admission = await this.appContext.transport.request(`/api/rooms/${encodeURIComponent(this.id)}/connect`, { method: "POST" });
1435
+ if (admission.participantId !== this.participantId) throw new Error("room seat changed during reconnect");
1436
+ this.admission.ticket = admission.ticket;
1437
+ }
1438
+ const payload = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.joined"]), () => this.websocket.joinRealtimeRoom({
1439
+ roomId: this.id,
1440
+ ticket: this.admission.ticket,
1441
+ instanceId: this.instanceId,
1442
+ after,
1443
+ requestId
1444
+ }))).payload;
1445
+ if (typeof payload.participantId !== "string" || !Array.isArray(payload.members)) throw new Error("invalid room join response");
1446
+ this._participantId = payload.participantId;
1447
+ this._members = payload.members;
1448
+ const sequence = typeof payload.sequence === "number" ? payload.sequence : 0;
1449
+ if (after !== void 0 && sequence > after) missed = await this.catchUp(payload.missed, after, sequence);
1450
+ this.lastSequence = sequence;
1451
+ } catch (error) {
1452
+ this.joinBuffer = null;
1453
+ throw error;
1454
+ }
1455
+ if (this.explicitLeave || this._state === "closed" || this._state === "expired") throw new Error("room is closed");
1456
+ this.hasJoined = true;
1457
+ this.setState("joined");
1458
+ this.notifyMembers();
1459
+ if (missed) {
1460
+ const { lost } = missed;
1461
+ if (lost) for (const handler of this.outOfSyncHandlers) handler(lost[0], lost[1]);
1462
+ for (const event of missed.events) this.emit(event);
1463
+ }
1464
+ const buffered = this.joinBuffer ?? [];
1465
+ this.joinBuffer = null;
1466
+ for (const item of buffered) {
1467
+ const sequence = item.payload.sequence;
1468
+ if (typeof sequence === "number" && sequence <= this.lastSequence) continue;
1469
+ this.handleEvent(item);
1470
+ }
1471
+ if (isReconnect) this.rtcConnection?.then((rtc) => rtc.verify()).catch(() => void 0);
1472
+ }
1473
+ request(requestId, expected, send) {
1474
+ return new Promise((resolve, reject) => {
1475
+ const timer = setTimeout(() => {
1476
+ this.pending.delete(requestId);
1477
+ reject(/* @__PURE__ */ new Error("room request timed out"));
1478
+ }, 2e4);
1479
+ this.pending.set(requestId, {
1480
+ expected,
1481
+ resolve,
1482
+ reject,
1483
+ timer
1484
+ });
1485
+ send().catch((error) => {
1486
+ this.pending.delete(requestId);
1487
+ clearTimeout(timer);
1488
+ reject(error instanceof Error ? error : new Error(String(error)));
1489
+ });
1490
+ });
1491
+ }
1492
+ /** Detaches the entry awaiting this requestId, so the caller can settle it. */
1493
+ takePending(requestId) {
1494
+ const pending = requestId ? this.pending.get(requestId) : void 0;
1495
+ if (!pending || !requestId) return null;
1496
+ this.pending.delete(requestId);
1497
+ clearTimeout(pending.timer);
1498
+ return pending;
1499
+ }
1500
+ handleEvent(event) {
1501
+ if (!isRoomEvent(event, this.id)) {
1502
+ if (event.type === "system.request.error") this.takePending(event.requestId)?.reject(requestError(event));
1503
+ return;
1504
+ }
1505
+ if (event.type === "realtime.room.request.error") {
1506
+ const failure = requestError(event);
1507
+ if (event.requestId) {
1508
+ this.takePending(event.requestId)?.reject(failure);
1509
+ return;
1510
+ }
1511
+ for (const handler of this.sendErrorHandlers) handler(failure);
1512
+ return;
1513
+ }
1514
+ const awaiting = event.requestId ? this.pending.get(event.requestId) : void 0;
1515
+ if (awaiting?.expected.has(event.type)) {
1516
+ this.takePending(event.requestId);
1517
+ awaiting.resolve(event);
1518
+ }
1519
+ if (this.joinBuffer && APP_ROOM_DELTA_EVENTS.has(event.type)) {
1520
+ if (this.joinBuffer.length < APP_ROOM_MAX_JOIN_BUFFER) this.joinBuffer.push(event);
1521
+ return;
1522
+ }
1523
+ const payload = event.payload;
1524
+ if (APP_ROOM_DELTA_EVENTS.has(event.type) && typeof payload.sequence === "number") this.observeSequence(payload.sequence);
1525
+ if (APP_ROOM_MEMBER_EVENTS.has(event.type)) {
1526
+ const member = payload.member;
1527
+ if (member?.participantId) {
1528
+ const members = new Map(this._members.map((item) => [item.participantId, item]));
1529
+ if (event.type === "realtime.room.member.left") members.delete(member.participantId);
1530
+ else members.set(member.participantId, member);
1531
+ this._members = [...members.values()];
1532
+ this.notifyMembers();
1533
+ }
1534
+ return;
1535
+ }
1536
+ if (event.type === "realtime.room.closed") {
1537
+ this.hasJoined = false;
1538
+ if (payload.reason === "expired") this.setState("expired");
1539
+ else this.setState("closed");
1540
+ this.rejectPending(/* @__PURE__ */ new Error(`room closed: ${typeof payload.reason === "string" ? payload.reason : "unknown"}`));
1541
+ this.dispose();
1542
+ return;
1543
+ }
1544
+ if (event.type !== "realtime.room.event") return;
1545
+ const { id, timestamp, payload: roomPayload } = event;
1546
+ this.emit({
1547
+ id,
1548
+ timestamp,
1549
+ roomId: roomPayload.roomId,
1550
+ sequence: roomPayload.sequence,
1551
+ type: roomPayload.event,
1552
+ data: roomPayload.data,
1553
+ clientEventId: roomPayload.clientEventId,
1554
+ sender: roomPayload.sender,
1555
+ self: roomPayload.sender.participantId === this.participantId
1556
+ });
1557
+ }
1558
+ async catchUp(first, after, sequence) {
1559
+ if (!first) return {
1560
+ events: [],
1561
+ lost: [after + 1, sequence]
1562
+ };
1563
+ const events = this.toHistory(first).events;
1564
+ const { truncated } = first;
1565
+ let { cursor } = first;
1566
+ while (cursor < sequence) {
1567
+ const requestId = randomId();
1568
+ const page = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
1569
+ roomId: this.id,
1570
+ after: cursor,
1571
+ requestId
1572
+ })).catch(() => null))?.payload;
1573
+ if (!page || page.cursor <= cursor) break;
1574
+ events.push(...this.toHistory(page).events);
1575
+ cursor = page.cursor;
1576
+ }
1577
+ const kept = events.filter((event) => event.sequence <= sequence);
1578
+ const lost = truncated ? [after + 1, kept[0]?.sequence ?? sequence] : cursor < sequence ? [cursor + 1, sequence] : null;
1579
+ return {
1580
+ events: kept,
1581
+ lost: lost && lost[1] > lost[0] ? lost : null
1582
+ };
1583
+ }
1584
+ emit(item) {
1585
+ this.handlers.get(item.type)?.forEach((handler) => {
1586
+ handler(item);
1587
+ });
1588
+ this.allHandlers.forEach((handler) => {
1589
+ handler(item);
1590
+ });
1591
+ }
1592
+ toHistory(history) {
1593
+ return {
1594
+ events: history.events.map((event) => ({
1595
+ ...event,
1596
+ data: event.data,
1597
+ roomId: this.id,
1598
+ self: event.sender.participantId === this.participantId
1599
+ })),
1600
+ cursor: history.cursor,
1601
+ truncated: history.truncated
1602
+ };
1603
+ }
1604
+ observeSequence(sequence) {
1605
+ if (this.lastSequence > 0 && sequence > this.lastSequence + 1) for (const handler of this.outOfSyncHandlers) handler(this.lastSequence + 1, sequence);
1606
+ if (sequence > this.lastSequence) this.lastSequence = sequence;
1607
+ }
1608
+ assertJoined() {
1609
+ if (this._state !== "joined" || !this.hasJoined) throw new Error("room is not joined");
1610
+ }
1611
+ setState(state) {
1612
+ if (this._state === state) return;
1613
+ this._state = state;
1614
+ for (const handler of this.stateHandlers) handler(state);
1615
+ }
1616
+ notifyMembers() {
1617
+ const members = this.members;
1618
+ for (const handler of this.membersHandlers) handler(members);
1619
+ }
1620
+ rejectPending(error) {
1621
+ for (const [requestId, pending] of this.pending) {
1622
+ this.pending.delete(requestId);
1623
+ clearTimeout(pending.timer);
1624
+ pending.reject(error);
1625
+ }
1626
+ }
1627
+ expire() {
1628
+ if (this.explicitLeave || this._state === "closed" || this._state === "expired") return;
1629
+ this.hasJoined = false;
1630
+ this.setState("expired");
1631
+ this.rejectPending(/* @__PURE__ */ new Error("room expired"));
1632
+ this.dispose();
1633
+ }
1634
+ dispose() {
1635
+ const rtc = this.rtcConnection;
1636
+ this.rtcConnection = null;
1637
+ rtc?.then((connection) => connection.close()).catch(() => void 0);
1638
+ this.offEvent();
1639
+ this.offOpen();
1640
+ this.offClose();
1641
+ this.offReconnecting();
1642
+ if (this.expiryTimer) clearTimeout(this.expiryTimer);
1643
+ this.expiryTimer = null;
1644
+ }
1645
+ };
1646
+ var AppRealtimeApi = class {
1647
+ transport;
1648
+ websocket;
1649
+ getContext;
1650
+ constructor(transport, websocket, getContext) {
1651
+ this.transport = transport;
1652
+ this.websocket = websocket;
1653
+ this.getContext = getContext;
1654
+ }
1655
+ async createRoom(input = {}) {
1656
+ const appId = await this.requireAppId();
1657
+ const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms`, {
1658
+ method: "POST",
1659
+ headers: { "Content-Type": "application/json" },
1660
+ body: JSON.stringify(input)
1661
+ });
1662
+ return this.openRoom(appId, admission);
1663
+ }
1664
+ async joinRoom(input) {
1665
+ const appId = await this.requireAppId();
1666
+ const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms/join`, {
1667
+ method: "POST",
1668
+ headers: { "Content-Type": "application/json" },
1669
+ body: JSON.stringify(input)
1670
+ });
1671
+ return this.openRoom(appId, admission);
1672
+ }
1673
+ async requireAppId() {
1674
+ const context = await this.getContext();
1675
+ if (!context?.app?.id) throw new Error("App context is unavailable — not running inside a published app runtime.");
1676
+ return context.app.id;
1677
+ }
1678
+ async openRoom(appId, admission) {
1679
+ const room = new AppRoom(this.websocket, admission, {
1680
+ transport: this.transport,
1681
+ appId
1682
+ });
1683
+ try {
1684
+ await room.connect();
1685
+ return room;
1686
+ } catch (error) {
1687
+ await room.leave().catch(() => void 0);
1688
+ throw error;
1689
+ }
1690
+ }
1691
+ };
1692
+ /** @deprecated Use `AppRealtimeApi`. */
1693
+ var WorkRealtimeApi = class extends AppRealtimeApi {};
1694
+ /** @deprecated Use `AppRoom`. */
1695
+ var WorkRoom = class extends AppRoom {};
1696
+ //#endregion
1697
+ export { app_realtime_exports as a, AppRtcError as c, WorkRoom as i, AppRoom as n, AppRoomRtc as o, WorkRealtimeApi as r, AppRtcChannel as s, AppRealtimeApi as t };