Skip to content

Commit 62c18a4

Browse files
authored
feat(realtime): add support to configure Broadcast Replay (#1623)
1 parent 4830019 commit 62c18a4

File tree

3 files changed

+76
-3
lines changed

3 files changed

+76
-3
lines changed

packages/core/realtime-js/README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ channel.subscribe((status, err) => {
8080
- `REALTIME_URL` is `'ws://localhost:4000/socket'` when developing locally and `'wss://<project_ref>.supabase.co/realtime/v1'` when connecting to your Supabase project.
8181
- `API_KEY` is a JWT whose claims must contain `exp` and `role` (existing database role).
8282
- Channel name can be any `string`.
83+
- Setting `private` to `true` means that the client will use RLS to determine if the user can connect or not to a given channel.
8384

8485
## Broadcast
8586

@@ -106,9 +107,38 @@ channel.subscribe(async (status) => {
106107

107108
### Notes:
108109

109-
- Setting `ack` to `true` means that the `channel.send` promise will resolve once server replies with acknowledgement that it received the broadcast message request.
110+
- Setting `ack` to `true` means that the `channel.send` promise will resolve once server replies with acknowledgment that it received the broadcast message request.
110111
- Setting `self` to `true` means that the client will receive the broadcast message it sent out.
111-
- Setting `private` to `true` means that the client will use RLS to determine if the user can connect or not to a given channel.
112+
113+
### Broadcast Replay
114+
115+
Broadcast Replay enables **private** channels to access messages that were sent earlier. Only messages published via [Broadcast From the Database](https://supabase.com/docs/guides/realtime/broadcast#trigger-broadcast-messages-from-your-database) are available for replay.
116+
117+
You can configure replay with the following options:
118+
119+
- **`since`** (Required): The epoch timestamp in milliseconds, specifying the earliest point from which messages should be retrieved.
120+
- **`limit`** (Optional): The number of messages to return. This must be a positive integer, with a maximum value of 25.
121+
122+
Example:
123+
124+
```typescript
125+
const twelveHours = 12 * 60 * 60 * 1000
126+
const twelveHoursAgo = Date.now() - twelveHours
127+
128+
const config = { private: true, broadcast: { replay: { since: twelveHoursAgo, limit: 10 } } }
129+
130+
supabase
131+
.channel('main:room', { config })
132+
.on('broadcast', { event: 'my_event' }, (payload) => {
133+
if (payload?.meta?.replayed) {
134+
console.log('This message was sent earlier:', payload)
135+
} else {
136+
console.log('This is a new message', payload)
137+
}
138+
// ...
139+
})
140+
.subscribe()
141+
```
112142

113143
## Presence
114144

packages/core/realtime-js/src/RealtimeChannel.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ import type {
1111
import * as Transformers from './lib/transformers'
1212
import { httpEndpointURL } from './lib/transformers'
1313

14+
type ReplayOption = {
15+
since: number
16+
limit?: number
17+
}
18+
1419
export type RealtimeChannelOptions = {
1520
config: {
1621
/**
1722
* self option enables client to receive message it broadcast
1823
* ack option instructs server to acknowledge that broadcast message was received
24+
* replay option instructs server to replay broadcast messages
1925
*/
20-
broadcast?: { self?: boolean; ack?: boolean }
26+
broadcast?: { self?: boolean; ack?: boolean; replay?: ReplayOption }
2127
/**
2228
* key option is used to track presence payload across clients
2329
*/
@@ -203,6 +209,10 @@ export default class RealtimeChannel {
203209

204210
this.broadcastEndpointURL = httpEndpointURL(this.socket.endPoint)
205211
this.private = this.params.config.private || false
212+
213+
if (!this.private && this.params.config?.broadcast?.replay) {
214+
throw `tried to use replay on public channel '${this.topic}'. It must be a private channel.`
215+
}
206216
}
207217

208218
/** Subscribe registers your client with the server */
@@ -386,6 +396,10 @@ export default class RealtimeChannel {
386396
callback: (payload: {
387397
type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
388398
event: string
399+
meta?: {
400+
replayed?: boolean
401+
id: string
402+
}
389403
[key: string]: any
390404
}) => void
391405
): RealtimeChannel
@@ -395,6 +409,10 @@ export default class RealtimeChannel {
395409
callback: (payload: {
396410
type: `${REALTIME_LISTEN_TYPES.BROADCAST}`
397411
event: string
412+
meta?: {
413+
replayed?: boolean
414+
id: string
415+
}
398416
payload: T
399417
}) => void
400418
): RealtimeChannel

packages/core/realtime-js/test/RealtimeClient.channels.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,31 @@ afterEach(() => {
1414

1515
describe('channel', () => {
1616
let channel
17+
test('throws error on replay for a public channel', () => {
18+
expect(() => {
19+
channel = testSetup.socket.channel('topic', {
20+
config: { broadcast: { replay: { since: 0 } } },
21+
})
22+
}).toThrow(
23+
"tried to use replay on public channel 'realtime:topic'. It must be a private channel."
24+
)
25+
})
26+
27+
test('returns channel with private channel using replay', () => {
28+
channel = testSetup.socket.channel('topic', {
29+
config: { private: true, broadcast: { replay: { since: 0 } } },
30+
})
31+
32+
assert.deepStrictEqual(channel.socket, testSetup.socket)
33+
assert.equal(channel.topic, 'realtime:topic')
34+
assert.deepEqual(channel.params, {
35+
config: {
36+
broadcast: { replay: { since: 0 } },
37+
presence: { key: '', enabled: false },
38+
private: true,
39+
},
40+
})
41+
})
1742

1843
test('returns channel with given topic and params', () => {
1944
channel = testSetup.socket.channel('topic')

0 commit comments

Comments
 (0)