@depup/supabase__realtime-js 2.99.3-depup.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 (104) hide show
  1. package/README.md +32 -0
  2. package/changes.json +14 -0
  3. package/dist/main/RealtimeChannel.d.ts +370 -0
  4. package/dist/main/RealtimeChannel.d.ts.map +1 -0
  5. package/dist/main/RealtimeChannel.js +644 -0
  6. package/dist/main/RealtimeChannel.js.map +1 -0
  7. package/dist/main/RealtimeClient.d.ts +238 -0
  8. package/dist/main/RealtimeClient.d.ts.map +1 -0
  9. package/dist/main/RealtimeClient.js +870 -0
  10. package/dist/main/RealtimeClient.js.map +1 -0
  11. package/dist/main/RealtimePresence.d.ts +76 -0
  12. package/dist/main/RealtimePresence.d.ts.map +1 -0
  13. package/dist/main/RealtimePresence.js +237 -0
  14. package/dist/main/RealtimePresence.js.map +1 -0
  15. package/dist/main/index.d.ts +6 -0
  16. package/dist/main/index.d.ts.map +1 -0
  17. package/dist/main/index.js +18 -0
  18. package/dist/main/index.js.map +1 -0
  19. package/dist/main/lib/constants.d.ts +39 -0
  20. package/dist/main/lib/constants.d.ts.map +1 -0
  21. package/dist/main/lib/constants.js +48 -0
  22. package/dist/main/lib/constants.js.map +1 -0
  23. package/dist/main/lib/push.d.ts +48 -0
  24. package/dist/main/lib/push.d.ts.map +1 -0
  25. package/dist/main/lib/push.js +102 -0
  26. package/dist/main/lib/push.js.map +1 -0
  27. package/dist/main/lib/serializer.d.ts +33 -0
  28. package/dist/main/lib/serializer.d.ts.map +1 -0
  29. package/dist/main/lib/serializer.js +155 -0
  30. package/dist/main/lib/serializer.js.map +1 -0
  31. package/dist/main/lib/timer.d.ts +22 -0
  32. package/dist/main/lib/timer.d.ts.map +1 -0
  33. package/dist/main/lib/timer.js +39 -0
  34. package/dist/main/lib/timer.js.map +1 -0
  35. package/dist/main/lib/transformers.d.ts +109 -0
  36. package/dist/main/lib/transformers.d.ts.map +1 -0
  37. package/dist/main/lib/transformers.js +241 -0
  38. package/dist/main/lib/transformers.js.map +1 -0
  39. package/dist/main/lib/version.d.ts +2 -0
  40. package/dist/main/lib/version.d.ts.map +1 -0
  41. package/dist/main/lib/version.js +11 -0
  42. package/dist/main/lib/version.js.map +1 -0
  43. package/dist/main/lib/websocket-factory.d.ts +81 -0
  44. package/dist/main/lib/websocket-factory.d.ts.map +1 -0
  45. package/dist/main/lib/websocket-factory.js +131 -0
  46. package/dist/main/lib/websocket-factory.js.map +1 -0
  47. package/dist/module/RealtimeChannel.d.ts +370 -0
  48. package/dist/module/RealtimeChannel.d.ts.map +1 -0
  49. package/dist/module/RealtimeChannel.js +639 -0
  50. package/dist/module/RealtimeChannel.js.map +1 -0
  51. package/dist/module/RealtimeClient.d.ts +238 -0
  52. package/dist/module/RealtimeClient.d.ts.map +1 -0
  53. package/dist/module/RealtimeClient.js +866 -0
  54. package/dist/module/RealtimeClient.js.map +1 -0
  55. package/dist/module/RealtimePresence.d.ts +76 -0
  56. package/dist/module/RealtimePresence.d.ts.map +1 -0
  57. package/dist/module/RealtimePresence.js +233 -0
  58. package/dist/module/RealtimePresence.js.map +1 -0
  59. package/dist/module/index.d.ts +6 -0
  60. package/dist/module/index.d.ts.map +1 -0
  61. package/dist/module/index.js +6 -0
  62. package/dist/module/index.js.map +1 -0
  63. package/dist/module/lib/constants.d.ts +39 -0
  64. package/dist/module/lib/constants.d.ts.map +1 -0
  65. package/dist/module/lib/constants.js +45 -0
  66. package/dist/module/lib/constants.js.map +1 -0
  67. package/dist/module/lib/push.d.ts +48 -0
  68. package/dist/module/lib/push.d.ts.map +1 -0
  69. package/dist/module/lib/push.js +99 -0
  70. package/dist/module/lib/push.js.map +1 -0
  71. package/dist/module/lib/serializer.d.ts +33 -0
  72. package/dist/module/lib/serializer.d.ts.map +1 -0
  73. package/dist/module/lib/serializer.js +152 -0
  74. package/dist/module/lib/serializer.js.map +1 -0
  75. package/dist/module/lib/timer.d.ts +22 -0
  76. package/dist/module/lib/timer.d.ts.map +1 -0
  77. package/dist/module/lib/timer.js +36 -0
  78. package/dist/module/lib/timer.js.map +1 -0
  79. package/dist/module/lib/transformers.d.ts +109 -0
  80. package/dist/module/lib/transformers.d.ts.map +1 -0
  81. package/dist/module/lib/transformers.js +229 -0
  82. package/dist/module/lib/transformers.js.map +1 -0
  83. package/dist/module/lib/version.d.ts +2 -0
  84. package/dist/module/lib/version.d.ts.map +1 -0
  85. package/dist/module/lib/version.js +8 -0
  86. package/dist/module/lib/version.js.map +1 -0
  87. package/dist/module/lib/websocket-factory.d.ts +81 -0
  88. package/dist/module/lib/websocket-factory.d.ts.map +1 -0
  89. package/dist/module/lib/websocket-factory.js +127 -0
  90. package/dist/module/lib/websocket-factory.js.map +1 -0
  91. package/dist/tsconfig.module.tsbuildinfo +1 -0
  92. package/dist/tsconfig.tsbuildinfo +1 -0
  93. package/package.json +85 -0
  94. package/src/RealtimeChannel.ts +1039 -0
  95. package/src/RealtimeClient.ts +1023 -0
  96. package/src/RealtimePresence.ts +346 -0
  97. package/src/index.ts +53 -0
  98. package/src/lib/constants.ts +49 -0
  99. package/src/lib/push.ts +121 -0
  100. package/src/lib/serializer.ts +203 -0
  101. package/src/lib/timer.ts +43 -0
  102. package/src/lib/transformers.ts +270 -0
  103. package/src/lib/version.ts +7 -0
  104. package/src/lib/websocket-factory.ts +192 -0
@@ -0,0 +1,1039 @@
1
+ import { CHANNEL_EVENTS, CHANNEL_STATES, MAX_PUSH_BUFFER_SIZE } from './lib/constants'
2
+ import Push from './lib/push'
3
+ import type RealtimeClient from './RealtimeClient'
4
+ import Timer from './lib/timer'
5
+ import RealtimePresence, { REALTIME_PRESENCE_LISTEN_EVENTS } from './RealtimePresence'
6
+ import type {
7
+ RealtimePresenceJoinPayload,
8
+ RealtimePresenceLeavePayload,
9
+ RealtimePresenceState,
10
+ } from './RealtimePresence'
11
+ import * as Transformers from './lib/transformers'
12
+ import { httpEndpointURL } from './lib/transformers'
13
+
14
+ type ReplayOption = {
15
+ since: number
16
+ limit?: number
17
+ }
18
+
19
+ export type RealtimeChannelOptions = {
20
+ config: {
21
+ /**
22
+ * self option enables client to receive message it broadcast
23
+ * ack option instructs server to acknowledge that broadcast message was received
24
+ * replay option instructs server to replay broadcast messages
25
+ */
26
+ broadcast?: { self?: boolean; ack?: boolean; replay?: ReplayOption }
27
+ /**
28
+ * key option is used to track presence payload across clients
29
+ */
30
+ presence?: { key?: string; enabled?: boolean }
31
+ /**
32
+ * defines if the channel is private or not and if RLS policies will be used to check data
33
+ */
34
+ private?: boolean
35
+ }
36
+ }
37
+
38
+ type RealtimeChangesPayloadBase = {
39
+ schema: string
40
+ table: string
41
+ }
42
+
43
+ type RealtimeBroadcastChangesPayloadBase = RealtimeChangesPayloadBase & {
44
+ id: string
45
+ }
46
+
47
+ export type RealtimeBroadcastInsertPayload<T extends { [key: string]: any }> =
48
+ RealtimeBroadcastChangesPayloadBase & {
49
+ operation: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT}`
50
+ record: T
51
+ old_record: null
52
+ }
53
+
54
+ export type RealtimeBroadcastUpdatePayload<T extends { [key: string]: any }> =
55
+ RealtimeBroadcastChangesPayloadBase & {
56
+ operation: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE}`
57
+ record: T
58
+ old_record: T
59
+ }
60
+
61
+ export type RealtimeBroadcastDeletePayload<T extends { [key: string]: any }> =
62
+ RealtimeBroadcastChangesPayloadBase & {
63
+ operation: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE}`
64
+ record: null
65
+ old_record: T
66
+ }
67
+
68
+ export type RealtimeBroadcastPayload<T extends { [key: string]: any }> =
69
+ | RealtimeBroadcastInsertPayload<T>
70
+ | RealtimeBroadcastUpdatePayload<T>
71
+ | RealtimeBroadcastDeletePayload<T>
72
+
73
+ type RealtimePostgresChangesPayloadBase = {
74
+ schema: string
75
+ table: string
76
+ commit_timestamp: string
77
+ errors: string[]
78
+ }
79
+
80
+ export type RealtimePostgresInsertPayload<T extends { [key: string]: any }> =
81
+ RealtimePostgresChangesPayloadBase & {
82
+ eventType: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT}`
83
+ new: T
84
+ old: {}
85
+ }
86
+
87
+ export type RealtimePostgresUpdatePayload<T extends { [key: string]: any }> =
88
+ RealtimePostgresChangesPayloadBase & {
89
+ eventType: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE}`
90
+ new: T
91
+ old: Partial<T>
92
+ }
93
+
94
+ export type RealtimePostgresDeletePayload<T extends { [key: string]: any }> =
95
+ RealtimePostgresChangesPayloadBase & {
96
+ eventType: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE}`
97
+ new: {}
98
+ old: Partial<T>
99
+ }
100
+
101
+ export type RealtimePostgresChangesPayload<T extends { [key: string]: any }> =
102
+ | RealtimePostgresInsertPayload<T>
103
+ | RealtimePostgresUpdatePayload<T>
104
+ | RealtimePostgresDeletePayload<T>
105
+
106
+ export type RealtimePostgresChangesFilter<T extends `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT}`> = {
107
+ /**
108
+ * The type of database change to listen to.
109
+ */
110
+ event: T
111
+ /**
112
+ * The database schema to listen to.
113
+ */
114
+ schema: string
115
+ /**
116
+ * The database table to listen to.
117
+ */
118
+ table?: string
119
+ /**
120
+ * Receive database changes when filter is matched.
121
+ */
122
+ filter?: string
123
+ }
124
+
125
+ export type RealtimeChannelSendResponse = 'ok' | 'timed out' | 'error'
126
+
127
+ export enum REALTIME_POSTGRES_CHANGES_LISTEN_EVENT {
128
+ ALL = '*',
129
+ INSERT = 'INSERT',
130
+ UPDATE = 'UPDATE',
131
+ DELETE = 'DELETE',
132
+ }
133
+
134
+ export enum REALTIME_LISTEN_TYPES {
135
+ BROADCAST = 'broadcast',
136
+ PRESENCE = 'presence',
137
+ POSTGRES_CHANGES = 'postgres_changes',
138
+ SYSTEM = 'system',
139
+ }
140
+
141
+ export enum REALTIME_SUBSCRIBE_STATES {
142
+ SUBSCRIBED = 'SUBSCRIBED',
143
+ TIMED_OUT = 'TIMED_OUT',
144
+ CLOSED = 'CLOSED',
145
+ CHANNEL_ERROR = 'CHANNEL_ERROR',
146
+ }
147
+
148
+ export const REALTIME_CHANNEL_STATES = CHANNEL_STATES
149
+
150
+ interface PostgresChangesFilters {
151
+ postgres_changes: {
152
+ id: string
153
+ event: string
154
+ schema?: string
155
+ table?: string
156
+ filter?: string
157
+ }[]
158
+ }
159
+ /** A channel is the basic building block of Realtime
160
+ * and narrows the scope of data flow to subscribed clients.
161
+ * You can think of a channel as a chatroom where participants are able to see who's online
162
+ * and send and receive messages.
163
+ */
164
+ export default class RealtimeChannel {
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
180
+ broadcastEndpointURL: string
181
+ subTopic: string
182
+ private: boolean
183
+
184
+ /**
185
+ * Creates a channel that can broadcast messages, sync presence, and listen to Postgres changes.
186
+ *
187
+ * The topic determines which realtime stream you are subscribing to. Config options let you
188
+ * enable acknowledgement for broadcasts, presence tracking, or private channels.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * import RealtimeClient from '@supabase/realtime-js'
193
+ *
194
+ * const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
195
+ * params: { apikey: 'public-anon-key' },
196
+ * })
197
+ * const channel = new RealtimeChannel('realtime:public:messages', { config: {} }, client)
198
+ * ```
199
+ */
200
+ constructor(
201
+ /** Topic name can be any string. */
202
+ public topic: string,
203
+ public params: RealtimeChannelOptions = { config: {} },
204
+ public socket: RealtimeClient
205
+ ) {
206
+ this.subTopic = topic.replace(/^realtime:/i, '')
207
+ this.params.config = {
208
+ ...{
209
+ broadcast: { ack: false, self: false },
210
+ presence: { key: '', enabled: false },
211
+ private: false,
212
+ },
213
+ ...params.config,
214
+ }
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
+ })
224
+ this._onClose(() => {
225
+ this.rejoinTimer.reset()
226
+ this.socket.log('channel', `close ${this.topic} ${this._joinRef()}`)
227
+ this.state = CHANNEL_STATES.closed
228
+ this.socket._remove(this)
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
+ })
246
+
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)
260
+
261
+ this.broadcastEndpointURL = httpEndpointURL(this.socket.endPoint)
262
+ this.private = this.params.config.private || false
263
+
264
+ if (!this.private && this.params.config?.broadcast?.replay) {
265
+ throw `tried to use replay on public channel '${this.topic}'. It must be a private channel.`
266
+ }
267
+ }
268
+
269
+ /** Subscribe registers your client with the server */
270
+ subscribe(
271
+ callback?: (status: REALTIME_SUBSCRIBE_STATES, err?: Error) => void,
272
+ timeout = this.timeout
273
+ ): RealtimeChannel {
274
+ if (!this.socket.isConnected()) {
275
+ this.socket.connect()
276
+ }
277
+ if (this.state == CHANNEL_STATES.closed) {
278
+ const {
279
+ config: { broadcast, presence, private: isPrivate },
280
+ } = this.params
281
+
282
+ const postgres_changes = this.bindings.postgres_changes?.map((r) => r.filter) ?? []
283
+
284
+ const presence_enabled =
285
+ (!!this.bindings[REALTIME_LISTEN_TYPES.PRESENCE] &&
286
+ this.bindings[REALTIME_LISTEN_TYPES.PRESENCE].length > 0) ||
287
+ this.params.config.presence?.enabled === true
288
+ const accessTokenPayload: { access_token?: string } = {}
289
+ const config = {
290
+ broadcast,
291
+ presence: { ...presence, enabled: presence_enabled },
292
+ postgres_changes,
293
+ private: isPrivate,
294
+ }
295
+
296
+ if (this.socket.accessTokenValue) {
297
+ accessTokenPayload.access_token = this.socket.accessTokenValue
298
+ }
299
+
300
+ this._onError((e: Error) => callback?.(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, e))
301
+
302
+ this._onClose(() => callback?.(REALTIME_SUBSCRIBE_STATES.CLOSED))
303
+
304
+ this.updateJoinPayload({ ...{ config }, ...accessTokenPayload })
305
+
306
+ this.joinedOnce = true
307
+ this._rejoin(timeout)
308
+
309
+ this.joinPush
310
+ .receive('ok', async ({ postgres_changes }: PostgresChangesFilters) => {
311
+ // Only refresh auth if using callback-based tokens
312
+ if (!this.socket._isManualToken()) {
313
+ this.socket.setAuth()
314
+ }
315
+ if (postgres_changes === undefined) {
316
+ callback?.(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED)
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
357
+ }
358
+ })
359
+ .receive('error', (error: { [key: string]: any }) => {
360
+ this.state = CHANNEL_STATES.errored
361
+ callback?.(
362
+ REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR,
363
+ new Error(JSON.stringify(Object.values(error).join(', ') || 'error'))
364
+ )
365
+ return
366
+ })
367
+ .receive('timeout', () => {
368
+ callback?.(REALTIME_SUBSCRIBE_STATES.TIMED_OUT)
369
+ return
370
+ })
371
+ }
372
+ return this
373
+ }
374
+
375
+ /**
376
+ * Returns the current presence state for this channel.
377
+ *
378
+ * The shape is a map keyed by presence key (for example a user id) where each entry contains the
379
+ * tracked metadata for that user.
380
+ */
381
+ presenceState<T extends { [key: string]: any } = {}>(): RealtimePresenceState<T> {
382
+ return this.presence.state as RealtimePresenceState<T>
383
+ }
384
+
385
+ /**
386
+ * Sends the supplied payload to the presence tracker so other subscribers can see that this
387
+ * client is online. Use `untrack` to stop broadcasting presence for the same key.
388
+ */
389
+ async track(
390
+ payload: { [key: string]: any },
391
+ opts: { [key: string]: any } = {}
392
+ ): Promise<RealtimeChannelSendResponse> {
393
+ return await this.send(
394
+ {
395
+ type: 'presence',
396
+ event: 'track',
397
+ payload,
398
+ },
399
+ opts.timeout || this.timeout
400
+ )
401
+ }
402
+
403
+ /**
404
+ * Removes the current presence state for this client.
405
+ */
406
+ async untrack(opts: { [key: string]: any } = {}): Promise<RealtimeChannelSendResponse> {
407
+ return await this.send(
408
+ {
409
+ type: 'presence',
410
+ event: 'untrack',
411
+ },
412
+ opts
413
+ )
414
+ }
415
+
416
+ /**
417
+ * Creates an event handler that listens to changes.
418
+ */
419
+ on(
420
+ type: `${REALTIME_LISTEN_TYPES.PRESENCE}`,
421
+ filter: { event: `${REALTIME_PRESENCE_LISTEN_EVENTS.SYNC}` },
422
+ callback: () => void
423
+ ): RealtimeChannel
424
+ on<T extends { [key: string]: any }>(
425
+ type: `${REALTIME_LISTEN_TYPES.PRESENCE}`,
426
+ filter: { event: `${REALTIME_PRESENCE_LISTEN_EVENTS.JOIN}` },
427
+ callback: (payload: RealtimePresenceJoinPayload<T>) => void
428
+ ): RealtimeChannel
429
+ on<T extends { [key: string]: any }>(
430
+ type: `${REALTIME_LISTEN_TYPES.PRESENCE}`,
431
+ filter: { event: `${REALTIME_PRESENCE_LISTEN_EVENTS.LEAVE}` },
432
+ callback: (payload: RealtimePresenceLeavePayload<T>) => void
433
+ ): RealtimeChannel
434
+ on<T extends { [key: string]: any }>(
435
+ type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`,
436
+ filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL}`>,
437
+ callback: (payload: RealtimePostgresChangesPayload<T>) => void
438
+ ): RealtimeChannel
439
+ on<T extends { [key: string]: any }>(
440
+ type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`,
441
+ filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT}`>,
442
+ callback: (payload: RealtimePostgresInsertPayload<T>) => void
443
+ ): RealtimeChannel
444
+ on<T extends { [key: string]: any }>(
445
+ type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`,
446
+ filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE}`>,
447
+ callback: (payload: RealtimePostgresUpdatePayload<T>) => void
448
+ ): RealtimeChannel
449
+ on<T extends { [key: string]: any }>(
450
+ type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`,
451
+ filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE}`>,
452
+ callback: (payload: RealtimePostgresDeletePayload<T>) => void
453
+ ): RealtimeChannel
454
+ on<T extends { [key: string]: any }>(
455
+ type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`,
456
+ filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT}`>,
457
+ callback: (payload: RealtimePostgresChangesPayload<T>) => void
458
+ ): RealtimeChannel
459
+ /**
460
+ * The following is placed here to display on supabase.com/docs/reference/javascript/subscribe.
461
+ * @param type One of "broadcast", "presence", or "postgres_changes".
462
+ * @param filter Custom object specific to the Realtime feature detailing which payloads to receive.
463
+ * @param callback Function to be invoked when event handler is triggered.
464
+ */
465
+ on(
466
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`,
467
+ filter: { event: string },
468
+ callback: (payload: {
469
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
470
+ event: string
471
+ meta?: {
472
+ replayed?: boolean
473
+ id: string
474
+ }
475
+ [key: string]: any
476
+ }) => void
477
+ ): RealtimeChannel
478
+ on<T extends { [key: string]: any }>(
479
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`,
480
+ filter: { event: string },
481
+ callback: (payload: {
482
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
483
+ event: string
484
+ meta?: {
485
+ replayed?: boolean
486
+ id: string
487
+ }
488
+ payload: T
489
+ }) => void
490
+ ): RealtimeChannel
491
+ on<T extends Record<string, unknown>>(
492
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`,
493
+ filter: { event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL },
494
+ callback: (payload: {
495
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
496
+ event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL
497
+ payload: RealtimeBroadcastPayload<T>
498
+ }) => void
499
+ ): RealtimeChannel
500
+ on<T extends { [key: string]: any }>(
501
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`,
502
+ filter: { event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT },
503
+ callback: (payload: {
504
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
505
+ event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT
506
+ payload: RealtimeBroadcastInsertPayload<T>
507
+ }) => void
508
+ ): RealtimeChannel
509
+ on<T extends { [key: string]: any }>(
510
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`,
511
+ filter: { event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE },
512
+ callback: (payload: {
513
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
514
+ event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE
515
+ payload: RealtimeBroadcastUpdatePayload<T>
516
+ }) => void
517
+ ): RealtimeChannel
518
+ on<T extends { [key: string]: any }>(
519
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`,
520
+ filter: { event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE },
521
+ callback: (payload: {
522
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
523
+ event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE
524
+ payload: RealtimeBroadcastDeletePayload<T>
525
+ }) => void
526
+ ): RealtimeChannel
527
+ on<T extends { [key: string]: any }>(
528
+ type: `${REALTIME_LISTEN_TYPES.SYSTEM}`,
529
+ filter: {},
530
+ callback: (payload: any) => void
531
+ ): RealtimeChannel
532
+ on(
533
+ type: `${REALTIME_LISTEN_TYPES}`,
534
+ filter: { event: string; [key: string]: string },
535
+ callback: (payload: any) => void
536
+ ): RealtimeChannel {
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())
543
+ }
544
+ return this._on(type, filter, callback)
545
+ }
546
+ /**
547
+ * Sends a broadcast message explicitly via REST API.
548
+ *
549
+ * This method always uses the REST API endpoint regardless of WebSocket connection state.
550
+ * Useful when you want to guarantee REST delivery or when gradually migrating from implicit REST fallback.
551
+ *
552
+ * @param event The name of the broadcast event
553
+ * @param payload Payload to be sent (required)
554
+ * @param opts Options including timeout
555
+ * @returns Promise resolving to object with success status, and error details if failed
556
+ */
557
+ async httpSend(
558
+ event: string,
559
+ payload: any,
560
+ opts: { timeout?: number } = {}
561
+ ): Promise<{ success: true } | { success: false; status: number; error: string }> {
562
+ if (payload === undefined || payload === null) {
563
+ return Promise.reject('Payload is required for httpSend()')
564
+ }
565
+
566
+ const headers: Record<string, string> = {
567
+ apikey: this.socket.apiKey ? this.socket.apiKey : '',
568
+ 'Content-Type': 'application/json',
569
+ }
570
+
571
+ if (this.socket.accessTokenValue) {
572
+ headers['Authorization'] = `Bearer ${this.socket.accessTokenValue}`
573
+ }
574
+
575
+ const options = {
576
+ method: 'POST',
577
+ headers,
578
+ body: JSON.stringify({
579
+ messages: [
580
+ {
581
+ topic: this.subTopic,
582
+ event,
583
+ payload: payload,
584
+ private: this.private,
585
+ },
586
+ ],
587
+ }),
588
+ }
589
+
590
+ const response = await this._fetchWithTimeout(
591
+ this.broadcastEndpointURL,
592
+ options,
593
+ opts.timeout ?? this.timeout
594
+ )
595
+
596
+ if (response.status === 202) {
597
+ return { success: true }
598
+ }
599
+
600
+ let errorMessage = response.statusText
601
+ try {
602
+ const errorBody = await response.json()
603
+ errorMessage = errorBody.error || errorBody.message || errorMessage
604
+ } catch {}
605
+
606
+ return Promise.reject(new Error(errorMessage))
607
+ }
608
+
609
+ /**
610
+ * Sends a message into the channel.
611
+ *
612
+ * @param args Arguments to send to channel
613
+ * @param args.type The type of event to send
614
+ * @param args.event The name of the event being sent
615
+ * @param args.payload Payload to be sent
616
+ * @param opts Options to be used during the send process
617
+ */
618
+ async send(
619
+ args: {
620
+ type: 'broadcast' | 'presence' | 'postgres_changes'
621
+ event: string
622
+ payload?: any
623
+ [key: string]: any
624
+ },
625
+ opts: { [key: string]: any } = {}
626
+ ): Promise<RealtimeChannelSendResponse> {
627
+ if (!this._canPush() && args.type === 'broadcast') {
628
+ console.warn(
629
+ 'Realtime send() is automatically falling back to REST API. ' +
630
+ 'This behavior will be deprecated in the future. ' +
631
+ 'Please use httpSend() explicitly for REST delivery.'
632
+ )
633
+
634
+ const { event, payload: endpoint_payload } = args
635
+ const headers: Record<string, string> = {
636
+ apikey: this.socket.apiKey ? this.socket.apiKey : '',
637
+ 'Content-Type': 'application/json',
638
+ }
639
+
640
+ if (this.socket.accessTokenValue) {
641
+ headers['Authorization'] = `Bearer ${this.socket.accessTokenValue}`
642
+ }
643
+
644
+ const options = {
645
+ method: 'POST',
646
+ headers,
647
+ body: JSON.stringify({
648
+ messages: [
649
+ {
650
+ topic: this.subTopic,
651
+ event,
652
+ payload: endpoint_payload,
653
+ private: this.private,
654
+ },
655
+ ],
656
+ }),
657
+ }
658
+
659
+ try {
660
+ const response = await this._fetchWithTimeout(
661
+ this.broadcastEndpointURL,
662
+ options,
663
+ opts.timeout ?? this.timeout
664
+ )
665
+
666
+ await response.body?.cancel()
667
+ return response.ok ? 'ok' : 'error'
668
+ } catch (error: any) {
669
+ if (error.name === 'AbortError') {
670
+ return 'timed out'
671
+ } else {
672
+ return 'error'
673
+ }
674
+ }
675
+ } else {
676
+ return new Promise((resolve) => {
677
+ const push = this._push(args.type, args, opts.timeout || this.timeout)
678
+
679
+ if (args.type === 'broadcast' && !this.params?.config?.broadcast?.ack) {
680
+ resolve('ok')
681
+ }
682
+
683
+ push.receive('ok', () => resolve('ok'))
684
+ push.receive('error', () => resolve('error'))
685
+ push.receive('timeout', () => resolve('timed out'))
686
+ })
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Updates the payload that will be sent the next time the channel joins (reconnects).
692
+ * Useful for rotating access tokens or updating config without re-creating the channel.
693
+ */
694
+ updateJoinPayload(payload: { [key: string]: any }): void {
695
+ this.joinPush.updatePayload(payload)
696
+ }
697
+
698
+ /**
699
+ * Leaves the channel.
700
+ *
701
+ * Unsubscribes from server events, and instructs channel to terminate on server.
702
+ * Triggers onClose() hooks.
703
+ *
704
+ * To receive leave acknowledgements, use the a `receive` hook to bind to the server ack, ie:
705
+ * channel.unsubscribe().receive("ok", () => alert("left!") )
706
+ */
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
+
718
+ return new Promise<RealtimeChannelSendResponse>((resolve) => {
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()
739
+ })
740
+ }
741
+ /**
742
+ * Teardown the channel.
743
+ *
744
+ * Destroys and stops related timers.
745
+ */
746
+ 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 = {}
753
+ }
754
+
755
+ /** @internal */
756
+
757
+ async _fetchWithTimeout(url: string, options: { [key: string]: any }, timeout: number) {
758
+ const controller = new AbortController()
759
+ const id = setTimeout(() => controller.abort(), timeout)
760
+
761
+ const response = await this.socket.fetch(url, {
762
+ ...options,
763
+ signal: controller.signal,
764
+ })
765
+
766
+ clearTimeout(id)
767
+
768
+ return response
769
+ }
770
+
771
+ /** @internal */
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`
775
+ }
776
+ let pushEvent = new Push(this, event, payload, timeout)
777
+ if (this._canPush()) {
778
+ pushEvent.send()
779
+ } else {
780
+ this._addToPushBuffer(pushEvent)
781
+ }
782
+
783
+ return pushEvent
784
+ }
785
+
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
+ }
803
+ }
804
+
805
+ /**
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.
810
+ *
811
+ * @internal
812
+ */
813
+ _onMessage(_event: string, payload: any, _ref?: string) {
814
+ return payload
815
+ }
816
+
817
+ /** @internal */
818
+ _isMember(topic: string): boolean {
819
+ return this.topic === topic
820
+ }
821
+
822
+ /** @internal */
823
+ _joinRef(): string {
824
+ return this.joinPush.ref
825
+ }
826
+
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
+ }
839
+
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
+ }
889
+
890
+ /** @internal */
891
+ _isClosed(): boolean {
892
+ return this.state === CHANNEL_STATES.closed
893
+ }
894
+
895
+ /** @internal */
896
+ _isJoined(): boolean {
897
+ return this.state === CHANNEL_STATES.joined
898
+ }
899
+
900
+ /** @internal */
901
+ _isJoining(): boolean {
902
+ return this.state === CHANNEL_STATES.joining
903
+ }
904
+
905
+ /** @internal */
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
957
+ }
958
+ }
959
+
960
+ return true
961
+ }
962
+
963
+ /**
964
+ * Compares two optional filter values for equality.
965
+ * Treats undefined, null, and empty string as equivalent empty values.
966
+ * @internal
967
+ */
968
+ private static isFilterValueEqual(
969
+ serverValue: string | undefined | null,
970
+ clientValue: string | undefined
971
+ ): boolean {
972
+ const normalizedServer = serverValue ?? undefined
973
+ const normalizedClient = clientValue ?? undefined
974
+ return normalizedServer === normalizedClient
975
+ }
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
+
1022
+ /** @internal */
1023
+ private _getPayloadRecords(payload: any) {
1024
+ const records = {
1025
+ new: {},
1026
+ old: {},
1027
+ }
1028
+
1029
+ if (payload.type === 'INSERT' || payload.type === 'UPDATE') {
1030
+ records.new = Transformers.convertChangeData(payload.columns, payload.record)
1031
+ }
1032
+
1033
+ if (payload.type === 'UPDATE' || payload.type === 'DELETE') {
1034
+ records.old = Transformers.convertChangeData(payload.columns, payload.old_record)
1035
+ }
1036
+
1037
+ return records
1038
+ }
1039
+ }