@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
@@ -1,6 +1,7 @@
1
- import { CHANNEL_EVENTS, CHANNEL_STATES } from './lib/constants'
2
- import type { ChannelState } from './lib/constants'
1
+ import { CHANNEL_EVENTS, CHANNEL_STATES, MAX_PUSH_BUFFER_SIZE } from './lib/constants'
2
+ import Push from './lib/push'
3
3
  import type RealtimeClient from './RealtimeClient'
4
+ import Timer from './lib/timer'
4
5
  import RealtimePresence, { REALTIME_PRESENCE_LISTEN_EVENTS } from './RealtimePresence'
5
6
  import type {
6
7
  RealtimePresenceJoinPayload,
@@ -9,8 +10,6 @@ import type {
9
10
  } from './RealtimePresence'
10
11
  import * as Transformers from './lib/transformers'
11
12
  import { httpEndpointURL } from './lib/transformers'
12
- import ChannelAdapter from './phoenix/channelAdapter'
13
- import { ChannelBindingCallback, ChannelOnErrorCallback } from './phoenix/types'
14
13
 
15
14
  type ReplayOption = {
16
15
  since: number
@@ -148,7 +147,7 @@ export enum REALTIME_SUBSCRIBE_STATES {
148
147
 
149
148
  export const REALTIME_CHANNEL_STATES = CHANNEL_STATES
150
149
 
151
- type PostgresChangesFilters = {
150
+ interface PostgresChangesFilters {
152
151
  postgres_changes: {
153
152
  id: string
154
153
  event: string
@@ -157,52 +156,30 @@ type PostgresChangesFilters = {
157
156
  filter?: string
158
157
  }[]
159
158
  }
160
-
161
- type Binding = {
162
- type: string
163
- filter: { [key: string]: any }
164
- callback: ChannelBindingCallback
165
- ref: number
166
- id?: string
167
- }
168
-
169
159
  /** A channel is the basic building block of Realtime
170
160
  * and narrows the scope of data flow to subscribed clients.
171
161
  * You can think of a channel as a chatroom where participants are able to see who's online
172
162
  * and send and receive messages.
173
163
  */
174
164
  export default class RealtimeChannel {
175
- bindings: Record<string, Binding[]> = {}
176
- subTopic: string
165
+ bindings: {
166
+ [key: string]: {
167
+ type: string
168
+ filter: { [key: string]: any }
169
+ callback: Function
170
+ id?: string
171
+ }[]
172
+ } = {}
173
+ timeout: number
174
+ state: CHANNEL_STATES = CHANNEL_STATES.closed
175
+ joinedOnce = false
176
+ joinPush: Push
177
+ rejoinTimer: Timer
178
+ pushBuffer: Push[] = []
179
+ presence: RealtimePresence
177
180
  broadcastEndpointURL: string
181
+ subTopic: string
178
182
  private: boolean
179
- presence: RealtimePresence
180
- /** @internal */
181
- channelAdapter: ChannelAdapter
182
-
183
- get state() {
184
- return this.channelAdapter.state
185
- }
186
-
187
- set state(state: ChannelState) {
188
- this.channelAdapter.state = state
189
- }
190
-
191
- get joinedOnce() {
192
- return this.channelAdapter.joinedOnce
193
- }
194
-
195
- get timeout() {
196
- return this.socket.timeout
197
- }
198
-
199
- get joinPush() {
200
- return this.channelAdapter.joinPush
201
- }
202
-
203
- get rejoinTimer() {
204
- return this.channelAdapter.rejoinTimer
205
- }
206
183
 
207
184
  /**
208
185
  * Creates a channel that can broadcast messages, sync presence, and listen to Postgres changes.
@@ -235,17 +212,53 @@ export default class RealtimeChannel {
235
212
  },
236
213
  ...params.config,
237
214
  }
238
-
239
- this.channelAdapter = new ChannelAdapter(this.socket.socketAdapter, topic, this.params)
240
- this.presence = new RealtimePresence(this)
241
-
215
+ this.timeout = this.socket.timeout
216
+ this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout)
217
+ this.rejoinTimer = new Timer(() => this._rejoinUntilConnected(), this.socket.reconnectAfterMs)
218
+ this.joinPush.receive('ok', () => {
219
+ this.state = CHANNEL_STATES.joined
220
+ this.rejoinTimer.reset()
221
+ this.pushBuffer.forEach((pushEvent: Push) => pushEvent.send())
222
+ this.pushBuffer = []
223
+ })
242
224
  this._onClose(() => {
225
+ this.rejoinTimer.reset()
226
+ this.socket.log('channel', `close ${this.topic} ${this._joinRef()}`)
227
+ this.state = CHANNEL_STATES.closed
243
228
  this.socket._remove(this)
244
229
  })
230
+ this._onError((reason: string) => {
231
+ if (this._isLeaving() || this._isClosed()) {
232
+ return
233
+ }
234
+ this.socket.log('channel', `error ${this.topic}`, reason)
235
+ this.state = CHANNEL_STATES.errored
236
+ this.rejoinTimer.scheduleTimeout()
237
+ })
238
+ this.joinPush.receive('timeout', () => {
239
+ if (!this._isJoining()) {
240
+ return
241
+ }
242
+ this.socket.log('channel', `timeout ${this.topic}`, this.joinPush.timeout)
243
+ this.state = CHANNEL_STATES.errored
244
+ this.rejoinTimer.scheduleTimeout()
245
+ })
245
246
 
246
- this._updateFilterTransform()
247
+ this.joinPush.receive('error', (reason: any) => {
248
+ if (this._isLeaving() || this._isClosed()) {
249
+ return
250
+ }
251
+ this.socket.log('channel', `error ${this.topic}`, reason)
252
+ this.state = CHANNEL_STATES.errored
253
+ this.rejoinTimer.scheduleTimeout()
254
+ })
255
+ this._on(CHANNEL_EVENTS.reply, {}, (payload: any, ref: string) => {
256
+ this._trigger(this._replyEventName(ref), payload)
257
+ })
258
+
259
+ this.presence = new RealtimePresence(this)
247
260
 
248
- this.broadcastEndpointURL = httpEndpointURL(this.socket.socketAdapter.endPointURL())
261
+ this.broadcastEndpointURL = httpEndpointURL(this.socket.endPoint)
249
262
  this.private = this.params.config.private || false
250
263
 
251
264
  if (!this.private && this.params.config?.broadcast?.replay) {
@@ -261,7 +274,7 @@ export default class RealtimeChannel {
261
274
  if (!this.socket.isConnected()) {
262
275
  this.socket.connect()
263
276
  }
264
- if (this.channelAdapter.isClosed()) {
277
+ if (this.state == CHANNEL_STATES.closed) {
265
278
  const {
266
279
  config: { broadcast, presence, private: isPrivate },
267
280
  } = this.params
@@ -284,18 +297,16 @@ export default class RealtimeChannel {
284
297
  accessTokenPayload.access_token = this.socket.accessTokenValue
285
298
  }
286
299
 
287
- this._onError((reason: unknown) => {
288
- callback?.(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, reason as Error)
289
- })
300
+ this._onError((e: Error) => callback?.(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, e))
290
301
 
291
302
  this._onClose(() => callback?.(REALTIME_SUBSCRIBE_STATES.CLOSED))
292
303
 
293
304
  this.updateJoinPayload({ ...{ config }, ...accessTokenPayload })
294
305
 
295
- this._updateFilterMessage()
306
+ this.joinedOnce = true
307
+ this._rejoin(timeout)
296
308
 
297
- this.channelAdapter
298
- .subscribe(timeout)
309
+ this.joinPush
299
310
  .receive('ok', async ({ postgres_changes }: PostgresChangesFilters) => {
300
311
  // Only refresh auth if using callback-based tokens
301
312
  if (!this.socket._isManualToken()) {
@@ -304,9 +315,46 @@ export default class RealtimeChannel {
304
315
  if (postgres_changes === undefined) {
305
316
  callback?.(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED)
306
317
  return
318
+ } else {
319
+ const clientPostgresBindings = this.bindings.postgres_changes
320
+ const bindingsLen = clientPostgresBindings?.length ?? 0
321
+ const newPostgresBindings = []
322
+
323
+ for (let i = 0; i < bindingsLen; i++) {
324
+ const clientPostgresBinding = clientPostgresBindings[i]
325
+ const {
326
+ filter: { event, schema, table, filter },
327
+ } = clientPostgresBinding
328
+ const serverPostgresFilter = postgres_changes && postgres_changes[i]
329
+
330
+ if (
331
+ serverPostgresFilter &&
332
+ serverPostgresFilter.event === event &&
333
+ RealtimeChannel.isFilterValueEqual(serverPostgresFilter.schema, schema) &&
334
+ RealtimeChannel.isFilterValueEqual(serverPostgresFilter.table, table) &&
335
+ RealtimeChannel.isFilterValueEqual(serverPostgresFilter.filter, filter)
336
+ ) {
337
+ newPostgresBindings.push({
338
+ ...clientPostgresBinding,
339
+ id: serverPostgresFilter.id,
340
+ })
341
+ } else {
342
+ this.unsubscribe()
343
+ this.state = CHANNEL_STATES.errored
344
+
345
+ callback?.(
346
+ REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR,
347
+ new Error('mismatch between server and client bindings for postgres changes')
348
+ )
349
+ return
350
+ }
351
+ }
352
+
353
+ this.bindings.postgres_changes = newPostgresBindings
354
+
355
+ callback && callback(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED)
356
+ return
307
357
  }
308
-
309
- this._updatePostgresBindings(postgres_changes, callback)
310
358
  })
311
359
  .receive('error', (error: { [key: string]: any }) => {
312
360
  this.state = CHANNEL_STATES.errored
@@ -314,59 +362,16 @@ export default class RealtimeChannel {
314
362
  REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR,
315
363
  new Error(JSON.stringify(Object.values(error).join(', ') || 'error'))
316
364
  )
365
+ return
317
366
  })
318
367
  .receive('timeout', () => {
319
368
  callback?.(REALTIME_SUBSCRIBE_STATES.TIMED_OUT)
369
+ return
320
370
  })
321
371
  }
322
372
  return this
323
373
  }
324
374
 
325
- private _updatePostgresBindings(
326
- postgres_changes: PostgresChangesFilters['postgres_changes'],
327
- callback?: (status: REALTIME_SUBSCRIBE_STATES, err?: Error) => void
328
- ) {
329
- const clientPostgresBindings = this.bindings.postgres_changes
330
- const bindingsLen = clientPostgresBindings?.length ?? 0
331
- const newPostgresBindings = []
332
-
333
- for (let i = 0; i < bindingsLen; i++) {
334
- const clientPostgresBinding = clientPostgresBindings[i]
335
- const {
336
- filter: { event, schema, table, filter },
337
- } = clientPostgresBinding
338
- const serverPostgresFilter = postgres_changes && postgres_changes[i]
339
-
340
- if (
341
- serverPostgresFilter &&
342
- serverPostgresFilter.event === event &&
343
- RealtimeChannel.isFilterValueEqual(serverPostgresFilter.schema, schema) &&
344
- RealtimeChannel.isFilterValueEqual(serverPostgresFilter.table, table) &&
345
- RealtimeChannel.isFilterValueEqual(serverPostgresFilter.filter, filter)
346
- ) {
347
- newPostgresBindings.push({
348
- ...clientPostgresBinding,
349
- id: serverPostgresFilter.id,
350
- })
351
- } else {
352
- this.unsubscribe()
353
- this.state = CHANNEL_STATES.errored
354
-
355
- callback?.(
356
- REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR,
357
- new Error('mismatch between server and client bindings for postgres changes')
358
- )
359
- return
360
- }
361
- }
362
-
363
- this.bindings.postgres_changes = newPostgresBindings
364
-
365
- if (this.state != CHANNEL_STATES.errored && callback) {
366
- callback(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED)
367
- }
368
- }
369
-
370
375
  /**
371
376
  * Returns the current presence state for this channel.
372
377
  *
@@ -426,11 +431,6 @@ export default class RealtimeChannel {
426
431
  filter: { event: `${REALTIME_PRESENCE_LISTEN_EVENTS.LEAVE}` },
427
432
  callback: (payload: RealtimePresenceLeavePayload<T>) => void
428
433
  ): RealtimeChannel
429
- on<T extends { [key: string]: any }>(
430
- type: `${REALTIME_LISTEN_TYPES.PRESENCE}`,
431
- filter: { event: '*' },
432
- callback: (payload?: RealtimePresenceJoinPayload<T> | RealtimePresenceLeavePayload<T>) => void
433
- ): RealtimeChannel
434
434
  on<T extends { [key: string]: any }>(
435
435
  type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`,
436
436
  filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL}`>,
@@ -534,9 +534,12 @@ export default class RealtimeChannel {
534
534
  filter: { event: string; [key: string]: string },
535
535
  callback: (payload: any) => void
536
536
  ): RealtimeChannel {
537
- if (this.channelAdapter.isJoined() && type === REALTIME_LISTEN_TYPES.PRESENCE) {
538
- this.socket.log('channel', `cannot add presence callbacks for ${this.topic} after joining.`)
539
- throw new Error('cannot add presence callbacks after joining a channel')
537
+ if (this.state === CHANNEL_STATES.joined && type === REALTIME_LISTEN_TYPES.PRESENCE) {
538
+ this.socket.log(
539
+ 'channel',
540
+ `resubscribe to ${this.topic} due to change in presence callbacks on joined channel`
541
+ )
542
+ this.unsubscribe().then(async () => await this.subscribe())
540
543
  }
541
544
  return this._on(type, filter, callback)
542
545
  }
@@ -621,7 +624,7 @@ export default class RealtimeChannel {
621
624
  },
622
625
  opts: { [key: string]: any } = {}
623
626
  ): Promise<RealtimeChannelSendResponse> {
624
- if (!this.channelAdapter.canPush() && args.type === 'broadcast') {
627
+ if (!this._canPush() && args.type === 'broadcast') {
625
628
  console.warn(
626
629
  'Realtime send() is automatically falling back to REST API. ' +
627
630
  'This behavior will be deprecated in the future. ' +
@@ -671,7 +674,7 @@ export default class RealtimeChannel {
671
674
  }
672
675
  } else {
673
676
  return new Promise((resolve) => {
674
- const push = this.channelAdapter.push(args.type, args, opts.timeout || this.timeout)
677
+ const push = this._push(args.type, args, opts.timeout || this.timeout)
675
678
 
676
679
  if (args.type === 'broadcast' && !this.params?.config?.broadcast?.ack) {
677
680
  resolve('ok')
@@ -688,8 +691,8 @@ export default class RealtimeChannel {
688
691
  * Updates the payload that will be sent the next time the channel joins (reconnects).
689
692
  * Useful for rotating access tokens or updating config without re-creating the channel.
690
693
  */
691
- updateJoinPayload(payload: Record<string, any>) {
692
- this.channelAdapter.updateJoinPayload(payload)
694
+ updateJoinPayload(payload: { [key: string]: any }): void {
695
+ this.joinPush.updatePayload(payload)
693
696
  }
694
697
 
695
698
  /**
@@ -701,24 +704,56 @@ export default class RealtimeChannel {
701
704
  * To receive leave acknowledgements, use the a `receive` hook to bind to the server ack, ie:
702
705
  * channel.unsubscribe().receive("ok", () => alert("left!") )
703
706
  */
704
- async unsubscribe(timeout = this.timeout) {
707
+ unsubscribe(timeout = this.timeout): Promise<'ok' | 'timed out' | 'error'> {
708
+ this.state = CHANNEL_STATES.leaving
709
+ const onClose = () => {
710
+ this.socket.log('channel', `leave ${this.topic}`)
711
+ this._trigger(CHANNEL_EVENTS.close, 'leave', this._joinRef())
712
+ }
713
+
714
+ this.joinPush.destroy()
715
+
716
+ let leavePush: Push | null = null
717
+
705
718
  return new Promise<RealtimeChannelSendResponse>((resolve) => {
706
- this.channelAdapter
707
- .unsubscribe(timeout)
708
- .receive('ok', () => resolve('ok'))
709
- .receive('timeout', () => resolve('timed out'))
710
- .receive('error', () => resolve('error'))
719
+ leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout)
720
+ leavePush
721
+ .receive('ok', () => {
722
+ onClose()
723
+ resolve('ok')
724
+ })
725
+ .receive('timeout', () => {
726
+ onClose()
727
+ resolve('timed out')
728
+ })
729
+ .receive('error', () => {
730
+ resolve('error')
731
+ })
732
+
733
+ leavePush.send()
734
+ if (!this._canPush()) {
735
+ leavePush.trigger('ok', {})
736
+ }
737
+ }).finally(() => {
738
+ leavePush?.destroy()
711
739
  })
712
740
  }
713
-
714
741
  /**
742
+ * Teardown the channel.
743
+ *
715
744
  * Destroys and stops related timers.
716
745
  */
717
746
  teardown() {
718
- this.channelAdapter.teardown()
747
+ this.pushBuffer.forEach((push: Push) => push.destroy())
748
+ this.pushBuffer = []
749
+ this.rejoinTimer.reset()
750
+ this.joinPush.destroy()
751
+ this.state = CHANNEL_STATES.closed
752
+ this.bindings = {}
719
753
  }
720
754
 
721
755
  /** @internal */
756
+
722
757
  async _fetchWithTimeout(url: string, options: { [key: string]: any }, timeout: number) {
723
758
  const controller = new AbortController()
724
759
  const id = setTimeout(() => controller.abort(), timeout)
@@ -734,112 +769,195 @@ export default class RealtimeChannel {
734
769
  }
735
770
 
736
771
  /** @internal */
737
- _on(type: string, filter: { [key: string]: any }, callback: ChannelBindingCallback) {
738
- const typeLower = type.toLocaleLowerCase()
739
-
740
- const ref = this.channelAdapter.on(type, callback)
741
-
742
- const binding: Binding = {
743
- type: typeLower,
744
- filter: filter,
745
- callback: callback,
746
- ref: ref,
772
+ _push(event: string, payload: { [key: string]: any }, timeout = this.timeout) {
773
+ if (!this.joinedOnce) {
774
+ throw `tried to push '${event}' to '${this.topic}' before joining. Use channel.subscribe() before pushing events`
747
775
  }
748
-
749
- if (this.bindings[typeLower]) {
750
- this.bindings[typeLower].push(binding)
776
+ let pushEvent = new Push(this, event, payload, timeout)
777
+ if (this._canPush()) {
778
+ pushEvent.send()
751
779
  } else {
752
- this.bindings[typeLower] = [binding]
780
+ this._addToPushBuffer(pushEvent)
753
781
  }
754
782
 
755
- this._updateFilterMessage()
783
+ return pushEvent
784
+ }
756
785
 
757
- return this
786
+ /** @internal */
787
+ _addToPushBuffer(pushEvent: Push) {
788
+ pushEvent.startTimeout()
789
+ this.pushBuffer.push(pushEvent)
790
+
791
+ // Enforce buffer size limit
792
+ if (this.pushBuffer.length > MAX_PUSH_BUFFER_SIZE) {
793
+ const removedPush = this.pushBuffer.shift()
794
+ if (removedPush) {
795
+ removedPush.destroy()
796
+ this.socket.log(
797
+ 'channel',
798
+ `discarded push due to buffer overflow: ${removedPush.event}`,
799
+ removedPush.payload
800
+ )
801
+ }
802
+ }
758
803
  }
759
804
 
760
805
  /**
761
- * Registers a callback that will be executed when the channel closes.
806
+ * Overridable message hook
807
+ *
808
+ * Receives all events for specialized message handling before dispatching to the channel callbacks.
809
+ * Must return the payload, modified or unmodified.
762
810
  *
763
811
  * @internal
764
812
  */
765
- private _onClose(callback: ChannelBindingCallback) {
766
- this.channelAdapter.onClose(callback)
813
+ _onMessage(_event: string, payload: any, _ref?: string) {
814
+ return payload
767
815
  }
768
816
 
769
- /**
770
- * Registers a callback that will be executed when the channel encounteres an error.
771
- *
772
- * @internal
773
- */
774
- private _onError(callback: ChannelOnErrorCallback) {
775
- this.channelAdapter.onError(callback)
817
+ /** @internal */
818
+ _isMember(topic: string): boolean {
819
+ return this.topic === topic
776
820
  }
777
821
 
778
822
  /** @internal */
779
- private _updateFilterMessage() {
780
- this.channelAdapter.updateFilterBindings((binding, payload: any, ref) => {
781
- const typeLower = binding.event.toLocaleLowerCase()
823
+ _joinRef(): string {
824
+ return this.joinPush.ref
825
+ }
782
826
 
783
- if (this._notThisChannelEvent(typeLower, ref)) {
784
- return false
785
- }
827
+ /** @internal */
828
+ _trigger(type: string, payload?: any, ref?: string) {
829
+ const typeLower = type.toLocaleLowerCase()
830
+ const { close, error, leave, join } = CHANNEL_EVENTS
831
+ const events: string[] = [close, error, leave, join]
832
+ if (ref && events.indexOf(typeLower) >= 0 && ref !== this._joinRef()) {
833
+ return
834
+ }
835
+ let handledPayload = this._onMessage(typeLower, payload, ref)
836
+ if (payload && !handledPayload) {
837
+ throw 'channel onMessage callbacks must return the payload, modified or unmodified'
838
+ }
786
839
 
787
- const bind = this.bindings[typeLower]?.find((bind) => bind.ref === binding.ref)
840
+ if (['insert', 'update', 'delete'].includes(typeLower)) {
841
+ this.bindings.postgres_changes
842
+ ?.filter((bind) => {
843
+ return bind.filter?.event === '*' || bind.filter?.event?.toLocaleLowerCase() === typeLower
844
+ })
845
+ .map((bind) => bind.callback(handledPayload, ref))
846
+ } else {
847
+ this.bindings[typeLower]
848
+ ?.filter((bind) => {
849
+ if (['broadcast', 'presence', 'postgres_changes'].includes(typeLower)) {
850
+ if ('id' in bind) {
851
+ const bindId = bind.id
852
+ const bindEvent = bind.filter?.event
853
+ return (
854
+ bindId &&
855
+ payload.ids?.includes(bindId) &&
856
+ (bindEvent === '*' ||
857
+ bindEvent?.toLocaleLowerCase() === payload.data?.type.toLocaleLowerCase())
858
+ )
859
+ } else {
860
+ const bindEvent = bind?.filter?.event?.toLocaleLowerCase()
861
+ return bindEvent === '*' || bindEvent === payload?.event?.toLocaleLowerCase()
862
+ }
863
+ } else {
864
+ return bind.type.toLocaleLowerCase() === typeLower
865
+ }
866
+ })
867
+ .map((bind) => {
868
+ if (typeof handledPayload === 'object' && 'ids' in handledPayload) {
869
+ const postgresChanges = handledPayload.data
870
+ const { schema, table, commit_timestamp, type, errors } = postgresChanges
871
+ const enrichedPayload = {
872
+ schema: schema,
873
+ table: table,
874
+ commit_timestamp: commit_timestamp,
875
+ eventType: type,
876
+ new: {},
877
+ old: {},
878
+ errors: errors,
879
+ }
880
+ handledPayload = {
881
+ ...enrichedPayload,
882
+ ...this._getPayloadRecords(postgresChanges),
883
+ }
884
+ }
885
+ bind.callback(handledPayload, ref)
886
+ })
887
+ }
888
+ }
788
889
 
789
- if (!bind) {
790
- return true
791
- }
890
+ /** @internal */
891
+ _isClosed(): boolean {
892
+ return this.state === CHANNEL_STATES.closed
893
+ }
792
894
 
793
- if (['broadcast', 'presence', 'postgres_changes'].includes(typeLower)) {
794
- if ('id' in bind) {
795
- const bindId = bind.id
796
- const bindEvent = bind.filter?.event
797
- return (
798
- bindId &&
799
- payload.ids?.includes(bindId) &&
800
- (bindEvent === '*' ||
801
- bindEvent?.toLocaleLowerCase() === payload.data?.type.toLocaleLowerCase())
802
- )
803
- } else {
804
- const bindEvent = bind?.filter?.event?.toLocaleLowerCase()
805
- return bindEvent === '*' || bindEvent === payload?.event?.toLocaleLowerCase()
806
- }
807
- } else {
808
- return bind.type.toLocaleLowerCase() === typeLower
809
- }
810
- })
895
+ /** @internal */
896
+ _isJoined(): boolean {
897
+ return this.state === CHANNEL_STATES.joined
811
898
  }
812
899
 
813
900
  /** @internal */
814
- private _notThisChannelEvent(event: string, ref?: string | null) {
815
- const { close, error, leave, join } = CHANNEL_EVENTS
816
- const events: string[] = [close, error, leave, join]
817
- return ref && events.includes(event) && ref !== this.joinPush.ref
901
+ _isJoining(): boolean {
902
+ return this.state === CHANNEL_STATES.joining
818
903
  }
819
904
 
820
905
  /** @internal */
821
- private _updateFilterTransform() {
822
- this.channelAdapter.updatePayloadTransform((event, payload: any, ref) => {
823
- if (typeof payload === 'object' && 'ids' in payload) {
824
- const postgresChanges = payload.data
825
- const { schema, table, commit_timestamp, type, errors } = postgresChanges
826
- const enrichedPayload = {
827
- schema: schema,
828
- table: table,
829
- commit_timestamp: commit_timestamp,
830
- eventType: type,
831
- new: {},
832
- old: {},
833
- errors: errors,
834
- }
835
- return {
836
- ...enrichedPayload,
837
- ...this._getPayloadRecords(postgresChanges),
838
- }
906
+ _isLeaving(): boolean {
907
+ return this.state === CHANNEL_STATES.leaving
908
+ }
909
+
910
+ /** @internal */
911
+ _replyEventName(ref: string): string {
912
+ return `chan_reply_${ref}`
913
+ }
914
+
915
+ /** @internal */
916
+ _on(type: string, filter: { [key: string]: any }, callback: Function) {
917
+ const typeLower = type.toLocaleLowerCase()
918
+ const binding = {
919
+ type: typeLower,
920
+ filter: filter,
921
+ callback: callback,
922
+ }
923
+
924
+ if (this.bindings[typeLower]) {
925
+ this.bindings[typeLower].push(binding)
926
+ } else {
927
+ this.bindings[typeLower] = [binding]
928
+ }
929
+
930
+ return this
931
+ }
932
+
933
+ /** @internal */
934
+ _off(type: string, filter: { [key: string]: any }) {
935
+ const typeLower = type.toLocaleLowerCase()
936
+
937
+ if (this.bindings[typeLower]) {
938
+ this.bindings[typeLower] = this.bindings[typeLower].filter((bind) => {
939
+ return !(
940
+ bind.type?.toLocaleLowerCase() === typeLower &&
941
+ RealtimeChannel.isEqual(bind.filter, filter)
942
+ )
943
+ })
944
+ }
945
+ return this
946
+ }
947
+
948
+ /** @internal */
949
+ private static isEqual(obj1: { [key: string]: string }, obj2: { [key: string]: string }) {
950
+ if (Object.keys(obj1).length !== Object.keys(obj2).length) {
951
+ return false
952
+ }
953
+
954
+ for (const k in obj1) {
955
+ if (obj1[k] !== obj2[k]) {
956
+ return false
839
957
  }
958
+ }
840
959
 
841
- return payload
842
- })
960
+ return true
843
961
  }
844
962
 
845
963
  /**
@@ -856,6 +974,51 @@ export default class RealtimeChannel {
856
974
  return normalizedServer === normalizedClient
857
975
  }
858
976
 
977
+ /** @internal */
978
+ private _rejoinUntilConnected() {
979
+ this.rejoinTimer.scheduleTimeout()
980
+ if (this.socket.isConnected()) {
981
+ this._rejoin()
982
+ }
983
+ }
984
+
985
+ /**
986
+ * Registers a callback that will be executed when the channel closes.
987
+ *
988
+ * @internal
989
+ */
990
+ private _onClose(callback: Function) {
991
+ this._on(CHANNEL_EVENTS.close, {}, callback)
992
+ }
993
+
994
+ /**
995
+ * Registers a callback that will be executed when the channel encounteres an error.
996
+ *
997
+ * @internal
998
+ */
999
+ private _onError(callback: Function) {
1000
+ this._on(CHANNEL_EVENTS.error, {}, (reason: string) => callback(reason))
1001
+ }
1002
+
1003
+ /**
1004
+ * Returns `true` if the socket is connected and the channel has been joined.
1005
+ *
1006
+ * @internal
1007
+ */
1008
+ private _canPush(): boolean {
1009
+ return this.socket.isConnected() && this._isJoined()
1010
+ }
1011
+
1012
+ /** @internal */
1013
+ private _rejoin(timeout = this.timeout): void {
1014
+ if (this._isLeaving()) {
1015
+ return
1016
+ }
1017
+ this.socket._leaveOpenTopic(this.topic)
1018
+ this.state = CHANNEL_STATES.joining
1019
+ this.joinPush.resend(timeout)
1020
+ }
1021
+
859
1022
  /** @internal */
860
1023
  private _getPayloadRecords(payload: any) {
861
1024
  const records = {