@supabase/realtime-js 2.99.2 → 2.100.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/main/RealtimeChannel.d.ts +35 -28
  2. package/dist/main/RealtimeChannel.d.ts.map +1 -1
  3. package/dist/main/RealtimeChannel.js +140 -301
  4. package/dist/main/RealtimeChannel.js.map +1 -1
  5. package/dist/main/RealtimeClient.d.ts +38 -57
  6. package/dist/main/RealtimeClient.d.ts.map +1 -1
  7. package/dist/main/RealtimeClient.js +232 -520
  8. package/dist/main/RealtimeClient.js.map +1 -1
  9. package/dist/main/RealtimePresence.d.ts +8 -24
  10. package/dist/main/RealtimePresence.d.ts.map +1 -1
  11. package/dist/main/RealtimePresence.js +6 -202
  12. package/dist/main/RealtimePresence.js.map +1 -1
  13. package/dist/main/lib/constants.d.ts +39 -35
  14. package/dist/main/lib/constants.d.ts.map +1 -1
  15. package/dist/main/lib/constants.js +30 -35
  16. package/dist/main/lib/constants.js.map +1 -1
  17. package/dist/main/lib/version.d.ts +1 -1
  18. package/dist/main/lib/version.d.ts.map +1 -1
  19. package/dist/main/lib/version.js +1 -1
  20. package/dist/main/lib/version.js.map +1 -1
  21. package/dist/main/lib/websocket-factory.d.ts +0 -9
  22. package/dist/main/lib/websocket-factory.d.ts.map +1 -1
  23. package/dist/main/lib/websocket-factory.js +0 -12
  24. package/dist/main/lib/websocket-factory.js.map +1 -1
  25. package/dist/main/phoenix/channelAdapter.d.ts +32 -0
  26. package/dist/main/phoenix/channelAdapter.d.ts.map +1 -0
  27. package/dist/main/phoenix/channelAdapter.js +103 -0
  28. package/dist/main/phoenix/channelAdapter.js.map +1 -0
  29. package/dist/main/phoenix/presenceAdapter.d.ts +53 -0
  30. package/dist/main/phoenix/presenceAdapter.d.ts.map +1 -0
  31. package/dist/main/phoenix/presenceAdapter.js +93 -0
  32. package/dist/main/phoenix/presenceAdapter.js.map +1 -0
  33. package/dist/main/phoenix/socketAdapter.d.ts +38 -0
  34. package/dist/main/phoenix/socketAdapter.d.ts.map +1 -0
  35. package/dist/main/phoenix/socketAdapter.js +114 -0
  36. package/dist/main/phoenix/socketAdapter.js.map +1 -0
  37. package/dist/main/phoenix/types.d.ts +5 -0
  38. package/dist/main/phoenix/types.d.ts.map +1 -0
  39. package/dist/main/phoenix/types.js +3 -0
  40. package/dist/main/phoenix/types.js.map +1 -0
  41. package/dist/module/RealtimeChannel.d.ts +35 -28
  42. package/dist/module/RealtimeChannel.d.ts.map +1 -1
  43. package/dist/module/RealtimeChannel.js +141 -302
  44. package/dist/module/RealtimeChannel.js.map +1 -1
  45. package/dist/module/RealtimeClient.d.ts +38 -57
  46. package/dist/module/RealtimeClient.d.ts.map +1 -1
  47. package/dist/module/RealtimeClient.js +233 -521
  48. package/dist/module/RealtimeClient.js.map +1 -1
  49. package/dist/module/RealtimePresence.d.ts +8 -24
  50. package/dist/module/RealtimePresence.d.ts.map +1 -1
  51. package/dist/module/RealtimePresence.js +5 -202
  52. package/dist/module/RealtimePresence.js.map +1 -1
  53. package/dist/module/lib/constants.d.ts +39 -35
  54. package/dist/module/lib/constants.d.ts.map +1 -1
  55. package/dist/module/lib/constants.js +30 -35
  56. package/dist/module/lib/constants.js.map +1 -1
  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 +0 -9
  62. package/dist/module/lib/websocket-factory.d.ts.map +1 -1
  63. package/dist/module/lib/websocket-factory.js +0 -12
  64. package/dist/module/lib/websocket-factory.js.map +1 -1
  65. package/dist/module/phoenix/channelAdapter.d.ts +32 -0
  66. package/dist/module/phoenix/channelAdapter.d.ts.map +1 -0
  67. package/dist/module/phoenix/channelAdapter.js +100 -0
  68. package/dist/module/phoenix/channelAdapter.js.map +1 -0
  69. package/dist/module/phoenix/presenceAdapter.d.ts +53 -0
  70. package/dist/module/phoenix/presenceAdapter.d.ts.map +1 -0
  71. package/dist/module/phoenix/presenceAdapter.js +90 -0
  72. package/dist/module/phoenix/presenceAdapter.js.map +1 -0
  73. package/dist/module/phoenix/socketAdapter.d.ts +38 -0
  74. package/dist/module/phoenix/socketAdapter.d.ts.map +1 -0
  75. package/dist/module/phoenix/socketAdapter.js +111 -0
  76. package/dist/module/phoenix/socketAdapter.js.map +1 -0
  77. package/dist/module/phoenix/types.d.ts +5 -0
  78. package/dist/module/phoenix/types.d.ts.map +1 -0
  79. package/dist/module/phoenix/types.js +2 -0
  80. package/dist/module/phoenix/types.js.map +1 -0
  81. package/dist/tsconfig.module.tsbuildinfo +1 -1
  82. package/dist/tsconfig.tsbuildinfo +1 -1
  83. package/package.json +2 -2
  84. package/src/RealtimeChannel.ts +201 -364
  85. package/src/RealtimeClient.ts +296 -583
  86. package/src/RealtimePresence.ts +10 -287
  87. package/src/lib/constants.ts +50 -37
  88. package/src/lib/version.ts +1 -1
  89. package/src/lib/websocket-factory.ts +0 -13
  90. package/src/phoenix/channelAdapter.ts +147 -0
  91. package/src/phoenix/presenceAdapter.ts +116 -0
  92. package/src/phoenix/socketAdapter.ts +168 -0
  93. package/src/phoenix/types.ts +32 -0
  94. package/dist/main/lib/push.d.ts +0 -48
  95. package/dist/main/lib/push.d.ts.map +0 -1
  96. package/dist/main/lib/push.js +0 -102
  97. package/dist/main/lib/push.js.map +0 -1
  98. package/dist/main/lib/timer.d.ts +0 -22
  99. package/dist/main/lib/timer.d.ts.map +0 -1
  100. package/dist/main/lib/timer.js +0 -39
  101. package/dist/main/lib/timer.js.map +0 -1
  102. package/dist/module/lib/push.d.ts +0 -48
  103. package/dist/module/lib/push.d.ts.map +0 -1
  104. package/dist/module/lib/push.js +0 -99
  105. package/dist/module/lib/push.js.map +0 -1
  106. package/dist/module/lib/timer.d.ts +0 -22
  107. package/dist/module/lib/timer.d.ts.map +0 -1
  108. package/dist/module/lib/timer.js +0 -36
  109. package/dist/module/lib/timer.js.map +0 -1
  110. package/src/lib/push.ts +0 -121
  111. package/src/lib/timer.ts +0 -43
@@ -4,10 +4,9 @@ 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"));
8
7
  const transformers_1 = require("./lib/transformers");
9
8
  const RealtimeChannel_1 = tslib_1.__importDefault(require("./RealtimeChannel"));
10
- const noop = () => { };
9
+ const socketAdapter_1 = tslib_1.__importDefault(require("./phoenix/socketAdapter"));
11
10
  // Connection-related constants
12
11
  const CONNECTION_TIMEOUTS = {
13
12
  HEARTBEAT_INTERVAL: 25000,
@@ -23,6 +22,54 @@ const WORKER_SCRIPT = `
23
22
  }
24
23
  });`;
25
24
  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
+ }
26
73
  /**
27
74
  * Initializes the Socket.
28
75
  *
@@ -54,39 +101,20 @@ class RealtimeClient {
54
101
  */
55
102
  constructor(endPoint, options) {
56
103
  var _a;
104
+ this.channels = new Array();
57
105
  this.accessTokenValue = null;
106
+ this.accessToken = null;
58
107
  this.apiKey = null;
59
- this._manuallySetToken = false;
60
- this.channels = new Array();
61
- this.endPoint = '';
62
108
  this.httpEndpoint = '';
63
109
  /** @deprecated headers cannot be set on websocket connections */
64
110
  this.headers = {};
65
111
  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;
72
112
  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 = [];
78
113
  this.serializer = new serializer_1.default();
79
- this.stateChangeCallbacks = {
80
- open: [],
81
- close: [],
82
- error: [],
83
- message: [],
84
- };
85
- this.accessToken = null;
86
- this._connectionState = 'disconnected';
87
- this._wasManualDisconnect = false;
114
+ this._manuallySetToken = false;
88
115
  this._authPromise = null;
89
- this._heartbeatSentAt = null;
116
+ this._workerHeartbeatTimer = undefined;
117
+ this._pendingWorkerHeartbeatRef = null;
90
118
  /**
91
119
  * Use either custom fetch, if provided, or default fetch to make HTTP requests
92
120
  *
@@ -103,11 +131,9 @@ class RealtimeClient {
103
131
  throw new Error('API key is required to connect to Realtime');
104
132
  }
105
133
  this.apiKey = options.params.apikey;
106
- // Initialize endpoint URLs
107
- this.endPoint = `${endPoint}/${constants_1.TRANSPORTS.websocket}`;
134
+ const socketAdapterOptions = this._initializeOptions(options);
135
+ this.socketAdapter = new socketAdapter_1.default(endPoint, socketAdapterOptions);
108
136
  this.httpEndpoint = (0, transformers_1.httpEndpointURL)(endPoint);
109
- this._initializeOptions(options);
110
- this._setupReconnectionTimer();
111
137
  this.fetch = this._resolveFetch(options === null || options === void 0 ? void 0 : options.fetch);
112
138
  }
113
139
  /**
@@ -115,55 +141,44 @@ class RealtimeClient {
115
141
  */
116
142
  connect() {
117
143
  // Skip if already connecting, disconnecting, or connected
118
- if (this.isConnecting() ||
119
- this.isDisconnecting() ||
120
- (this.conn !== null && this.isConnected())) {
144
+ if (this.isConnecting() || this.isDisconnecting() || this.isConnected()) {
121
145
  return;
122
146
  }
123
- this._setConnectionState('connecting');
124
147
  // Trigger auth if needed and not already in progress
125
148
  // This ensures auth is called for standalone RealtimeClient usage
126
149
  // while avoiding race conditions with SupabaseClient's immediate setAuth call
127
150
  if (this.accessToken && !this._authPromise) {
128
151
  this._setAuthSafely('connect');
129
152
  }
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}`);
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
+ ' })');
157
171
  }
172
+ throw new Error(`WebSocket not available: ${errorMessage}`);
158
173
  }
159
- this._setupConnectionHandlers();
174
+ this._handleNodeJsRaceCondition();
160
175
  }
161
176
  /**
162
177
  * Returns the URL of the websocket.
163
178
  * @returns string The URL of the websocket.
164
179
  */
165
180
  endpointURL() {
166
- return this._appendParams(this.endPoint, Object.assign({}, this.params, { vsn: this.vsn }));
181
+ return this.socketAdapter.endPointURL();
167
182
  }
168
183
  /**
169
184
  * Disconnects the socket.
@@ -171,34 +186,14 @@ class RealtimeClient {
171
186
  * @param code A numeric status code to send on disconnect.
172
187
  * @param reason A custom reason for the disconnect.
173
188
  */
174
- disconnect(code, reason) {
189
+ async disconnect(code, reason) {
175
190
  if (this.isDisconnecting()) {
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');
191
+ return 'ok';
201
192
  }
193
+ return await this.socketAdapter.disconnect(() => {
194
+ clearInterval(this._workerHeartbeatTimer);
195
+ this._terminateWorker();
196
+ }, code, reason);
202
197
  }
203
198
  /**
204
199
  * Returns all created channels
@@ -207,65 +202,63 @@ class RealtimeClient {
207
202
  return this.channels;
208
203
  }
209
204
  /**
210
- * Unsubscribes and removes a single channel
205
+ * Unsubscribes, removes and tears down a single channel
211
206
  * @param channel A RealtimeChannel instance
212
207
  */
213
208
  async removeChannel(channel) {
214
209
  const status = await channel.unsubscribe();
210
+ if (status === 'ok') {
211
+ channel.teardown();
212
+ }
215
213
  if (this.channels.length === 0) {
216
214
  this.disconnect();
217
215
  }
218
216
  return status;
219
217
  }
220
218
  /**
221
- * Unsubscribes and removes all channels
219
+ * Unsubscribes, removes and tears down all channels
222
220
  */
223
221
  async removeAllChannels() {
224
- const values_1 = await Promise.all(this.channels.map((channel) => channel.unsubscribe()));
225
- this.channels = [];
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);
226
228
  this.disconnect();
227
- return values_1;
229
+ return result;
228
230
  }
229
231
  /**
230
232
  * Logs the message.
231
233
  *
232
- * For customized logging, `this.logger` can be overridden.
234
+ * For customized logging, `this.logger` can be overridden in Client constructor.
233
235
  */
234
236
  log(kind, msg, data) {
235
- this.logger(kind, msg, data);
237
+ this.socketAdapter.log(kind, msg, data);
236
238
  }
237
239
  /**
238
240
  * Returns the current state of the socket.
239
241
  */
240
242
  connectionState() {
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
- }
243
+ return this.socketAdapter.connectionState() || constants_1.CONNECTION_STATE.closed;
251
244
  }
252
245
  /**
253
246
  * Returns `true` is the connection is open.
254
247
  */
255
248
  isConnected() {
256
- return this.connectionState() === constants_1.CONNECTION_STATE.Open;
249
+ return this.socketAdapter.isConnected();
257
250
  }
258
251
  /**
259
252
  * Returns `true` if the connection is currently connecting.
260
253
  */
261
254
  isConnecting() {
262
- return this._connectionState === 'connecting';
255
+ return this.socketAdapter.isConnecting();
263
256
  }
264
257
  /**
265
258
  * Returns `true` if the connection is currently disconnecting.
266
259
  */
267
260
  isDisconnecting() {
268
- return this._connectionState === 'disconnecting';
261
+ return this.socketAdapter.isDisconnecting();
269
262
  }
270
263
  /**
271
264
  * Creates (or reuses) a {@link RealtimeChannel} for the provided topic.
@@ -292,20 +285,7 @@ class RealtimeClient {
292
285
  * If the socket is not connected, the message gets enqueued within a local buffer, and sent out when a connection is next established.
293
286
  */
294
287
  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
- }
288
+ this.socketAdapter.push(data);
309
289
  }
310
290
  /**
311
291
  * Sets the JWT access token used for channel subscription authorization and Realtime RLS.
@@ -348,70 +328,14 @@ class RealtimeClient {
348
328
  * Sends a heartbeat message if the socket is connected.
349
329
  */
350
330
  async 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');
331
+ this.socketAdapter.sendHeartbeat();
399
332
  }
400
333
  /**
401
334
  * Sets a callback that receives lifecycle events for internal heartbeat messages.
402
335
  * Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
403
336
  */
404
337
  onHeartbeat(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
- }
338
+ this.socketAdapter.heartbeatCallback = this._wrapHeartbeatCallback(callback);
415
339
  }
416
340
  /**
417
341
  * Return the next message ref, accounting for overflows
@@ -419,29 +343,10 @@ class RealtimeClient {
419
343
  * @internal
420
344
  */
421
345
  _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();
346
+ return this.socketAdapter.makeRef();
430
347
  }
431
348
  /**
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.
349
+ * Removes a channel from RealtimeClient
445
350
  *
446
351
  * @param channel An open subscription.
447
352
  *
@@ -450,262 +355,6 @@ class RealtimeClient {
450
355
  _remove(channel) {
451
356
  this.channels = this.channels.filter((c) => c.topic !== channel.topic);
452
357
  }
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
- }
709
358
  /**
710
359
  * Perform the actual auth operation
711
360
  * @internal
@@ -748,8 +397,8 @@ class RealtimeClient {
748
397
  version: constants_1.DEFAULT_VERSION,
749
398
  };
750
399
  tokenToSend && channel.updateJoinPayload(payload);
751
- if (channel.joinedOnce && channel._isJoined()) {
752
- channel._push(constants_1.CHANNEL_EVENTS.access_token, {
400
+ if (channel.joinedOnce && channel.channelAdapter.isJoined()) {
401
+ channel.channelAdapter.push(constants_1.CHANNEL_EVENTS.access_token, {
753
402
  access_token: tokenToSend,
754
403
  });
755
404
  }
@@ -777,85 +426,139 @@ class RealtimeClient {
777
426
  });
778
427
  }
779
428
  }
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
- }
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);
793
436
  });
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();
794
457
  }
795
- catch (e) {
796
- this.log('error', `error triggering ${event} callbacks`, e);
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`);
797
475
  }
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
+ });
798
492
  }
799
493
  /**
800
- * Setup reconnection timer with proper configuration
494
+ * Terminate the Web Worker and clear the reference
801
495
  * @internal
802
496
  */
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);
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;
812
515
  }
813
516
  /**
814
- * Initialize client options with defaults
517
+ * Initialize socket options with defaults
815
518
  * @internal
816
519
  */
817
520
  _initializeOptions(options) {
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) => {
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) => {
840
536
  return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK;
841
537
  });
842
- switch (this.vsn) {
538
+ let defaultEncode;
539
+ let defaultDecode;
540
+ switch (result.vsn) {
843
541
  case constants_1.VSN_1_0_0:
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
- });
542
+ defaultEncode = (payload, callback) => {
543
+ return callback(JSON.stringify(payload));
544
+ };
545
+ defaultDecode = (payload, callback) => {
546
+ return callback(JSON.parse(payload));
547
+ };
852
548
  break;
853
549
  case constants_1.VSN_2_0_0:
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);
550
+ defaultEncode = this.serializer.encode.bind(this.serializer);
551
+ defaultDecode = this.serializer.decode.bind(this.serializer);
856
552
  break;
857
553
  default:
858
- throw new Error(`Unsupported serializer version: ${this.vsn}`);
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 });
859
562
  }
860
563
  // Handle worker setup
861
564
  if (this.worker) {
@@ -863,6 +566,15 @@ class RealtimeClient {
863
566
  throw new Error('Web Worker is not supported');
864
567
  }
865
568
  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();
866
578
  }
867
579
  }
868
580
  }