@supabase/realtime-js 2.99.3-canary.0 → 2.99.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/main/RealtimeChannel.d.ts +28 -35
  2. package/dist/main/RealtimeChannel.d.ts.map +1 -1
  3. package/dist/main/RealtimeChannel.js +301 -140
  4. package/dist/main/RealtimeChannel.js.map +1 -1
  5. package/dist/main/RealtimeClient.d.ts +57 -38
  6. package/dist/main/RealtimeClient.d.ts.map +1 -1
  7. package/dist/main/RealtimeClient.js +520 -232
  8. package/dist/main/RealtimeClient.js.map +1 -1
  9. package/dist/main/RealtimePresence.d.ts +24 -8
  10. package/dist/main/RealtimePresence.d.ts.map +1 -1
  11. package/dist/main/RealtimePresence.js +202 -6
  12. package/dist/main/RealtimePresence.js.map +1 -1
  13. package/dist/main/lib/constants.d.ts +35 -39
  14. package/dist/main/lib/constants.d.ts.map +1 -1
  15. package/dist/main/lib/constants.js +35 -30
  16. package/dist/main/lib/constants.js.map +1 -1
  17. package/dist/main/lib/push.d.ts +48 -0
  18. package/dist/main/lib/push.d.ts.map +1 -0
  19. package/dist/main/lib/push.js +102 -0
  20. package/dist/main/lib/push.js.map +1 -0
  21. package/dist/main/lib/timer.d.ts +22 -0
  22. package/dist/main/lib/timer.d.ts.map +1 -0
  23. package/dist/main/lib/timer.js +39 -0
  24. package/dist/main/lib/timer.js.map +1 -0
  25. package/dist/main/lib/version.d.ts +1 -1
  26. package/dist/main/lib/version.d.ts.map +1 -1
  27. package/dist/main/lib/version.js +1 -1
  28. package/dist/main/lib/version.js.map +1 -1
  29. package/dist/main/lib/websocket-factory.d.ts +9 -0
  30. package/dist/main/lib/websocket-factory.d.ts.map +1 -1
  31. package/dist/main/lib/websocket-factory.js +12 -0
  32. package/dist/main/lib/websocket-factory.js.map +1 -1
  33. package/dist/module/RealtimeChannel.d.ts +28 -35
  34. package/dist/module/RealtimeChannel.d.ts.map +1 -1
  35. package/dist/module/RealtimeChannel.js +302 -141
  36. package/dist/module/RealtimeChannel.js.map +1 -1
  37. package/dist/module/RealtimeClient.d.ts +57 -38
  38. package/dist/module/RealtimeClient.d.ts.map +1 -1
  39. package/dist/module/RealtimeClient.js +521 -233
  40. package/dist/module/RealtimeClient.js.map +1 -1
  41. package/dist/module/RealtimePresence.d.ts +24 -8
  42. package/dist/module/RealtimePresence.d.ts.map +1 -1
  43. package/dist/module/RealtimePresence.js +202 -5
  44. package/dist/module/RealtimePresence.js.map +1 -1
  45. package/dist/module/lib/constants.d.ts +35 -39
  46. package/dist/module/lib/constants.d.ts.map +1 -1
  47. package/dist/module/lib/constants.js +35 -30
  48. package/dist/module/lib/constants.js.map +1 -1
  49. package/dist/module/lib/push.d.ts +48 -0
  50. package/dist/module/lib/push.d.ts.map +1 -0
  51. package/dist/module/lib/push.js +99 -0
  52. package/dist/module/lib/push.js.map +1 -0
  53. package/dist/module/lib/timer.d.ts +22 -0
  54. package/dist/module/lib/timer.d.ts.map +1 -0
  55. package/dist/module/lib/timer.js +36 -0
  56. package/dist/module/lib/timer.js.map +1 -0
  57. package/dist/module/lib/version.d.ts +1 -1
  58. package/dist/module/lib/version.d.ts.map +1 -1
  59. package/dist/module/lib/version.js +1 -1
  60. package/dist/module/lib/version.js.map +1 -1
  61. package/dist/module/lib/websocket-factory.d.ts +9 -0
  62. package/dist/module/lib/websocket-factory.d.ts.map +1 -1
  63. package/dist/module/lib/websocket-factory.js +12 -0
  64. package/dist/module/lib/websocket-factory.js.map +1 -1
  65. package/dist/tsconfig.module.tsbuildinfo +1 -1
  66. package/dist/tsconfig.tsbuildinfo +1 -1
  67. package/package.json +3 -3
  68. package/src/RealtimeChannel.ts +364 -201
  69. package/src/RealtimeClient.ts +583 -296
  70. package/src/RealtimePresence.ts +287 -10
  71. package/src/lib/constants.ts +37 -50
  72. package/src/lib/push.ts +121 -0
  73. package/src/lib/timer.ts +43 -0
  74. package/src/lib/version.ts +1 -1
  75. package/src/lib/websocket-factory.ts +13 -0
  76. package/dist/main/phoenix/channelAdapter.d.ts +0 -32
  77. package/dist/main/phoenix/channelAdapter.d.ts.map +0 -1
  78. package/dist/main/phoenix/channelAdapter.js +0 -103
  79. package/dist/main/phoenix/channelAdapter.js.map +0 -1
  80. package/dist/main/phoenix/presenceAdapter.d.ts +0 -53
  81. package/dist/main/phoenix/presenceAdapter.d.ts.map +0 -1
  82. package/dist/main/phoenix/presenceAdapter.js +0 -93
  83. package/dist/main/phoenix/presenceAdapter.js.map +0 -1
  84. package/dist/main/phoenix/socketAdapter.d.ts +0 -38
  85. package/dist/main/phoenix/socketAdapter.d.ts.map +0 -1
  86. package/dist/main/phoenix/socketAdapter.js +0 -114
  87. package/dist/main/phoenix/socketAdapter.js.map +0 -1
  88. package/dist/main/phoenix/types.d.ts +0 -5
  89. package/dist/main/phoenix/types.d.ts.map +0 -1
  90. package/dist/main/phoenix/types.js +0 -3
  91. package/dist/main/phoenix/types.js.map +0 -1
  92. package/dist/module/phoenix/channelAdapter.d.ts +0 -32
  93. package/dist/module/phoenix/channelAdapter.d.ts.map +0 -1
  94. package/dist/module/phoenix/channelAdapter.js +0 -100
  95. package/dist/module/phoenix/channelAdapter.js.map +0 -1
  96. package/dist/module/phoenix/presenceAdapter.d.ts +0 -53
  97. package/dist/module/phoenix/presenceAdapter.d.ts.map +0 -1
  98. package/dist/module/phoenix/presenceAdapter.js +0 -90
  99. package/dist/module/phoenix/presenceAdapter.js.map +0 -1
  100. package/dist/module/phoenix/socketAdapter.d.ts +0 -38
  101. package/dist/module/phoenix/socketAdapter.d.ts.map +0 -1
  102. package/dist/module/phoenix/socketAdapter.js +0 -111
  103. package/dist/module/phoenix/socketAdapter.js.map +0 -1
  104. package/dist/module/phoenix/types.d.ts +0 -5
  105. package/dist/module/phoenix/types.d.ts.map +0 -1
  106. package/dist/module/phoenix/types.js +0 -2
  107. package/dist/module/phoenix/types.js.map +0 -1
  108. package/src/phoenix/channelAdapter.ts +0 -147
  109. package/src/phoenix/presenceAdapter.ts +0 -116
  110. package/src/phoenix/socketAdapter.ts +0 -168
  111. package/src/phoenix/types.ts +0 -32
@@ -4,9 +4,10 @@ const tslib_1 = require("tslib");
4
4
  const websocket_factory_1 = tslib_1.__importDefault(require("./lib/websocket-factory"));
5
5
  const constants_1 = require("./lib/constants");
6
6
  const serializer_1 = tslib_1.__importDefault(require("./lib/serializer"));
7
+ const timer_1 = tslib_1.__importDefault(require("./lib/timer"));
7
8
  const transformers_1 = require("./lib/transformers");
8
9
  const RealtimeChannel_1 = tslib_1.__importDefault(require("./RealtimeChannel"));
9
- const socketAdapter_1 = tslib_1.__importDefault(require("./phoenix/socketAdapter"));
10
+ const noop = () => { };
10
11
  // Connection-related constants
11
12
  const CONNECTION_TIMEOUTS = {
12
13
  HEARTBEAT_INTERVAL: 25000,
@@ -22,54 +23,6 @@ const WORKER_SCRIPT = `
22
23
  }
23
24
  });`;
24
25
  class RealtimeClient {
25
- get endPoint() {
26
- return this.socketAdapter.endPoint;
27
- }
28
- get timeout() {
29
- return this.socketAdapter.timeout;
30
- }
31
- get transport() {
32
- return this.socketAdapter.transport;
33
- }
34
- get heartbeatCallback() {
35
- return this.socketAdapter.heartbeatCallback;
36
- }
37
- get heartbeatIntervalMs() {
38
- return this.socketAdapter.heartbeatIntervalMs;
39
- }
40
- get heartbeatTimer() {
41
- if (this.worker) {
42
- return this._workerHeartbeatTimer;
43
- }
44
- return this.socketAdapter.heartbeatTimer;
45
- }
46
- get pendingHeartbeatRef() {
47
- if (this.worker) {
48
- return this._pendingWorkerHeartbeatRef;
49
- }
50
- return this.socketAdapter.pendingHeartbeatRef;
51
- }
52
- get reconnectTimer() {
53
- return this.socketAdapter.reconnectTimer;
54
- }
55
- get vsn() {
56
- return this.socketAdapter.vsn;
57
- }
58
- get encode() {
59
- return this.socketAdapter.encode;
60
- }
61
- get decode() {
62
- return this.socketAdapter.decode;
63
- }
64
- get reconnectAfterMs() {
65
- return this.socketAdapter.reconnectAfterMs;
66
- }
67
- get sendBuffer() {
68
- return this.socketAdapter.sendBuffer;
69
- }
70
- get stateChangeCallbacks() {
71
- return this.socketAdapter.stateChangeCallbacks;
72
- }
73
26
  /**
74
27
  * Initializes the Socket.
75
28
  *
@@ -101,20 +54,39 @@ class RealtimeClient {
101
54
  */
102
55
  constructor(endPoint, options) {
103
56
  var _a;
104
- this.channels = new Array();
105
57
  this.accessTokenValue = null;
106
- this.accessToken = null;
107
58
  this.apiKey = null;
59
+ this._manuallySetToken = false;
60
+ this.channels = new Array();
61
+ this.endPoint = '';
108
62
  this.httpEndpoint = '';
109
63
  /** @deprecated headers cannot be set on websocket connections */
110
64
  this.headers = {};
111
65
  this.params = {};
66
+ this.timeout = constants_1.DEFAULT_TIMEOUT;
67
+ this.transport = null;
68
+ this.heartbeatIntervalMs = CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL;
69
+ this.heartbeatTimer = undefined;
70
+ this.pendingHeartbeatRef = null;
71
+ this.heartbeatCallback = noop;
112
72
  this.ref = 0;
73
+ this.reconnectTimer = null;
74
+ this.vsn = constants_1.DEFAULT_VSN;
75
+ this.logger = noop;
76
+ this.conn = null;
77
+ this.sendBuffer = [];
113
78
  this.serializer = new serializer_1.default();
114
- this._manuallySetToken = false;
79
+ this.stateChangeCallbacks = {
80
+ open: [],
81
+ close: [],
82
+ error: [],
83
+ message: [],
84
+ };
85
+ this.accessToken = null;
86
+ this._connectionState = 'disconnected';
87
+ this._wasManualDisconnect = false;
115
88
  this._authPromise = null;
116
- this._workerHeartbeatTimer = undefined;
117
- this._pendingWorkerHeartbeatRef = null;
89
+ this._heartbeatSentAt = null;
118
90
  /**
119
91
  * Use either custom fetch, if provided, or default fetch to make HTTP requests
120
92
  *
@@ -131,9 +103,11 @@ class RealtimeClient {
131
103
  throw new Error('API key is required to connect to Realtime');
132
104
  }
133
105
  this.apiKey = options.params.apikey;
134
- const socketAdapterOptions = this._initializeOptions(options);
135
- this.socketAdapter = new socketAdapter_1.default(endPoint, socketAdapterOptions);
106
+ // Initialize endpoint URLs
107
+ this.endPoint = `${endPoint}/${constants_1.TRANSPORTS.websocket}`;
136
108
  this.httpEndpoint = (0, transformers_1.httpEndpointURL)(endPoint);
109
+ this._initializeOptions(options);
110
+ this._setupReconnectionTimer();
137
111
  this.fetch = this._resolveFetch(options === null || options === void 0 ? void 0 : options.fetch);
138
112
  }
139
113
  /**
@@ -141,44 +115,55 @@ class RealtimeClient {
141
115
  */
142
116
  connect() {
143
117
  // Skip if already connecting, disconnecting, or connected
144
- if (this.isConnecting() || this.isDisconnecting() || this.isConnected()) {
118
+ if (this.isConnecting() ||
119
+ this.isDisconnecting() ||
120
+ (this.conn !== null && this.isConnected())) {
145
121
  return;
146
122
  }
123
+ this._setConnectionState('connecting');
147
124
  // Trigger auth if needed and not already in progress
148
125
  // This ensures auth is called for standalone RealtimeClient usage
149
126
  // while avoiding race conditions with SupabaseClient's immediate setAuth call
150
127
  if (this.accessToken && !this._authPromise) {
151
128
  this._setAuthSafely('connect');
152
129
  }
153
- this._setupConnectionHandlers();
154
- try {
155
- this.socketAdapter.connect();
156
- }
157
- catch (error) {
158
- const errorMessage = error.message;
159
- // Provide helpful error message based on environment
160
- if (errorMessage.includes('Node.js')) {
161
- throw new Error(`${errorMessage}\n\n` +
162
- 'To use Realtime in Node.js, you need to provide a WebSocket implementation:\n\n' +
163
- 'Option 1: Use Node.js 22+ which has native WebSocket support\n' +
164
- 'Option 2: Install and provide the "ws" package:\n\n' +
165
- ' npm install ws\n\n' +
166
- ' import ws from "ws"\n' +
167
- ' const client = new RealtimeClient(url, {\n' +
168
- ' ...options,\n' +
169
- ' transport: ws\n' +
170
- ' })');
130
+ // Establish WebSocket connection
131
+ if (this.transport) {
132
+ // Use custom transport if provided
133
+ this.conn = new this.transport(this.endpointURL());
134
+ }
135
+ else {
136
+ // Try to use native WebSocket
137
+ try {
138
+ this.conn = websocket_factory_1.default.createWebSocket(this.endpointURL());
139
+ }
140
+ catch (error) {
141
+ this._setConnectionState('disconnected');
142
+ const errorMessage = error.message;
143
+ // Provide helpful error message based on environment
144
+ if (errorMessage.includes('Node.js')) {
145
+ throw new Error(`${errorMessage}\n\n` +
146
+ 'To use Realtime in Node.js, you need to provide a WebSocket implementation:\n\n' +
147
+ 'Option 1: Use Node.js 22+ which has native WebSocket support\n' +
148
+ 'Option 2: Install and provide the "ws" package:\n\n' +
149
+ ' npm install ws\n\n' +
150
+ ' import ws from "ws"\n' +
151
+ ' const client = new RealtimeClient(url, {\n' +
152
+ ' ...options,\n' +
153
+ ' transport: ws\n' +
154
+ ' })');
155
+ }
156
+ throw new Error(`WebSocket not available: ${errorMessage}`);
171
157
  }
172
- throw new Error(`WebSocket not available: ${errorMessage}`);
173
158
  }
174
- this._handleNodeJsRaceCondition();
159
+ this._setupConnectionHandlers();
175
160
  }
176
161
  /**
177
162
  * Returns the URL of the websocket.
178
163
  * @returns string The URL of the websocket.
179
164
  */
180
165
  endpointURL() {
181
- return this.socketAdapter.endPointURL();
166
+ return this._appendParams(this.endPoint, Object.assign({}, this.params, { vsn: this.vsn }));
182
167
  }
183
168
  /**
184
169
  * Disconnects the socket.
@@ -186,14 +171,34 @@ class RealtimeClient {
186
171
  * @param code A numeric status code to send on disconnect.
187
172
  * @param reason A custom reason for the disconnect.
188
173
  */
189
- async disconnect(code, reason) {
174
+ disconnect(code, reason) {
190
175
  if (this.isDisconnecting()) {
191
- return 'ok';
176
+ return;
177
+ }
178
+ this._setConnectionState('disconnecting', true);
179
+ if (this.conn) {
180
+ // Setup fallback timer to prevent hanging in disconnecting state
181
+ const fallbackTimer = setTimeout(() => {
182
+ this._setConnectionState('disconnected');
183
+ }, 100);
184
+ this.conn.onclose = () => {
185
+ clearTimeout(fallbackTimer);
186
+ this._setConnectionState('disconnected');
187
+ };
188
+ // Close the WebSocket connection if close method exists
189
+ if (typeof this.conn.close === 'function') {
190
+ if (code) {
191
+ this.conn.close(code, reason !== null && reason !== void 0 ? reason : '');
192
+ }
193
+ else {
194
+ this.conn.close();
195
+ }
196
+ }
197
+ this._teardownConnection();
198
+ }
199
+ else {
200
+ this._setConnectionState('disconnected');
192
201
  }
193
- return await this.socketAdapter.disconnect(() => {
194
- clearInterval(this._workerHeartbeatTimer);
195
- this._terminateWorker();
196
- }, code, reason);
197
202
  }
198
203
  /**
199
204
  * Returns all created channels
@@ -202,63 +207,65 @@ class RealtimeClient {
202
207
  return this.channels;
203
208
  }
204
209
  /**
205
- * Unsubscribes, removes and tears down a single channel
210
+ * Unsubscribes and removes a single channel
206
211
  * @param channel A RealtimeChannel instance
207
212
  */
208
213
  async removeChannel(channel) {
209
214
  const status = await channel.unsubscribe();
210
- if (status === 'ok') {
211
- channel.teardown();
212
- }
213
215
  if (this.channels.length === 0) {
214
216
  this.disconnect();
215
217
  }
216
218
  return status;
217
219
  }
218
220
  /**
219
- * Unsubscribes, removes and tears down all channels
221
+ * Unsubscribes and removes all channels
220
222
  */
221
223
  async removeAllChannels() {
222
- const promises = this.channels.map(async (channel) => {
223
- const result = await channel.unsubscribe();
224
- channel.teardown();
225
- return result;
226
- });
227
- const result = await Promise.all(promises);
224
+ const values_1 = await Promise.all(this.channels.map((channel) => channel.unsubscribe()));
225
+ this.channels = [];
228
226
  this.disconnect();
229
- return result;
227
+ return values_1;
230
228
  }
231
229
  /**
232
230
  * Logs the message.
233
231
  *
234
- * For customized logging, `this.logger` can be overridden in Client constructor.
232
+ * For customized logging, `this.logger` can be overridden.
235
233
  */
236
234
  log(kind, msg, data) {
237
- this.socketAdapter.log(kind, msg, data);
235
+ this.logger(kind, msg, data);
238
236
  }
239
237
  /**
240
238
  * Returns the current state of the socket.
241
239
  */
242
240
  connectionState() {
243
- return this.socketAdapter.connectionState() || constants_1.CONNECTION_STATE.closed;
241
+ switch (this.conn && this.conn.readyState) {
242
+ case constants_1.SOCKET_STATES.connecting:
243
+ return constants_1.CONNECTION_STATE.Connecting;
244
+ case constants_1.SOCKET_STATES.open:
245
+ return constants_1.CONNECTION_STATE.Open;
246
+ case constants_1.SOCKET_STATES.closing:
247
+ return constants_1.CONNECTION_STATE.Closing;
248
+ default:
249
+ return constants_1.CONNECTION_STATE.Closed;
250
+ }
244
251
  }
245
252
  /**
246
253
  * Returns `true` is the connection is open.
247
254
  */
248
255
  isConnected() {
249
- return this.socketAdapter.isConnected();
256
+ return this.connectionState() === constants_1.CONNECTION_STATE.Open;
250
257
  }
251
258
  /**
252
259
  * Returns `true` if the connection is currently connecting.
253
260
  */
254
261
  isConnecting() {
255
- return this.socketAdapter.isConnecting();
262
+ return this._connectionState === 'connecting';
256
263
  }
257
264
  /**
258
265
  * Returns `true` if the connection is currently disconnecting.
259
266
  */
260
267
  isDisconnecting() {
261
- return this.socketAdapter.isDisconnecting();
268
+ return this._connectionState === 'disconnecting';
262
269
  }
263
270
  /**
264
271
  * Creates (or reuses) a {@link RealtimeChannel} for the provided topic.
@@ -285,7 +292,20 @@ class RealtimeClient {
285
292
  * If the socket is not connected, the message gets enqueued within a local buffer, and sent out when a connection is next established.
286
293
  */
287
294
  push(data) {
288
- this.socketAdapter.push(data);
295
+ const { topic, event, payload, ref } = data;
296
+ const callback = () => {
297
+ this.encode(data, (result) => {
298
+ var _a;
299
+ (_a = this.conn) === null || _a === void 0 ? void 0 : _a.send(result);
300
+ });
301
+ };
302
+ this.log('push', `${topic} ${event} (${ref})`, payload);
303
+ if (this.isConnected()) {
304
+ callback();
305
+ }
306
+ else {
307
+ this.sendBuffer.push(callback);
308
+ }
289
309
  }
290
310
  /**
291
311
  * Sets the JWT access token used for channel subscription authorization and Realtime RLS.
@@ -328,14 +348,70 @@ class RealtimeClient {
328
348
  * Sends a heartbeat message if the socket is connected.
329
349
  */
330
350
  async sendHeartbeat() {
331
- this.socketAdapter.sendHeartbeat();
351
+ var _a;
352
+ if (!this.isConnected()) {
353
+ try {
354
+ this.heartbeatCallback('disconnected');
355
+ }
356
+ catch (e) {
357
+ this.log('error', 'error in heartbeat callback', e);
358
+ }
359
+ return;
360
+ }
361
+ // Handle heartbeat timeout and force reconnection if needed
362
+ if (this.pendingHeartbeatRef) {
363
+ this.pendingHeartbeatRef = null;
364
+ this._heartbeatSentAt = null;
365
+ this.log('transport', 'heartbeat timeout. Attempting to re-establish connection');
366
+ try {
367
+ this.heartbeatCallback('timeout');
368
+ }
369
+ catch (e) {
370
+ this.log('error', 'error in heartbeat callback', e);
371
+ }
372
+ // Force reconnection after heartbeat timeout
373
+ this._wasManualDisconnect = false;
374
+ (_a = this.conn) === null || _a === void 0 ? void 0 : _a.close(constants_1.WS_CLOSE_NORMAL, 'heartbeat timeout');
375
+ setTimeout(() => {
376
+ var _a;
377
+ if (!this.isConnected()) {
378
+ (_a = this.reconnectTimer) === null || _a === void 0 ? void 0 : _a.scheduleTimeout();
379
+ }
380
+ }, CONNECTION_TIMEOUTS.HEARTBEAT_TIMEOUT_FALLBACK);
381
+ return;
382
+ }
383
+ // Send heartbeat message to server
384
+ this._heartbeatSentAt = Date.now();
385
+ this.pendingHeartbeatRef = this._makeRef();
386
+ this.push({
387
+ topic: 'phoenix',
388
+ event: 'heartbeat',
389
+ payload: {},
390
+ ref: this.pendingHeartbeatRef,
391
+ });
392
+ try {
393
+ this.heartbeatCallback('sent');
394
+ }
395
+ catch (e) {
396
+ this.log('error', 'error in heartbeat callback', e);
397
+ }
398
+ this._setAuthSafely('heartbeat');
332
399
  }
333
400
  /**
334
401
  * Sets a callback that receives lifecycle events for internal heartbeat messages.
335
402
  * Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
336
403
  */
337
404
  onHeartbeat(callback) {
338
- this.socketAdapter.heartbeatCallback = this._wrapHeartbeatCallback(callback);
405
+ this.heartbeatCallback = callback;
406
+ }
407
+ /**
408
+ * Flushes send buffer
409
+ */
410
+ flushSendBuffer() {
411
+ if (this.isConnected() && this.sendBuffer.length > 0) {
412
+ this.sendBuffer.forEach((callback) => callback());
413
+ this.sendBuffer = [];
414
+ }
339
415
  }
340
416
  /**
341
417
  * Return the next message ref, accounting for overflows
@@ -343,10 +419,29 @@ class RealtimeClient {
343
419
  * @internal
344
420
  */
345
421
  _makeRef() {
346
- return this.socketAdapter.makeRef();
422
+ let newRef = this.ref + 1;
423
+ if (newRef === this.ref) {
424
+ this.ref = 0;
425
+ }
426
+ else {
427
+ this.ref = newRef;
428
+ }
429
+ return this.ref.toString();
347
430
  }
348
431
  /**
349
- * Removes a channel from RealtimeClient
432
+ * Unsubscribe from channels with the specified topic.
433
+ *
434
+ * @internal
435
+ */
436
+ _leaveOpenTopic(topic) {
437
+ let dupChannel = this.channels.find((c) => c.topic === topic && (c._isJoined() || c._isJoining()));
438
+ if (dupChannel) {
439
+ this.log('transport', `leaving duplicate topic "${topic}"`);
440
+ dupChannel.unsubscribe();
441
+ }
442
+ }
443
+ /**
444
+ * Removes a subscription from the socket.
350
445
  *
351
446
  * @param channel An open subscription.
352
447
  *
@@ -355,6 +450,262 @@ class RealtimeClient {
355
450
  _remove(channel) {
356
451
  this.channels = this.channels.filter((c) => c.topic !== channel.topic);
357
452
  }
453
+ /** @internal */
454
+ _onConnMessage(rawMessage) {
455
+ this.decode(rawMessage.data, (msg) => {
456
+ // Handle heartbeat responses
457
+ if (msg.topic === 'phoenix' &&
458
+ msg.event === 'phx_reply' &&
459
+ msg.ref &&
460
+ msg.ref === this.pendingHeartbeatRef) {
461
+ const latency = this._heartbeatSentAt ? Date.now() - this._heartbeatSentAt : undefined;
462
+ try {
463
+ this.heartbeatCallback(msg.payload.status === 'ok' ? 'ok' : 'error', latency);
464
+ }
465
+ catch (e) {
466
+ this.log('error', 'error in heartbeat callback', e);
467
+ }
468
+ this._heartbeatSentAt = null;
469
+ this.pendingHeartbeatRef = null;
470
+ }
471
+ // Log incoming message
472
+ const { topic, event, payload, ref } = msg;
473
+ const refString = ref ? `(${ref})` : '';
474
+ const status = payload.status || '';
475
+ this.log('receive', `${status} ${topic} ${event} ${refString}`.trim(), payload);
476
+ // Route message to appropriate channels
477
+ this.channels
478
+ .filter((channel) => channel._isMember(topic))
479
+ .forEach((channel) => channel._trigger(event, payload, ref));
480
+ this._triggerStateCallbacks('message', msg);
481
+ });
482
+ }
483
+ /**
484
+ * Clear specific timer
485
+ * @internal
486
+ */
487
+ _clearTimer(timer) {
488
+ var _a;
489
+ if (timer === 'heartbeat' && this.heartbeatTimer) {
490
+ clearInterval(this.heartbeatTimer);
491
+ this.heartbeatTimer = undefined;
492
+ }
493
+ else if (timer === 'reconnect') {
494
+ (_a = this.reconnectTimer) === null || _a === void 0 ? void 0 : _a.reset();
495
+ }
496
+ }
497
+ /**
498
+ * Clear all timers
499
+ * @internal
500
+ */
501
+ _clearAllTimers() {
502
+ this._clearTimer('heartbeat');
503
+ this._clearTimer('reconnect');
504
+ }
505
+ /**
506
+ * Setup connection handlers for WebSocket events
507
+ * @internal
508
+ */
509
+ _setupConnectionHandlers() {
510
+ if (!this.conn)
511
+ return;
512
+ // Set binary type if supported (browsers and most WebSocket implementations)
513
+ if ('binaryType' in this.conn) {
514
+ ;
515
+ this.conn.binaryType = 'arraybuffer';
516
+ }
517
+ this.conn.onopen = () => this._onConnOpen();
518
+ this.conn.onerror = (error) => this._onConnError(error);
519
+ this.conn.onmessage = (event) => this._onConnMessage(event);
520
+ this.conn.onclose = (event) => this._onConnClose(event);
521
+ if (this.conn.readyState === constants_1.SOCKET_STATES.open) {
522
+ this._onConnOpen();
523
+ }
524
+ }
525
+ /**
526
+ * Teardown connection and cleanup resources
527
+ * @internal
528
+ */
529
+ _teardownConnection() {
530
+ if (this.conn) {
531
+ if (this.conn.readyState === constants_1.SOCKET_STATES.open ||
532
+ this.conn.readyState === constants_1.SOCKET_STATES.connecting) {
533
+ try {
534
+ this.conn.close();
535
+ }
536
+ catch (e) {
537
+ this.log('error', 'Error closing connection', e);
538
+ }
539
+ }
540
+ this.conn.onopen = null;
541
+ this.conn.onerror = null;
542
+ this.conn.onmessage = null;
543
+ this.conn.onclose = null;
544
+ this.conn = null;
545
+ }
546
+ this._clearAllTimers();
547
+ this._terminateWorker();
548
+ this.channels.forEach((channel) => channel.teardown());
549
+ }
550
+ /** @internal */
551
+ _onConnOpen() {
552
+ this._setConnectionState('connected');
553
+ this.log('transport', `connected to ${this.endpointURL()}`);
554
+ // Wait for any pending auth operations before flushing send buffer
555
+ // This ensures channel join messages include the correct access token
556
+ const authPromise = this._authPromise ||
557
+ (this.accessToken && !this.accessTokenValue ? this.setAuth() : Promise.resolve());
558
+ authPromise
559
+ .then(() => {
560
+ // When subscribe() is called before the accessToken callback has
561
+ // resolved (common on React Native / Expo where token storage is
562
+ // async), the phx_join payload captured at subscribe()-time will
563
+ // have no access_token. By this point auth has settled and
564
+ // this.accessTokenValue holds the real JWT.
565
+ //
566
+ // The stale join messages sitting in sendBuffer captured the old
567
+ // (token-less) payload in a closure, so we cannot simply flush
568
+ // them. Instead we:
569
+ // 1. Patch each channel's joinPush payload with the real token
570
+ // 2. Drop the stale buffered messages
571
+ // 3. Re-send the join for any channel still in "joining" state
572
+ //
573
+ // On browsers this is a harmless no-op: accessTokenValue was
574
+ // already set synchronously before subscribe() ran, so the join
575
+ // payload already had the correct token.
576
+ if (this.accessTokenValue) {
577
+ this.channels.forEach((channel) => {
578
+ channel.updateJoinPayload({ access_token: this.accessTokenValue });
579
+ });
580
+ this.sendBuffer = [];
581
+ this.channels.forEach((channel) => {
582
+ if (channel._isJoining()) {
583
+ channel.joinPush.sent = false;
584
+ channel.joinPush.send();
585
+ }
586
+ });
587
+ }
588
+ this.flushSendBuffer();
589
+ })
590
+ .catch((e) => {
591
+ this.log('error', 'error waiting for auth on connect', e);
592
+ // Proceed anyway to avoid hanging connections
593
+ this.flushSendBuffer();
594
+ });
595
+ this._clearTimer('reconnect');
596
+ if (!this.worker) {
597
+ this._startHeartbeat();
598
+ }
599
+ else {
600
+ if (!this.workerRef) {
601
+ this._startWorkerHeartbeat();
602
+ }
603
+ }
604
+ this._triggerStateCallbacks('open');
605
+ }
606
+ /** @internal */
607
+ _startHeartbeat() {
608
+ this.heartbeatTimer && clearInterval(this.heartbeatTimer);
609
+ this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.heartbeatIntervalMs);
610
+ }
611
+ /** @internal */
612
+ _startWorkerHeartbeat() {
613
+ if (this.workerUrl) {
614
+ this.log('worker', `starting worker for from ${this.workerUrl}`);
615
+ }
616
+ else {
617
+ this.log('worker', `starting default worker`);
618
+ }
619
+ const objectUrl = this._workerObjectUrl(this.workerUrl);
620
+ this.workerRef = new Worker(objectUrl);
621
+ this.workerRef.onerror = (error) => {
622
+ this.log('worker', 'worker error', error.message);
623
+ this._terminateWorker();
624
+ };
625
+ this.workerRef.onmessage = (event) => {
626
+ if (event.data.event === 'keepAlive') {
627
+ this.sendHeartbeat();
628
+ }
629
+ };
630
+ this.workerRef.postMessage({
631
+ event: 'start',
632
+ interval: this.heartbeatIntervalMs,
633
+ });
634
+ }
635
+ /**
636
+ * Terminate the Web Worker and clear the reference
637
+ * @internal
638
+ */
639
+ _terminateWorker() {
640
+ if (this.workerRef) {
641
+ this.log('worker', 'terminating worker');
642
+ this.workerRef.terminate();
643
+ this.workerRef = undefined;
644
+ }
645
+ }
646
+ /** @internal */
647
+ _onConnClose(event) {
648
+ var _a;
649
+ this._setConnectionState('disconnected');
650
+ this.log('transport', 'close', event);
651
+ this._triggerChanError();
652
+ this._clearTimer('heartbeat');
653
+ // Only schedule reconnection if it wasn't a manual disconnect
654
+ if (!this._wasManualDisconnect) {
655
+ (_a = this.reconnectTimer) === null || _a === void 0 ? void 0 : _a.scheduleTimeout();
656
+ }
657
+ this._triggerStateCallbacks('close', event);
658
+ }
659
+ /** @internal */
660
+ _onConnError(error) {
661
+ this._setConnectionState('disconnected');
662
+ this.log('transport', `${error}`);
663
+ this._triggerChanError();
664
+ this._triggerStateCallbacks('error', error);
665
+ try {
666
+ this.heartbeatCallback('error');
667
+ }
668
+ catch (e) {
669
+ this.log('error', 'error in heartbeat callback', e);
670
+ }
671
+ }
672
+ /** @internal */
673
+ _triggerChanError() {
674
+ this.channels.forEach((channel) => channel._trigger(constants_1.CHANNEL_EVENTS.error));
675
+ }
676
+ /** @internal */
677
+ _appendParams(url, params) {
678
+ if (Object.keys(params).length === 0) {
679
+ return url;
680
+ }
681
+ const prefix = url.match(/\?/) ? '&' : '?';
682
+ const query = new URLSearchParams(params);
683
+ return `${url}${prefix}${query}`;
684
+ }
685
+ _workerObjectUrl(url) {
686
+ let result_url;
687
+ if (url) {
688
+ result_url = url;
689
+ }
690
+ else {
691
+ const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' });
692
+ result_url = URL.createObjectURL(blob);
693
+ }
694
+ return result_url;
695
+ }
696
+ /**
697
+ * Set connection state with proper state management
698
+ * @internal
699
+ */
700
+ _setConnectionState(state, manual = false) {
701
+ this._connectionState = state;
702
+ if (state === 'connecting') {
703
+ this._wasManualDisconnect = false;
704
+ }
705
+ else if (state === 'disconnecting') {
706
+ this._wasManualDisconnect = manual;
707
+ }
708
+ }
358
709
  /**
359
710
  * Perform the actual auth operation
360
711
  * @internal
@@ -397,8 +748,8 @@ class RealtimeClient {
397
748
  version: constants_1.DEFAULT_VERSION,
398
749
  };
399
750
  tokenToSend && channel.updateJoinPayload(payload);
400
- if (channel.joinedOnce && channel.channelAdapter.isJoined()) {
401
- channel.channelAdapter.push(constants_1.CHANNEL_EVENTS.access_token, {
751
+ if (channel.joinedOnce && channel._isJoined()) {
752
+ channel._push(constants_1.CHANNEL_EVENTS.access_token, {
402
753
  access_token: tokenToSend,
403
754
  });
404
755
  }
@@ -426,139 +777,85 @@ class RealtimeClient {
426
777
  });
427
778
  }
428
779
  }
429
- /** @internal */
430
- _setupConnectionHandlers() {
431
- this.socketAdapter.onOpen(() => {
432
- const authPromise = this._authPromise ||
433
- (this.accessToken && !this.accessTokenValue ? this.setAuth() : Promise.resolve());
434
- authPromise.catch((e) => {
435
- this.log('error', 'error waiting for auth on connect', e);
780
+ /**
781
+ * Trigger state change callbacks with proper error handling
782
+ * @internal
783
+ */
784
+ _triggerStateCallbacks(event, data) {
785
+ try {
786
+ this.stateChangeCallbacks[event].forEach((callback) => {
787
+ try {
788
+ callback(data);
789
+ }
790
+ catch (e) {
791
+ this.log('error', `error in ${event} callback`, e);
792
+ }
436
793
  });
437
- if (this.worker && !this.workerRef) {
438
- this._startWorkerHeartbeat();
439
- }
440
- });
441
- this.socketAdapter.onClose(() => {
442
- if (this.worker && this.workerRef) {
443
- this._terminateWorker();
444
- }
445
- });
446
- this.socketAdapter.onMessage((message) => {
447
- if (message.ref && message.ref === this._pendingWorkerHeartbeatRef) {
448
- this._pendingWorkerHeartbeatRef = null;
449
- }
450
- });
451
- }
452
- /** @internal */
453
- _handleNodeJsRaceCondition() {
454
- if (this.socketAdapter.isConnected()) {
455
- // hack: ensure onConnOpen is called
456
- this.socketAdapter.getSocket().onConnOpen();
457
794
  }
458
- }
459
- /** @internal */
460
- _wrapHeartbeatCallback(heartbeatCallback) {
461
- return (status, latency) => {
462
- if (status == 'sent')
463
- this._setAuthSafely();
464
- if (heartbeatCallback)
465
- heartbeatCallback(status, latency);
466
- };
467
- }
468
- /** @internal */
469
- _startWorkerHeartbeat() {
470
- if (this.workerUrl) {
471
- this.log('worker', `starting worker for from ${this.workerUrl}`);
472
- }
473
- else {
474
- this.log('worker', `starting default worker`);
795
+ catch (e) {
796
+ this.log('error', `error triggering ${event} callbacks`, e);
475
797
  }
476
- const objectUrl = this._workerObjectUrl(this.workerUrl);
477
- this.workerRef = new Worker(objectUrl);
478
- this.workerRef.onerror = (error) => {
479
- this.log('worker', 'worker error', error.message);
480
- this._terminateWorker();
481
- this.disconnect();
482
- };
483
- this.workerRef.onmessage = (event) => {
484
- if (event.data.event === 'keepAlive') {
485
- this.sendHeartbeat();
486
- }
487
- };
488
- this.workerRef.postMessage({
489
- event: 'start',
490
- interval: this.heartbeatIntervalMs,
491
- });
492
798
  }
493
799
  /**
494
- * Terminate the Web Worker and clear the reference
800
+ * Setup reconnection timer with proper configuration
495
801
  * @internal
496
802
  */
497
- _terminateWorker() {
498
- if (this.workerRef) {
499
- this.log('worker', 'terminating worker');
500
- this.workerRef.terminate();
501
- this.workerRef = undefined;
502
- }
503
- }
504
- /** @internal */
505
- _workerObjectUrl(url) {
506
- let result_url;
507
- if (url) {
508
- result_url = url;
509
- }
510
- else {
511
- const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' });
512
- result_url = URL.createObjectURL(blob);
513
- }
514
- return result_url;
803
+ _setupReconnectionTimer() {
804
+ this.reconnectTimer = new timer_1.default(async () => {
805
+ setTimeout(async () => {
806
+ await this._waitForAuthIfNeeded();
807
+ if (!this.isConnected()) {
808
+ this.connect();
809
+ }
810
+ }, CONNECTION_TIMEOUTS.RECONNECT_DELAY);
811
+ }, this.reconnectAfterMs);
515
812
  }
516
813
  /**
517
- * Initialize socket options with defaults
814
+ * Initialize client options with defaults
518
815
  * @internal
519
816
  */
520
817
  _initializeOptions(options) {
521
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
522
- this.worker = (_a = options === null || options === void 0 ? void 0 : options.worker) !== null && _a !== void 0 ? _a : false;
523
- this.accessToken = (_b = options === null || options === void 0 ? void 0 : options.accessToken) !== null && _b !== void 0 ? _b : null;
524
- const result = {};
525
- result.timeout = (_c = options === null || options === void 0 ? void 0 : options.timeout) !== null && _c !== void 0 ? _c : constants_1.DEFAULT_TIMEOUT;
526
- result.heartbeatIntervalMs =
527
- (_d = options === null || options === void 0 ? void 0 : options.heartbeatIntervalMs) !== null && _d !== void 0 ? _d : CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL;
528
- result.vsn = (_e = options === null || options === void 0 ? void 0 : options.vsn) !== null && _e !== void 0 ? _e : constants_1.DEFAULT_VSN;
529
- // @ts-ignore - mismatch between phoenix and supabase
530
- result.transport = (_f = options === null || options === void 0 ? void 0 : options.transport) !== null && _f !== void 0 ? _f : websocket_factory_1.default.getWebSocketConstructor();
531
- result.params = options === null || options === void 0 ? void 0 : options.params;
532
- result.logger = options === null || options === void 0 ? void 0 : options.logger;
533
- result.heartbeatCallback = this._wrapHeartbeatCallback(options === null || options === void 0 ? void 0 : options.heartbeatCallback);
534
- result.reconnectAfterMs =
535
- (_g = options === null || options === void 0 ? void 0 : options.reconnectAfterMs) !== null && _g !== void 0 ? _g : ((tries) => {
818
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
819
+ // Set defaults
820
+ this.transport = (_a = options === null || options === void 0 ? void 0 : options.transport) !== null && _a !== void 0 ? _a : null;
821
+ this.timeout = (_b = options === null || options === void 0 ? void 0 : options.timeout) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_TIMEOUT;
822
+ this.heartbeatIntervalMs =
823
+ (_c = options === null || options === void 0 ? void 0 : options.heartbeatIntervalMs) !== null && _c !== void 0 ? _c : CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL;
824
+ this.worker = (_d = options === null || options === void 0 ? void 0 : options.worker) !== null && _d !== void 0 ? _d : false;
825
+ this.accessToken = (_e = options === null || options === void 0 ? void 0 : options.accessToken) !== null && _e !== void 0 ? _e : null;
826
+ this.heartbeatCallback = (_f = options === null || options === void 0 ? void 0 : options.heartbeatCallback) !== null && _f !== void 0 ? _f : noop;
827
+ this.vsn = (_g = options === null || options === void 0 ? void 0 : options.vsn) !== null && _g !== void 0 ? _g : constants_1.DEFAULT_VSN;
828
+ // Handle special cases
829
+ if (options === null || options === void 0 ? void 0 : options.params)
830
+ this.params = options.params;
831
+ if (options === null || options === void 0 ? void 0 : options.logger)
832
+ this.logger = options.logger;
833
+ if ((options === null || options === void 0 ? void 0 : options.logLevel) || (options === null || options === void 0 ? void 0 : options.log_level)) {
834
+ this.logLevel = options.logLevel || options.log_level;
835
+ this.params = Object.assign(Object.assign({}, this.params), { log_level: this.logLevel });
836
+ }
837
+ // Set up functions with defaults
838
+ this.reconnectAfterMs =
839
+ (_h = options === null || options === void 0 ? void 0 : options.reconnectAfterMs) !== null && _h !== void 0 ? _h : ((tries) => {
536
840
  return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK;
537
841
  });
538
- let defaultEncode;
539
- let defaultDecode;
540
- switch (result.vsn) {
842
+ switch (this.vsn) {
541
843
  case constants_1.VSN_1_0_0:
542
- defaultEncode = (payload, callback) => {
543
- return callback(JSON.stringify(payload));
544
- };
545
- defaultDecode = (payload, callback) => {
546
- return callback(JSON.parse(payload));
547
- };
844
+ this.encode =
845
+ (_j = options === null || options === void 0 ? void 0 : options.encode) !== null && _j !== void 0 ? _j : ((payload, callback) => {
846
+ return callback(JSON.stringify(payload));
847
+ });
848
+ this.decode =
849
+ (_k = options === null || options === void 0 ? void 0 : options.decode) !== null && _k !== void 0 ? _k : ((payload, callback) => {
850
+ return callback(JSON.parse(payload));
851
+ });
548
852
  break;
549
853
  case constants_1.VSN_2_0_0:
550
- defaultEncode = this.serializer.encode.bind(this.serializer);
551
- defaultDecode = this.serializer.decode.bind(this.serializer);
854
+ this.encode = (_l = options === null || options === void 0 ? void 0 : options.encode) !== null && _l !== void 0 ? _l : this.serializer.encode.bind(this.serializer);
855
+ this.decode = (_m = options === null || options === void 0 ? void 0 : options.decode) !== null && _m !== void 0 ? _m : this.serializer.decode.bind(this.serializer);
552
856
  break;
553
857
  default:
554
- throw new Error(`Unsupported serializer version: ${result.vsn}`);
555
- }
556
- result.encode = (_h = options === null || options === void 0 ? void 0 : options.encode) !== null && _h !== void 0 ? _h : defaultEncode;
557
- result.decode = (_j = options === null || options === void 0 ? void 0 : options.decode) !== null && _j !== void 0 ? _j : defaultDecode;
558
- result.beforeReconnect = this._reconnectAuth.bind(this);
559
- if ((options === null || options === void 0 ? void 0 : options.logLevel) || (options === null || options === void 0 ? void 0 : options.log_level)) {
560
- this.logLevel = options.logLevel || options.log_level;
561
- result.params = Object.assign(Object.assign({}, result.params), { log_level: this.logLevel });
858
+ throw new Error(`Unsupported serializer version: ${this.vsn}`);
562
859
  }
563
860
  // Handle worker setup
564
861
  if (this.worker) {
@@ -566,15 +863,6 @@ class RealtimeClient {
566
863
  throw new Error('Web Worker is not supported');
567
864
  }
568
865
  this.workerUrl = options === null || options === void 0 ? void 0 : options.workerUrl;
569
- result.autoSendHeartbeat = !this.worker;
570
- }
571
- return result;
572
- }
573
- /** @internal */
574
- async _reconnectAuth() {
575
- await this._waitForAuthIfNeeded();
576
- if (!this.isConnected()) {
577
- this.connect();
578
866
  }
579
867
  }
580
868
  }