@supabase/realtime-js 2.79.1-canary.1 → 2.80.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 (42) hide show
  1. package/README.md +37 -5
  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.map +1 -1
  7. package/dist/main/RealtimeClient.js +7 -54
  8. package/dist/main/RealtimeClient.js.map +1 -1
  9. package/dist/main/index.js +5 -40
  10. package/dist/main/index.js.map +1 -1
  11. package/dist/main/lib/constants.d.ts +2 -2
  12. package/dist/main/lib/constants.d.ts.map +1 -1
  13. package/dist/main/lib/transformers.d.ts.map +1 -1
  14. package/dist/main/lib/transformers.js +14 -4
  15. package/dist/main/lib/transformers.js.map +1 -1
  16. package/dist/main/lib/version.d.ts +1 -1
  17. package/dist/main/lib/version.d.ts.map +1 -1
  18. package/dist/main/lib/version.js +1 -1
  19. package/dist/main/lib/version.js.map +1 -1
  20. package/dist/module/RealtimeChannel.d.ts +99 -0
  21. package/dist/module/RealtimeChannel.d.ts.map +1 -1
  22. package/dist/module/RealtimeChannel.js +56 -0
  23. package/dist/module/RealtimeChannel.js.map +1 -1
  24. package/dist/module/RealtimeClient.d.ts.map +1 -1
  25. package/dist/module/RealtimeClient.js +2 -15
  26. package/dist/module/RealtimeClient.js.map +1 -1
  27. package/dist/module/lib/constants.d.ts +2 -2
  28. package/dist/module/lib/constants.d.ts.map +1 -1
  29. package/dist/module/lib/transformers.d.ts.map +1 -1
  30. package/dist/module/lib/transformers.js +14 -4
  31. package/dist/module/lib/transformers.js.map +1 -1
  32. package/dist/module/lib/version.d.ts +1 -1
  33. package/dist/module/lib/version.d.ts.map +1 -1
  34. package/dist/module/lib/version.js +1 -1
  35. package/dist/module/lib/version.js.map +1 -1
  36. package/dist/tsconfig.module.tsbuildinfo +1 -0
  37. package/dist/tsconfig.tsbuildinfo +1 -0
  38. package/package.json +7 -6
  39. package/src/RealtimeChannel.ts +158 -1
  40. package/src/RealtimeClient.ts +2 -16
  41. package/src/lib/transformers.ts +17 -4
  42. package/src/lib/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/realtime-js",
3
- "version": "2.79.1-canary.1",
3
+ "version": "2.80.0",
4
4
  "description": "Listen to realtime updates to your PostgreSQL database",
5
5
  "keywords": [
6
6
  "realtime",
@@ -28,9 +28,7 @@
28
28
  "author": "Supabase",
29
29
  "license": "MIT",
30
30
  "scripts": {
31
- "clean": "rimraf dist docs/v2",
32
- "format": "prettier --write \"{src,test}/**/*.ts\"",
33
- "build": "npm run clean && npm run format && npm run build:main && npm run build:module",
31
+ "build": "npm run build:main && npm run build:module",
34
32
  "build:main": "tsc -p tsconfig.json",
35
33
  "build:module": "tsc -p tsconfig.module.json",
36
34
  "test": "vitest run",
@@ -43,9 +41,9 @@
43
41
  },
44
42
  "dependencies": {
45
43
  "@types/phoenix": "^1.6.6",
46
- "ws": "^8.18.2",
47
44
  "@types/ws": "^8.18.1",
48
- "@supabase/node-fetch": "2.6.15"
45
+ "tslib": "2.8.1",
46
+ "ws": "^8.18.2"
49
47
  },
50
48
  "devDependencies": {
51
49
  "@arethetypeswrong/cli": "^0.16.4",
@@ -56,5 +54,8 @@
56
54
  "mock-socket": "^9.3.1",
57
55
  "nyc": "^15.1.0",
58
56
  "web-worker": "1.2.0"
57
+ },
58
+ "engines": {
59
+ "node": ">=20.0.0"
59
60
  }
60
61
  }
@@ -11,13 +11,19 @@ import type {
11
11
  import * as Transformers from './lib/transformers'
12
12
  import { httpEndpointURL } from './lib/transformers'
13
13
 
14
+ type ReplayOption = {
15
+ since: number
16
+ limit?: number
17
+ }
18
+
14
19
  export type RealtimeChannelOptions = {
15
20
  config: {
16
21
  /**
17
22
  * self option enables client to receive message it broadcast
18
23
  * ack option instructs server to acknowledge that broadcast message was received
24
+ * replay option instructs server to replay broadcast messages
19
25
  */
20
- broadcast?: { self?: boolean; ack?: boolean }
26
+ broadcast?: { self?: boolean; ack?: boolean; replay?: ReplayOption }
21
27
  /**
22
28
  * key option is used to track presence payload across clients
23
29
  */
@@ -29,6 +35,41 @@ export type RealtimeChannelOptions = {
29
35
  }
30
36
  }
31
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
+
32
73
  type RealtimePostgresChangesPayloadBase = {
33
74
  schema: string
34
75
  table: string
@@ -203,6 +244,10 @@ export default class RealtimeChannel {
203
244
 
204
245
  this.broadcastEndpointURL = httpEndpointURL(this.socket.endPoint)
205
246
  this.private = this.params.config.private || false
247
+
248
+ if (!this.private && this.params.config?.broadcast?.replay) {
249
+ throw `tried to use replay on public channel '${this.topic}'. It must be a private channel.`
250
+ }
206
251
  }
207
252
 
208
253
  /** Subscribe registers your client with the server */
@@ -386,6 +431,10 @@ export default class RealtimeChannel {
386
431
  callback: (payload: {
387
432
  type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
388
433
  event: string
434
+ meta?: {
435
+ replayed?: boolean
436
+ id: string
437
+ }
389
438
  [key: string]: any
390
439
  }) => void
391
440
  ): RealtimeChannel
@@ -395,9 +444,49 @@ export default class RealtimeChannel {
395
444
  callback: (payload: {
396
445
  type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
397
446
  event: string
447
+ meta?: {
448
+ replayed?: boolean
449
+ id: string
450
+ }
398
451
  payload: T
399
452
  }) => void
400
453
  ): RealtimeChannel
454
+ on<T extends Record<string, unknown>>(
455
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`,
456
+ filter: { event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL },
457
+ callback: (payload: {
458
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
459
+ event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL
460
+ payload: RealtimeBroadcastPayload<T>
461
+ }) => void
462
+ ): RealtimeChannel
463
+ on<T extends { [key: string]: any }>(
464
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`,
465
+ filter: { event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT },
466
+ callback: (payload: {
467
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
468
+ event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT
469
+ payload: RealtimeBroadcastInsertPayload<T>
470
+ }) => void
471
+ ): RealtimeChannel
472
+ on<T extends { [key: string]: any }>(
473
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`,
474
+ filter: { event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE },
475
+ callback: (payload: {
476
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
477
+ event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE
478
+ payload: RealtimeBroadcastUpdatePayload<T>
479
+ }) => void
480
+ ): RealtimeChannel
481
+ on<T extends { [key: string]: any }>(
482
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`,
483
+ filter: { event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE },
484
+ callback: (payload: {
485
+ type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
486
+ event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE
487
+ payload: RealtimeBroadcastDeletePayload<T>
488
+ }) => void
489
+ ): RealtimeChannel
401
490
  on<T extends { [key: string]: any }>(
402
491
  type: `${REALTIME_LISTEN_TYPES.SYSTEM}`,
403
492
  filter: {},
@@ -417,6 +506,68 @@ export default class RealtimeChannel {
417
506
  }
418
507
  return this._on(type, filter, callback)
419
508
  }
509
+ /**
510
+ * Sends a broadcast message explicitly via REST API.
511
+ *
512
+ * This method always uses the REST API endpoint regardless of WebSocket connection state.
513
+ * Useful when you want to guarantee REST delivery or when gradually migrating from implicit REST fallback.
514
+ *
515
+ * @param event The name of the broadcast event
516
+ * @param payload Payload to be sent (required)
517
+ * @param opts Options including timeout
518
+ * @returns Promise resolving to object with success status, and error details if failed
519
+ */
520
+ async httpSend(
521
+ event: string,
522
+ payload: any,
523
+ opts: { timeout?: number } = {}
524
+ ): Promise<{ success: true } | { success: false; status: number; error: string }> {
525
+ const authorization = this.socket.accessTokenValue
526
+ ? `Bearer ${this.socket.accessTokenValue}`
527
+ : ''
528
+
529
+ if (payload === undefined || payload === null) {
530
+ return Promise.reject('Payload is required for httpSend()')
531
+ }
532
+
533
+ const options = {
534
+ method: 'POST',
535
+ headers: {
536
+ Authorization: authorization,
537
+ apikey: this.socket.apiKey ? this.socket.apiKey : '',
538
+ 'Content-Type': 'application/json',
539
+ },
540
+ body: JSON.stringify({
541
+ messages: [
542
+ {
543
+ topic: this.subTopic,
544
+ event,
545
+ payload: payload,
546
+ private: this.private,
547
+ },
548
+ ],
549
+ }),
550
+ }
551
+
552
+ const response = await this._fetchWithTimeout(
553
+ this.broadcastEndpointURL,
554
+ options,
555
+ opts.timeout ?? this.timeout
556
+ )
557
+
558
+ if (response.status === 202) {
559
+ return { success: true }
560
+ }
561
+
562
+ let errorMessage = response.statusText
563
+ try {
564
+ const errorBody = await response.json()
565
+ errorMessage = errorBody.error || errorBody.message || errorMessage
566
+ } catch {}
567
+
568
+ return Promise.reject(new Error(errorMessage))
569
+ }
570
+
420
571
  /**
421
572
  * Sends a message into the channel.
422
573
  *
@@ -436,6 +587,12 @@ export default class RealtimeChannel {
436
587
  opts: { [key: string]: any } = {}
437
588
  ): Promise<RealtimeChannelSendResponse> {
438
589
  if (!this._canPush() && args.type === 'broadcast') {
590
+ console.warn(
591
+ 'Realtime send() is automatically falling back to REST API. ' +
592
+ 'This behavior will be deprecated in the future. ' +
593
+ 'Please use httpSend() explicitly for REST delivery.'
594
+ )
595
+
439
596
  const { event, payload: endpoint_payload } = args
440
597
  const authorization = this.socket.accessTokenValue
441
598
  ? `Bearer ${this.socket.accessTokenValue}`
@@ -465,24 +465,10 @@ export default class RealtimeClient {
465
465
  * @internal
466
466
  */
467
467
  _resolveFetch = (customFetch?: Fetch): Fetch => {
468
- let _fetch: Fetch
469
468
  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
469
+ return (...args) => customFetch(...args)
484
470
  }
485
- return (...args) => _fetch(...args)
471
+ return (...args) => fetch(...args)
486
472
  }
487
473
 
488
474
  /**
@@ -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.79.1-canary.1'
7
+ export const version = '2.80.0'