@supabase/realtime-js 2.80.1-canary.1 → 2.80.1-canary.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 (54) hide show
  1. package/README.md +39 -7
  2. package/dist/main/RealtimeChannel.d.ts +99 -0
  3. package/dist/main/RealtimeChannel.d.ts.map +1 -1
  4. package/dist/main/RealtimeChannel.js +61 -40
  5. package/dist/main/RealtimeChannel.js.map +1 -1
  6. package/dist/main/RealtimeClient.d.ts +2 -0
  7. package/dist/main/RealtimeClient.d.ts.map +1 -1
  8. package/dist/main/RealtimeClient.js +30 -62
  9. package/dist/main/RealtimeClient.js.map +1 -1
  10. package/dist/main/index.js +5 -40
  11. package/dist/main/index.js.map +1 -1
  12. package/dist/main/lib/constants.d.ts +5 -3
  13. package/dist/main/lib/constants.d.ts.map +1 -1
  14. package/dist/main/lib/constants.js +4 -2
  15. package/dist/main/lib/constants.js.map +1 -1
  16. package/dist/main/lib/serializer.d.ts +30 -0
  17. package/dist/main/lib/serializer.d.ts.map +1 -1
  18. package/dist/main/lib/serializer.js +198 -6
  19. package/dist/main/lib/serializer.js.map +1 -1
  20. package/dist/main/lib/transformers.d.ts.map +1 -1
  21. package/dist/main/lib/transformers.js +14 -4
  22. package/dist/main/lib/transformers.js.map +1 -1
  23. package/dist/main/lib/version.d.ts +1 -1
  24. package/dist/main/lib/version.js +1 -1
  25. package/dist/module/RealtimeChannel.d.ts +99 -0
  26. package/dist/module/RealtimeChannel.d.ts.map +1 -1
  27. package/dist/module/RealtimeChannel.js +56 -0
  28. package/dist/module/RealtimeChannel.js.map +1 -1
  29. package/dist/module/RealtimeClient.d.ts +2 -0
  30. package/dist/module/RealtimeClient.d.ts.map +1 -1
  31. package/dist/module/RealtimeClient.js +26 -24
  32. package/dist/module/RealtimeClient.js.map +1 -1
  33. package/dist/module/lib/constants.d.ts +5 -3
  34. package/dist/module/lib/constants.d.ts.map +1 -1
  35. package/dist/module/lib/constants.js +3 -1
  36. package/dist/module/lib/constants.js.map +1 -1
  37. package/dist/module/lib/serializer.d.ts +30 -0
  38. package/dist/module/lib/serializer.d.ts.map +1 -1
  39. package/dist/module/lib/serializer.js +197 -5
  40. package/dist/module/lib/serializer.js.map +1 -1
  41. package/dist/module/lib/transformers.d.ts.map +1 -1
  42. package/dist/module/lib/transformers.js +14 -4
  43. package/dist/module/lib/transformers.js.map +1 -1
  44. package/dist/module/lib/version.d.ts +1 -1
  45. package/dist/module/lib/version.js +1 -1
  46. package/dist/tsconfig.module.tsbuildinfo +1 -0
  47. package/dist/tsconfig.tsbuildinfo +1 -0
  48. package/package.json +10 -9
  49. package/src/RealtimeChannel.ts +158 -1
  50. package/src/RealtimeClient.ts +30 -24
  51. package/src/lib/constants.ts +4 -1
  52. package/src/lib/serializer.ts +271 -5
  53. package/src/lib/transformers.ts +17 -4
  54. package/src/lib/version.ts +1 -1
@@ -7,7 +7,9 @@ import {
7
7
  DEFAULT_TIMEOUT,
8
8
  SOCKET_STATES,
9
9
  TRANSPORTS,
10
- VSN,
10
+ DEFAULT_VSN,
11
+ VSN_1_0_0,
12
+ VSN_2_0_0,
11
13
  WS_CLOSE_NORMAL,
12
14
  } from './lib/constants'
13
15
 
@@ -70,6 +72,7 @@ export type RealtimeClientOptions = {
70
72
  timeout?: number
71
73
  heartbeatIntervalMs?: number
72
74
  heartbeatCallback?: (status: HeartbeatStatus) => void
75
+ vsn?: string
73
76
  logger?: Function
74
77
  encode?: Function
75
78
  decode?: Function
@@ -109,6 +112,7 @@ export default class RealtimeClient {
109
112
  heartbeatCallback: (status: HeartbeatStatus) => void = noop
110
113
  ref: number = 0
111
114
  reconnectTimer: Timer | null = null
115
+ vsn: string = DEFAULT_VSN
112
116
  logger: Function = noop
113
117
  logLevel?: LogLevel
114
118
  encode!: Function
@@ -226,7 +230,7 @@ export default class RealtimeClient {
226
230
  * @returns string The URL of the websocket.
227
231
  */
228
232
  endpointURL(): string {
229
- return this._appendParams(this.endPoint, Object.assign({}, this.params, { vsn: VSN }))
233
+ return this._appendParams(this.endPoint, Object.assign({}, this.params, { vsn: this.vsn }))
230
234
  }
231
235
 
232
236
  /**
@@ -465,24 +469,10 @@ export default class RealtimeClient {
465
469
  * @internal
466
470
  */
467
471
  _resolveFetch = (customFetch?: Fetch): Fetch => {
468
- let _fetch: Fetch
469
472
  if (customFetch) {
470
- _fetch = customFetch
471
- } else if (typeof fetch === 'undefined') {
472
- // Node.js environment without native fetch
473
- _fetch = (...args) =>
474
- import('@supabase/node-fetch' as any)
475
- .then(({ default: fetch }) => fetch(...args))
476
- .catch((error) => {
477
- throw new Error(
478
- `Failed to load @supabase/node-fetch: ${error.message}. ` +
479
- `This is required for HTTP requests in Node.js environments without native fetch.`
480
- )
481
- })
482
- } else {
483
- _fetch = fetch
473
+ return (...args) => customFetch(...args)
484
474
  }
485
- return (...args) => _fetch(...args)
475
+ return (...args) => fetch(...args)
486
476
  }
487
477
 
488
478
  /**
@@ -825,6 +815,8 @@ export default class RealtimeClient {
825
815
  this.worker = options?.worker ?? false
826
816
  this.accessToken = options?.accessToken ?? null
827
817
  this.heartbeatCallback = options?.heartbeatCallback ?? noop
818
+ this.vsn = options?.vsn ?? DEFAULT_VSN
819
+
828
820
  // Handle special cases
829
821
  if (options?.params) this.params = options.params
830
822
  if (options?.logger) this.logger = options.logger
@@ -840,13 +832,27 @@ export default class RealtimeClient {
840
832
  return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK
841
833
  })
842
834
 
843
- this.encode =
844
- options?.encode ??
845
- ((payload: JSON, callback: Function) => {
846
- return callback(JSON.stringify(payload))
847
- })
835
+ switch (this.vsn) {
836
+ case VSN_1_0_0:
837
+ this.encode =
838
+ options?.encode ??
839
+ ((payload: JSON, callback: Function) => {
840
+ return callback(JSON.stringify(payload))
841
+ })
848
842
 
849
- this.decode = options?.decode ?? this.serializer.decode.bind(this.serializer)
843
+ this.decode =
844
+ options?.decode ??
845
+ ((payload: string, callback: Function) => {
846
+ return callback(JSON.parse(payload))
847
+ })
848
+ break
849
+ case VSN_2_0_0:
850
+ this.encode = options?.encode ?? this.serializer.encode.bind(this.serializer)
851
+ this.decode = options?.decode ?? this.serializer.decode.bind(this.serializer)
852
+ break
853
+ default:
854
+ throw new Error(`Unsupported serializer version: ${this.vsn}`)
855
+ }
850
856
 
851
857
  // Handle worker setup
852
858
  if (this.worker) {
@@ -1,7 +1,10 @@
1
1
  import { version } from './version'
2
2
 
3
3
  export const DEFAULT_VERSION = `realtime-js/${version}`
4
- export const VSN: string = '1.0.0'
4
+
5
+ export const VSN_1_0_0: string = '1.0.0'
6
+ export const VSN_2_0_0: string = '2.0.0'
7
+ export const DEFAULT_VSN: string = VSN_1_0_0
5
8
 
6
9
  export const VERSION = version
7
10
 
@@ -1,16 +1,160 @@
1
1
  // This file draws heavily from https://github.com/phoenixframework/phoenix/commit/cf098e9cf7a44ee6479d31d911a97d3c7430c6fe
2
2
  // License: https://github.com/phoenixframework/phoenix/blob/master/LICENSE.md
3
+ import { CHANNEL_EVENTS } from '../lib/constants'
4
+
5
+ export type Msg<T> = {
6
+ join_ref: string
7
+ ref: string
8
+ topic: string
9
+ event: string
10
+ payload: T
11
+ }
3
12
 
4
13
  export default class Serializer {
5
14
  HEADER_LENGTH = 1
15
+ META_LENGTH = 4
16
+ USER_BROADCAST_PUSH_META_LENGTH = 5
17
+ KINDS = { push: 0, reply: 1, broadcast: 2, userBroadcastPush: 3, userBroadcast: 4 }
18
+ BINARY_ENCODING = 0
19
+ JSON_ENCODING = 1
20
+ BROADCAST = 'broadcast'
21
+
22
+ encode(
23
+ msg: Msg<{ [key: string]: any } | ArrayBuffer>,
24
+ callback: (result: ArrayBuffer | string) => any
25
+ ) {
26
+ if (this._isArrayBuffer(msg.payload)) {
27
+ return callback(this._binaryEncodePush(msg as Msg<ArrayBuffer>))
28
+ }
29
+
30
+ if (
31
+ msg.event === this.BROADCAST &&
32
+ !(msg.payload instanceof ArrayBuffer) &&
33
+ typeof msg.payload.event === 'string'
34
+ ) {
35
+ return callback(
36
+ this._binaryEncodeUserBroadcastPush(msg as Msg<{ event: string } & { [key: string]: any }>)
37
+ )
38
+ }
39
+
40
+ let payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload]
41
+ return callback(JSON.stringify(payload))
42
+ }
43
+
44
+ private _binaryEncodePush(message: Msg<ArrayBuffer>) {
45
+ const { join_ref, ref, event, topic, payload } = message
46
+ const metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length
47
+
48
+ const header = new ArrayBuffer(this.HEADER_LENGTH + metaLength)
49
+ let view = new DataView(header)
50
+ let offset = 0
51
+
52
+ view.setUint8(offset++, this.KINDS.push) // kind
53
+ view.setUint8(offset++, join_ref.length)
54
+ view.setUint8(offset++, ref.length)
55
+ view.setUint8(offset++, topic.length)
56
+ view.setUint8(offset++, event.length)
57
+ Array.from(join_ref, (char) => view.setUint8(offset++, char.charCodeAt(0)))
58
+ Array.from(ref, (char) => view.setUint8(offset++, char.charCodeAt(0)))
59
+ Array.from(topic, (char) => view.setUint8(offset++, char.charCodeAt(0)))
60
+ Array.from(event, (char) => view.setUint8(offset++, char.charCodeAt(0)))
61
+
62
+ var combined = new Uint8Array(header.byteLength + payload.byteLength)
63
+ combined.set(new Uint8Array(header), 0)
64
+ combined.set(new Uint8Array(payload), header.byteLength)
65
+
66
+ return combined.buffer
67
+ }
68
+
69
+ private _binaryEncodeUserBroadcastPush(message: Msg<{ event: string } & { [key: string]: any }>) {
70
+ if (this._isArrayBuffer(message.payload?.payload)) {
71
+ return this._encodeBinaryUserBroadcastPush(message)
72
+ } else {
73
+ return this._encodeJsonUserBroadcastPush(message)
74
+ }
75
+ }
76
+
77
+ private _encodeBinaryUserBroadcastPush(message: Msg<{ event: string } & { [key: string]: any }>) {
78
+ const { join_ref, ref, topic } = message
79
+ const userEvent = message.payload.event
80
+ const userPayload = message.payload?.payload ?? new ArrayBuffer(0)
81
+
82
+ const metaLength =
83
+ this.USER_BROADCAST_PUSH_META_LENGTH +
84
+ join_ref.length +
85
+ ref.length +
86
+ topic.length +
87
+ userEvent.length
88
+
89
+ const header = new ArrayBuffer(this.HEADER_LENGTH + metaLength)
90
+ let view = new DataView(header)
91
+ let offset = 0
92
+
93
+ view.setUint8(offset++, this.KINDS.userBroadcastPush) // kind
94
+ view.setUint8(offset++, join_ref.length)
95
+ view.setUint8(offset++, ref.length)
96
+ view.setUint8(offset++, topic.length)
97
+ view.setUint8(offset++, userEvent.length)
98
+ view.setUint8(offset++, this.BINARY_ENCODING)
99
+ Array.from(join_ref, (char) => view.setUint8(offset++, char.charCodeAt(0)))
100
+ Array.from(ref, (char) => view.setUint8(offset++, char.charCodeAt(0)))
101
+ Array.from(topic, (char) => view.setUint8(offset++, char.charCodeAt(0)))
102
+ Array.from(userEvent, (char) => view.setUint8(offset++, char.charCodeAt(0)))
103
+
104
+ var combined = new Uint8Array(header.byteLength + userPayload.byteLength)
105
+ combined.set(new Uint8Array(header), 0)
106
+ combined.set(new Uint8Array(userPayload), header.byteLength)
107
+
108
+ return combined.buffer
109
+ }
110
+
111
+ private _encodeJsonUserBroadcastPush(message: Msg<{ event: string } & { [key: string]: any }>) {
112
+ const { join_ref, ref, topic } = message
113
+ const userEvent = message.payload.event
114
+ const userPayload = message.payload?.payload ?? {}
115
+
116
+ const encoder = new TextEncoder() // Encodes to UTF-8
117
+ const encodedUserPayload = encoder.encode(JSON.stringify(userPayload)).buffer
118
+
119
+ const metaLength =
120
+ this.USER_BROADCAST_PUSH_META_LENGTH +
121
+ join_ref.length +
122
+ ref.length +
123
+ topic.length +
124
+ userEvent.length
125
+
126
+ const header = new ArrayBuffer(this.HEADER_LENGTH + metaLength)
127
+ let view = new DataView(header)
128
+ let offset = 0
129
+
130
+ view.setUint8(offset++, this.KINDS.userBroadcastPush) // kind
131
+ view.setUint8(offset++, join_ref.length)
132
+ view.setUint8(offset++, ref.length)
133
+ view.setUint8(offset++, topic.length)
134
+ view.setUint8(offset++, userEvent.length)
135
+ view.setUint8(offset++, this.JSON_ENCODING)
136
+ Array.from(join_ref, (char) => view.setUint8(offset++, char.charCodeAt(0)))
137
+ Array.from(ref, (char) => view.setUint8(offset++, char.charCodeAt(0)))
138
+ Array.from(topic, (char) => view.setUint8(offset++, char.charCodeAt(0)))
139
+ Array.from(userEvent, (char) => view.setUint8(offset++, char.charCodeAt(0)))
140
+
141
+ var combined = new Uint8Array(header.byteLength + encodedUserPayload.byteLength)
142
+ combined.set(new Uint8Array(header), 0)
143
+ combined.set(new Uint8Array(encodedUserPayload), header.byteLength)
144
+
145
+ return combined.buffer
146
+ }
6
147
 
7
148
  decode(rawPayload: ArrayBuffer | string, callback: Function) {
8
- if (rawPayload.constructor === ArrayBuffer) {
9
- return callback(this._binaryDecode(rawPayload))
149
+ if (this._isArrayBuffer(rawPayload)) {
150
+ let result = this._binaryDecode(rawPayload as ArrayBuffer)
151
+ return callback(result)
10
152
  }
11
153
 
12
154
  if (typeof rawPayload === 'string') {
13
- return callback(JSON.parse(rawPayload))
155
+ const jsonPayload = JSON.parse(rawPayload)
156
+ const [join_ref, ref, topic, event, payload] = jsonPayload
157
+ return callback({ join_ref, ref, topic, event, payload })
14
158
  }
15
159
 
16
160
  return callback({})
@@ -18,9 +162,84 @@ export default class Serializer {
18
162
 
19
163
  private _binaryDecode(buffer: ArrayBuffer) {
20
164
  const view = new DataView(buffer)
165
+ const kind = view.getUint8(0)
21
166
  const decoder = new TextDecoder()
167
+ switch (kind) {
168
+ case this.KINDS.push:
169
+ return this._decodePush(buffer, view, decoder)
170
+ case this.KINDS.reply:
171
+ return this._decodeReply(buffer, view, decoder)
172
+ case this.KINDS.broadcast:
173
+ return this._decodeBroadcast(buffer, view, decoder)
174
+ case this.KINDS.userBroadcast:
175
+ return this._decodeUserBroadcast(buffer, view, decoder)
176
+ }
177
+ }
178
+
179
+ private _decodePush(
180
+ buffer: ArrayBuffer,
181
+ view: DataView,
182
+ decoder: TextDecoder
183
+ ): {
184
+ join_ref: string
185
+ ref: null
186
+ topic: string
187
+ event: string
188
+ payload: { [key: string]: any }
189
+ } {
190
+ const joinRefSize = view.getUint8(1)
191
+ const topicSize = view.getUint8(2)
192
+ const eventSize = view.getUint8(3)
193
+ let offset = this.HEADER_LENGTH + this.META_LENGTH - 1 // pushes have no ref
194
+ const joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))
195
+ offset = offset + joinRefSize
196
+ const topic = decoder.decode(buffer.slice(offset, offset + topicSize))
197
+ offset = offset + topicSize
198
+ const event = decoder.decode(buffer.slice(offset, offset + eventSize))
199
+ offset = offset + eventSize
200
+ const data = JSON.parse(decoder.decode(buffer.slice(offset, buffer.byteLength)))
201
+ return {
202
+ join_ref: joinRef,
203
+ ref: null,
204
+ topic: topic,
205
+ event: event,
206
+ payload: data,
207
+ }
208
+ }
22
209
 
23
- return this._decodeBroadcast(buffer, view, decoder)
210
+ private _decodeReply(
211
+ buffer: ArrayBuffer,
212
+ view: DataView,
213
+ decoder: TextDecoder
214
+ ): {
215
+ join_ref: string
216
+ ref: string
217
+ topic: string
218
+ event: CHANNEL_EVENTS.reply
219
+ payload: { status: string; response: { [key: string]: any } }
220
+ } {
221
+ const joinRefSize = view.getUint8(1)
222
+ const refSize = view.getUint8(2)
223
+ const topicSize = view.getUint8(3)
224
+ const eventSize = view.getUint8(4)
225
+ let offset = this.HEADER_LENGTH + this.META_LENGTH
226
+ const joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))
227
+ offset = offset + joinRefSize
228
+ const ref = decoder.decode(buffer.slice(offset, offset + refSize))
229
+ offset = offset + refSize
230
+ const topic = decoder.decode(buffer.slice(offset, offset + topicSize))
231
+ offset = offset + topicSize
232
+ const event = decoder.decode(buffer.slice(offset, offset + eventSize))
233
+ offset = offset + eventSize
234
+ const data = JSON.parse(decoder.decode(buffer.slice(offset, buffer.byteLength)))
235
+ const payload = { status: event, response: data }
236
+ return {
237
+ join_ref: joinRef,
238
+ ref: ref,
239
+ topic: topic,
240
+ event: CHANNEL_EVENTS.reply,
241
+ payload: payload,
242
+ }
24
243
  }
25
244
 
26
245
  private _decodeBroadcast(
@@ -28,6 +247,7 @@ export default class Serializer {
28
247
  view: DataView,
29
248
  decoder: TextDecoder
30
249
  ): {
250
+ join_ref: null
31
251
  ref: null
32
252
  topic: string
33
253
  event: string
@@ -42,6 +262,52 @@ export default class Serializer {
42
262
  offset = offset + eventSize
43
263
  const data = JSON.parse(decoder.decode(buffer.slice(offset, buffer.byteLength)))
44
264
 
45
- return { ref: null, topic: topic, event: event, payload: data }
265
+ return { join_ref: null, ref: null, topic: topic, event: event, payload: data }
266
+ }
267
+
268
+ private _decodeUserBroadcast(
269
+ buffer: ArrayBuffer,
270
+ view: DataView,
271
+ decoder: TextDecoder
272
+ ): {
273
+ join_ref: null
274
+ ref: null
275
+ topic: string
276
+ event: string
277
+ payload: { [key: string]: any }
278
+ } {
279
+ const topicSize = view.getUint8(1)
280
+ const userEventSize = view.getUint8(2)
281
+ const metadataSize = view.getUint8(3)
282
+ const payloadEncoding = view.getUint8(4)
283
+
284
+ let offset = this.HEADER_LENGTH + 4
285
+ const topic = decoder.decode(buffer.slice(offset, offset + topicSize))
286
+ offset = offset + topicSize
287
+ const userEvent = decoder.decode(buffer.slice(offset, offset + userEventSize))
288
+ offset = offset + userEventSize
289
+ const metadata = decoder.decode(buffer.slice(offset, offset + metadataSize))
290
+ offset = offset + metadataSize
291
+
292
+ const payload = buffer.slice(offset, buffer.byteLength)
293
+ const parsedPayload =
294
+ payloadEncoding === this.JSON_ENCODING ? JSON.parse(decoder.decode(payload)) : payload
295
+
296
+ const data: { [key: string]: any } = {
297
+ type: this.BROADCAST,
298
+ event: userEvent,
299
+ payload: parsedPayload,
300
+ }
301
+
302
+ // Metadata is optional and always JSON encoded
303
+ if (metadataSize > 0) {
304
+ data['meta'] = JSON.parse(metadata)
305
+ }
306
+
307
+ return { join_ref: null, ref: null, topic: topic, event: this.BROADCAST, payload: data }
308
+ }
309
+
310
+ private _isArrayBuffer(buffer: any): boolean {
311
+ return buffer instanceof ArrayBuffer || buffer?.constructor?.name === 'ArrayBuffer'
46
312
  }
47
313
  }
@@ -251,8 +251,21 @@ export const toTimestampString = (value: RecordValue): RecordValue => {
251
251
  }
252
252
 
253
253
  export const httpEndpointURL = (socketUrl: string): string => {
254
- let url = socketUrl
255
- url = url.replace(/^ws/i, 'http')
256
- url = url.replace(/(\/socket\/websocket|\/socket|\/websocket)\/?$/i, '')
257
- return url.replace(/\/+$/, '') + '/api/broadcast'
254
+ const wsUrl = new URL(socketUrl)
255
+
256
+ wsUrl.protocol = wsUrl.protocol.replace(/^ws/i, 'http')
257
+
258
+ wsUrl.pathname = wsUrl.pathname
259
+ .replace(/\/+$/, '') // remove all trailing slashes
260
+ .replace(/\/socket\/websocket$/i, '') // remove the socket/websocket path
261
+ .replace(/\/socket$/i, '') // remove the socket path
262
+ .replace(/\/websocket$/i, '') // remove the websocket path
263
+
264
+ if (wsUrl.pathname === '' || wsUrl.pathname === '/') {
265
+ wsUrl.pathname = '/api/broadcast'
266
+ } else {
267
+ wsUrl.pathname = wsUrl.pathname + '/api/broadcast'
268
+ }
269
+
270
+ return wsUrl.href
258
271
  }
@@ -4,4 +4,4 @@
4
4
  // - Debugging and support (identifying which version is running)
5
5
  // - Telemetry and logging (version reporting in errors/analytics)
6
6
  // - Ensuring build artifacts match the published package version
7
- export const version = '2.80.1-canary.1'
7
+ export const version = '2.80.1-canary.3'