@finalai/websockets 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @finalai/websockets might be problematic. Click here for more details.

@@ -0,0 +1,1432 @@
1
+ import {
2
+ compareWebsocketGatewayCursors,
3
+ websocketGatewayDataSchema,
4
+ websocketGatewayProtocolVersion,
5
+ websocketGatewayServerFrameSchema
6
+ } from "@finalai/websocket-gateway-protocol";
7
+ import { WebSocket as PartySocketWebSocket } from "partysocket";
8
+ const openReadyState = 1;
9
+ const defaultAckTimeoutMs = 3e4;
10
+ const defaultMaxQueuedFrames = 256;
11
+ const randomId = () => typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
12
+ const bytesToBase64 = (bytes) => {
13
+ let binary = "";
14
+ const chunkSize = 32768;
15
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
16
+ const chunk = bytes.subarray(offset, offset + chunkSize);
17
+ binary += String.fromCharCode(...chunk);
18
+ }
19
+ return btoa(binary);
20
+ };
21
+ const base64ToArrayBuffer = (data) => {
22
+ const binary = atob(data);
23
+ const bytes = new Uint8Array(binary.length);
24
+ for (let index = 0; index < binary.length; index += 1) {
25
+ bytes[index] = binary.charCodeAt(index);
26
+ }
27
+ return bytes.buffer;
28
+ };
29
+ const encodeProxyPayload = (data) => {
30
+ if (typeof data === "string") return data;
31
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
32
+ return { encoding: "base64", data: bytesToBase64(bytes) };
33
+ };
34
+ const decodeProxyPayload = (data) => typeof data === "string" ? data : base64ToArrayBuffer(data.data);
35
+ const resolveAuth = async (auth) => typeof auth === "function" ? auth() : auth;
36
+ const compareCursors = compareWebsocketGatewayCursors;
37
+ const utf8ByteLength = (value) => {
38
+ let bytes = 0;
39
+ for (let index = 0; index < value.length; index += 1) {
40
+ const codePoint = value.charCodeAt(index);
41
+ if (codePoint < 128) {
42
+ bytes += 1;
43
+ } else if (codePoint < 2048) {
44
+ bytes += 2;
45
+ } else if (codePoint >= 55296 && codePoint <= 56319 && index + 1 < value.length) {
46
+ const trailing = value.charCodeAt(index + 1);
47
+ if (trailing >= 56320 && trailing <= 57343) {
48
+ bytes += 4;
49
+ index += 1;
50
+ } else {
51
+ bytes += 3;
52
+ }
53
+ } else {
54
+ bytes += 3;
55
+ }
56
+ }
57
+ return bytes;
58
+ };
59
+ const maxBufferedChannelFrames = 1e4;
60
+ class WebsocketsConnection {
61
+ constructor(options) {
62
+ this.options = options;
63
+ }
64
+ options;
65
+ socket = null;
66
+ handlers = /* @__PURE__ */ new Set();
67
+ replayRegistrations = /* @__PURE__ */ new Map();
68
+ heartbeatTimer = null;
69
+ channelSubscriptions = /* @__PURE__ */ new Map();
70
+ realtimeSubscriptions = /* @__PURE__ */ new Map();
71
+ realtimeSubscribeQueue = /* @__PURE__ */ new Set();
72
+ realtimeSubscribeQueued = false;
73
+ realtimeUnsubscribeQueue = /* @__PURE__ */ new Set();
74
+ realtimeUnsubscribeQueued = false;
75
+ socketAuthToken = null;
76
+ authenticated = false;
77
+ queuedFrames = [];
78
+ queuedBytes = 0;
79
+ manuallyClosed = false;
80
+ hasAuthenticated = false;
81
+ diagnostics = {
82
+ bufferedBytes: 0,
83
+ bufferedBytesHighWater: 0,
84
+ bufferedFrames: 0,
85
+ bufferedFramesHighWater: 0,
86
+ bufferLimitFlushes: 0,
87
+ channelMessageBytes: 0,
88
+ channelMessageFrames: 0,
89
+ channelStreams: [],
90
+ channelSubscriptions: 0,
91
+ gapFillErrors: 0,
92
+ gapFillRequests: 0,
93
+ gapFillResponses: 0,
94
+ gapFillSendErrors: 0,
95
+ gapFillTimeouts: 0,
96
+ frameHandlers: 0,
97
+ frameHandlersHighWater: 0,
98
+ lastCloseCode: null,
99
+ lastCloseReason: null,
100
+ pendingGapFills: 0,
101
+ pendingSubscribeAcks: 0,
102
+ queuedBytes: 0,
103
+ queuedFrames: 0,
104
+ realtimeMessageBytes: 0,
105
+ realtimeMessageFrames: 0,
106
+ realtimeSubscriptions: 0,
107
+ receivedBytes: 0,
108
+ receivedFrames: 0,
109
+ reconnects: 0,
110
+ rpcCallBytes: 0,
111
+ rpcCallFrames: 0,
112
+ sentBytes: 0,
113
+ sentFrames: 0,
114
+ socketBufferedBytes: 0,
115
+ socketBufferedBytesHighWater: 0,
116
+ socketCloses: 0,
117
+ socketOpens: 0,
118
+ subscribeAcks: 0,
119
+ subscribeErrors: 0,
120
+ subscribeRequests: 0,
121
+ subscribeSuperseded: 0,
122
+ subscribeTimeouts: 0
123
+ };
124
+ async connect() {
125
+ this.manuallyClosed = false;
126
+ await this.openSocket();
127
+ }
128
+ async openSocket() {
129
+ const WebSocketCtor = this.options.WebSocketCtor ?? globalThis.WebSocket;
130
+ if (WebSocketCtor == null) {
131
+ throw new Error("WebSocket is not available in this runtime");
132
+ }
133
+ const socket = new PartySocketWebSocket(() => this.resolveSocketUrlAndAuth(), void 0, {
134
+ WebSocket: WebSocketCtor,
135
+ minReconnectionDelay: 500,
136
+ maxReconnectionDelay: 15e3,
137
+ reconnectionDelayGrowFactor: 2,
138
+ maxRetries: this.options.reconnect === false ? 0 : Infinity
139
+ });
140
+ this.socket = socket;
141
+ socket.addEventListener("message", (event) => {
142
+ const rawText = String(event.data);
143
+ let raw;
144
+ try {
145
+ raw = JSON.parse(rawText);
146
+ } catch {
147
+ return;
148
+ }
149
+ const parsed = websocketGatewayServerFrameSchema.safeParse(raw);
150
+ if (!parsed.success) return;
151
+ const frameByteLength = utf8ByteLength(rawText);
152
+ this.diagnostics.receivedFrames += 1;
153
+ this.diagnostics.receivedBytes += frameByteLength;
154
+ if (parsed.data.type === "channel.message") {
155
+ this.diagnostics.channelMessageFrames += 1;
156
+ this.diagnostics.channelMessageBytes += frameByteLength;
157
+ }
158
+ if (parsed.data.type === "realtime.message") {
159
+ this.diagnostics.realtimeMessageFrames += 1;
160
+ this.diagnostics.realtimeMessageBytes += frameByteLength;
161
+ }
162
+ for (const frame of this.routeIncomingFrame(parsed.data, frameByteLength)) {
163
+ for (const handler of this.handlers) handler(frame);
164
+ }
165
+ });
166
+ socket.addEventListener("close", (event) => {
167
+ this.clearHeartbeat();
168
+ this.authenticated = false;
169
+ this.diagnostics.socketCloses += 1;
170
+ this.diagnostics.lastCloseCode = event.code;
171
+ this.diagnostics.lastCloseReason = event.reason;
172
+ });
173
+ await new Promise((resolve, reject) => {
174
+ let settled = false;
175
+ socket.addEventListener("open", () => {
176
+ this.diagnostics.socketOpens += 1;
177
+ this.authenticated = false;
178
+ void this.authenticateInitialSocket().then(() => {
179
+ this.authenticated = true;
180
+ this.startHeartbeat();
181
+ this.resubscribeChannels();
182
+ this.resubscribeRealtimeTopics();
183
+ this.replayRegistrations.forEach((replay) => replay());
184
+ this.flushQueuedFrames();
185
+ if (this.hasAuthenticated) {
186
+ this.diagnostics.reconnects += 1;
187
+ this.options.onReconnect?.();
188
+ }
189
+ this.hasAuthenticated = true;
190
+ if (!settled) {
191
+ settled = true;
192
+ resolve();
193
+ }
194
+ }).catch((cause) => {
195
+ this.clearHeartbeat();
196
+ this.authenticated = false;
197
+ if (!settled && this.socket === socket) {
198
+ this.socket = null;
199
+ }
200
+ if (!settled) {
201
+ socket.close(4001, "websockets auth failed");
202
+ settled = true;
203
+ reject(cause instanceof Error ? cause : new Error("Websockets auth failed"));
204
+ } else {
205
+ socket.reconnect(4001, "websockets auth failed");
206
+ }
207
+ });
208
+ });
209
+ socket.addEventListener("error", () => {
210
+ if (!settled) {
211
+ settled = true;
212
+ reject(new Error("Websockets open failed"));
213
+ }
214
+ });
215
+ });
216
+ }
217
+ async resolveSocketUrlAndAuth() {
218
+ this.socketAuthToken = await resolveAuth(this.options.auth);
219
+ const url = new URL(this.options.url);
220
+ url.searchParams.set("protocolVersion", String(websocketGatewayProtocolVersion));
221
+ return url.toString();
222
+ }
223
+ authenticateInitialSocket() {
224
+ const token = this.socketAuthToken;
225
+ this.socketAuthToken = null;
226
+ if (token == null) {
227
+ return Promise.reject(new Error("Websockets auth token was not resolved"));
228
+ }
229
+ return this.auth.authorize({ token });
230
+ }
231
+ startHeartbeat() {
232
+ this.clearHeartbeat();
233
+ const intervalMs = this.options.heartbeatIntervalMs;
234
+ if (intervalMs == null || intervalMs <= 0) return;
235
+ this.heartbeatTimer = setInterval(() => {
236
+ if (this.socket == null || this.socket.readyState !== openReadyState) return;
237
+ this.send({ type: "ping", nonce: randomId() });
238
+ }, intervalMs);
239
+ }
240
+ clearHeartbeat() {
241
+ if (this.heartbeatTimer == null) return;
242
+ clearInterval(this.heartbeatTimer);
243
+ this.heartbeatTimer = null;
244
+ }
245
+ // Per-channel frame routing: dedupes replay/live overlap by cursor, buffers
246
+ // live frames until the subscribe ack so replay dispatches first, and
247
+ // gap-fills skipped messages when a frame's prevCursor doesn't chain.
248
+ routeIncomingFrame(frame, frameByteLength) {
249
+ if (frame.type === "realtime.subscribed") {
250
+ const state = this.realtimeSubscriptions.get(frame.topic);
251
+ if (state != null) {
252
+ state.pendingSubscribe = false;
253
+ if (frame.resyncRequired) state.lastCursor = void 0;
254
+ if (frame.cursor != null) state.lastCursor = frame.cursor;
255
+ if (frame.resyncRequired) state.onResyncRequired?.();
256
+ state.onSubscribed?.();
257
+ const buffered = this.settleSubscriptionBuffer(state, []);
258
+ queueMicrotask(() => {
259
+ if (this.realtimeSubscriptions.get(frame.topic) !== state) return;
260
+ this.dispatchRealtimeDeliveries(state, buffered);
261
+ });
262
+ }
263
+ return [frame];
264
+ }
265
+ if (frame.type === "realtime.error") {
266
+ this.realtimeSubscriptions.get(frame.topic)?.onError?.({
267
+ code: frame.code,
268
+ message: frame.message,
269
+ retryable: frame.retryable
270
+ });
271
+ return [frame];
272
+ }
273
+ if (frame.type === "realtime.history") {
274
+ const state = this.realtimeSubscriptions.get(frame.topic);
275
+ if (state != null && state.gapFillRequestId === frame.requestId) {
276
+ this.diagnostics.gapFillResponses += 1;
277
+ this.clearSubscriptionGapFill(state);
278
+ this.dispatchRealtimeDeliveries(
279
+ state,
280
+ this.settleSubscriptionBuffer(
281
+ state,
282
+ frame.messages.map((message) => ({ message }))
283
+ )
284
+ );
285
+ }
286
+ return [frame];
287
+ }
288
+ if (frame.type === "realtime.presence.event") return [frame];
289
+ if (frame.type === "realtime.message") {
290
+ const state = this.realtimeSubscriptions.get(frame.topic);
291
+ if (state == null) return [frame];
292
+ if (state.pendingSubscribe || state.gapFillRequestId != null) {
293
+ this.bufferSubscriptionDelivery(state, frame, frameByteLength);
294
+ if (state.buffered.length >= maxBufferedChannelFrames) {
295
+ this.diagnostics.bufferLimitFlushes += 1;
296
+ state.pendingSubscribe = false;
297
+ state.onResyncRequired?.();
298
+ this.dispatchRealtimeDeliveries(state, this.settleSubscriptionBuffer(state, []));
299
+ }
300
+ return [];
301
+ }
302
+ const cursor = frame.message.cursor;
303
+ if (frame.prevCursor != null && state.lastCursor != null && cursor != null && frame.prevCursor !== state.lastCursor && compareCursors(frame.prevCursor, state.lastCursor) > 0) {
304
+ this.beginRealtimeGapFill(state, frame, frameByteLength);
305
+ return [];
306
+ }
307
+ const dispatchable = this.dispatchableSubscriptionDeliveries(state, [frame]);
308
+ this.dispatchRealtimeDeliveries(state, dispatchable);
309
+ return dispatchable.length === 0 ? [] : [frame];
310
+ }
311
+ if (frame.type === "channel.message") {
312
+ const state = this.channelSubscriptions.get(frame.message.channel);
313
+ if (state == null) return [frame];
314
+ state.receivedFrames += 1;
315
+ state.receivedBytes += frameByteLength;
316
+ if (state.pendingAckId != null || state.gapFillRequestId != null) {
317
+ this.bufferSubscriptionDelivery(state, frame, frameByteLength);
318
+ if (state.buffered.length >= maxBufferedChannelFrames) {
319
+ this.diagnostics.bufferLimitFlushes += 1;
320
+ return this.channelFrames(this.settleSubscriptionBuffer(state, []));
321
+ }
322
+ return [];
323
+ }
324
+ if (frame.prevCursor != null && state.lastCursor != null && frame.message.cursor != null && frame.prevCursor !== state.lastCursor && compareCursors(frame.prevCursor, state.lastCursor) > 0) {
325
+ this.beginChannelGapFill(frame.message.channel, state, frame, frameByteLength);
326
+ return [];
327
+ }
328
+ return this.channelFrames(this.dispatchableSubscriptionDeliveries(state, [frame]));
329
+ }
330
+ if (frame.type === "ack" || frame.type === "error") {
331
+ const state = [...this.channelSubscriptions.values()].find(
332
+ (candidate) => candidate.pendingAckId != null && candidate.pendingAckId === frame.requestId
333
+ );
334
+ if (state != null) {
335
+ if (frame.type === "error") {
336
+ this.diagnostics.subscribeErrors += 1;
337
+ } else {
338
+ this.diagnostics.subscribeAcks += 1;
339
+ }
340
+ state.pendingAckId = null;
341
+ if (state.pendingAckTimer != null) {
342
+ clearTimeout(state.pendingAckTimer);
343
+ state.pendingAckTimer = null;
344
+ }
345
+ const settled = this.channelFrames(this.settleSubscriptionBuffer(state, []));
346
+ if (frame.type === "ack" && frame.cursor != null && state.lastCursor == null) {
347
+ state.lastCursor = frame.cursor;
348
+ }
349
+ if (frame.type === "error") {
350
+ state.onError?.(frame);
351
+ } else {
352
+ state.onSubscribed?.();
353
+ }
354
+ return [...settled, frame];
355
+ }
356
+ if (frame.type === "error") {
357
+ const realtimeGapFillState = [...this.realtimeSubscriptions.values()].find(
358
+ (candidate) => candidate.gapFillRequestId === frame.requestId
359
+ );
360
+ if (realtimeGapFillState != null) {
361
+ this.diagnostics.gapFillErrors += 1;
362
+ this.clearSubscriptionGapFill(realtimeGapFillState);
363
+ this.dispatchRealtimeDeliveries(
364
+ realtimeGapFillState,
365
+ this.settleSubscriptionBuffer(realtimeGapFillState, [])
366
+ );
367
+ realtimeGapFillState.onError?.({
368
+ code: frame.code,
369
+ message: frame.message,
370
+ retryable: true
371
+ });
372
+ return [frame];
373
+ }
374
+ const gapFillState = [...this.channelSubscriptions.values()].find(
375
+ (candidate) => candidate.gapFillRequestId != null && candidate.gapFillRequestId === frame.requestId
376
+ );
377
+ if (gapFillState != null) {
378
+ this.diagnostics.gapFillErrors += 1;
379
+ this.clearSubscriptionGapFill(gapFillState);
380
+ const settled = this.channelFrames(this.settleSubscriptionBuffer(gapFillState, []));
381
+ gapFillState.onError?.(frame);
382
+ return [...settled, frame];
383
+ }
384
+ }
385
+ return [frame];
386
+ }
387
+ if (frame.type === "channel.history") {
388
+ const state = [...this.channelSubscriptions.values()].find(
389
+ (candidate) => candidate.gapFillRequestId != null && candidate.gapFillRequestId === frame.requestId
390
+ );
391
+ if (state == null) return [frame];
392
+ this.diagnostics.gapFillResponses += 1;
393
+ this.clearSubscriptionGapFill(state);
394
+ return this.channelFrames(
395
+ this.settleSubscriptionBuffer(
396
+ state,
397
+ frame.messages.map((message) => ({ message }))
398
+ )
399
+ );
400
+ }
401
+ return [frame];
402
+ }
403
+ dispatchableSubscriptionDeliveries(state, deliveries) {
404
+ const dispatchable = [];
405
+ for (const delivery of deliveries) {
406
+ const cursor = delivery.message.cursor;
407
+ if (cursor != null) {
408
+ if (state.lastCursor != null && compareCursors(cursor, state.lastCursor) <= 0) continue;
409
+ state.lastCursor = cursor;
410
+ }
411
+ dispatchable.push(delivery);
412
+ }
413
+ return dispatchable;
414
+ }
415
+ settleSubscriptionBuffer(state, fetched) {
416
+ const buffered = state.buffered;
417
+ state.buffered = [];
418
+ state.bufferedBytes = 0;
419
+ const retained = [
420
+ ...fetched.filter((delivery) => delivery.message.cursor != null),
421
+ ...buffered.filter((delivery) => delivery.message.cursor != null)
422
+ ].sort(
423
+ (left, right) => compareCursors(left.message.cursor ?? "0-0", right.message.cursor ?? "0-0")
424
+ );
425
+ const ephemeral = buffered.filter((delivery) => delivery.message.cursor == null);
426
+ return this.dispatchableSubscriptionDeliveries(state, [...retained, ...ephemeral]);
427
+ }
428
+ bufferSubscriptionDelivery(state, delivery, byteLength) {
429
+ state.buffered.push(delivery);
430
+ state.bufferedBytes += byteLength;
431
+ state.bufferedFramesHighWater = Math.max(state.bufferedFramesHighWater, state.buffered.length);
432
+ state.bufferedBytesHighWater = Math.max(state.bufferedBytesHighWater, state.bufferedBytes);
433
+ const diagnostics = this.currentBufferDiagnostics();
434
+ this.diagnostics.bufferedFramesHighWater = Math.max(
435
+ this.diagnostics.bufferedFramesHighWater,
436
+ diagnostics.frames
437
+ );
438
+ this.diagnostics.bufferedBytesHighWater = Math.max(
439
+ this.diagnostics.bufferedBytesHighWater,
440
+ diagnostics.bytes
441
+ );
442
+ }
443
+ currentBufferDiagnostics() {
444
+ let bytes = 0;
445
+ let frames = 0;
446
+ for (const state of this.channelSubscriptions.values()) {
447
+ bytes += state.bufferedBytes;
448
+ frames += state.buffered.length;
449
+ }
450
+ for (const state of this.realtimeSubscriptions.values()) {
451
+ bytes += state.bufferedBytes;
452
+ frames += state.buffered.length;
453
+ }
454
+ return { bytes, frames };
455
+ }
456
+ clearSubscriptionGapFill(state) {
457
+ state.gapFillRequestId = null;
458
+ if (state.gapFillTimer != null) clearTimeout(state.gapFillTimer);
459
+ state.gapFillTimer = null;
460
+ }
461
+ beginSubscriptionGapFill(state, delivery, deliveryByteLength, sendHistory, onUnavailable) {
462
+ const requestId = randomId();
463
+ this.diagnostics.gapFillRequests += 1;
464
+ state.gapFillRequestId = requestId;
465
+ this.bufferSubscriptionDelivery(state, delivery, deliveryByteLength);
466
+ const recoverWithoutHistory = () => {
467
+ if (state.gapFillRequestId !== requestId) return;
468
+ this.clearSubscriptionGapFill(state);
469
+ onUnavailable(this.settleSubscriptionBuffer(state, []));
470
+ };
471
+ state.gapFillTimer = setTimeout(() => {
472
+ this.diagnostics.gapFillTimeouts += 1;
473
+ recoverWithoutHistory();
474
+ }, this.options.ackTimeoutMs ?? defaultAckTimeoutMs);
475
+ try {
476
+ sendHistory(requestId);
477
+ } catch {
478
+ this.diagnostics.gapFillSendErrors += 1;
479
+ recoverWithoutHistory();
480
+ }
481
+ }
482
+ channelFrames(deliveries) {
483
+ return deliveries.map((delivery) => ({ type: "channel.message", ...delivery }));
484
+ }
485
+ dispatchRealtimeDeliveries(state, deliveries) {
486
+ for (const delivery of deliveries) state.handler(delivery.message);
487
+ }
488
+ beginRealtimeGapFill(state, frame, frameByteLength) {
489
+ this.beginSubscriptionGapFill(
490
+ state,
491
+ frame,
492
+ frameByteLength,
493
+ (requestId) => {
494
+ this.send({
495
+ type: "realtime.history",
496
+ id: requestId,
497
+ topic: state.topic,
498
+ afterCursor: state.lastCursor,
499
+ limit: 1e3
500
+ });
501
+ },
502
+ (deliveries) => {
503
+ this.dispatchRealtimeDeliveries(state, deliveries);
504
+ state.onResyncRequired?.();
505
+ }
506
+ );
507
+ }
508
+ beginChannelGapFill(channel, state, frame, frameByteLength) {
509
+ this.beginSubscriptionGapFill(
510
+ state,
511
+ frame,
512
+ frameByteLength,
513
+ (requestId) => {
514
+ this.send({
515
+ type: "channel.history",
516
+ id: requestId,
517
+ channel,
518
+ afterCursor: state.lastCursor,
519
+ limit: 1e3
520
+ });
521
+ },
522
+ (deliveries) => {
523
+ for (const flushed of this.channelFrames(deliveries)) {
524
+ for (const handler of this.handlers) handler(flushed);
525
+ }
526
+ }
527
+ );
528
+ }
529
+ resubscribeChannels() {
530
+ for (const [channel, subscription] of this.channelSubscriptions) {
531
+ this.sendSubscribeFrame(channel, subscription);
532
+ }
533
+ }
534
+ resubscribeRealtimeTopics() {
535
+ this.realtimeSubscribeQueue.clear();
536
+ this.realtimeSubscribeQueued = false;
537
+ this.realtimeUnsubscribeQueue.clear();
538
+ this.realtimeUnsubscribeQueued = false;
539
+ this.queuedFrames = this.queuedFrames.filter(
540
+ (queued) => queued.frame.type !== "realtime.subscribe" && queued.frame.type !== "realtime.unsubscribe"
541
+ );
542
+ this.queuedBytes = this.queuedFrames.reduce((total, queued) => total + queued.sizeBytes, 0);
543
+ this.sendRealtimeSubscribeBatches([...this.realtimeSubscriptions.values()]);
544
+ }
545
+ queueRealtimeSubscribe(topic) {
546
+ this.realtimeSubscribeQueue.add(topic);
547
+ if (this.realtimeSubscribeQueued) return;
548
+ this.realtimeSubscribeQueued = true;
549
+ queueMicrotask(() => {
550
+ this.realtimeSubscribeQueued = false;
551
+ const states = [...this.realtimeSubscribeQueue].flatMap((queuedTopic) => {
552
+ const state = this.realtimeSubscriptions.get(queuedTopic);
553
+ return state == null ? [] : [state];
554
+ });
555
+ this.realtimeSubscribeQueue.clear();
556
+ if (states.length > 0 && !this.manuallyClosed) this.sendRealtimeSubscribeBatches(states);
557
+ });
558
+ }
559
+ sendRealtimeSubscribeBatches(states) {
560
+ for (let offset = 0; offset < states.length; offset += 64) {
561
+ const batch = states.slice(offset, offset + 64);
562
+ for (const state of batch) state.pendingSubscribe = true;
563
+ this.send({
564
+ type: "realtime.subscribe",
565
+ id: randomId(),
566
+ subscriptions: batch.map((state) => ({
567
+ topic: state.topic,
568
+ afterCursor: state.lastCursor ?? state.afterCursor ?? "$"
569
+ }))
570
+ });
571
+ }
572
+ }
573
+ queueRealtimeUnsubscribe(topic) {
574
+ this.realtimeUnsubscribeQueue.add(topic);
575
+ if (this.realtimeUnsubscribeQueued) return;
576
+ this.realtimeUnsubscribeQueued = true;
577
+ queueMicrotask(() => {
578
+ this.realtimeUnsubscribeQueued = false;
579
+ const topics = [...this.realtimeUnsubscribeQueue];
580
+ this.realtimeUnsubscribeQueue.clear();
581
+ if (this.manuallyClosed) return;
582
+ for (let offset = 0; offset < topics.length; offset += 64) {
583
+ this.send({ type: "realtime.unsubscribe", topics: topics.slice(offset, offset + 64) });
584
+ }
585
+ });
586
+ }
587
+ sendSubscribeFrame(channel, subscription) {
588
+ const requestId = randomId();
589
+ this.diagnostics.subscribeRequests += 1;
590
+ if (subscription.pendingAckId != null) this.diagnostics.subscribeSuperseded += 1;
591
+ subscription.pendingAckId = requestId;
592
+ if (subscription.pendingAckTimer != null) clearTimeout(subscription.pendingAckTimer);
593
+ subscription.pendingAckTimer = setTimeout(() => {
594
+ if (subscription.pendingAckId !== requestId) return;
595
+ this.diagnostics.subscribeTimeouts += 1;
596
+ subscription.pendingAckId = null;
597
+ subscription.pendingAckTimer = null;
598
+ for (const flushed of this.channelFrames(this.settleSubscriptionBuffer(subscription, []))) {
599
+ for (const handler of this.handlers) handler(flushed);
600
+ }
601
+ }, this.options.ackTimeoutMs ?? defaultAckTimeoutMs);
602
+ this.send({
603
+ type: "channel.subscribe",
604
+ id: requestId,
605
+ channel,
606
+ afterCursor: subscription.lastCursor ?? subscription.afterCursor ?? "$"
607
+ });
608
+ }
609
+ auth = {
610
+ authorize: (args) => {
611
+ const requestId = randomId();
612
+ const result = this.waitForAck(requestId, "auth");
613
+ this.sendAuthFrame({ id: requestId, type: "auth", token: args.token });
614
+ return result;
615
+ }
616
+ };
617
+ onFrame(handler) {
618
+ this.handlers.add(handler);
619
+ this.diagnostics.frameHandlersHighWater = Math.max(
620
+ this.diagnostics.frameHandlersHighWater,
621
+ this.handlers.size
622
+ );
623
+ return () => this.handlers.delete(handler);
624
+ }
625
+ registerReplay(key, replay) {
626
+ this.replayRegistrations.set(key, replay);
627
+ return () => this.replayRegistrations.delete(key);
628
+ }
629
+ waitForAck(requestId, operation) {
630
+ return new Promise((resolve, reject) => {
631
+ const timeout = setTimeout(() => {
632
+ unsubscribe();
633
+ reject(new Error(`Websockets ${operation} acknowledgement timed out`));
634
+ }, this.options.ackTimeoutMs ?? defaultAckTimeoutMs);
635
+ const unsubscribe = this.onFrame((frame) => {
636
+ if (frame.type === "ack" && frame.requestId === requestId && frame.operation === operation) {
637
+ unsubscribe();
638
+ clearTimeout(timeout);
639
+ resolve();
640
+ }
641
+ if (frame.type === "error" && frame.requestId === requestId) {
642
+ unsubscribe();
643
+ clearTimeout(timeout);
644
+ reject(new Error(frame.message));
645
+ }
646
+ });
647
+ });
648
+ }
649
+ channel(name) {
650
+ return new WebsocketsChannel(this, name);
651
+ }
652
+ realtime(topic) {
653
+ return new WebsocketsRealtimeTopic(this, topic);
654
+ }
655
+ registerRealtimeSubscription(args) {
656
+ this.realtimeUnsubscribeQueue.delete(args.topic);
657
+ const state = {
658
+ topic: args.topic,
659
+ pendingSubscribe: true,
660
+ afterCursor: args.options?.afterCursor,
661
+ gapFillRequestId: null,
662
+ gapFillTimer: null,
663
+ buffered: [],
664
+ bufferedBytes: 0,
665
+ bufferedBytesHighWater: 0,
666
+ bufferedFramesHighWater: 0,
667
+ handler: args.handler,
668
+ onError: args.options?.onError ?? null,
669
+ onResyncRequired: args.options?.onResyncRequired ?? null,
670
+ onSubscribed: args.options?.onSubscribed ?? null
671
+ };
672
+ this.realtimeSubscriptions.set(args.topic, state);
673
+ this.queueRealtimeSubscribe(args.topic);
674
+ return () => {
675
+ if (this.realtimeSubscriptions.get(args.topic) !== state) return;
676
+ this.clearSubscriptionGapFill(state);
677
+ this.realtimeSubscriptions.delete(args.topic);
678
+ this.realtimeSubscribeQueue.delete(args.topic);
679
+ this.queueRealtimeUnsubscribe(args.topic);
680
+ };
681
+ }
682
+ registerChannelSubscription(args) {
683
+ const subscription = {
684
+ afterCursor: args.afterCursor,
685
+ pendingAckId: null,
686
+ pendingAckTimer: null,
687
+ gapFillRequestId: null,
688
+ gapFillTimer: null,
689
+ buffered: [],
690
+ bufferedBytes: 0,
691
+ bufferedBytesHighWater: 0,
692
+ bufferedFramesHighWater: 0,
693
+ onError: args.onError ?? null,
694
+ onSubscribed: args.onSubscribed ?? null,
695
+ receivedBytes: 0,
696
+ receivedFrames: 0
697
+ };
698
+ this.channelSubscriptions.set(args.channel, subscription);
699
+ this.sendSubscribeFrame(args.channel, subscription);
700
+ return () => {
701
+ if (subscription.pendingAckTimer != null) clearTimeout(subscription.pendingAckTimer);
702
+ this.clearSubscriptionGapFill(subscription);
703
+ this.channelSubscriptions.delete(args.channel);
704
+ this.send({ type: "channel.unsubscribe", channel: args.channel });
705
+ };
706
+ }
707
+ rpc(name) {
708
+ return new WebsocketsRpc(this, name);
709
+ }
710
+ bridge(name) {
711
+ return new WebsocketsBridge(this, name);
712
+ }
713
+ proxy(upstream) {
714
+ return new WebsocketsProxy(this, upstream);
715
+ }
716
+ send(frame) {
717
+ if (this.socket == null) {
718
+ throw new Error("Websockets connection is not open");
719
+ }
720
+ if (this.manuallyClosed) {
721
+ throw new Error("Websockets connection is not open");
722
+ }
723
+ const encoded = JSON.stringify(frame);
724
+ const sizeBytes = utf8ByteLength(encoded);
725
+ if (!this.authenticated && frame.type !== "auth") {
726
+ if (this.socket.readyState !== openReadyState && this.options.reconnect === false) {
727
+ throw new Error("Websockets connection is not open");
728
+ }
729
+ if (this.queuedFrames.length >= (this.options.maxQueuedFrames ?? defaultMaxQueuedFrames)) {
730
+ throw new Error("Websockets queued frame limit exceeded");
731
+ }
732
+ this.queuedFrames.push({ encoded, frame, sizeBytes });
733
+ this.queuedBytes += sizeBytes;
734
+ return;
735
+ }
736
+ if (this.socket.readyState !== openReadyState) {
737
+ throw new Error("Websockets connection is not open");
738
+ }
739
+ this.sendEncodedFrame(frame, encoded, sizeBytes);
740
+ }
741
+ getDiagnostics() {
742
+ const buffers = this.currentBufferDiagnostics();
743
+ let pendingGapFills = 0;
744
+ let pendingSubscribeAcks = 0;
745
+ for (const state of this.channelSubscriptions.values()) {
746
+ if (state.gapFillRequestId != null) pendingGapFills += 1;
747
+ if (state.pendingAckId != null) pendingSubscribeAcks += 1;
748
+ }
749
+ for (const state of this.realtimeSubscriptions.values()) {
750
+ if (state.gapFillRequestId != null) pendingGapFills += 1;
751
+ }
752
+ return {
753
+ ...this.diagnostics,
754
+ bufferedBytes: buffers.bytes,
755
+ bufferedFrames: buffers.frames,
756
+ channelStreams: [...this.channelSubscriptions.entries()].map(([channel, state]) => ({
757
+ bufferedBytes: state.bufferedBytes,
758
+ bufferedBytesHighWater: state.bufferedBytesHighWater,
759
+ bufferedFrames: state.buffered.length,
760
+ bufferedFramesHighWater: state.bufferedFramesHighWater,
761
+ channel,
762
+ pendingGapFill: state.gapFillRequestId != null,
763
+ pendingSubscribeAck: state.pendingAckId != null,
764
+ receivedBytes: state.receivedBytes,
765
+ receivedFrames: state.receivedFrames
766
+ })),
767
+ channelSubscriptions: this.channelSubscriptions.size,
768
+ frameHandlers: this.handlers.size,
769
+ pendingGapFills,
770
+ pendingSubscribeAcks,
771
+ queuedBytes: this.queuedBytes,
772
+ queuedFrames: this.queuedFrames.length,
773
+ realtimeSubscriptions: this.realtimeSubscriptions.size,
774
+ socketBufferedBytes: this.socket?.bufferedAmount ?? 0
775
+ };
776
+ }
777
+ sendAuthFrame(frame) {
778
+ if (this.socket == null || this.socket.readyState !== openReadyState) {
779
+ throw new Error("Websockets connection is not open");
780
+ }
781
+ const encoded = JSON.stringify(frame);
782
+ this.sendEncodedFrame(frame, encoded, utf8ByteLength(encoded));
783
+ }
784
+ sendEncodedFrame(frame, encoded, sizeBytes) {
785
+ if (this.socket == null || this.socket.readyState !== openReadyState) {
786
+ throw new Error("Websockets connection is not open");
787
+ }
788
+ this.socket.send(encoded);
789
+ this.diagnostics.sentFrames += 1;
790
+ this.diagnostics.sentBytes += sizeBytes;
791
+ if (frame.type === "rpc.call") {
792
+ this.diagnostics.rpcCallFrames += 1;
793
+ this.diagnostics.rpcCallBytes += sizeBytes;
794
+ }
795
+ const socketBufferedBytes = this.socket.bufferedAmount;
796
+ this.diagnostics.socketBufferedBytesHighWater = Math.max(
797
+ this.diagnostics.socketBufferedBytesHighWater,
798
+ socketBufferedBytes
799
+ );
800
+ }
801
+ flushQueuedFrames() {
802
+ if (this.socket == null || this.socket.readyState !== openReadyState || !this.authenticated) {
803
+ return;
804
+ }
805
+ const frames = this.queuedFrames;
806
+ this.queuedFrames = [];
807
+ this.queuedBytes = 0;
808
+ for (const queued of frames) {
809
+ this.sendEncodedFrame(queued.frame, queued.encoded, queued.sizeBytes);
810
+ }
811
+ }
812
+ ping(nonce = randomId()) {
813
+ this.send({ type: "ping", nonce });
814
+ }
815
+ close(code, reason) {
816
+ this.manuallyClosed = true;
817
+ this.queuedFrames = [];
818
+ this.queuedBytes = 0;
819
+ this.realtimeSubscribeQueue.clear();
820
+ this.realtimeSubscribeQueued = false;
821
+ this.realtimeUnsubscribeQueue.clear();
822
+ this.realtimeUnsubscribeQueued = false;
823
+ this.clearHeartbeat();
824
+ this.socket?.close(code, reason);
825
+ }
826
+ }
827
+ class WebsocketsChannel {
828
+ constructor(connection, name) {
829
+ this.connection = connection;
830
+ this.name = name;
831
+ }
832
+ connection;
833
+ name;
834
+ subscribe(handler, options) {
835
+ const unsubscribe = this.connection.onFrame((frame) => {
836
+ if (frame.type === "channel.message" && frame.message.channel === this.name) {
837
+ handler(frame.message);
838
+ }
839
+ });
840
+ const unregisterSubscription = this.connection.registerChannelSubscription({
841
+ channel: this.name,
842
+ afterCursor: typeof options === "string" ? options : options?.afterCursor,
843
+ onError: typeof options === "string" ? void 0 : options?.onError,
844
+ onSubscribed: typeof options === "string" ? void 0 : options?.onSubscribed
845
+ });
846
+ return () => {
847
+ unsubscribe();
848
+ unregisterSubscription();
849
+ };
850
+ }
851
+ publish(data, options = {}) {
852
+ const publishOptions = typeof options === "string" ? { name: options } : options;
853
+ this.connection.send({
854
+ type: "channel.publish",
855
+ channel: this.name,
856
+ name: publishOptions.name ?? "message",
857
+ data,
858
+ idempotencyKey: publishOptions.idempotencyKey,
859
+ retain: publishOptions.retain
860
+ });
861
+ }
862
+ history(args = {}) {
863
+ const requestId = randomId();
864
+ this.connection.send({
865
+ type: "channel.history",
866
+ id: requestId,
867
+ channel: this.name,
868
+ afterCursor: args.afterCursor,
869
+ limit: args.limit ?? 100
870
+ });
871
+ return new Promise((resolve, reject) => {
872
+ const unsubscribe = this.connection.onFrame((frame) => {
873
+ if (frame.type === "channel.history" && frame.requestId === requestId) {
874
+ unsubscribe();
875
+ resolve(frame.messages);
876
+ }
877
+ if (frame.type === "error" && frame.requestId === requestId) {
878
+ unsubscribe();
879
+ reject(new Error(frame.message));
880
+ }
881
+ });
882
+ });
883
+ }
884
+ presence() {
885
+ return new WebsocketsPresence(this.connection, this.name);
886
+ }
887
+ }
888
+ class WebsocketsRealtimeTopic {
889
+ constructor(connection, topic) {
890
+ this.connection = connection;
891
+ this.topic = topic;
892
+ }
893
+ connection;
894
+ topic;
895
+ subscribe(handler, options) {
896
+ return this.connection.registerRealtimeSubscription({ topic: this.topic, handler, options });
897
+ }
898
+ presence() {
899
+ let data;
900
+ let unregisterReplay = null;
901
+ return {
902
+ enter: (nextData) => {
903
+ data = nextData;
904
+ unregisterReplay?.();
905
+ unregisterReplay = this.connection.registerReplay(`realtime-presence:${this.topic}`, () => {
906
+ this.connection.send({
907
+ type: "realtime.presence.enter",
908
+ topic: this.topic,
909
+ data
910
+ });
911
+ });
912
+ this.connection.send({
913
+ type: "realtime.presence.enter",
914
+ topic: this.topic,
915
+ data: nextData
916
+ });
917
+ },
918
+ update: (nextData) => {
919
+ data = nextData;
920
+ this.connection.send({
921
+ type: "realtime.presence.update",
922
+ topic: this.topic,
923
+ data: nextData
924
+ });
925
+ },
926
+ leave: () => {
927
+ unregisterReplay?.();
928
+ unregisterReplay = null;
929
+ this.connection.send({ type: "realtime.presence.leave", topic: this.topic });
930
+ },
931
+ get: (options = {}) => {
932
+ const requestId = randomId();
933
+ return new Promise((resolve, reject) => {
934
+ let unsubscribe = () => void 0;
935
+ const cleanup = () => {
936
+ unsubscribe();
937
+ options.signal?.removeEventListener("abort", handleAbort);
938
+ };
939
+ const handleAbort = () => {
940
+ cleanup();
941
+ reject(options.signal?.reason ?? new Error("Presence snapshot request aborted"));
942
+ };
943
+ if (options.signal?.aborted === true) {
944
+ handleAbort();
945
+ return;
946
+ }
947
+ unsubscribe = this.connection.onFrame((frame) => {
948
+ if (frame.type === "realtime.presence.snapshot" && frame.requestId === requestId && frame.topic === this.topic) {
949
+ cleanup();
950
+ resolve(frame.members);
951
+ }
952
+ if (frame.type === "realtime.error" && frame.requestId === requestId && frame.topic === this.topic) {
953
+ cleanup();
954
+ reject(new Error(frame.message));
955
+ }
956
+ });
957
+ options.signal?.addEventListener("abort", handleAbort, { once: true });
958
+ try {
959
+ this.connection.send({
960
+ id: requestId,
961
+ type: "realtime.presence.get",
962
+ topic: this.topic
963
+ });
964
+ } catch (error) {
965
+ cleanup();
966
+ reject(error);
967
+ }
968
+ });
969
+ },
970
+ subscribe: (handler) => this.connection.onFrame((frame) => {
971
+ if (frame.type !== "realtime.presence.event" || frame.topic !== this.topic) return;
972
+ handler(frame);
973
+ })
974
+ };
975
+ }
976
+ }
977
+ class WebsocketsPresence {
978
+ constructor(connection, channel) {
979
+ this.connection = connection;
980
+ this.channel = channel;
981
+ }
982
+ connection;
983
+ channel;
984
+ unregisterPresenceReplay = null;
985
+ data;
986
+ enter(data) {
987
+ this.data = data;
988
+ this.unregisterPresenceReplay?.();
989
+ this.unregisterPresenceReplay = this.connection.registerReplay(
990
+ `presence:${this.channel}`,
991
+ () => {
992
+ this.connection.send({ type: "presence.enter", channel: this.channel, data: this.data });
993
+ }
994
+ );
995
+ this.connection.send({ type: "presence.enter", channel: this.channel, data });
996
+ }
997
+ update(data) {
998
+ this.data = data;
999
+ this.connection.send({ type: "presence.update", channel: this.channel, data });
1000
+ }
1001
+ leave() {
1002
+ this.unregisterPresenceReplay?.();
1003
+ this.unregisterPresenceReplay = null;
1004
+ this.connection.send({ type: "presence.leave", channel: this.channel });
1005
+ }
1006
+ get(options = {}) {
1007
+ const requestId = randomId();
1008
+ return new Promise((resolve, reject) => {
1009
+ let unsubscribe = () => void 0;
1010
+ const cleanup = () => {
1011
+ unsubscribe();
1012
+ options.signal?.removeEventListener("abort", handleAbort);
1013
+ };
1014
+ const handleAbort = () => {
1015
+ cleanup();
1016
+ reject(options.signal?.reason ?? new Error("Presence snapshot request aborted"));
1017
+ };
1018
+ if (options.signal?.aborted === true) {
1019
+ handleAbort();
1020
+ return;
1021
+ }
1022
+ unsubscribe = this.connection.onFrame((frame) => {
1023
+ if (frame.type === "presence.snapshot" && frame.requestId === requestId) {
1024
+ cleanup();
1025
+ resolve(frame.members);
1026
+ }
1027
+ if (frame.type === "error" && frame.requestId === requestId) {
1028
+ cleanup();
1029
+ reject(new Error(frame.message));
1030
+ }
1031
+ });
1032
+ options.signal?.addEventListener("abort", handleAbort, { once: true });
1033
+ try {
1034
+ this.connection.send({
1035
+ type: "presence.get",
1036
+ id: requestId,
1037
+ channel: this.channel
1038
+ });
1039
+ } catch (error) {
1040
+ cleanup();
1041
+ reject(error);
1042
+ }
1043
+ });
1044
+ }
1045
+ subscribe(handler) {
1046
+ return this.connection.onFrame((frame) => {
1047
+ if (frame.type === "presence.event" && frame.channel === this.channel) {
1048
+ handler({ channel: frame.channel, action: frame.action, member: frame.member });
1049
+ }
1050
+ });
1051
+ }
1052
+ }
1053
+ class WebsocketsRpc {
1054
+ constructor(connection, name) {
1055
+ this.connection = connection;
1056
+ this.name = name;
1057
+ }
1058
+ connection;
1059
+ name;
1060
+ connect(handler) {
1061
+ this.connection.send({ type: "rpc.register", name: this.name });
1062
+ const unregisterReplay = this.connection.registerReplay(`rpc:${this.name}`, () => {
1063
+ this.connection.send({ type: "rpc.register", name: this.name });
1064
+ });
1065
+ const unsubscribe = this.connection.onFrame((frame) => {
1066
+ if (frame.type !== "rpc.call" || frame.name !== this.name) return;
1067
+ Promise.resolve(handler(websocketGatewayDataSchema.parse(frame.data))).then((data) => {
1068
+ this.connection.send({ type: "rpc.respond", correlationId: frame.correlationId, data });
1069
+ }).catch((cause) => {
1070
+ this.connection.send({
1071
+ type: "rpc.respond",
1072
+ correlationId: frame.correlationId,
1073
+ error: {
1074
+ code: "handler_failed",
1075
+ message: cause instanceof Error ? cause.message : String(cause)
1076
+ }
1077
+ });
1078
+ });
1079
+ });
1080
+ return () => {
1081
+ unsubscribe();
1082
+ unregisterReplay();
1083
+ };
1084
+ }
1085
+ call(data, timeoutMs = 3e4, signal) {
1086
+ const correlationId = randomId();
1087
+ return new Promise((resolve, reject) => {
1088
+ if (signal?.aborted === true) {
1089
+ reject(signal.reason);
1090
+ return;
1091
+ }
1092
+ const cleanup = () => {
1093
+ clearTimeout(timer);
1094
+ unsubscribe();
1095
+ signal?.removeEventListener("abort", abort);
1096
+ };
1097
+ const abort = () => {
1098
+ cleanup();
1099
+ reject(signal?.reason ?? new Error("Websockets RPC aborted"));
1100
+ };
1101
+ const timer = setTimeout(() => {
1102
+ cleanup();
1103
+ reject(new Error("Websockets RPC timed out"));
1104
+ }, timeoutMs);
1105
+ const unsubscribe = this.connection.onFrame((frame) => {
1106
+ if (frame.type !== "rpc.response" || frame.correlationId !== correlationId) return;
1107
+ cleanup();
1108
+ if (frame.error != null) reject(new Error(frame.error.message));
1109
+ else
1110
+ resolve(
1111
+ frame.data === void 0 ? void 0 : websocketGatewayDataSchema.parse(frame.data)
1112
+ );
1113
+ });
1114
+ signal?.addEventListener("abort", abort, { once: true });
1115
+ try {
1116
+ this.connection.send({ type: "rpc.call", name: this.name, correlationId, data, timeoutMs });
1117
+ } catch (cause) {
1118
+ cleanup();
1119
+ reject(cause);
1120
+ }
1121
+ });
1122
+ }
1123
+ }
1124
+ class WebsocketsBridge {
1125
+ constructor(connection, name) {
1126
+ this.connection = connection;
1127
+ this.name = name;
1128
+ }
1129
+ connection;
1130
+ name;
1131
+ join(role, handler) {
1132
+ this.connection.send({ type: "bridge.join", bridge: this.name, role });
1133
+ const unregisterReplay = this.connection.registerReplay(`bridge:${this.name}`, () => {
1134
+ this.connection.send({ type: "bridge.join", bridge: this.name, role });
1135
+ });
1136
+ const unsubscribe = this.connection.onFrame((frame) => {
1137
+ if (frame.type === "bridge.message" && frame.bridge === this.name) {
1138
+ handler?.(websocketGatewayDataSchema.parse(frame.data));
1139
+ }
1140
+ });
1141
+ return () => {
1142
+ unsubscribe();
1143
+ unregisterReplay();
1144
+ this.connection.send({ type: "bridge.leave", bridge: this.name });
1145
+ };
1146
+ }
1147
+ async joinReady(role, handler) {
1148
+ const requestId = randomId();
1149
+ const acknowledged = this.connection.waitForAck(requestId, "bridge.join");
1150
+ const unsubscribe = this.connection.onFrame((frame) => {
1151
+ if (frame.type === "bridge.message" && frame.bridge === this.name) {
1152
+ handler?.(websocketGatewayDataSchema.parse(frame.data));
1153
+ }
1154
+ });
1155
+ const unregisterReplay = this.connection.registerReplay(`bridge:${this.name}`, () => {
1156
+ this.connection.send({ type: "bridge.join", bridge: this.name, role });
1157
+ });
1158
+ this.connection.send({
1159
+ id: requestId,
1160
+ type: "bridge.join",
1161
+ bridge: this.name,
1162
+ role
1163
+ });
1164
+ try {
1165
+ await acknowledged;
1166
+ } catch (error) {
1167
+ unsubscribe();
1168
+ unregisterReplay();
1169
+ throw error;
1170
+ }
1171
+ return () => {
1172
+ unsubscribe();
1173
+ unregisterReplay();
1174
+ this.connection.send({ type: "bridge.leave", bridge: this.name });
1175
+ };
1176
+ }
1177
+ send(data) {
1178
+ this.connection.send({ type: "bridge.send", bridge: this.name, data });
1179
+ }
1180
+ }
1181
+ class WebsocketsProxy {
1182
+ constructor(connection, upstream) {
1183
+ this.connection = connection;
1184
+ this.upstreamId = typeof upstream === "string" ? upstream : upstream.id;
1185
+ }
1186
+ connection;
1187
+ upstreamId;
1188
+ connect(handlers = {}) {
1189
+ const unsubscribe = this.connection.onFrame((frame) => {
1190
+ switch (frame.type) {
1191
+ case "proxy.open":
1192
+ if (frame.upstreamId === this.upstreamId) {
1193
+ handlers.onOpen?.(frame.upstreamId);
1194
+ }
1195
+ return;
1196
+ case "proxy.message":
1197
+ handlers.onMessage?.(decodeProxyPayload(frame.data));
1198
+ return;
1199
+ case "proxy.closed":
1200
+ handlers.onClose?.({ code: frame.code, reason: frame.reason });
1201
+ return;
1202
+ }
1203
+ });
1204
+ this.connection.send({ type: "proxy.connect", upstreamId: this.upstreamId });
1205
+ return unsubscribe;
1206
+ }
1207
+ send(data) {
1208
+ this.connection.send({ type: "proxy.send", data: encodeProxyPayload(data) });
1209
+ }
1210
+ close(code, reason) {
1211
+ this.connection.send({ type: "proxy.close", code, reason });
1212
+ }
1213
+ }
1214
+ class WebsocketsWorker {
1215
+ constructor(connection) {
1216
+ this.connection = connection;
1217
+ }
1218
+ connection;
1219
+ helloArgs = null;
1220
+ inFlightLeases = /* @__PURE__ */ new Map();
1221
+ pendingTerminalLeaseOperations = /* @__PURE__ */ new Map();
1222
+ unregisterHelloReplay = null;
1223
+ unregisterTerminalOperationReplay = null;
1224
+ hello(args) {
1225
+ this.helloArgs = {
1226
+ workerId: args.workerId,
1227
+ namespace: args.namespace,
1228
+ appVersion: args.appVersion,
1229
+ queues: args.queues,
1230
+ maxConcurrency: args.maxConcurrency ?? 1,
1231
+ inFlightLeaseIds: args.inFlightLeaseIds ?? []
1232
+ };
1233
+ this.inFlightLeases.clear();
1234
+ for (const lease of args.inFlightLeases ?? []) {
1235
+ this.inFlightLeases.set(lease.leaseId, lease.queue);
1236
+ }
1237
+ if ((args.inFlightLeases == null || args.inFlightLeases.length === 0) && args.queues.length === 1) {
1238
+ for (const leaseId of args.inFlightLeaseIds ?? []) {
1239
+ this.inFlightLeases.set(leaseId, args.queues[0]);
1240
+ }
1241
+ }
1242
+ this.unregisterHelloReplay?.();
1243
+ this.unregisterHelloReplay = this.connection.registerReplay(`worker:${args.workerId}`, () => {
1244
+ this.sendHello();
1245
+ });
1246
+ this.unregisterTerminalOperationReplay?.();
1247
+ this.unregisterTerminalOperationReplay = this.connection.registerReplay(
1248
+ `worker:${args.workerId}:terminal-lease-operations`,
1249
+ () => {
1250
+ this.replayPendingTerminalLeaseOperations();
1251
+ }
1252
+ );
1253
+ this.sendHello();
1254
+ }
1255
+ sendHello() {
1256
+ if (this.helloArgs == null) return;
1257
+ this.connection.send({
1258
+ type: "worker.hello",
1259
+ workerId: this.helloArgs.workerId,
1260
+ namespace: this.helloArgs.namespace,
1261
+ appVersion: this.helloArgs.appVersion,
1262
+ queues: this.helloArgs.queues,
1263
+ ...(() => {
1264
+ if (!(this.helloArgs.maxConcurrency === 1))
1265
+ return { maxConcurrency: this.helloArgs.maxConcurrency };
1266
+ return {};
1267
+ })(),
1268
+ inFlightLeaseIds: this.helloArgs.inFlightLeaseIds,
1269
+ inFlightLeases: [...this.inFlightLeases.entries()].map(([leaseId, queue]) => ({
1270
+ leaseId,
1271
+ queue
1272
+ }))
1273
+ });
1274
+ }
1275
+ onOffer(handler) {
1276
+ return this.connection.onFrame((frame) => {
1277
+ if (frame.type !== "work.offer") return;
1278
+ this.inFlightLeases.set(frame.leaseId, frame.queue);
1279
+ handler({
1280
+ queue: frame.queue,
1281
+ leaseId: frame.leaseId,
1282
+ payload: websocketGatewayDataSchema.parse(frame.payload)
1283
+ });
1284
+ });
1285
+ }
1286
+ accept(leaseId) {
1287
+ return this.sendLeaseOperation("work.accept", leaseId);
1288
+ }
1289
+ heartbeat(leaseId) {
1290
+ return this.sendLeaseOperation("work.heartbeat", leaseId);
1291
+ }
1292
+ complete(leaseId, result) {
1293
+ return this.sendTerminalLeaseOperation("work.complete", leaseId, { result }).then(() => {
1294
+ this.inFlightLeases.delete(leaseId);
1295
+ });
1296
+ }
1297
+ fail(leaseId, error) {
1298
+ return this.sendTerminalLeaseOperation("work.fail", leaseId, { error }).then(() => {
1299
+ this.inFlightLeases.delete(leaseId);
1300
+ });
1301
+ }
1302
+ drain(reason) {
1303
+ const requestId = randomId();
1304
+ const result = this.connection.waitForAck(requestId, "work.drain");
1305
+ this.connection.send({ id: requestId, type: "work.drain", reason });
1306
+ return result;
1307
+ }
1308
+ close(code, reason) {
1309
+ this.unregisterHelloReplay?.();
1310
+ this.unregisterTerminalOperationReplay?.();
1311
+ this.connection.close(code, reason);
1312
+ }
1313
+ sendTerminalLeaseOperation(operation, leaseId, extra = {}) {
1314
+ this.pendingTerminalLeaseOperations.get(leaseId)?.unsubscribe();
1315
+ const requestId = randomId();
1316
+ const unsubscribe = this.connection.onFrame((frame) => {
1317
+ if (frame.type === "ack" && frame.requestId === requestId && frame.operation === operation) {
1318
+ this.clearPendingTerminalLeaseOperation(leaseId);
1319
+ this.inFlightLeases.delete(leaseId);
1320
+ }
1321
+ if (frame.type === "error" && frame.requestId === requestId) {
1322
+ this.clearPendingTerminalLeaseOperation(leaseId);
1323
+ this.inFlightLeases.delete(leaseId);
1324
+ }
1325
+ });
1326
+ this.pendingTerminalLeaseOperations.set(leaseId, {
1327
+ operation,
1328
+ requestId,
1329
+ result: extra.result,
1330
+ error: extra.error,
1331
+ unsubscribe
1332
+ });
1333
+ const result = this.connection.waitForAck(requestId, operation).then(() => {
1334
+ this.clearPendingTerminalLeaseOperation(leaseId);
1335
+ });
1336
+ this.sendTerminalLeaseOperationFrame(leaseId);
1337
+ return result;
1338
+ }
1339
+ clearPendingTerminalLeaseOperation(leaseId) {
1340
+ const pending = this.pendingTerminalLeaseOperations.get(leaseId);
1341
+ pending?.unsubscribe();
1342
+ this.pendingTerminalLeaseOperations.delete(leaseId);
1343
+ }
1344
+ replayPendingTerminalLeaseOperations() {
1345
+ for (const leaseId of this.pendingTerminalLeaseOperations.keys()) {
1346
+ this.sendTerminalLeaseOperationFrame(leaseId);
1347
+ }
1348
+ }
1349
+ sendTerminalLeaseOperationFrame(leaseId) {
1350
+ const pending = this.pendingTerminalLeaseOperations.get(leaseId);
1351
+ if (pending == null) return;
1352
+ switch (pending.operation) {
1353
+ case "work.complete":
1354
+ this.connection.send({
1355
+ id: pending.requestId,
1356
+ type: "work.complete",
1357
+ leaseId,
1358
+ result: pending.result
1359
+ });
1360
+ break;
1361
+ case "work.fail":
1362
+ this.connection.send({
1363
+ id: pending.requestId,
1364
+ type: "work.fail",
1365
+ leaseId,
1366
+ error: pending.error ?? "Worker failed"
1367
+ });
1368
+ break;
1369
+ }
1370
+ }
1371
+ sendLeaseOperation(operation, leaseId, extra = {}) {
1372
+ const requestId = randomId();
1373
+ const result = this.connection.waitForAck(requestId, operation);
1374
+ switch (operation) {
1375
+ case "work.accept":
1376
+ this.connection.send({ id: requestId, type: "work.accept", leaseId });
1377
+ break;
1378
+ case "work.heartbeat":
1379
+ this.connection.send({ id: requestId, type: "work.heartbeat", leaseId });
1380
+ break;
1381
+ case "work.complete":
1382
+ this.connection.send({
1383
+ id: requestId,
1384
+ type: "work.complete",
1385
+ leaseId,
1386
+ result: extra.result
1387
+ });
1388
+ break;
1389
+ case "work.fail":
1390
+ this.connection.send({
1391
+ id: requestId,
1392
+ type: "work.fail",
1393
+ leaseId,
1394
+ error: extra.error ?? "Worker failed"
1395
+ });
1396
+ break;
1397
+ }
1398
+ return result;
1399
+ }
1400
+ }
1401
+ const client = {
1402
+ connect: async (options) => {
1403
+ const connection = new WebsocketsConnection(options);
1404
+ await connection.connect();
1405
+ return connection;
1406
+ }
1407
+ };
1408
+ const worker = {
1409
+ connect: async (options) => {
1410
+ const connection = new WebsocketsConnection(options);
1411
+ await connection.connect();
1412
+ return new WebsocketsWorker(connection);
1413
+ }
1414
+ };
1415
+ const capability = (channels, actions) => ({
1416
+ channels,
1417
+ actions
1418
+ });
1419
+ export {
1420
+ WebsocketsBridge,
1421
+ WebsocketsChannel,
1422
+ WebsocketsConnection,
1423
+ WebsocketsPresence,
1424
+ WebsocketsProxy,
1425
+ WebsocketsRealtimeTopic,
1426
+ WebsocketsRpc,
1427
+ WebsocketsWorker,
1428
+ capability,
1429
+ client,
1430
+ worker
1431
+ };
1432
+ //# sourceMappingURL=index.mjs.map