@tonytomo/gosocket-client 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3060 @@
1
+ // src/clock.ts
2
+ var RealClock = class {
3
+ now() {
4
+ return Date.now();
5
+ }
6
+ setTimeout(fn, ms) {
7
+ const id = globalThis.setTimeout(fn, ms);
8
+ let stopped = false;
9
+ return {
10
+ stop: () => {
11
+ if (!stopped) {
12
+ stopped = true;
13
+ globalThis.clearTimeout(id);
14
+ }
15
+ }
16
+ };
17
+ }
18
+ clearTimeout(t) {
19
+ t.stop();
20
+ }
21
+ };
22
+
23
+ // src/events.ts
24
+ var SyncEventEmitter = class {
25
+ listeners = /* @__PURE__ */ new Map();
26
+ on(event, handler) {
27
+ let set = this.listeners.get(event);
28
+ if (!set) {
29
+ set = /* @__PURE__ */ new Set();
30
+ this.listeners.set(event, set);
31
+ }
32
+ set.add(handler);
33
+ return () => {
34
+ set?.delete(handler);
35
+ };
36
+ }
37
+ emit(event, payload) {
38
+ const set = this.listeners.get(event);
39
+ if (set) {
40
+ for (const handler of Array.from(set)) {
41
+ try {
42
+ handler(payload);
43
+ } catch {
44
+ }
45
+ }
46
+ }
47
+ }
48
+ removeAllListeners() {
49
+ this.listeners.clear();
50
+ }
51
+ };
52
+
53
+ // src/heartbeat.ts
54
+ var HeartbeatManager = class {
55
+ constructor(config, clock, actions) {
56
+ this.clock = clock;
57
+ this.actions = actions;
58
+ this.enabled = config?.enabled ?? true;
59
+ this.intervalMs = config?.intervalMs && config.intervalMs > 0 ? config.intervalMs : 25e3;
60
+ this.timeoutMs = config?.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : 1e4;
61
+ }
62
+ clock;
63
+ actions;
64
+ enabled;
65
+ intervalMs;
66
+ timeoutMs;
67
+ intervalTimer = null;
68
+ pongTimer = null;
69
+ pingSentAt = 0;
70
+ isRunning = false;
71
+ start() {
72
+ if (!this.enabled) return;
73
+ this.stop();
74
+ this.isRunning = true;
75
+ this.scheduleNextPing();
76
+ }
77
+ stop() {
78
+ this.isRunning = false;
79
+ if (this.intervalTimer) {
80
+ this.clock.clearTimeout(this.intervalTimer);
81
+ this.intervalTimer = null;
82
+ }
83
+ if (this.pongTimer) {
84
+ this.clock.clearTimeout(this.pongTimer);
85
+ this.pongTimer = null;
86
+ }
87
+ this.pingSentAt = 0;
88
+ }
89
+ handlePong() {
90
+ if (!this.isRunning) return;
91
+ if (this.pongTimer) {
92
+ this.clock.clearTimeout(this.pongTimer);
93
+ this.pongTimer = null;
94
+ const sample = Math.max(0, this.clock.now() - this.pingSentAt);
95
+ this.pingSentAt = 0;
96
+ this.actions.onPong?.(sample);
97
+ this.scheduleNextPing();
98
+ }
99
+ }
100
+ scheduleNextPing() {
101
+ if (!this.isRunning) return;
102
+ this.intervalTimer = this.clock.setTimeout(() => {
103
+ this.intervalTimer = null;
104
+ this.tick();
105
+ }, this.intervalMs);
106
+ }
107
+ tick() {
108
+ if (!this.isRunning) return;
109
+ this.pingSentAt = this.clock.now();
110
+ try {
111
+ this.actions.sendPing();
112
+ } catch {
113
+ return;
114
+ }
115
+ this.pongTimer = this.clock.setTimeout(() => {
116
+ this.pongTimer = null;
117
+ if (!this.isRunning) return;
118
+ const elapsed = this.clock.now() - this.pingSentAt;
119
+ this.stop();
120
+ this.actions.onTimeout(elapsed > 0 ? elapsed : this.timeoutMs);
121
+ }, this.timeoutMs);
122
+ }
123
+ };
124
+
125
+ // src/logger.ts
126
+ function redactUrl(rawUrl) {
127
+ if (!rawUrl) return rawUrl;
128
+ try {
129
+ const u = new URL(rawUrl);
130
+ if (u.searchParams.has("key")) {
131
+ u.searchParams.set("key", "***");
132
+ }
133
+ return u.toString();
134
+ } catch {
135
+ return rawUrl.replace(/([?&]key=)[^& \s'"]*/g, "$1***");
136
+ }
137
+ }
138
+ function redactString(str, secrets) {
139
+ if (!str) return str;
140
+ let result = redactUrl(str);
141
+ for (const secret of secrets) {
142
+ if (secret && secret.length > 0) {
143
+ result = result.split(secret).join("***");
144
+ }
145
+ }
146
+ return result;
147
+ }
148
+ function redactValue(value, secrets) {
149
+ if (value === null || value === void 0) {
150
+ return value;
151
+ }
152
+ if (typeof value === "string") {
153
+ return redactString(value, secrets);
154
+ }
155
+ if (Array.isArray(value)) {
156
+ return value.map((item) => redactValue(item, secrets));
157
+ }
158
+ if (typeof value === "object") {
159
+ const out = {};
160
+ for (const [k, v] of Object.entries(value)) {
161
+ out[k] = redactValue(v, secrets);
162
+ }
163
+ return out;
164
+ }
165
+ return value;
166
+ }
167
+ var RedactingLogger = class {
168
+ constructor(logger, getSecrets) {
169
+ this.logger = logger;
170
+ this.getSecrets = getSecrets;
171
+ }
172
+ logger;
173
+ getSecrets;
174
+ log(level, msg, fields) {
175
+ if (!this.logger) return;
176
+ const secrets = this.getSecrets().filter((s) => Boolean(s && s.length > 0));
177
+ const redactedMsg = redactString(msg, secrets);
178
+ const redactedFields = fields ? redactValue(fields, secrets) : void 0;
179
+ this.logger(level, redactedMsg, redactedFields);
180
+ }
181
+ debug(msg, fields) {
182
+ this.log("debug", msg, fields);
183
+ }
184
+ info(msg, fields) {
185
+ this.log("info", msg, fields);
186
+ }
187
+ warn(msg, fields) {
188
+ this.log("warn", msg, fields);
189
+ }
190
+ error(msg, fields) {
191
+ this.log("error", msg, fields);
192
+ }
193
+ };
194
+
195
+ // src/reconnect.ts
196
+ function computeReconnectDelay(attempt, config = {}) {
197
+ const base = config.baseDelayMs && config.baseDelayMs > 0 ? config.baseDelayMs : 500;
198
+ const max = config.maxDelayMs && config.maxDelayMs > 0 ? config.maxDelayMs : 3e4;
199
+ const jitter = config.jitter ?? "full";
200
+ const rand = config.random ?? Math.random;
201
+ const shift = Math.min(Math.max(0, attempt), 30);
202
+ const backoff = Math.min(max, base * Math.pow(2, shift));
203
+ if (jitter === "none") {
204
+ return backoff;
205
+ }
206
+ if (jitter === "equal") {
207
+ const half = backoff / 2;
208
+ return half + rand() * half;
209
+ }
210
+ return rand() * backoff;
211
+ }
212
+
213
+ // src/types/error-base.ts
214
+ function redactSecrets(text) {
215
+ if (!text) return text;
216
+ return text.replace(/([?&]key=)[^& \s'"]*/g, "$1***");
217
+ }
218
+ var SdkError = class extends Error {
219
+ constructor(message, options) {
220
+ super(redactSecrets(message), options);
221
+ this.name = "SdkError";
222
+ }
223
+ };
224
+
225
+ // src/validate.ts
226
+ var ROOM_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
227
+ var CHANNEL_NAME_RE = /^[a-zA-Z0-9_-]{1,32}$/;
228
+ var ValidationError = class extends SdkError {
229
+ field;
230
+ rule;
231
+ value;
232
+ limit;
233
+ constructor(field, rule, value, limit) {
234
+ let msg;
235
+ if (rule === "too_large") {
236
+ msg = `Validation failed on ${field}: ${rule} (limit ${limit} bytes, got ${value})`;
237
+ } else {
238
+ msg = `Validation failed on ${field}: ${rule} ("${value}")`;
239
+ }
240
+ super(msg);
241
+ this.name = "ValidationError";
242
+ this.field = field;
243
+ this.rule = rule;
244
+ this.value = value;
245
+ this.limit = limit;
246
+ }
247
+ };
248
+ var BinaryPayloadTooLargeError = class extends ValidationError {
249
+ byteLength;
250
+ maxBytes;
251
+ constructor(byteLength, maxBytes) {
252
+ super("binary_payload", "too_large", `${byteLength} bytes`, maxBytes);
253
+ this.name = "BinaryPayloadTooLargeError";
254
+ this.byteLength = byteLength;
255
+ this.maxBytes = maxBytes;
256
+ }
257
+ };
258
+ function truncateString(s, maxLen) {
259
+ if (s.length <= maxLen) {
260
+ return s;
261
+ }
262
+ return s.slice(0, maxLen) + "...";
263
+ }
264
+ function validateRoomName(room, config) {
265
+ if (config?.enabled === false) {
266
+ return;
267
+ }
268
+ if (!ROOM_NAME_RE.test(room)) {
269
+ throw new ValidationError("room", "name_syntax", truncateString(room, 64));
270
+ }
271
+ }
272
+ function validateChannelName(channel, config) {
273
+ if (config?.enabled === false) {
274
+ return;
275
+ }
276
+ if (channel === "presence") {
277
+ throw new ValidationError("channel", "reserved_channel", channel);
278
+ }
279
+ if (!CHANNEL_NAME_RE.test(channel)) {
280
+ throw new ValidationError("channel", "name_syntax", truncateString(channel, 32));
281
+ }
282
+ }
283
+ function validateTextPayloadSize(marshaledLength, config) {
284
+ if (config?.enabled === false) {
285
+ return;
286
+ }
287
+ const max = config?.maxTextPayloadBytes ?? 65536 - 512;
288
+ if (marshaledLength > max) {
289
+ throw new ValidationError("payload", "too_large", `${marshaledLength} bytes`, max);
290
+ }
291
+ }
292
+ function validateBinaryPayloadSize(byteLength, config) {
293
+ if (config?.enabled === false) {
294
+ return;
295
+ }
296
+ const max = config?.maxBinaryPayloadBytes ?? 65535;
297
+ if (byteLength > max) {
298
+ throw new BinaryPayloadTooLargeError(byteLength, max);
299
+ }
300
+ }
301
+
302
+ // src/policy.ts
303
+ function presetsFor(kind, overrides) {
304
+ const presets = {};
305
+ presets["presence"] = {
306
+ reliable: true,
307
+ maxRate: 0,
308
+ coalesce: false
309
+ };
310
+ switch (kind) {
311
+ case "chat":
312
+ presets["chat"] = {
313
+ reliable: true,
314
+ maxRate: 5,
315
+ coalesce: false
316
+ };
317
+ break;
318
+ case "game":
319
+ presets["state"] = {
320
+ reliable: false,
321
+ maxRate: 60,
322
+ coalesce: false
323
+ };
324
+ presets["chat"] = {
325
+ reliable: true,
326
+ maxRate: 5,
327
+ coalesce: false
328
+ };
329
+ presets["actions"] = {
330
+ reliable: true,
331
+ maxRate: 20,
332
+ coalesce: false
333
+ };
334
+ break;
335
+ case "custom":
336
+ default:
337
+ presets["default"] = {
338
+ reliable: true,
339
+ maxRate: 0,
340
+ coalesce: false
341
+ };
342
+ break;
343
+ }
344
+ if (overrides) {
345
+ for (const [ch, override] of Object.entries(overrides)) {
346
+ presets[ch] = {
347
+ ...presets[ch] ?? { reliable: true, maxRate: 0, coalesce: false },
348
+ ...override
349
+ };
350
+ }
351
+ }
352
+ return presets;
353
+ }
354
+ function resolvePolicy(room, channel, provider, overrides) {
355
+ const overrideKey = `${room}:${channel}`;
356
+ if (overrides[overrideKey]) {
357
+ return overrides[overrideKey];
358
+ }
359
+ if (provider) {
360
+ const policy = provider.channelPolicy(room, channel);
361
+ if (policy) {
362
+ return policy;
363
+ }
364
+ }
365
+ return {
366
+ reliable: true,
367
+ maxRate: 0,
368
+ coalesce: false
369
+ };
370
+ }
371
+
372
+ // src/preflight.ts
373
+ var CapabilityLimitError = class extends SdkError {
374
+ limit;
375
+ requested;
376
+ allowed;
377
+ constructor(limit, requested, allowed) {
378
+ super(`Capability limit exceeded for "${limit}": requested ${requested}, allowed ${allowed}`);
379
+ this.name = "CapabilityLimitError";
380
+ this.limit = limit;
381
+ this.requested = requested;
382
+ this.allowed = allowed;
383
+ }
384
+ };
385
+ function checkCapabilityPreflight(capabilities, req, ownedRoomsCount) {
386
+ if (!capabilities || !capabilities.limits) {
387
+ return;
388
+ }
389
+ const limits = capabilities.limits;
390
+ if (req.metadata !== void 0 && req.metadata !== null && limits.max_metadata_bytes !== null && limits.max_metadata_bytes !== void 0) {
391
+ const metaBytes = new TextEncoder().encode(JSON.stringify(req.metadata)).length;
392
+ if (metaBytes > limits.max_metadata_bytes) {
393
+ throw new CapabilityLimitError(
394
+ "max_metadata_bytes",
395
+ metaBytes,
396
+ limits.max_metadata_bytes
397
+ );
398
+ }
399
+ }
400
+ if (limits.max_channels_per_room !== null && limits.max_channels_per_room !== void 0) {
401
+ const effective = /* @__PURE__ */ new Set(["presence"]);
402
+ const preset = presetsFor(req.kind ?? "custom");
403
+ for (const ch of Object.keys(preset)) {
404
+ effective.add(ch);
405
+ }
406
+ if (req.channels) {
407
+ for (const ch of Object.keys(req.channels)) {
408
+ effective.add(ch);
409
+ }
410
+ }
411
+ if (effective.size > limits.max_channels_per_room) {
412
+ throw new CapabilityLimitError(
413
+ "max_channels_per_room",
414
+ effective.size,
415
+ limits.max_channels_per_room
416
+ );
417
+ }
418
+ }
419
+ if (limits.max_rooms_per_key !== null && limits.max_rooms_per_key !== void 0) {
420
+ if (ownedRoomsCount >= limits.max_rooms_per_key) {
421
+ throw new CapabilityLimitError(
422
+ "max_rooms_per_key",
423
+ ownedRoomsCount + 1,
424
+ limits.max_rooms_per_key
425
+ );
426
+ }
427
+ }
428
+ }
429
+
430
+ // src/types/errors.ts
431
+ var AuthError = class extends SdkError {
432
+ code = "AUTH_FAILED";
433
+ constructor(message = "Authentication failed", options) {
434
+ super(message, options);
435
+ this.name = "AuthError";
436
+ }
437
+ };
438
+ var ConnectError = class extends SdkError {
439
+ cause;
440
+ constructor(message, cause) {
441
+ super(message);
442
+ this.name = "ConnectError";
443
+ if (cause && typeof cause === "object") {
444
+ const sanitizedCause = {};
445
+ for (const [k, v] of Object.entries(cause)) {
446
+ if (typeof v === "string") {
447
+ sanitizedCause[k] = redactSecrets(v);
448
+ } else {
449
+ sanitizedCause[k] = v;
450
+ }
451
+ }
452
+ this.cause = sanitizedCause;
453
+ } else {
454
+ this.cause = cause;
455
+ }
456
+ }
457
+ get closeCode() {
458
+ if (this.cause && typeof this.cause === "object" && "closeCode" in this.cause) {
459
+ return this.cause.closeCode;
460
+ }
461
+ return void 0;
462
+ }
463
+ get reason() {
464
+ if (this.cause && typeof this.cause === "object" && "reason" in this.cause) {
465
+ return this.cause.reason;
466
+ }
467
+ return void 0;
468
+ }
469
+ };
470
+ var PolicyViolationError = class extends SdkError {
471
+ code = "POLICY_VIOLATION";
472
+ closeCode;
473
+ reason;
474
+ constructor(message = "Policy violation", closeCode = 1008, reason) {
475
+ super(message);
476
+ this.name = "PolicyViolationError";
477
+ this.closeCode = closeCode;
478
+ this.reason = reason;
479
+ }
480
+ };
481
+ var RequestTimeoutError = class extends SdkError {
482
+ constructor(message = "Request timed out") {
483
+ super(message);
484
+ this.name = "RequestTimeoutError";
485
+ }
486
+ };
487
+ var ConcurrentRequestError = class extends SdkError {
488
+ constructor(message = "Concurrent request for the same operation and room is not permitted") {
489
+ super(message);
490
+ this.name = "ConcurrentRequestError";
491
+ }
492
+ };
493
+ var ServerError = class extends SdkError {
494
+ code;
495
+ channel;
496
+ retryAfterMs;
497
+ constructor(code, message, channel, retryAfterMs) {
498
+ super(message);
499
+ this.name = "ServerError";
500
+ this.code = code;
501
+ this.channel = channel;
502
+ this.retryAfterMs = retryAfterMs;
503
+ }
504
+ };
505
+ var NotReadyError = class extends SdkError {
506
+ constructor(message = "Client is not ready for outbound operations") {
507
+ super(message);
508
+ this.name = "NotReadyError";
509
+ }
510
+ };
511
+ var ChannelNotReliableError = class extends SdkError {
512
+ channel;
513
+ constructor(channel, message = `Channel "${channel}" does not support reliable delivery`) {
514
+ super(message);
515
+ this.name = "ChannelNotReliableError";
516
+ this.channel = channel;
517
+ }
518
+ };
519
+ var RateLimitedError = class extends SdkError {
520
+ retryAfterMs;
521
+ channel;
522
+ source;
523
+ constructor(retryAfterMs, channel, source, message = `Rate limit exceeded on channel "${channel}"; retry after ${retryAfterMs}ms (${source})`) {
524
+ super(message);
525
+ this.name = "RateLimitedError";
526
+ this.retryAfterMs = retryAfterMs;
527
+ this.channel = channel;
528
+ this.source = source;
529
+ }
530
+ };
531
+ var BinaryMapValidationError = class extends SdkError {
532
+ channel;
533
+ id;
534
+ reason;
535
+ constructor(channel, id, reason) {
536
+ super(`Binary map validation failed for channel "${channel}" (id ${id}): ${reason}`);
537
+ this.name = "BinaryMapValidationError";
538
+ this.channel = channel;
539
+ this.id = id;
540
+ this.reason = reason;
541
+ }
542
+ };
543
+ var BinaryMapExhaustedError = class extends SdkError {
544
+ constructor(message = "All 254 binary channel IDs are in use") {
545
+ super(message);
546
+ this.name = "BinaryMapExhaustedError";
547
+ }
548
+ };
549
+ var BinaryIdCollisionError = class extends SdkError {
550
+ id;
551
+ existing;
552
+ rejected;
553
+ constructor(id, existing, rejected) {
554
+ super(`Binary channel ID ${id} collides with existing assignment (${existing.join(", ")})`);
555
+ this.name = "BinaryIdCollisionError";
556
+ this.id = id;
557
+ this.existing = existing;
558
+ this.rejected = rejected;
559
+ }
560
+ };
561
+ var BinaryChannelUnmappedError = class extends SdkError {
562
+ room;
563
+ channel;
564
+ constructor(room, channel) {
565
+ super(`Channel "${channel}" in room "${room}" is not mapped to a binary channel ID`);
566
+ this.name = "BinaryChannelUnmappedError";
567
+ this.room = room;
568
+ this.channel = channel;
569
+ }
570
+ };
571
+
572
+ // src/transport.ts
573
+ var BrowserWebSocketTransport = class {
574
+ ws = null;
575
+ onopen = null;
576
+ onmessage = () => {
577
+ };
578
+ onclose = () => {
579
+ };
580
+ onerror = () => {
581
+ };
582
+ bufferedAmount() {
583
+ return this.ws ? this.ws.bufferedAmount : 0;
584
+ }
585
+ get readyState() {
586
+ return this.ws ? this.ws.readyState : 3;
587
+ }
588
+ connect(url) {
589
+ return new Promise((resolve, reject) => {
590
+ let settled = false;
591
+ const redacted = redactUrl(url);
592
+ try {
593
+ this.ws = new WebSocket(url);
594
+ this.ws.binaryType = "arraybuffer";
595
+ } catch (err) {
596
+ settled = true;
597
+ reject(
598
+ new ConnectError(`Failed to construct WebSocket for ${redacted}`, err)
599
+ );
600
+ return;
601
+ }
602
+ const handleOpen = () => {
603
+ if (!settled) {
604
+ settled = true;
605
+ this.cleanupPreOpen();
606
+ this.onopen?.();
607
+ resolve();
608
+ }
609
+ };
610
+ const handleErrorPreOpen = (ev) => {
611
+ this.onerror(ev);
612
+ };
613
+ const handleClosePreOpen = (ev) => {
614
+ if (!settled) {
615
+ settled = true;
616
+ this.cleanupPreOpen();
617
+ const cause = {
618
+ closeCode: ev.code,
619
+ reason: ev.reason,
620
+ wasClean: ev.wasClean
621
+ };
622
+ const err = new ConnectError(
623
+ `Connection closed before open for ${redacted} (code: ${ev.code}, reason: ${ev.reason || "none"})`,
624
+ cause
625
+ );
626
+ this.onclose({ code: ev.code, reason: ev.reason, wasClean: ev.wasClean });
627
+ reject(err);
628
+ }
629
+ };
630
+ this.cleanupPreOpen = () => {
631
+ this.ws?.removeEventListener("open", handleOpen);
632
+ this.ws?.removeEventListener("error", handleErrorPreOpen);
633
+ this.ws?.removeEventListener("close", handleClosePreOpen);
634
+ };
635
+ this.ws.addEventListener("open", handleOpen);
636
+ this.ws.addEventListener("error", handleErrorPreOpen);
637
+ this.ws.addEventListener("close", handleClosePreOpen);
638
+ this.ws.onmessage = (ev) => {
639
+ this.onmessage({ data: ev.data });
640
+ };
641
+ this.ws.onclose = (ev) => {
642
+ if (settled) {
643
+ this.onclose({ code: ev.code, reason: ev.reason, wasClean: ev.wasClean });
644
+ }
645
+ };
646
+ this.ws.onerror = (ev) => {
647
+ if (settled) {
648
+ this.onerror(ev);
649
+ }
650
+ };
651
+ });
652
+ }
653
+ cleanupPreOpen = () => {
654
+ };
655
+ send(data) {
656
+ if (!this.ws || this.ws.readyState !== 1) {
657
+ throw new ConnectError("Cannot send on closed WebSocket");
658
+ }
659
+ this.ws.send(data);
660
+ }
661
+ close(code, reason) {
662
+ if (this.ws) {
663
+ try {
664
+ this.ws.close(code, reason);
665
+ } catch {
666
+ }
667
+ }
668
+ }
669
+ };
670
+
671
+ // src/types/protocol.ts
672
+ function isErrorFrame(x) {
673
+ return typeof x === "object" && x !== null && x.type === "error";
674
+ }
675
+ function isMessageFrame(x) {
676
+ return typeof x === "object" && x !== null && x.type === "message";
677
+ }
678
+ function isWelcomeFrame(x) {
679
+ return typeof x === "object" && x !== null && x.type === "welcome";
680
+ }
681
+
682
+ // src/backpressure.ts
683
+ var BackpressureManager = class {
684
+ enabled;
685
+ highWaterMarkBytes;
686
+ lowWaterMarkBytes;
687
+ pollIntervalMs;
688
+ blocked = false;
689
+ pollTimer = null;
690
+ transport;
691
+ clock;
692
+ onStateChange;
693
+ onWakeWriter;
694
+ constructor(transport, clock, config, onStateChange, onWakeWriter) {
695
+ this.transport = transport;
696
+ this.clock = clock;
697
+ this.enabled = config?.enabled ?? true;
698
+ this.highWaterMarkBytes = config?.highWaterMarkBytes ?? 1 << 20;
699
+ this.lowWaterMarkBytes = config?.lowWaterMarkBytes ?? 1 << 19;
700
+ this.pollIntervalMs = config?.pollIntervalMs ?? 50;
701
+ this.onStateChange = onStateChange;
702
+ this.onWakeWriter = onWakeWriter;
703
+ }
704
+ start() {
705
+ if (!this.enabled || this.highWaterMarkBytes <= 0) {
706
+ return;
707
+ }
708
+ this.stop();
709
+ this.scheduleNextPoll();
710
+ }
711
+ scheduleNextPoll() {
712
+ this.pollTimer = this.clock.setTimeout(() => {
713
+ this.check();
714
+ if (this.pollTimer) {
715
+ this.scheduleNextPoll();
716
+ }
717
+ }, this.pollIntervalMs);
718
+ }
719
+ stop() {
720
+ if (this.pollTimer) {
721
+ this.clock.clearTimeout(this.pollTimer);
722
+ this.pollTimer = null;
723
+ }
724
+ this.blocked = false;
725
+ }
726
+ isBlocked() {
727
+ if (!this.enabled || this.highWaterMarkBytes <= 0) {
728
+ return false;
729
+ }
730
+ return this.blocked;
731
+ }
732
+ check() {
733
+ if (!this.enabled || this.highWaterMarkBytes <= 0) {
734
+ return false;
735
+ }
736
+ const current = this.transport.bufferedAmount();
737
+ if (!this.blocked && current >= this.highWaterMarkBytes) {
738
+ this.blocked = true;
739
+ this.onStateChange({
740
+ state: "high",
741
+ bufferedAmount: current
742
+ });
743
+ } else if (this.blocked && current <= this.lowWaterMarkBytes) {
744
+ this.blocked = false;
745
+ this.onStateChange({
746
+ state: "low",
747
+ bufferedAmount: current
748
+ });
749
+ this.onWakeWriter();
750
+ }
751
+ return this.blocked;
752
+ }
753
+ };
754
+
755
+ // src/coalesce.ts
756
+ var CoalesceManager = class {
757
+ slots = /* @__PURE__ */ new Map();
758
+ slotKeys = [];
759
+ rrIndex = 0;
760
+ clock;
761
+ onWake;
762
+ onCoalesced;
763
+ constructor(clock, onWake, onCoalesced) {
764
+ this.clock = clock;
765
+ this.onWake = onWake;
766
+ this.onCoalesced = onCoalesced;
767
+ }
768
+ makeKey(room, channel, frameType) {
769
+ return `${room}:${channel}:${frameType}`;
770
+ }
771
+ update(room, channel, frameType, payload, wire, policy, includeSender) {
772
+ const key = this.makeKey(room, channel, frameType);
773
+ let slot = this.slots.get(key);
774
+ if (!slot) {
775
+ slot = {
776
+ room,
777
+ channel,
778
+ frameType,
779
+ latestWire: wire,
780
+ latestPayload: payload,
781
+ includeSender,
782
+ hasNew: true,
783
+ isReady: false
784
+ };
785
+ this.slots.set(key, slot);
786
+ this.slotKeys.push(key);
787
+ this.scheduleSlotFlush(slot, policy);
788
+ return;
789
+ }
790
+ if (slot.hasNew) {
791
+ if (policy.merge && frameType === "text") {
792
+ const result = policy.merge(slot.latestPayload, payload);
793
+ if (result.skip) {
794
+ this.onCoalesced({
795
+ room,
796
+ channel,
797
+ merged: false,
798
+ skipped: true
799
+ });
800
+ return;
801
+ }
802
+ slot.latestPayload = result.value;
803
+ const frameObj = {
804
+ type: "publish",
805
+ room,
806
+ channel,
807
+ payload: result.value,
808
+ reliable: false
809
+ };
810
+ if (includeSender !== void 0) {
811
+ frameObj.include_sender = includeSender;
812
+ }
813
+ slot.latestWire = JSON.stringify(frameObj);
814
+ this.onCoalesced({
815
+ room,
816
+ channel,
817
+ merged: true,
818
+ skipped: false
819
+ });
820
+ } else {
821
+ slot.latestPayload = payload;
822
+ slot.latestWire = wire;
823
+ slot.includeSender = includeSender;
824
+ this.onCoalesced({
825
+ room,
826
+ channel,
827
+ merged: false,
828
+ skipped: false
829
+ });
830
+ }
831
+ } else {
832
+ slot.latestPayload = payload;
833
+ slot.latestWire = wire;
834
+ slot.includeSender = includeSender;
835
+ slot.hasNew = true;
836
+ slot.isReady = false;
837
+ this.scheduleSlotFlush(slot, policy);
838
+ }
839
+ }
840
+ scheduleSlotFlush(slot, policy) {
841
+ const driver = policy.coalesceDriver ?? "timer";
842
+ if (driver === "raf") {
843
+ if (typeof requestAnimationFrame === "function") {
844
+ if (slot.rafHandle) {
845
+ cancelAnimationFrame(slot.rafHandle);
846
+ }
847
+ slot.rafHandle = requestAnimationFrame(() => {
848
+ slot.rafHandle = null;
849
+ slot.isReady = true;
850
+ this.onWake();
851
+ });
852
+ return;
853
+ }
854
+ if (slot.timerHandle) {
855
+ this.clock.clearTimeout(slot.timerHandle);
856
+ }
857
+ slot.timerHandle = this.clock.setTimeout(() => {
858
+ slot.timerHandle = null;
859
+ slot.isReady = true;
860
+ this.onWake();
861
+ }, 16);
862
+ return;
863
+ }
864
+ queueMicrotask(() => {
865
+ if (slot.hasNew) {
866
+ slot.isReady = true;
867
+ this.onWake();
868
+ }
869
+ });
870
+ }
871
+ takeReady() {
872
+ const n = this.slotKeys.length;
873
+ if (n === 0) return void 0;
874
+ for (let i = 0; i < n; i++) {
875
+ const idx = (this.rrIndex + i) % n;
876
+ const key = this.slotKeys[idx];
877
+ const slot = this.slots.get(key);
878
+ if (slot && slot.hasNew && slot.isReady) {
879
+ const item = {
880
+ room: slot.room,
881
+ channel: slot.channel,
882
+ frameType: slot.frameType,
883
+ wire: slot.latestWire
884
+ };
885
+ slot.hasNew = false;
886
+ slot.isReady = false;
887
+ slot.latestPayload = void 0;
888
+ slot.latestWire = "";
889
+ this.rrIndex = (idx + 1) % n;
890
+ return item;
891
+ }
892
+ }
893
+ return void 0;
894
+ }
895
+ readyAllPending() {
896
+ for (const slot of this.slots.values()) {
897
+ if (slot.hasNew) {
898
+ slot.isReady = true;
899
+ }
900
+ }
901
+ }
902
+ flushChannel(room, channel, onFlush) {
903
+ for (const slot of this.slots.values()) {
904
+ if (slot.room === room && slot.channel === channel && slot.hasNew) {
905
+ const wire = slot.latestWire;
906
+ slot.hasNew = false;
907
+ slot.isReady = false;
908
+ slot.latestPayload = void 0;
909
+ slot.latestWire = "";
910
+ if (slot.timerHandle) {
911
+ this.clock.clearTimeout(slot.timerHandle);
912
+ slot.timerHandle = null;
913
+ }
914
+ if (slot.rafHandle && typeof cancelAnimationFrame === "function") {
915
+ cancelAnimationFrame(slot.rafHandle);
916
+ slot.rafHandle = null;
917
+ }
918
+ onFlush(wire);
919
+ }
920
+ }
921
+ }
922
+ clearRoom(room) {
923
+ const toDelete = [];
924
+ for (const [key, slot] of this.slots.entries()) {
925
+ if (slot.room === room) {
926
+ if (slot.timerHandle) {
927
+ this.clock.clearTimeout(slot.timerHandle);
928
+ slot.timerHandle = null;
929
+ }
930
+ if (slot.rafHandle && typeof cancelAnimationFrame === "function") {
931
+ cancelAnimationFrame(slot.rafHandle);
932
+ slot.rafHandle = null;
933
+ }
934
+ toDelete.push(key);
935
+ }
936
+ }
937
+ for (const key of toDelete) {
938
+ this.slots.delete(key);
939
+ const idx = this.slotKeys.indexOf(key);
940
+ if (idx !== -1) {
941
+ this.slotKeys.splice(idx, 1);
942
+ }
943
+ }
944
+ }
945
+ countPending() {
946
+ let count = 0;
947
+ for (const slot of this.slots.values()) {
948
+ if (slot.hasNew) {
949
+ count++;
950
+ }
951
+ }
952
+ return count;
953
+ }
954
+ clear() {
955
+ const count = this.countPending();
956
+ for (const slot of this.slots.values()) {
957
+ if (slot.timerHandle) {
958
+ this.clock.clearTimeout(slot.timerHandle);
959
+ slot.timerHandle = null;
960
+ }
961
+ if (slot.rafHandle && typeof cancelAnimationFrame === "function") {
962
+ cancelAnimationFrame(slot.rafHandle);
963
+ slot.rafHandle = null;
964
+ }
965
+ }
966
+ this.slots.clear();
967
+ this.slotKeys.length = 0;
968
+ this.rrIndex = 0;
969
+ return count;
970
+ }
971
+ };
972
+
973
+ // src/outbound.ts
974
+ var OutboundQueue = class {
975
+ items = [];
976
+ head = 0;
977
+ capacity;
978
+ constructor(capacity) {
979
+ this.capacity = capacity;
980
+ }
981
+ get size() {
982
+ return this.items.length - this.head;
983
+ }
984
+ enqueue(item) {
985
+ this.items.push(item);
986
+ }
987
+ dequeue() {
988
+ if (this.head >= this.items.length) {
989
+ return void 0;
990
+ }
991
+ const item = this.items[this.head];
992
+ this.items[this.head] = void 0;
993
+ this.head++;
994
+ if (this.head > 64 && this.head * 2 >= this.items.length) {
995
+ this.items = this.items.slice(this.head);
996
+ this.head = 0;
997
+ }
998
+ return item;
999
+ }
1000
+ dropOldest() {
1001
+ return this.dequeue();
1002
+ }
1003
+ peek() {
1004
+ if (this.head >= this.items.length) {
1005
+ return void 0;
1006
+ }
1007
+ return this.items[this.head];
1008
+ }
1009
+ clear() {
1010
+ const count = this.size;
1011
+ this.items = [];
1012
+ this.head = 0;
1013
+ return count;
1014
+ }
1015
+ };
1016
+
1017
+ // src/ratelimit.ts
1018
+ var TokenBucket = class {
1019
+ ratePerSec;
1020
+ burst;
1021
+ tokens;
1022
+ lastRefillMs;
1023
+ clock;
1024
+ constructor(ratePerSec, burst, clock) {
1025
+ this.ratePerSec = ratePerSec;
1026
+ this.burst = burst;
1027
+ this.clock = clock;
1028
+ this.tokens = burst;
1029
+ this.lastRefillMs = clock.now();
1030
+ }
1031
+ advanceTo(now) {
1032
+ if (now > this.lastRefillMs) {
1033
+ const elapsedSec = (now - this.lastRefillMs) / 1e3;
1034
+ this.tokens = Math.min(this.burst, this.tokens + elapsedSec * this.ratePerSec);
1035
+ this.lastRefillMs = now;
1036
+ }
1037
+ }
1038
+ tryConsume() {
1039
+ const now = this.clock.now();
1040
+ this.advanceTo(now);
1041
+ if (this.tokens >= 1) {
1042
+ this.tokens -= 1;
1043
+ return true;
1044
+ }
1045
+ return false;
1046
+ }
1047
+ retryAfterMs(now) {
1048
+ this.advanceTo(now);
1049
+ if (this.tokens >= 1) {
1050
+ return 0;
1051
+ }
1052
+ const needed = 1 - this.tokens;
1053
+ const ms = Math.ceil(needed / this.ratePerSec * 1e3);
1054
+ return Math.max(1, ms);
1055
+ }
1056
+ };
1057
+ var RateLimiterRegistry = class {
1058
+ limiters = /* @__PURE__ */ new Map();
1059
+ serverBackoffs = /* @__PURE__ */ new Map();
1060
+ recentRoomByChan = /* @__PURE__ */ new Map();
1061
+ clock;
1062
+ constructor(clock) {
1063
+ this.clock = clock;
1064
+ }
1065
+ makeKey(room, channel) {
1066
+ return `${room}:${channel}`;
1067
+ }
1068
+ get(room, channel, ratePerSec) {
1069
+ const key = this.makeKey(room, channel);
1070
+ let bucket = this.limiters.get(key);
1071
+ if (!bucket || bucket.ratePerSec !== ratePerSec) {
1072
+ const burst = Math.max(2, ratePerSec * 2);
1073
+ bucket = new TokenBucket(ratePerSec, burst, this.clock);
1074
+ this.limiters.set(key, bucket);
1075
+ }
1076
+ return bucket;
1077
+ }
1078
+ recordPublish(room, channel) {
1079
+ this.recentRoomByChan.set(channel, room);
1080
+ }
1081
+ getRecentRoom(channel) {
1082
+ return this.recentRoomByChan.get(channel);
1083
+ }
1084
+ pause(room, channel, untilMs) {
1085
+ const key = this.makeKey(room, channel);
1086
+ const existing = this.serverBackoffs.get(key) ?? 0;
1087
+ if (untilMs > existing) {
1088
+ this.serverBackoffs.set(key, untilMs);
1089
+ }
1090
+ }
1091
+ isPaused(room, channel, now) {
1092
+ const key = this.makeKey(room, channel);
1093
+ const until = this.serverBackoffs.get(key);
1094
+ if (until !== void 0 && until > now) {
1095
+ return {
1096
+ paused: true,
1097
+ retryAfterMs: Math.max(1, until - now)
1098
+ };
1099
+ }
1100
+ if (room !== "") {
1101
+ const fallbackKey = this.makeKey("", channel);
1102
+ const fallbackUntil = this.serverBackoffs.get(fallbackKey);
1103
+ if (fallbackUntil !== void 0 && fallbackUntil > now) {
1104
+ return {
1105
+ paused: true,
1106
+ retryAfterMs: Math.max(1, fallbackUntil - now)
1107
+ };
1108
+ }
1109
+ }
1110
+ return { paused: false, retryAfterMs: 0 };
1111
+ }
1112
+ clearRoom(room) {
1113
+ const prefix = `${room}:`;
1114
+ for (const key of this.limiters.keys()) {
1115
+ if (key.startsWith(prefix)) {
1116
+ this.limiters.delete(key);
1117
+ }
1118
+ }
1119
+ for (const key of this.serverBackoffs.keys()) {
1120
+ if (key.startsWith(prefix)) {
1121
+ this.serverBackoffs.delete(key);
1122
+ }
1123
+ }
1124
+ for (const [ch, r] of this.recentRoomByChan.entries()) {
1125
+ if (r === room) {
1126
+ this.recentRoomByChan.delete(ch);
1127
+ }
1128
+ }
1129
+ }
1130
+ clearAll() {
1131
+ this.limiters.clear();
1132
+ this.serverBackoffs.clear();
1133
+ this.recentRoomByChan.clear();
1134
+ }
1135
+ };
1136
+
1137
+ // src/writer.ts
1138
+ var WriterLoop = class {
1139
+ transport;
1140
+ reliableQueue;
1141
+ unreliableQueue;
1142
+ coalesceManager;
1143
+ backpressure;
1144
+ reliableBurst;
1145
+ unreliableBurst;
1146
+ canWrite;
1147
+ onError;
1148
+ pumping = false;
1149
+ scheduled = false;
1150
+ stopped = false;
1151
+ activePumpPromise = null;
1152
+ constructor(options) {
1153
+ this.transport = options.transport;
1154
+ this.reliableQueue = options.reliableQueue;
1155
+ this.unreliableQueue = options.unreliableQueue;
1156
+ this.coalesceManager = options.coalesceManager;
1157
+ this.backpressure = options.backpressure;
1158
+ this.reliableBurst = options.reliableBurst ?? 32;
1159
+ this.unreliableBurst = options.unreliableBurst ?? 8;
1160
+ this.canWrite = options.canWrite;
1161
+ this.onError = options.onError;
1162
+ }
1163
+ hasWork() {
1164
+ return this.reliableQueue.size > 0 || this.unreliableQueue.size > 0 || this.coalesceManager.countPending() > 0;
1165
+ }
1166
+ wake() {
1167
+ if (this.stopped || this.scheduled || this.pumping || !this.canWrite() || this.backpressure.isBlocked()) {
1168
+ return;
1169
+ }
1170
+ this.pump().catch((err) => {
1171
+ this.onError(err);
1172
+ });
1173
+ }
1174
+ async pump() {
1175
+ if (this.stopped || this.pumping || !this.canWrite() || this.backpressure.isBlocked()) {
1176
+ return;
1177
+ }
1178
+ this.pumping = true;
1179
+ let writeErrorOccurred = false;
1180
+ let resolveActive = () => {
1181
+ };
1182
+ this.activePumpPromise = new Promise((resolve) => {
1183
+ resolveActive = resolve;
1184
+ });
1185
+ try {
1186
+ for (let i = 0; i < this.reliableBurst && this.canWrite(); i++) {
1187
+ if (this.backpressure.isBlocked()) break;
1188
+ const frame = this.reliableQueue.dequeue();
1189
+ if (!frame) break;
1190
+ try {
1191
+ this.transport.send(frame.wire);
1192
+ } catch (err) {
1193
+ writeErrorOccurred = true;
1194
+ this.stop();
1195
+ this.onError(err);
1196
+ return;
1197
+ }
1198
+ this.backpressure.check();
1199
+ await Promise.resolve();
1200
+ }
1201
+ for (let i = 0; i < this.unreliableBurst && this.canWrite(); i++) {
1202
+ if (this.backpressure.isBlocked()) break;
1203
+ const slot = this.coalesceManager.takeReady();
1204
+ if (slot) {
1205
+ try {
1206
+ this.transport.send(slot.wire);
1207
+ } catch (err) {
1208
+ writeErrorOccurred = true;
1209
+ this.stop();
1210
+ this.onError(err);
1211
+ return;
1212
+ }
1213
+ this.backpressure.check();
1214
+ await Promise.resolve();
1215
+ } else {
1216
+ const frame = this.unreliableQueue.dequeue();
1217
+ if (!frame) break;
1218
+ try {
1219
+ this.transport.send(frame.wire);
1220
+ } catch (err) {
1221
+ writeErrorOccurred = true;
1222
+ this.stop();
1223
+ this.onError(err);
1224
+ return;
1225
+ }
1226
+ this.backpressure.check();
1227
+ await Promise.resolve();
1228
+ }
1229
+ }
1230
+ } finally {
1231
+ this.pumping = false;
1232
+ this.activePumpPromise = null;
1233
+ resolveActive();
1234
+ }
1235
+ if (!writeErrorOccurred && !this.stopped && this.hasWork() && !this.backpressure.isBlocked() && this.canWrite()) {
1236
+ queueMicrotask(() => {
1237
+ this.wake();
1238
+ });
1239
+ }
1240
+ }
1241
+ async flushNow() {
1242
+ this.coalesceManager.readyAllPending();
1243
+ while (!this.stopped && this.hasWork() && !this.backpressure.isBlocked() && this.canWrite()) {
1244
+ if (this.activePumpPromise) {
1245
+ await this.activePumpPromise;
1246
+ } else {
1247
+ await this.pump();
1248
+ }
1249
+ }
1250
+ }
1251
+ stop() {
1252
+ this.stopped = true;
1253
+ this.scheduled = false;
1254
+ this.pumping = false;
1255
+ }
1256
+ resume() {
1257
+ this.stopped = false;
1258
+ this.wake();
1259
+ }
1260
+ };
1261
+
1262
+ // src/binary.ts
1263
+ var BinaryMapBuilder = class {
1264
+ byChannel = /* @__PURE__ */ new Map();
1265
+ byId = /* @__PURE__ */ new Map();
1266
+ set(channel, id) {
1267
+ if (channel === "" || channel === "presence") {
1268
+ throw new BinaryMapValidationError(channel, id, "reserved_channel");
1269
+ }
1270
+ if (!Number.isInteger(id) || id < 1 || id > 254) {
1271
+ throw new BinaryMapValidationError(channel, id, "out_of_range");
1272
+ }
1273
+ const existingId = this.byChannel.get(channel);
1274
+ if (existingId !== void 0) {
1275
+ if (existingId === id) {
1276
+ return this;
1277
+ }
1278
+ throw new BinaryMapValidationError(channel, id, "duplicate_channel");
1279
+ }
1280
+ const existingChannel = this.byId.get(id);
1281
+ if (existingChannel !== void 0) {
1282
+ if (existingChannel === channel) {
1283
+ return this;
1284
+ }
1285
+ throw new BinaryMapValidationError(channel, id, "duplicate_id");
1286
+ }
1287
+ this.byChannel.set(channel, id);
1288
+ this.byId.set(id, channel);
1289
+ return this;
1290
+ }
1291
+ autoSet(channel) {
1292
+ if (channel === "" || channel === "presence") {
1293
+ throw new BinaryMapValidationError(channel, 0, "reserved_channel");
1294
+ }
1295
+ const existingId = this.byChannel.get(channel);
1296
+ if (existingId !== void 0) {
1297
+ throw new BinaryMapValidationError(channel, existingId, "duplicate_channel");
1298
+ }
1299
+ for (let id = 1; id <= 254; id++) {
1300
+ if (!this.byId.has(id)) {
1301
+ this.byChannel.set(channel, id);
1302
+ this.byId.set(id, channel);
1303
+ return this;
1304
+ }
1305
+ }
1306
+ throw new BinaryMapExhaustedError();
1307
+ }
1308
+ build() {
1309
+ return Object.freeze(Object.fromEntries(this.byChannel));
1310
+ }
1311
+ };
1312
+ function validateBinaryMap(map) {
1313
+ if (!map || Object.keys(map).length === 0) {
1314
+ return;
1315
+ }
1316
+ const builder = new BinaryMapBuilder();
1317
+ const sortedChannels = Object.keys(map).sort();
1318
+ for (const ch of sortedChannels) {
1319
+ builder.set(ch, map[ch]);
1320
+ }
1321
+ }
1322
+
1323
+ // src/presence.ts
1324
+ var PresenceManager = class {
1325
+ states = /* @__PURE__ */ new Map();
1326
+ autoInvalidateOnReliableGap;
1327
+ constructor(config) {
1328
+ this.autoInvalidateOnReliableGap = config?.autoInvalidateOnReliableGap ?? false;
1329
+ }
1330
+ get(room) {
1331
+ return this.states.get(room);
1332
+ }
1333
+ init(room, members) {
1334
+ const existing = this.states.get(room);
1335
+ if (!existing) {
1336
+ this.states.set(room, {
1337
+ known: /* @__PURE__ */ new Set(),
1338
+ serverCount: members,
1339
+ invalidated: false,
1340
+ invalidReason: ""
1341
+ });
1342
+ } else {
1343
+ existing.serverCount = members;
1344
+ }
1345
+ }
1346
+ remove(room) {
1347
+ this.states.delete(room);
1348
+ }
1349
+ clear() {
1350
+ this.states.clear();
1351
+ }
1352
+ invalidateAll() {
1353
+ const rooms = [];
1354
+ for (const [room, state] of this.states.entries()) {
1355
+ state.invalidated = true;
1356
+ state.invalidReason = "reconnected";
1357
+ rooms.push(room);
1358
+ }
1359
+ return rooms;
1360
+ }
1361
+ knownMembers(room, selfLabel) {
1362
+ const state = this.states.get(room);
1363
+ if (!state) return [];
1364
+ const list = Array.from(state.known).filter((m) => m !== selfLabel);
1365
+ list.sort();
1366
+ return list;
1367
+ }
1368
+ lastServerMemberCount(room) {
1369
+ const state = this.states.get(room);
1370
+ if (!state) return { count: 0, ok: false };
1371
+ return { count: state.serverCount, ok: true };
1372
+ }
1373
+ presenceInvalidated(room) {
1374
+ const state = this.states.get(room);
1375
+ if (!state) return false;
1376
+ return state.invalidated;
1377
+ }
1378
+ resetPresence(room) {
1379
+ const state = this.states.get(room);
1380
+ if (state) {
1381
+ state.known.clear();
1382
+ state.invalidated = false;
1383
+ state.invalidReason = "";
1384
+ }
1385
+ }
1386
+ };
1387
+
1388
+ // src/seq.ts
1389
+ var SeqTracker = class {
1390
+ states = /* @__PURE__ */ new Map();
1391
+ makeKey(room, channel) {
1392
+ return `${room}:${channel}`;
1393
+ }
1394
+ lastSeq(room, channel) {
1395
+ const key = this.makeKey(room, channel);
1396
+ const state = this.states.get(key);
1397
+ if (!state || !state.hasBase) {
1398
+ return { seq: 0, ok: false };
1399
+ }
1400
+ return { seq: state.last, ok: true };
1401
+ }
1402
+ process(room, channel, seq, reliable) {
1403
+ const key = this.makeKey(room, channel);
1404
+ let state = this.states.get(key);
1405
+ if (!state || !state.hasBase) {
1406
+ state = { last: seq, hasBase: true };
1407
+ this.states.set(key, state);
1408
+ return {};
1409
+ }
1410
+ if (seq === 0) {
1411
+ return {};
1412
+ }
1413
+ if (seq > state.last) {
1414
+ let gap;
1415
+ if (seq > state.last + 1) {
1416
+ const missed = seq - state.last - 1;
1417
+ gap = {
1418
+ prev: state.last,
1419
+ current: seq,
1420
+ missed,
1421
+ reliable
1422
+ };
1423
+ }
1424
+ state.last = seq;
1425
+ return { gap };
1426
+ }
1427
+ if (seq === state.last) {
1428
+ return {};
1429
+ }
1430
+ state.last = seq;
1431
+ return {};
1432
+ }
1433
+ rebaselineAll() {
1434
+ for (const state of this.states.values()) {
1435
+ state.hasBase = false;
1436
+ }
1437
+ }
1438
+ clearRoom(room) {
1439
+ const prefix = `${room}:`;
1440
+ for (const key of this.states.keys()) {
1441
+ if (key.startsWith(prefix)) {
1442
+ this.states.delete(key);
1443
+ }
1444
+ }
1445
+ }
1446
+ clear() {
1447
+ this.states.clear();
1448
+ }
1449
+ };
1450
+
1451
+ // src/errors-policy.ts
1452
+ function classifyError(code, frame, overrides) {
1453
+ if (overrides && overrides[code]) {
1454
+ const custom = overrides[code];
1455
+ return {
1456
+ action: custom.action,
1457
+ retryAfterMs: custom.retryAfterMs
1458
+ };
1459
+ }
1460
+ switch (code) {
1461
+ case "UNAUTHORIZED":
1462
+ case "FORBIDDEN_SCOPE":
1463
+ return { action: "fatal" };
1464
+ case "NOT_ROOM_OWNER":
1465
+ case "BAD_REQUEST":
1466
+ case "ROOM_NOT_FOUND":
1467
+ return { action: "drop_intent" };
1468
+ case "ROOM_ALREADY_EXISTS":
1469
+ case "INVALID_PASSCODE":
1470
+ return { action: "app_prompt" };
1471
+ case "ROOM_FULL":
1472
+ return { action: "retry_backoff", retryAfterMs: 1e3 };
1473
+ case "PASSCODE_RATE_LIMITED": {
1474
+ const retry = frame?.retry_after_ms && frame.retry_after_ms > 0 ? frame.retry_after_ms : 6e4;
1475
+ return { action: "retry_backoff", retryAfterMs: retry };
1476
+ }
1477
+ case "MAX_ROOMS_REACHED":
1478
+ return { action: "retry_backoff", retryAfterMs: 5e3 };
1479
+ case "BINARY_NOT_ALLOWED":
1480
+ case "CHANNEL_NOT_RELIABLE":
1481
+ case "CHANNEL_NOT_FOUND":
1482
+ case "RESERVED_CHANNEL_NAME":
1483
+ case "CHANNEL_READ_ONLY":
1484
+ return { action: "ignore" };
1485
+ case "RATE_LIMITED": {
1486
+ const retry = frame?.retry_after_ms && frame.retry_after_ms > 0 ? frame.retry_after_ms : 250;
1487
+ return { action: "retry_backoff", retryAfterMs: retry };
1488
+ }
1489
+ case "INTERNAL_ERROR":
1490
+ return { action: "retry_backoff", retryAfterMs: 2e3 };
1491
+ default:
1492
+ return { action: "ignore" };
1493
+ }
1494
+ }
1495
+
1496
+ // src/rtt.ts
1497
+ var RttTracker = class {
1498
+ stats;
1499
+ getStats() {
1500
+ return this.stats ? { ...this.stats } : void 0;
1501
+ }
1502
+ recordSample(sampleMs) {
1503
+ if (!this.stats || this.stats.samples === 0) {
1504
+ this.stats = {
1505
+ last: sampleMs,
1506
+ min: sampleMs,
1507
+ max: sampleMs,
1508
+ mean: sampleMs,
1509
+ samples: 1
1510
+ };
1511
+ } else {
1512
+ this.stats.last = sampleMs;
1513
+ this.stats.samples += 1;
1514
+ if (sampleMs < this.stats.min) {
1515
+ this.stats.min = sampleMs;
1516
+ }
1517
+ if (sampleMs > this.stats.max) {
1518
+ this.stats.max = sampleMs;
1519
+ }
1520
+ this.stats.mean = 0.8 * this.stats.mean + 0.2 * sampleMs;
1521
+ }
1522
+ return { ...this.stats };
1523
+ }
1524
+ reset() {
1525
+ this.stats = void 0;
1526
+ }
1527
+ };
1528
+
1529
+ // src/client.ts
1530
+ function buildWsUrl(baseUrl, apiKey) {
1531
+ const u = new URL(baseUrl);
1532
+ u.searchParams.set("key", apiKey);
1533
+ return u.toString();
1534
+ }
1535
+ var ROOM_NAME_REGEX = /room\s+"([^"]+)"/;
1536
+ function extractRoomFromMessage(message) {
1537
+ const match = message.match(ROOM_NAME_REGEX);
1538
+ return match ? match[1] : null;
1539
+ }
1540
+ var Client = class {
1541
+ config;
1542
+ clock;
1543
+ emitter = new SyncEventEmitter();
1544
+ logger;
1545
+ transport;
1546
+ _state = "idle";
1547
+ _capabilities;
1548
+ serverVersion;
1549
+ firstConnectDone = false;
1550
+ currentConnWelcomeReceived = false;
1551
+ isRestoringSession = false;
1552
+ reconnectAttempts = 0;
1553
+ reconnectTimer = null;
1554
+ welcomeTimer = null;
1555
+ intents = /* @__PURE__ */ new Map();
1556
+ waiters = /* @__PURE__ */ new Map();
1557
+ listRoomsLock = Promise.resolve();
1558
+ heartbeatManager;
1559
+ reliableQueue;
1560
+ unreliableQueue;
1561
+ coalesceManager;
1562
+ backpressureManager;
1563
+ rateLimiters;
1564
+ writer;
1565
+ policyOverrides = {};
1566
+ appliedPolicies = /* @__PURE__ */ new Set();
1567
+ _selfLabel = "";
1568
+ binaryOwner = /* @__PURE__ */ new Map();
1569
+ presenceManager;
1570
+ seqTracker = new SeqTracker();
1571
+ rttTracker = new RttTracker();
1572
+ createdRooms = /* @__PURE__ */ new Set();
1573
+ pingChain = Promise.resolve(0);
1574
+ pendingConnectResolve = null;
1575
+ pendingConnectReject = null;
1576
+ constructor(config) {
1577
+ if (!config.url) {
1578
+ throw new SdkError("WebSocket URL is required");
1579
+ }
1580
+ if (!config.apiKey) {
1581
+ throw new SdkError("API key is required");
1582
+ }
1583
+ this.config = config;
1584
+ this.clock = config.clock ?? new RealClock();
1585
+ this.transport = config.transport ?? new BrowserWebSocketTransport();
1586
+ this.presenceManager = new PresenceManager(config.presence);
1587
+ this.logger = new RedactingLogger(config.logger, () => [
1588
+ this.config.apiKey,
1589
+ ...Array.from(this.intents.values()).map((i) => i.passcode)
1590
+ ]);
1591
+ this.heartbeatManager = new HeartbeatManager(config.heartbeat, this.clock, {
1592
+ sendPing: () => {
1593
+ this.send({ type: "ping" });
1594
+ },
1595
+ onTimeout: (elapsedMs) => {
1596
+ this.logger.warn("Heartbeat timeout; closing connection", { elapsedMs });
1597
+ this.emitter.emit("heartbeatTimeout", { elapsedMs });
1598
+ this.transport.close(1008, "heartbeat timeout");
1599
+ },
1600
+ onPong: (sample) => {
1601
+ if (this.config.heartbeat?.trackRtt !== false) {
1602
+ const stats = this.rttTracker.recordSample(sample);
1603
+ this.emitter.emit("rtt", {
1604
+ sample,
1605
+ source: "heartbeat",
1606
+ stats
1607
+ });
1608
+ }
1609
+ }
1610
+ });
1611
+ const outboundCfg = config.outbound;
1612
+ this.reliableQueue = new OutboundQueue(outboundCfg?.reliableQueueSize ?? 256);
1613
+ this.unreliableQueue = new OutboundQueue(outboundCfg?.unreliableQueueSize ?? 16);
1614
+ this.coalesceManager = new CoalesceManager(
1615
+ this.clock,
1616
+ () => {
1617
+ this.writer.wake();
1618
+ },
1619
+ (ev) => {
1620
+ this.emitter.emit("coalesced", ev);
1621
+ }
1622
+ );
1623
+ this.backpressureManager = new BackpressureManager(
1624
+ this.transport,
1625
+ this.clock,
1626
+ config.backpressure,
1627
+ (ev) => {
1628
+ this.emitter.emit("backpressure", ev);
1629
+ },
1630
+ () => {
1631
+ this.writer.wake();
1632
+ }
1633
+ );
1634
+ this.rateLimiters = new RateLimiterRegistry(this.clock);
1635
+ this.writer = new WriterLoop({
1636
+ transport: this.transport,
1637
+ reliableQueue: this.reliableQueue,
1638
+ unreliableQueue: this.unreliableQueue,
1639
+ coalesceManager: this.coalesceManager,
1640
+ backpressure: this.backpressureManager,
1641
+ reliableBurst: outboundCfg?.reliableBurst ?? 32,
1642
+ unreliableBurst: outboundCfg?.unreliableBurst ?? 8,
1643
+ canWrite: () => (this._state === "ready" || this.isRestoringSession) && this.transport.readyState === 1,
1644
+ onError: (err) => {
1645
+ this.emitter.emit("error", {
1646
+ error: err instanceof Error ? err : new Error(String(err)),
1647
+ phase: "runtime"
1648
+ });
1649
+ }
1650
+ });
1651
+ }
1652
+ get state() {
1653
+ return this._state;
1654
+ }
1655
+ get capabilities() {
1656
+ return this._capabilities;
1657
+ }
1658
+ get selfLabel() {
1659
+ return this._selfLabel;
1660
+ }
1661
+ knownMembers(room) {
1662
+ return this.presenceManager.knownMembers(room, this._selfLabel);
1663
+ }
1664
+ lastServerMemberCount(room) {
1665
+ return this.presenceManager.lastServerMemberCount(room);
1666
+ }
1667
+ presenceInvalidated(room) {
1668
+ return this.presenceManager.presenceInvalidated(room);
1669
+ }
1670
+ resetPresence(room) {
1671
+ if (!this.intents.has(room)) {
1672
+ throw new NotReadyError(`Room "${room}" has no live intent`);
1673
+ }
1674
+ this.presenceManager.resetPresence(room);
1675
+ }
1676
+ lastSeq(room, channel) {
1677
+ return this.seqTracker.lastSeq(room, channel);
1678
+ }
1679
+ rttStats() {
1680
+ return this.rttTracker.getStats();
1681
+ }
1682
+ ping(timeoutMs) {
1683
+ if (this._state !== "ready") {
1684
+ return Promise.reject(new NotReadyError());
1685
+ }
1686
+ const runPing = () => {
1687
+ if (this._state !== "ready") {
1688
+ return Promise.reject(new NotReadyError());
1689
+ }
1690
+ const key = "pong:";
1691
+ if (this.waiters.has(key)) {
1692
+ return Promise.reject(
1693
+ new ConcurrentRequestError("Concurrent ping request is not permitted")
1694
+ );
1695
+ }
1696
+ const start = this.clock.now();
1697
+ const timeout = timeoutMs ?? this.config.heartbeat?.timeoutMs ?? 1e4;
1698
+ return new Promise((resolve, reject) => {
1699
+ const timer = this.clock.setTimeout(() => {
1700
+ this.waiters.delete(key);
1701
+ reject(new RequestTimeoutError("Timed out waiting for pong frame"));
1702
+ }, timeout);
1703
+ this.waiters.set(key, {
1704
+ resolve: () => {
1705
+ const sample = Math.max(0, this.clock.now() - start);
1706
+ const stats = this.rttTracker.recordSample(sample);
1707
+ this.emitter.emit("rtt", {
1708
+ sample,
1709
+ source: "on_demand",
1710
+ stats
1711
+ });
1712
+ resolve(sample);
1713
+ },
1714
+ reject,
1715
+ timer,
1716
+ room: "",
1717
+ responseType: "pong"
1718
+ });
1719
+ try {
1720
+ this.send({ type: "ping" });
1721
+ } catch (err) {
1722
+ this.clock.clearTimeout(timer);
1723
+ this.waiters.delete(key);
1724
+ reject(err);
1725
+ }
1726
+ });
1727
+ };
1728
+ const next = this.pingChain.then(runPing, runPing);
1729
+ this.pingChain = next.then(
1730
+ () => 0,
1731
+ () => 0
1732
+ );
1733
+ return next;
1734
+ }
1735
+ clearRoomState(room) {
1736
+ for (const [id, owner] of Array.from(this.binaryOwner.entries())) {
1737
+ if (owner.room === room) {
1738
+ this.binaryOwner.delete(id);
1739
+ }
1740
+ }
1741
+ this.presenceManager.remove(room);
1742
+ this.seqTracker.clearRoom(room);
1743
+ this.rateLimiters.clearRoom(room);
1744
+ this.coalesceManager.clearRoom(room);
1745
+ const prefix = `${room}:`;
1746
+ for (const key of Array.from(this.appliedPolicies)) {
1747
+ if (key.startsWith(prefix)) {
1748
+ this.appliedPolicies.delete(key);
1749
+ }
1750
+ }
1751
+ }
1752
+ isClosed() {
1753
+ return this._state === "closed";
1754
+ }
1755
+ on(event, handler) {
1756
+ return this.emitter.on(event, handler);
1757
+ }
1758
+ connect() {
1759
+ if (this._state === "connecting" || this._state === "awaiting_welcome" || this._state === "ready" || this._state === "reconnecting") {
1760
+ return Promise.reject(
1761
+ new SdkError(`Client is already in state: ${this._state}`)
1762
+ );
1763
+ }
1764
+ if (this._state === "closed") {
1765
+ this.firstConnectDone = false;
1766
+ this.reconnectAttempts = 0;
1767
+ this.intents.clear();
1768
+ this._capabilities = void 0;
1769
+ this.serverVersion = void 0;
1770
+ this._selfLabel = "";
1771
+ this.appliedPolicies.clear();
1772
+ this.rateLimiters.clearAll();
1773
+ this.coalesceManager.clear();
1774
+ this.discardOutboundQueues();
1775
+ this.binaryOwner.clear();
1776
+ this.presenceManager.clear();
1777
+ this.seqTracker.clear();
1778
+ this.rttTracker.reset();
1779
+ this.createdRooms.clear();
1780
+ this._state = "idle";
1781
+ }
1782
+ return new Promise((resolve, reject) => {
1783
+ this.pendingConnectResolve = resolve;
1784
+ this.pendingConnectReject = reject;
1785
+ this.initiateConnect(false);
1786
+ });
1787
+ }
1788
+ async close() {
1789
+ if (this._state === "closed") {
1790
+ return;
1791
+ }
1792
+ this.transitionState("closed");
1793
+ this.clearTimers();
1794
+ this.heartbeatManager.stop();
1795
+ this.backpressureManager.stop();
1796
+ this.writer.stop();
1797
+ this.rateLimiters.clearAll();
1798
+ this.coalesceManager.clear();
1799
+ this.discardOutboundQueues();
1800
+ this.appliedPolicies.clear();
1801
+ this.intents.clear();
1802
+ this.binaryOwner.clear();
1803
+ this.presenceManager.clear();
1804
+ this.seqTracker.clear();
1805
+ this.rttTracker.reset();
1806
+ this.createdRooms.clear();
1807
+ try {
1808
+ this.transport.close(1e3, "client closed");
1809
+ } catch {
1810
+ }
1811
+ this.emitter.emit("disconnected", {
1812
+ code: 1e3,
1813
+ reason: "client closed",
1814
+ wasClean: true
1815
+ });
1816
+ this.failPendingWaiters(new ConnectError("Client closed"));
1817
+ if (this.pendingConnectReject) {
1818
+ const reject = this.pendingConnectReject;
1819
+ this.pendingConnectResolve = null;
1820
+ this.pendingConnectReject = null;
1821
+ reject(new ConnectError("Client closed"));
1822
+ }
1823
+ }
1824
+ createRoom(req) {
1825
+ try {
1826
+ validateRoomName(req.room, this.config.validation);
1827
+ checkCapabilityPreflight(this._capabilities, req, this.createdRooms.size);
1828
+ } catch (err) {
1829
+ return Promise.reject(err);
1830
+ }
1831
+ return this.sendWithCorrelation("room_created", req.room, {
1832
+ ...req,
1833
+ type: "create_room"
1834
+ }).then((res) => {
1835
+ this.createdRooms.add(req.room);
1836
+ return res;
1837
+ });
1838
+ }
1839
+ joinRoomWithBinary(roomOrReq, passcodeOrNothing, binaryChannelsOrNothing) {
1840
+ let room;
1841
+ let passcode;
1842
+ let binaryChannels;
1843
+ if (typeof roomOrReq === "string") {
1844
+ room = roomOrReq;
1845
+ passcode = passcodeOrNothing ?? "";
1846
+ binaryChannels = binaryChannelsOrNothing ?? {};
1847
+ } else {
1848
+ room = roomOrReq.room;
1849
+ passcode = roomOrReq.passcode ?? "";
1850
+ binaryChannels = roomOrReq.binaryChannels ?? roomOrReq.binary_channels ?? {};
1851
+ }
1852
+ try {
1853
+ validateRoomName(room, this.config.validation);
1854
+ validateBinaryMap(binaryChannels);
1855
+ } catch (err) {
1856
+ return Promise.reject(err);
1857
+ }
1858
+ if (this._state !== "ready" && !this.isRestoringSession) {
1859
+ return Promise.reject(new SdkError(`Client not ready (state: ${this._state})`));
1860
+ }
1861
+ const sorted = Object.entries(binaryChannels).map(([ch, id]) => ({ ch, id })).sort((a, b) => a.id - b.id);
1862
+ let hasCollision = false;
1863
+ let collidingId = 0;
1864
+ const existingList = [];
1865
+ const rejectedList = [];
1866
+ for (const item of sorted) {
1867
+ const owner = this.binaryOwner.get(item.id);
1868
+ if (owner && owner.room !== room) {
1869
+ if (!hasCollision) {
1870
+ hasCollision = true;
1871
+ collidingId = item.id;
1872
+ }
1873
+ existingList.push(`${owner.room}/${owner.channel}`);
1874
+ rejectedList.push(`${room}/${item.ch}`);
1875
+ }
1876
+ }
1877
+ if (hasCollision) {
1878
+ this.emitter.emit("binaryIdCollision", {
1879
+ id: collidingId,
1880
+ existing: existingList,
1881
+ rejected: rejectedList
1882
+ });
1883
+ return Promise.reject(
1884
+ new BinaryIdCollisionError(collidingId, existingList, rejectedList)
1885
+ );
1886
+ }
1887
+ const prevOwners = /* @__PURE__ */ new Map();
1888
+ for (const [id, owner] of Array.from(this.binaryOwner.entries())) {
1889
+ if (owner.room === room) {
1890
+ prevOwners.set(id, owner);
1891
+ this.binaryOwner.delete(id);
1892
+ }
1893
+ }
1894
+ for (const item of sorted) {
1895
+ this.binaryOwner.set(item.id, { room, channel: item.ch });
1896
+ }
1897
+ const req = {
1898
+ type: "join_room",
1899
+ room,
1900
+ passcode: passcode || void 0,
1901
+ binary_channels: Object.keys(binaryChannels).length > 0 ? { ...binaryChannels } : void 0
1902
+ };
1903
+ return this.sendWithCorrelation("room_joined", room, req).then((res) => {
1904
+ let effectivePasscode = passcode;
1905
+ if (!effectivePasscode && this.intents.has(room)) {
1906
+ effectivePasscode = this.intents.get(room).passcode;
1907
+ }
1908
+ this.intents.set(room, {
1909
+ room,
1910
+ passcode: effectivePasscode,
1911
+ binaryChannels: Object.keys(binaryChannels).length > 0 ? { ...binaryChannels } : void 0
1912
+ });
1913
+ this.presenceManager.init(room, res.members);
1914
+ for (const item of sorted) {
1915
+ this.rateLimiters.recordPublish(room, item.ch);
1916
+ }
1917
+ if (Object.keys(binaryChannels).length > 0 || prevOwners.size > 0) {
1918
+ this.emitter.emit("binaryMappingChanged", {
1919
+ room,
1920
+ map: { ...binaryChannels }
1921
+ });
1922
+ }
1923
+ return res;
1924
+ }).catch((err) => {
1925
+ for (const item of sorted) {
1926
+ const owner = this.binaryOwner.get(item.id);
1927
+ if (owner && owner.room === room) {
1928
+ this.binaryOwner.delete(item.id);
1929
+ }
1930
+ }
1931
+ for (const [id, owner] of prevOwners.entries()) {
1932
+ this.binaryOwner.set(id, owner);
1933
+ }
1934
+ throw err;
1935
+ });
1936
+ }
1937
+ joinRoom(req) {
1938
+ return this.joinRoomWithBinary({
1939
+ ...req,
1940
+ binaryChannels: req.binary_channels
1941
+ });
1942
+ }
1943
+ leaveRoom(room) {
1944
+ return this.sendWithCorrelation("room_left", room, {
1945
+ type: "leave_room",
1946
+ room
1947
+ }).then((res) => {
1948
+ this.intents.delete(room);
1949
+ this.clearRoomState(room);
1950
+ return res;
1951
+ });
1952
+ }
1953
+ deleteRoom(room) {
1954
+ try {
1955
+ validateRoomName(room, this.config.validation);
1956
+ } catch (err) {
1957
+ return Promise.reject(err);
1958
+ }
1959
+ const key = `delete_room:${room}`;
1960
+ if (this.waiters.has(key)) {
1961
+ return Promise.reject(
1962
+ new ConcurrentRequestError(
1963
+ `Concurrent delete_room request for room "${room}" is not permitted`
1964
+ )
1965
+ );
1966
+ }
1967
+ if (this._state !== "ready") {
1968
+ return Promise.reject(new SdkError(`Client not ready (state: ${this._state})`));
1969
+ }
1970
+ const timeoutMs = this.config.requestTimeoutMs ?? 1e4;
1971
+ return new Promise((resolve, reject) => {
1972
+ const timer = this.clock.setTimeout(() => {
1973
+ this.waiters.delete(key);
1974
+ reject(
1975
+ new RequestTimeoutError(
1976
+ `Timed out waiting for room_deleted presence event on "${room}"`
1977
+ )
1978
+ );
1979
+ }, timeoutMs);
1980
+ this.waiters.set(key, {
1981
+ resolve: () => {
1982
+ this.createdRooms.delete(room);
1983
+ resolve();
1984
+ },
1985
+ reject: (err) => {
1986
+ reject(err);
1987
+ },
1988
+ timer,
1989
+ room,
1990
+ responseType: "delete_room"
1991
+ });
1992
+ try {
1993
+ const frame = { type: "delete_room", room };
1994
+ this.send(frame);
1995
+ } catch (err) {
1996
+ this.clock.clearTimeout(timer);
1997
+ this.waiters.delete(key);
1998
+ reject(err);
1999
+ }
2000
+ });
2001
+ }
2002
+ listRooms() {
2003
+ const execute = () => {
2004
+ const frame = { type: "list_rooms" };
2005
+ return this.sendWithCorrelation("room_list", "", frame);
2006
+ };
2007
+ const next = this.listRoomsLock.then(execute, execute);
2008
+ this.listRoomsLock = next.then(
2009
+ () => {
2010
+ },
2011
+ () => {
2012
+ }
2013
+ );
2014
+ return next;
2015
+ }
2016
+ send(frame) {
2017
+ if (this._state === "closed") {
2018
+ throw new SdkError("Cannot send frame on closed client");
2019
+ }
2020
+ const room = frame.room ?? "";
2021
+ const channel = frame.channel ?? "default";
2022
+ const wire = JSON.stringify(frame);
2023
+ if (this.reliableQueue.size >= this.reliableQueue.capacity) {
2024
+ const action = this.config.outbound?.reliableOverflow ?? "drop";
2025
+ if (action === "close") {
2026
+ this.emitter.emit("reliableOverflow", {
2027
+ room,
2028
+ channel,
2029
+ depth: this.reliableQueue.capacity,
2030
+ action: "closed"
2031
+ });
2032
+ this.transport.close(1008, "reliable queue overflow");
2033
+ return;
2034
+ } else {
2035
+ this.emitter.emit("reliableOverflow", {
2036
+ room,
2037
+ channel,
2038
+ depth: this.reliableQueue.capacity,
2039
+ action: "dropped"
2040
+ });
2041
+ return;
2042
+ }
2043
+ }
2044
+ this.reliableQueue.enqueue({
2045
+ room,
2046
+ channel,
2047
+ reliable: true,
2048
+ wire
2049
+ });
2050
+ this.writer.wake();
2051
+ }
2052
+ publish(req) {
2053
+ const channel = req.channel ?? "default";
2054
+ try {
2055
+ validateRoomName(req.room, this.config.validation);
2056
+ validateChannelName(channel, this.config.validation);
2057
+ const marshaled = JSON.stringify(req.payload);
2058
+ const payloadBytes = new TextEncoder().encode(marshaled).length;
2059
+ validateTextPayloadSize(payloadBytes, this.config.validation);
2060
+ } catch (err) {
2061
+ return Promise.reject(err);
2062
+ }
2063
+ if (this._state !== "ready") {
2064
+ return Promise.reject(new NotReadyError());
2065
+ }
2066
+ const policy = this.getPolicy(req.room, channel);
2067
+ const appliedKey = `${req.room}:${channel}`;
2068
+ if (!this.appliedPolicies.has(appliedKey)) {
2069
+ this.appliedPolicies.add(appliedKey);
2070
+ this.emitter.emit("policyApplied", {
2071
+ room: req.room,
2072
+ channel,
2073
+ policy
2074
+ });
2075
+ }
2076
+ const reliable = req.reliable ?? policy.reliable;
2077
+ if (reliable === true && policy.reliable === false) {
2078
+ return Promise.reject(new ChannelNotReliableError(channel));
2079
+ }
2080
+ const paused = this.rateLimiters.isPaused(req.room, channel, this.clock.now());
2081
+ if (paused.paused) {
2082
+ const err = new RateLimitedError(paused.retryAfterMs, channel, "local");
2083
+ this.emitter.emit("rateLimited", {
2084
+ source: "local",
2085
+ room: req.room,
2086
+ channel,
2087
+ retryAfterMs: paused.retryAfterMs
2088
+ });
2089
+ return Promise.reject(err);
2090
+ }
2091
+ if (policy.maxRate > 0 && this.config.outbound?.enableClientRateLimit !== false) {
2092
+ const bucket = this.rateLimiters.get(req.room, channel, policy.maxRate);
2093
+ if (!bucket.tryConsume()) {
2094
+ const retryAfterMs = bucket.retryAfterMs(this.clock.now());
2095
+ const err = new RateLimitedError(retryAfterMs, channel, "local");
2096
+ this.emitter.emit("rateLimited", {
2097
+ source: "local",
2098
+ room: req.room,
2099
+ channel,
2100
+ retryAfterMs
2101
+ });
2102
+ return Promise.reject(err);
2103
+ }
2104
+ }
2105
+ this.rateLimiters.recordPublish(req.room, channel);
2106
+ const frameObj = {
2107
+ type: "publish",
2108
+ room: req.room,
2109
+ channel,
2110
+ payload: req.payload,
2111
+ reliable
2112
+ };
2113
+ if (req.includeSender !== void 0) {
2114
+ frameObj.include_sender = req.includeSender;
2115
+ }
2116
+ const wire = JSON.stringify(frameObj);
2117
+ if (reliable) {
2118
+ if (this.reliableQueue.size >= this.reliableQueue.capacity) {
2119
+ const action = this.config.outbound?.reliableOverflow ?? "drop";
2120
+ if (action === "close") {
2121
+ this.emitter.emit("reliableOverflow", {
2122
+ room: req.room,
2123
+ channel,
2124
+ depth: this.reliableQueue.capacity,
2125
+ action: "closed"
2126
+ });
2127
+ this.transport.close(1008, "reliable queue overflow");
2128
+ return Promise.resolve();
2129
+ } else {
2130
+ this.emitter.emit("reliableOverflow", {
2131
+ room: req.room,
2132
+ channel,
2133
+ depth: this.reliableQueue.capacity,
2134
+ action: "dropped"
2135
+ });
2136
+ return Promise.resolve();
2137
+ }
2138
+ }
2139
+ this.reliableQueue.enqueue({
2140
+ room: req.room,
2141
+ channel,
2142
+ reliable: true,
2143
+ wire
2144
+ });
2145
+ } else {
2146
+ if (policy.coalesce) {
2147
+ this.coalesceManager.update(
2148
+ req.room,
2149
+ channel,
2150
+ "text",
2151
+ req.payload,
2152
+ wire,
2153
+ policy,
2154
+ req.includeSender
2155
+ );
2156
+ } else {
2157
+ if (this.unreliableQueue.size >= this.unreliableQueue.capacity) {
2158
+ this.unreliableQueue.dropOldest();
2159
+ this.emitter.emit("unreliableDropped", {
2160
+ room: req.room,
2161
+ channel,
2162
+ depth: this.unreliableQueue.capacity
2163
+ });
2164
+ }
2165
+ this.unreliableQueue.enqueue({
2166
+ room: req.room,
2167
+ channel,
2168
+ reliable: false,
2169
+ wire
2170
+ });
2171
+ }
2172
+ }
2173
+ this.writer.wake();
2174
+ return Promise.resolve();
2175
+ }
2176
+ publishBinary(roomOrReq, channelOrNothing, payloadOrNothing) {
2177
+ let room;
2178
+ let channel;
2179
+ let payloadRaw;
2180
+ let reqReliable;
2181
+ if (typeof roomOrReq === "string") {
2182
+ room = roomOrReq;
2183
+ channel = channelOrNothing;
2184
+ payloadRaw = payloadOrNothing;
2185
+ } else {
2186
+ room = roomOrReq.room;
2187
+ channel = roomOrReq.channel;
2188
+ payloadRaw = roomOrReq.payload;
2189
+ reqReliable = roomOrReq.reliable;
2190
+ }
2191
+ const pBytes = payloadRaw instanceof Uint8Array ? payloadRaw : new Uint8Array(payloadRaw);
2192
+ const maxBytes = this.config.validation?.maxBinaryPayloadBytes ?? this.config.maxBinaryPayloadBytes ?? 65535;
2193
+ try {
2194
+ validateRoomName(room, this.config.validation);
2195
+ validateChannelName(channel, this.config.validation);
2196
+ validateBinaryPayloadSize(pBytes.byteLength, {
2197
+ enabled: this.config.validation?.enabled,
2198
+ maxBinaryPayloadBytes: maxBytes
2199
+ });
2200
+ } catch (err) {
2201
+ return Promise.reject(err);
2202
+ }
2203
+ if (this._state !== "ready") {
2204
+ return Promise.reject(new NotReadyError());
2205
+ }
2206
+ let mappedId;
2207
+ for (const [id, owner] of this.binaryOwner.entries()) {
2208
+ if (owner.room === room && owner.channel === channel) {
2209
+ mappedId = id;
2210
+ break;
2211
+ }
2212
+ }
2213
+ if (mappedId === void 0) {
2214
+ return Promise.reject(new BinaryChannelUnmappedError(room, channel));
2215
+ }
2216
+ const policy = this.getPolicy(room, channel);
2217
+ const appliedKey = `${room}:${channel}`;
2218
+ if (!this.appliedPolicies.has(appliedKey)) {
2219
+ this.appliedPolicies.add(appliedKey);
2220
+ this.emitter.emit("policyApplied", { room, channel, policy });
2221
+ }
2222
+ const reliable = reqReliable ?? policy.reliable;
2223
+ if (reliable === true && policy.reliable === false) {
2224
+ return Promise.reject(new ChannelNotReliableError(channel));
2225
+ }
2226
+ const paused = this.rateLimiters.isPaused(room, channel, this.clock.now());
2227
+ if (paused.paused) {
2228
+ const err = new RateLimitedError(paused.retryAfterMs, channel, "local");
2229
+ this.emitter.emit("rateLimited", {
2230
+ source: "local",
2231
+ room,
2232
+ channel,
2233
+ retryAfterMs: paused.retryAfterMs
2234
+ });
2235
+ return Promise.reject(err);
2236
+ }
2237
+ if (policy.maxRate > 0 && this.config.outbound?.enableClientRateLimit !== false) {
2238
+ const bucket = this.rateLimiters.get(room, channel, policy.maxRate);
2239
+ if (!bucket.tryConsume()) {
2240
+ const retryAfterMs = bucket.retryAfterMs(this.clock.now());
2241
+ const err = new RateLimitedError(retryAfterMs, channel, "local");
2242
+ this.emitter.emit("rateLimited", {
2243
+ source: "local",
2244
+ room,
2245
+ channel,
2246
+ retryAfterMs
2247
+ });
2248
+ return Promise.reject(err);
2249
+ }
2250
+ }
2251
+ this.rateLimiters.recordPublish(room, channel);
2252
+ const wire = new Uint8Array(1 + pBytes.byteLength);
2253
+ wire[0] = mappedId;
2254
+ wire.set(pBytes, 1);
2255
+ if (reliable) {
2256
+ if (this.reliableQueue.size >= this.reliableQueue.capacity) {
2257
+ const action = this.config.outbound?.reliableOverflow ?? "drop";
2258
+ if (action === "close") {
2259
+ this.emitter.emit("reliableOverflow", {
2260
+ room,
2261
+ channel,
2262
+ depth: this.reliableQueue.capacity,
2263
+ action: "closed"
2264
+ });
2265
+ this.transport.close(1008, "reliable queue overflow");
2266
+ return Promise.resolve();
2267
+ } else {
2268
+ this.emitter.emit("reliableOverflow", {
2269
+ room,
2270
+ channel,
2271
+ depth: this.reliableQueue.capacity,
2272
+ action: "dropped"
2273
+ });
2274
+ return Promise.resolve();
2275
+ }
2276
+ }
2277
+ this.reliableQueue.enqueue({
2278
+ room,
2279
+ channel,
2280
+ reliable: true,
2281
+ wire,
2282
+ frameType: "binary"
2283
+ });
2284
+ } else {
2285
+ if (policy.coalesce) {
2286
+ this.coalesceManager.update(
2287
+ room,
2288
+ channel,
2289
+ "binary",
2290
+ pBytes,
2291
+ wire,
2292
+ policy
2293
+ );
2294
+ } else {
2295
+ if (this.unreliableQueue.size >= this.unreliableQueue.capacity) {
2296
+ this.unreliableQueue.dropOldest();
2297
+ this.emitter.emit("unreliableDropped", {
2298
+ room,
2299
+ channel,
2300
+ depth: this.unreliableQueue.capacity
2301
+ });
2302
+ }
2303
+ this.unreliableQueue.enqueue({
2304
+ room,
2305
+ channel,
2306
+ reliable: false,
2307
+ wire,
2308
+ frameType: "binary"
2309
+ });
2310
+ }
2311
+ }
2312
+ this.writer.wake();
2313
+ return Promise.resolve();
2314
+ }
2315
+ setChannelPolicy(room, channel, policy) {
2316
+ const key = `${room}:${channel}`;
2317
+ this.policyOverrides[key] = policy;
2318
+ if (!policy.coalesce) {
2319
+ this.coalesceManager.flushChannel(room, channel, (wire) => {
2320
+ try {
2321
+ this.transport.send(wire);
2322
+ } catch (err) {
2323
+ this.emitter.emit("error", {
2324
+ error: err instanceof Error ? err : new Error(String(err)),
2325
+ phase: "runtime"
2326
+ });
2327
+ }
2328
+ });
2329
+ }
2330
+ }
2331
+ queueDepth() {
2332
+ return {
2333
+ reliable: this.reliableQueue.size,
2334
+ unreliable: this.unreliableQueue.size,
2335
+ coalesced: this.coalesceManager.countPending()
2336
+ };
2337
+ }
2338
+ bufferedAmount() {
2339
+ return this.transport.bufferedAmount();
2340
+ }
2341
+ // @internal — test seam
2342
+ async __flushNow() {
2343
+ await this.writer.flushNow();
2344
+ }
2345
+ getPolicy(room, channel) {
2346
+ return resolvePolicy(
2347
+ room,
2348
+ channel,
2349
+ this.config.outbound?.policyProvider,
2350
+ this.policyOverrides
2351
+ );
2352
+ }
2353
+ discardOutboundQueues() {
2354
+ const reliable = this.reliableQueue.clear();
2355
+ const unreliable = this.unreliableQueue.clear();
2356
+ const coalesced = this.coalesceManager.clear();
2357
+ return { reliable, unreliable, coalesced };
2358
+ }
2359
+ getRoomIntents() {
2360
+ return Array.from(this.intents.values()).map((intent) => ({
2361
+ room: intent.room,
2362
+ passcode: intent.passcode,
2363
+ binaryChannels: intent.binaryChannels ? { ...intent.binaryChannels } : void 0
2364
+ }));
2365
+ }
2366
+ forgetRoom(room) {
2367
+ validateRoomName(room, this.config.validation);
2368
+ this.intents.delete(room);
2369
+ this.clearRoomState(room);
2370
+ }
2371
+ // --- Private lifecycle and state machine ---
2372
+ transitionState(to) {
2373
+ const from = this._state;
2374
+ if (from === to) return;
2375
+ this._state = to;
2376
+ this.emitter.emit("stateChange", { from, to });
2377
+ }
2378
+ initiateConnect(isReconnect) {
2379
+ if (this._state === "closed") return;
2380
+ this.currentConnWelcomeReceived = false;
2381
+ this.transitionState(isReconnect ? "reconnecting" : "connecting");
2382
+ let url;
2383
+ try {
2384
+ url = buildWsUrl(this.config.url, this.config.apiKey);
2385
+ } catch (err) {
2386
+ const redacted = redactUrl(this.config.url);
2387
+ const connErr = new ConnectError(
2388
+ `Invalid URL: ${redacted}`,
2389
+ err instanceof Error ? err : void 0
2390
+ );
2391
+ this.transitionState("closed");
2392
+ this.rejectPendingConnect(connErr);
2393
+ return;
2394
+ }
2395
+ this.setupTransportHandlers(isReconnect);
2396
+ this.transport.connect(url).catch((err) => {
2397
+ if (this._state === "closed") return;
2398
+ this.handlePreOpenFailure(err, isReconnect);
2399
+ });
2400
+ }
2401
+ setupTransportHandlers(isReconnect) {
2402
+ this.transport.onopen = () => {
2403
+ if (this._state === "closed") return;
2404
+ if (this._state === "connecting" || this._state === "reconnecting") {
2405
+ this.transitionState("awaiting_welcome");
2406
+ }
2407
+ if (!this.welcomeTimer) {
2408
+ const welcomeTimeoutMs = this.config.welcomeTimeoutMs ?? 1e4;
2409
+ this.welcomeTimer = this.clock.setTimeout(() => {
2410
+ this.welcomeTimer = null;
2411
+ this.handleWelcomeTimeout(isReconnect);
2412
+ }, welcomeTimeoutMs);
2413
+ }
2414
+ };
2415
+ this.transport.onmessage = (ev) => {
2416
+ this.handleIncomingData(ev.data, isReconnect);
2417
+ };
2418
+ this.transport.onclose = (ev) => {
2419
+ this.handleSocketClose(ev.code, ev.reason, ev.wasClean, isReconnect);
2420
+ };
2421
+ this.transport.onerror = (ev) => {
2422
+ this.logger.debug("Transport onerror", { error: ev });
2423
+ };
2424
+ }
2425
+ handleIncomingData(rawData, isReconnect) {
2426
+ if (rawData instanceof ArrayBuffer || rawData instanceof Uint8Array) {
2427
+ const bytes = rawData instanceof Uint8Array ? rawData : new Uint8Array(rawData);
2428
+ if (bytes.length === 0) {
2429
+ this.emitter.emit("protocolViolation", {
2430
+ room: "",
2431
+ reason: "empty binary frame"
2432
+ });
2433
+ return;
2434
+ }
2435
+ const id = bytes[0];
2436
+ const owner = this.binaryOwner.get(id);
2437
+ if (!owner) {
2438
+ this.emitter.emit("binaryUnknownChannel", { id });
2439
+ return;
2440
+ }
2441
+ if (owner.channel === "presence") {
2442
+ this.emitter.emit("protocolViolation", {
2443
+ room: owner.room,
2444
+ reason: "binary frame on presence channel"
2445
+ });
2446
+ return;
2447
+ }
2448
+ const payload = bytes.slice(1);
2449
+ this.emitter.emit("binaryMessage", {
2450
+ room: owner.room,
2451
+ channel: owner.channel,
2452
+ payload
2453
+ });
2454
+ return;
2455
+ }
2456
+ let frame;
2457
+ try {
2458
+ const str = typeof rawData === "string" ? rawData : String(rawData);
2459
+ frame = JSON.parse(str);
2460
+ } catch (err) {
2461
+ this.emitter.emit("error", {
2462
+ error: err instanceof Error ? err : new SdkError(String(err)),
2463
+ phase: "runtime"
2464
+ });
2465
+ return;
2466
+ }
2467
+ if (isWelcomeFrame(frame)) {
2468
+ this.handleWelcome(frame, isReconnect);
2469
+ return;
2470
+ }
2471
+ if (isMessageFrame(frame)) {
2472
+ if (this.intents.has(frame.room)) {
2473
+ const policy = this.getPolicy(frame.room, frame.channel);
2474
+ const { gap } = this.seqTracker.process(
2475
+ frame.room,
2476
+ frame.channel,
2477
+ frame.seq,
2478
+ policy.reliable
2479
+ );
2480
+ if (gap) {
2481
+ this.emitter.emit("sequenceGap", {
2482
+ room: frame.room,
2483
+ channel: frame.channel,
2484
+ prev: gap.prev,
2485
+ current: gap.current,
2486
+ missed: gap.missed,
2487
+ reliable: gap.reliable
2488
+ });
2489
+ if (frame.channel === "presence" && gap.reliable && this.presenceManager.autoInvalidateOnReliableGap) {
2490
+ const ps = this.presenceManager.get(frame.room);
2491
+ if (ps) {
2492
+ ps.invalidated = true;
2493
+ ps.invalidReason = "reliable presence gap";
2494
+ }
2495
+ this.emitter.emit("presenceInvalidated", {
2496
+ room: frame.room,
2497
+ reason: "reliable presence gap"
2498
+ });
2499
+ }
2500
+ }
2501
+ }
2502
+ if (frame.channel === "presence" && frame.from === "server") {
2503
+ this.handlePresenceMessage(frame);
2504
+ } else {
2505
+ this.emitter.emit("message", frame);
2506
+ }
2507
+ return;
2508
+ }
2509
+ if (frame.type === "pong") {
2510
+ this.heartbeatManager.handlePong();
2511
+ const key2 = "pong:";
2512
+ const waiter2 = this.waiters.get(key2);
2513
+ if (waiter2) {
2514
+ this.clock.clearTimeout(waiter2.timer);
2515
+ this.waiters.delete(key2);
2516
+ waiter2.resolve(frame);
2517
+ }
2518
+ return;
2519
+ }
2520
+ if (isErrorFrame(frame)) {
2521
+ this.handleErrorFrame(frame);
2522
+ return;
2523
+ }
2524
+ const key = `${frame.type}:${frame.room ?? ""}`;
2525
+ const waiter = this.waiters.get(key);
2526
+ if (waiter) {
2527
+ this.clock.clearTimeout(waiter.timer);
2528
+ this.waiters.delete(key);
2529
+ waiter.resolve(frame);
2530
+ }
2531
+ }
2532
+ handlePresenceMessage(frame) {
2533
+ const payload = frame.payload;
2534
+ if (!payload || typeof payload !== "object" || !payload.event) {
2535
+ this.emitter.emit("protocolViolation", {
2536
+ room: frame.room,
2537
+ reason: "malformed presence payload"
2538
+ });
2539
+ return;
2540
+ }
2541
+ const evt = payload.event;
2542
+ if (evt !== "join" && evt !== "leave" && evt !== "room_deleted") {
2543
+ this.emitter.emit("protocolViolation", {
2544
+ room: frame.room,
2545
+ reason: "unknown presence event"
2546
+ });
2547
+ return;
2548
+ }
2549
+ if (!payload.who || typeof payload.who !== "string" || payload.who === "") {
2550
+ this.emitter.emit("protocolViolation", {
2551
+ room: frame.room,
2552
+ reason: "presence event missing who"
2553
+ });
2554
+ return;
2555
+ }
2556
+ if ((evt === "join" || evt === "leave") && payload.who === this._selfLabel) {
2557
+ this.emitter.emit("protocolViolation", {
2558
+ room: frame.room,
2559
+ reason: "self presence event"
2560
+ });
2561
+ return;
2562
+ }
2563
+ const deleteKey = `delete_room:${frame.room}`;
2564
+ const hasDeleteWaiter = this.waiters.has(deleteKey);
2565
+ if (!this.intents.has(frame.room) && !hasDeleteWaiter) {
2566
+ this.logger.debug("Dropping presence event for unjoined room", {
2567
+ room: frame.room,
2568
+ event: evt
2569
+ });
2570
+ return;
2571
+ }
2572
+ if (evt === "room_deleted") {
2573
+ this.createdRooms.delete(frame.room);
2574
+ this.intents.delete(frame.room);
2575
+ this.clearRoomState(frame.room);
2576
+ this.emitter.emit("roomDeleted", { room: frame.room, who: payload.who });
2577
+ const waiter = this.waiters.get(deleteKey);
2578
+ if (waiter) {
2579
+ this.clock.clearTimeout(waiter.timer);
2580
+ this.waiters.delete(deleteKey);
2581
+ waiter.resolve(void 0);
2582
+ }
2583
+ return;
2584
+ }
2585
+ const ps = this.presenceManager.get(frame.room);
2586
+ if (evt === "join") {
2587
+ if (!ps) {
2588
+ this.presenceManager.init(frame.room, 1);
2589
+ const newPs = this.presenceManager.get(frame.room);
2590
+ newPs.known.add(payload.who);
2591
+ this.emitter.emit("presenceJoin", { room: frame.room, who: payload.who });
2592
+ } else {
2593
+ if (!ps.known.has(payload.who)) {
2594
+ ps.known.add(payload.who);
2595
+ ps.serverCount++;
2596
+ this.emitter.emit("presenceJoin", { room: frame.room, who: payload.who });
2597
+ }
2598
+ }
2599
+ } else if (evt === "leave") {
2600
+ if (ps) {
2601
+ if (ps.known.has(payload.who)) {
2602
+ ps.known.delete(payload.who);
2603
+ if (ps.serverCount > 0) {
2604
+ ps.serverCount--;
2605
+ }
2606
+ this.emitter.emit("presenceLeave", { room: frame.room, who: payload.who });
2607
+ }
2608
+ }
2609
+ }
2610
+ }
2611
+ handleWelcome(frame, isReconnect) {
2612
+ this.currentConnWelcomeReceived = true;
2613
+ if (this.welcomeTimer) {
2614
+ this.clock.clearTimeout(this.welcomeTimer);
2615
+ this.welcomeTimer = null;
2616
+ }
2617
+ const previousCapabilities = this._capabilities;
2618
+ const previousVersion = this.serverVersion;
2619
+ const capabilitiesChanged = this._capabilities !== void 0 && (this.serverVersion !== frame.server_version || JSON.stringify(this._capabilities) !== JSON.stringify(frame.capabilities));
2620
+ this._selfLabel = frame.label;
2621
+ this._capabilities = frame.capabilities;
2622
+ this.serverVersion = frame.server_version;
2623
+ this.reconnectAttempts = 0;
2624
+ this.rttTracker.reset();
2625
+ if (capabilitiesChanged) {
2626
+ this.emitter.emit("capabilitiesChanged", {
2627
+ previous: previousCapabilities,
2628
+ current: frame.capabilities ?? {},
2629
+ previousVersion,
2630
+ currentVersion: frame.server_version
2631
+ });
2632
+ }
2633
+ this.emitter.emit("connected", { welcome: frame });
2634
+ this.writer.resume();
2635
+ const proceedToReady = () => {
2636
+ this.firstConnectDone = true;
2637
+ this.transitionState("ready");
2638
+ this.heartbeatManager.start();
2639
+ this.backpressureManager.start();
2640
+ if (this.pendingConnectResolve) {
2641
+ const resolve = this.pendingConnectResolve;
2642
+ this.pendingConnectResolve = null;
2643
+ this.pendingConnectReject = null;
2644
+ resolve(frame);
2645
+ }
2646
+ };
2647
+ if (isReconnect || this.firstConnectDone) {
2648
+ const discarded = this.discardOutboundQueues();
2649
+ this.appliedPolicies.clear();
2650
+ this.emitter.emit("queuesDiscarded", discarded);
2651
+ for (const room of this.intents.keys()) {
2652
+ if (!this.presenceManager.get(room)) {
2653
+ this.presenceManager.init(room, 0);
2654
+ }
2655
+ }
2656
+ const invalidatedRooms = this.presenceManager.invalidateAll();
2657
+ for (const room of invalidatedRooms) {
2658
+ this.emitter.emit("presenceInvalidated", { room, reason: "reconnected" });
2659
+ }
2660
+ this.seqTracker.rebaselineAll();
2661
+ this.isRestoringSession = true;
2662
+ this.restoreSession().finally(() => {
2663
+ this.isRestoringSession = false;
2664
+ if (this._state === "closed") return;
2665
+ proceedToReady();
2666
+ });
2667
+ } else {
2668
+ proceedToReady();
2669
+ }
2670
+ }
2671
+ handleWelcomeTimeout(isReconnect) {
2672
+ this.logger.warn("Welcome timeout exceeded");
2673
+ const timeoutErr = new RequestTimeoutError(
2674
+ `Timed out waiting for welcome frame (${this.config.welcomeTimeoutMs ?? 1e4}ms)`
2675
+ );
2676
+ if (!isReconnect && !this.firstConnectDone) {
2677
+ this.transitionState("closed");
2678
+ this.rejectPendingConnect(timeoutErr);
2679
+ try {
2680
+ this.transport.close(1e3, "welcome timeout");
2681
+ } catch {
2682
+ }
2683
+ } else {
2684
+ this.emitter.emit("error", { error: timeoutErr, phase: "connect" });
2685
+ try {
2686
+ this.transport.close(1008, "welcome timeout");
2687
+ } catch {
2688
+ }
2689
+ this.triggerReconnect();
2690
+ }
2691
+ }
2692
+ handlePreOpenFailure(err, isReconnect) {
2693
+ if (this._state === "closed" || this._state === "reconnecting") return;
2694
+ const connErr = err instanceof ConnectError ? err : new ConnectError("Failed to establish connection", err);
2695
+ const closeCode = connErr.closeCode ?? 1006;
2696
+ const reason = connErr.reason ?? "";
2697
+ this.classifyAndDispatch(closeCode, reason, connErr, isReconnect);
2698
+ }
2699
+ handleSocketClose(closeCode, reason, _wasClean, isReconnect) {
2700
+ if (this._state === "closed") return;
2701
+ if (this.welcomeTimer) {
2702
+ this.clock.clearTimeout(this.welcomeTimer);
2703
+ this.welcomeTimer = null;
2704
+ }
2705
+ this.heartbeatManager.stop();
2706
+ this.backpressureManager.stop();
2707
+ this.writer.stop();
2708
+ this.failPendingWaiters(
2709
+ new ConnectError(
2710
+ `Socket closed (code: ${closeCode}, reason: ${reason || "none"})`,
2711
+ { closeCode, reason }
2712
+ )
2713
+ );
2714
+ const connErr = new ConnectError(
2715
+ `WebSocket closed before welcome (code: ${closeCode}, reason: ${reason || "none"})`,
2716
+ { closeCode, reason }
2717
+ );
2718
+ if (!this.currentConnWelcomeReceived && !this.firstConnectDone) {
2719
+ this.classifyAndDispatch(closeCode, reason, connErr, isReconnect);
2720
+ return;
2721
+ }
2722
+ if (closeCode === 1008 && reason !== "heartbeat timeout" && reason !== "welcome timeout") {
2723
+ this.transitionState("closed");
2724
+ this.emitter.emit("fatal", {
2725
+ code: "POLICY_VIOLATION",
2726
+ message: reason || "Policy violation"
2727
+ });
2728
+ if (this.pendingConnectReject) {
2729
+ this.rejectPendingConnect(
2730
+ new PolicyViolationError(reason || "Policy violation", closeCode, reason)
2731
+ );
2732
+ }
2733
+ return;
2734
+ }
2735
+ if (this.config.reconnect?.shouldReconnect) {
2736
+ const allow = this.config.reconnect.shouldReconnect({
2737
+ closeCode,
2738
+ firstConnect: false
2739
+ });
2740
+ if (!allow) {
2741
+ this.transitionState("closed");
2742
+ this.emitter.emit("fatal", {
2743
+ code: "CLOSED",
2744
+ message: reason || "Reconnect denied by shouldReconnect"
2745
+ });
2746
+ if (this.pendingConnectReject) {
2747
+ this.rejectPendingConnect(
2748
+ new ConnectError(reason || "Reconnect denied by shouldReconnect", {
2749
+ closeCode,
2750
+ reason
2751
+ })
2752
+ );
2753
+ }
2754
+ return;
2755
+ }
2756
+ }
2757
+ this.emitter.emit("disconnected", {
2758
+ code: closeCode,
2759
+ reason,
2760
+ wasClean: closeCode === 1e3
2761
+ });
2762
+ this.triggerReconnect();
2763
+ }
2764
+ classifyAndDispatch(closeCode, reason, err, isReconnect) {
2765
+ let isFatal = false;
2766
+ let fatalCode = "AUTH_FAILED";
2767
+ if (closeCode === 1008) {
2768
+ isFatal = true;
2769
+ fatalCode = "POLICY_VIOLATION";
2770
+ } else if (this.config.onAmbiguousAuth) {
2771
+ const decision = this.config.onAmbiguousAuth(closeCode, reason);
2772
+ isFatal = decision === "fatal";
2773
+ } else {
2774
+ const mode = this.config.authFailureMode ?? "heuristic";
2775
+ if (mode === "always-fatal") {
2776
+ isFatal = true;
2777
+ } else if (mode === "never-fatal") {
2778
+ isFatal = false;
2779
+ } else {
2780
+ isFatal = closeCode === 1006;
2781
+ }
2782
+ }
2783
+ if (this.config.reconnect?.shouldReconnect) {
2784
+ const allow = this.config.reconnect.shouldReconnect({
2785
+ closeCode,
2786
+ firstConnect: !this.firstConnectDone
2787
+ });
2788
+ if (!allow) {
2789
+ isFatal = true;
2790
+ fatalCode = "CLOSED";
2791
+ }
2792
+ }
2793
+ if (isFatal) {
2794
+ if (fatalCode === "AUTH_FAILED") {
2795
+ this.emitter.emit("errorClassified", {
2796
+ code: "UNAUTHORIZED",
2797
+ message: "browser auth heuristic classified pre-welcome 1006 as auth failure",
2798
+ action: "fatal",
2799
+ rawFrame: void 0
2800
+ });
2801
+ }
2802
+ this.transitionState("closed");
2803
+ this.emitter.emit("fatal", {
2804
+ code: fatalCode,
2805
+ message: fatalCode === "AUTH_FAILED" ? "Authentication failed (pre-upgrade rejection)" : reason || "Connection halted"
2806
+ });
2807
+ const outErr = fatalCode === "AUTH_FAILED" ? new AuthError("Authentication failed (pre-upgrade rejection)") : fatalCode === "POLICY_VIOLATION" ? new PolicyViolationError(reason || "Policy violation", closeCode, reason) : new ConnectError(reason || "Connection rejected", { closeCode, reason });
2808
+ this.rejectPendingConnect(outErr);
2809
+ return;
2810
+ }
2811
+ this.emitter.emit("error", { error: err, phase: isReconnect ? "runtime" : "connect" });
2812
+ this.triggerReconnect();
2813
+ }
2814
+ triggerReconnect() {
2815
+ if (this._state === "closed") return;
2816
+ this.transitionState("reconnecting");
2817
+ const maxAttempts = this.config.reconnect?.maxAttempts ?? Infinity;
2818
+ if (this.reconnectAttempts >= maxAttempts) {
2819
+ this.transitionState("closed");
2820
+ const msg = `Exceeded maximum reconnect attempts (${maxAttempts})`;
2821
+ this.emitter.emit("fatal", { code: "CLOSED", message: msg });
2822
+ this.rejectPendingConnect(new ConnectError(msg));
2823
+ return;
2824
+ }
2825
+ const attemptNumber = this.reconnectAttempts + 1;
2826
+ const delayMs = computeReconnectDelay(
2827
+ this.reconnectAttempts,
2828
+ this.config.reconnect
2829
+ );
2830
+ this.emitter.emit("reconnecting", { attempt: attemptNumber, delayMs });
2831
+ this.reconnectTimer = this.clock.setTimeout(() => {
2832
+ this.reconnectTimer = null;
2833
+ if (this._state === "closed") return;
2834
+ this.emitter.emit("reconnecting", { attempt: attemptNumber, delayMs: 0 });
2835
+ this.reconnectAttempts++;
2836
+ this.initiateConnect(true);
2837
+ }, delayMs);
2838
+ }
2839
+ async restoreSession() {
2840
+ const intentsList = Array.from(this.intents.values()).sort(
2841
+ (a, b) => a.room.localeCompare(b.room)
2842
+ );
2843
+ const restored = [];
2844
+ const failed = [];
2845
+ const roomFullPolicy = this.config.restore?.roomFullPolicy ?? "drop";
2846
+ const perRoomTimeout = this.config.restore?.restorePerRoomTimeoutMs ?? 5e3;
2847
+ for (const intent of intentsList) {
2848
+ if (this.isClosed()) break;
2849
+ const maxAttempts = roomFullPolicy === "retry" ? 3 : 1;
2850
+ let outcome = "drop";
2851
+ let outcomeCode = "INTERNAL_ERROR";
2852
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
2853
+ if (this.isClosed()) break;
2854
+ if (attempt > 0) {
2855
+ const backoff = 50 * Math.pow(2, attempt);
2856
+ await new Promise((res) => {
2857
+ const t = this.clock.setTimeout(res, backoff);
2858
+ if (this.isClosed()) {
2859
+ this.clock.clearTimeout(t);
2860
+ res();
2861
+ }
2862
+ });
2863
+ }
2864
+ const joinReq = {
2865
+ type: "join_room",
2866
+ room: intent.room,
2867
+ passcode: intent.passcode,
2868
+ binary_channels: intent.binaryChannels
2869
+ };
2870
+ try {
2871
+ const res = await this.sendWithCorrelation(
2872
+ "room_joined",
2873
+ intent.room,
2874
+ joinReq,
2875
+ perRoomTimeout
2876
+ );
2877
+ outcome = "joined";
2878
+ this.presenceManager.init(intent.room, res.members);
2879
+ if (intent.binaryChannels) {
2880
+ for (const [ch, id] of Object.entries(intent.binaryChannels)) {
2881
+ this.binaryOwner.set(id, { room: intent.room, channel: ch });
2882
+ }
2883
+ }
2884
+ break;
2885
+ } catch (err) {
2886
+ if (err instanceof ServerError) {
2887
+ outcomeCode = err.code;
2888
+ if (err.code === "ROOM_NOT_FOUND" || err.code === "FORBIDDEN_SCOPE") {
2889
+ outcome = "drop";
2890
+ break;
2891
+ }
2892
+ if (err.code === "INVALID_PASSCODE" || err.code === "PASSCODE_RATE_LIMITED") {
2893
+ outcome = "keep";
2894
+ break;
2895
+ }
2896
+ if (err.code === "ROOM_FULL") {
2897
+ if (roomFullPolicy === "retry" && attempt < maxAttempts - 1) {
2898
+ continue;
2899
+ }
2900
+ outcome = roomFullPolicy === "drop" ? "drop" : "keep";
2901
+ break;
2902
+ }
2903
+ outcome = "keep";
2904
+ break;
2905
+ } else {
2906
+ outcome = "keep";
2907
+ outcomeCode = "INTERNAL_ERROR";
2908
+ break;
2909
+ }
2910
+ }
2911
+ }
2912
+ if (outcome === "joined") {
2913
+ restored.push(intent.room);
2914
+ } else {
2915
+ if (outcome === "drop") {
2916
+ this.intents.delete(intent.room);
2917
+ this.clearRoomState(intent.room);
2918
+ }
2919
+ failed.push({ room: intent.room, code: outcomeCode });
2920
+ this.emitter.emit("intentFailed", { room: intent.room, code: outcomeCode });
2921
+ }
2922
+ }
2923
+ this.emitter.emit("sessionRestored", { restored, failed });
2924
+ }
2925
+ sendWithCorrelation(responseType, room, frame, customTimeoutMs) {
2926
+ const key = `${responseType}:${room}`;
2927
+ if (this.waiters.has(key)) {
2928
+ return Promise.reject(
2929
+ new ConcurrentRequestError(
2930
+ `Concurrent request for ${responseType} on room "${room}" is not permitted`
2931
+ )
2932
+ );
2933
+ }
2934
+ if (this._state !== "ready" && !this.isRestoringSession) {
2935
+ return Promise.reject(new SdkError(`Client not ready (state: ${this._state})`));
2936
+ }
2937
+ const timeoutMs = customTimeoutMs ?? this.config.requestTimeoutMs ?? 1e4;
2938
+ return new Promise((resolve, reject) => {
2939
+ const timer = this.clock.setTimeout(() => {
2940
+ this.waiters.delete(key);
2941
+ reject(
2942
+ new RequestTimeoutError(
2943
+ `Timed out waiting for ${responseType} on "${room}"`
2944
+ )
2945
+ );
2946
+ }, timeoutMs);
2947
+ this.waiters.set(key, {
2948
+ resolve: (val) => resolve(val),
2949
+ reject,
2950
+ timer,
2951
+ room,
2952
+ responseType
2953
+ });
2954
+ try {
2955
+ this.send(frame);
2956
+ } catch (err) {
2957
+ this.clock.clearTimeout(timer);
2958
+ this.waiters.delete(key);
2959
+ reject(err);
2960
+ }
2961
+ });
2962
+ }
2963
+ handleErrorFrame(frame) {
2964
+ const classification = classifyError(
2965
+ frame.code,
2966
+ frame,
2967
+ this.config.errorPolicy?.overrides
2968
+ );
2969
+ this.emitter.emit("errorClassified", {
2970
+ code: frame.code,
2971
+ message: frame.message,
2972
+ room: frame.room,
2973
+ channel: frame.channel,
2974
+ action: classification.action,
2975
+ retryAfterMs: classification.retryAfterMs,
2976
+ rawFrame: frame
2977
+ });
2978
+ if (frame.code === "RATE_LIMITED" && frame.channel) {
2979
+ const channel = frame.channel;
2980
+ const retryAfterMs = frame.retry_after_ms ?? 250;
2981
+ let room = frame.room;
2982
+ if (!room) {
2983
+ room = this.rateLimiters.getRecentRoom(channel) ?? "";
2984
+ }
2985
+ const untilMs = this.clock.now() + retryAfterMs;
2986
+ this.rateLimiters.pause(room, channel, untilMs);
2987
+ this.emitter.emit("rateLimited", {
2988
+ source: "server",
2989
+ room,
2990
+ channel,
2991
+ retryAfterMs
2992
+ });
2993
+ }
2994
+ const serverErr = new ServerError(
2995
+ frame.code,
2996
+ frame.message,
2997
+ frame.channel,
2998
+ frame.retry_after_ms
2999
+ );
3000
+ this.emitter.emit("error", { error: serverErr, phase: "runtime" });
3001
+ let matchedKey = null;
3002
+ if (frame.room) {
3003
+ for (const [key, waiter] of this.waiters.entries()) {
3004
+ if (waiter.room === frame.room) {
3005
+ matchedKey = key;
3006
+ break;
3007
+ }
3008
+ }
3009
+ }
3010
+ if (!matchedKey) {
3011
+ const extractedRoom = extractRoomFromMessage(frame.message);
3012
+ if (extractedRoom) {
3013
+ for (const [key, waiter] of this.waiters.entries()) {
3014
+ if (waiter.room === extractedRoom) {
3015
+ matchedKey = key;
3016
+ break;
3017
+ }
3018
+ }
3019
+ }
3020
+ }
3021
+ if (!matchedKey && this.waiters.size === 1) {
3022
+ matchedKey = Array.from(this.waiters.keys())[0];
3023
+ }
3024
+ if (matchedKey) {
3025
+ const waiter = this.waiters.get(matchedKey);
3026
+ this.clock.clearTimeout(waiter.timer);
3027
+ this.waiters.delete(matchedKey);
3028
+ waiter.reject(serverErr);
3029
+ }
3030
+ }
3031
+ failPendingWaiters(err) {
3032
+ for (const [key, waiter] of this.waiters.entries()) {
3033
+ this.clock.clearTimeout(waiter.timer);
3034
+ this.waiters.delete(key);
3035
+ waiter.reject(err);
3036
+ }
3037
+ }
3038
+ clearTimers() {
3039
+ if (this.reconnectTimer) {
3040
+ this.clock.clearTimeout(this.reconnectTimer);
3041
+ this.reconnectTimer = null;
3042
+ }
3043
+ if (this.welcomeTimer) {
3044
+ this.clock.clearTimeout(this.welcomeTimer);
3045
+ this.welcomeTimer = null;
3046
+ }
3047
+ }
3048
+ rejectPendingConnect(err) {
3049
+ if (this.pendingConnectReject) {
3050
+ const reject = this.pendingConnectReject;
3051
+ this.pendingConnectResolve = null;
3052
+ this.pendingConnectReject = null;
3053
+ reject(err);
3054
+ }
3055
+ }
3056
+ };
3057
+
3058
+ export { AuthError, BackpressureManager, BinaryChannelUnmappedError, BinaryIdCollisionError, BinaryMapBuilder, BinaryMapExhaustedError, BinaryMapValidationError, BinaryPayloadTooLargeError, BrowserWebSocketTransport, CHANNEL_NAME_RE, CapabilityLimitError, ChannelNotReliableError, Client, ConcurrentRequestError, ConnectError, NotReadyError, OutboundQueue, PolicyViolationError, PresenceManager, ROOM_NAME_RE, RateLimitedError, RateLimiterRegistry, RealClock, RequestTimeoutError, RttTracker, SdkError, SeqTracker, ServerError, TokenBucket, ValidationError, buildWsUrl, checkCapabilityPreflight, classifyError, computeReconnectDelay, isErrorFrame, isMessageFrame, isWelcomeFrame, presetsFor, redactString, redactUrl, redactValue, resolvePolicy, validateBinaryMap, validateBinaryPayloadSize, validateChannelName, validateRoomName, validateTextPayloadSize };
3059
+ //# sourceMappingURL=index.js.map
3060
+ //# sourceMappingURL=index.js.map