@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.
- package/README.md +32 -0
- package/changes.json +14 -0
- package/dist/main/RealtimeChannel.d.ts +370 -0
- package/dist/main/RealtimeChannel.d.ts.map +1 -0
- package/dist/main/RealtimeChannel.js +644 -0
- package/dist/main/RealtimeChannel.js.map +1 -0
- package/dist/main/RealtimeClient.d.ts +238 -0
- package/dist/main/RealtimeClient.d.ts.map +1 -0
- package/dist/main/RealtimeClient.js +870 -0
- package/dist/main/RealtimeClient.js.map +1 -0
- package/dist/main/RealtimePresence.d.ts +76 -0
- package/dist/main/RealtimePresence.d.ts.map +1 -0
- package/dist/main/RealtimePresence.js +237 -0
- package/dist/main/RealtimePresence.js.map +1 -0
- package/dist/main/index.d.ts +6 -0
- package/dist/main/index.d.ts.map +1 -0
- package/dist/main/index.js +18 -0
- package/dist/main/index.js.map +1 -0
- package/dist/main/lib/constants.d.ts +39 -0
- package/dist/main/lib/constants.d.ts.map +1 -0
- package/dist/main/lib/constants.js +48 -0
- package/dist/main/lib/constants.js.map +1 -0
- package/dist/main/lib/push.d.ts +48 -0
- package/dist/main/lib/push.d.ts.map +1 -0
- package/dist/main/lib/push.js +102 -0
- package/dist/main/lib/push.js.map +1 -0
- package/dist/main/lib/serializer.d.ts +33 -0
- package/dist/main/lib/serializer.d.ts.map +1 -0
- package/dist/main/lib/serializer.js +155 -0
- package/dist/main/lib/serializer.js.map +1 -0
- package/dist/main/lib/timer.d.ts +22 -0
- package/dist/main/lib/timer.d.ts.map +1 -0
- package/dist/main/lib/timer.js +39 -0
- package/dist/main/lib/timer.js.map +1 -0
- package/dist/main/lib/transformers.d.ts +109 -0
- package/dist/main/lib/transformers.d.ts.map +1 -0
- package/dist/main/lib/transformers.js +241 -0
- package/dist/main/lib/transformers.js.map +1 -0
- package/dist/main/lib/version.d.ts +2 -0
- package/dist/main/lib/version.d.ts.map +1 -0
- package/dist/main/lib/version.js +11 -0
- package/dist/main/lib/version.js.map +1 -0
- package/dist/main/lib/websocket-factory.d.ts +81 -0
- package/dist/main/lib/websocket-factory.d.ts.map +1 -0
- package/dist/main/lib/websocket-factory.js +131 -0
- package/dist/main/lib/websocket-factory.js.map +1 -0
- package/dist/module/RealtimeChannel.d.ts +370 -0
- package/dist/module/RealtimeChannel.d.ts.map +1 -0
- package/dist/module/RealtimeChannel.js +639 -0
- package/dist/module/RealtimeChannel.js.map +1 -0
- package/dist/module/RealtimeClient.d.ts +238 -0
- package/dist/module/RealtimeClient.d.ts.map +1 -0
- package/dist/module/RealtimeClient.js +866 -0
- package/dist/module/RealtimeClient.js.map +1 -0
- package/dist/module/RealtimePresence.d.ts +76 -0
- package/dist/module/RealtimePresence.d.ts.map +1 -0
- package/dist/module/RealtimePresence.js +233 -0
- package/dist/module/RealtimePresence.js.map +1 -0
- package/dist/module/index.d.ts +6 -0
- package/dist/module/index.d.ts.map +1 -0
- package/dist/module/index.js +6 -0
- package/dist/module/index.js.map +1 -0
- package/dist/module/lib/constants.d.ts +39 -0
- package/dist/module/lib/constants.d.ts.map +1 -0
- package/dist/module/lib/constants.js +45 -0
- package/dist/module/lib/constants.js.map +1 -0
- package/dist/module/lib/push.d.ts +48 -0
- package/dist/module/lib/push.d.ts.map +1 -0
- package/dist/module/lib/push.js +99 -0
- package/dist/module/lib/push.js.map +1 -0
- package/dist/module/lib/serializer.d.ts +33 -0
- package/dist/module/lib/serializer.d.ts.map +1 -0
- package/dist/module/lib/serializer.js +152 -0
- package/dist/module/lib/serializer.js.map +1 -0
- package/dist/module/lib/timer.d.ts +22 -0
- package/dist/module/lib/timer.d.ts.map +1 -0
- package/dist/module/lib/timer.js +36 -0
- package/dist/module/lib/timer.js.map +1 -0
- package/dist/module/lib/transformers.d.ts +109 -0
- package/dist/module/lib/transformers.d.ts.map +1 -0
- package/dist/module/lib/transformers.js +229 -0
- package/dist/module/lib/transformers.js.map +1 -0
- package/dist/module/lib/version.d.ts +2 -0
- package/dist/module/lib/version.d.ts.map +1 -0
- package/dist/module/lib/version.js +8 -0
- package/dist/module/lib/version.js.map +1 -0
- package/dist/module/lib/websocket-factory.d.ts +81 -0
- package/dist/module/lib/websocket-factory.d.ts.map +1 -0
- package/dist/module/lib/websocket-factory.js +127 -0
- package/dist/module/lib/websocket-factory.js.map +1 -0
- package/dist/tsconfig.module.tsbuildinfo +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +85 -0
- package/src/RealtimeChannel.ts +1039 -0
- package/src/RealtimeClient.ts +1023 -0
- package/src/RealtimePresence.ts +346 -0
- package/src/index.ts +53 -0
- package/src/lib/constants.ts +49 -0
- package/src/lib/push.ts +121 -0
- package/src/lib/serializer.ts +203 -0
- package/src/lib/timer.ts +43 -0
- package/src/lib/transformers.ts +270 -0
- package/src/lib/version.ts +7 -0
- 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
|
+
}
|