@supabase/realtime-js 1.7.4 → 1.8.0-next.1
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/dist/main/{RealtimeSubscription.d.ts → RealtimeChannel.d.ts} +37 -6
- package/dist/main/RealtimeChannel.d.ts.map +1 -0
- package/dist/main/{RealtimeSubscription.js → RealtimeChannel.js} +103 -25
- package/dist/main/RealtimeChannel.js.map +1 -0
- package/dist/main/RealtimeClient.d.ts +4 -4
- package/dist/main/RealtimeClient.d.ts.map +1 -1
- package/dist/main/RealtimeClient.js +36 -2
- package/dist/main/RealtimeClient.js.map +1 -1
- package/dist/main/RealtimePresence.d.ts +97 -0
- package/dist/main/RealtimePresence.d.ts.map +1 -0
- package/dist/main/RealtimePresence.js +201 -0
- package/dist/main/RealtimePresence.js.map +1 -0
- package/dist/main/index.d.ts +3 -2
- package/dist/main/index.d.ts.map +1 -1
- package/dist/main/index.js +5 -3
- package/dist/main/index.js.map +1 -1
- package/dist/main/lib/push.d.ts +3 -3
- package/dist/main/lib/push.d.ts.map +1 -1
- package/dist/main/lib/push.js +2 -2
- package/dist/main/lib/push.js.map +1 -1
- package/dist/main/lib/version.d.ts +1 -1
- package/dist/main/lib/version.js +1 -1
- package/dist/module/{RealtimeSubscription.d.ts → RealtimeChannel.d.ts} +37 -6
- package/dist/module/RealtimeChannel.d.ts.map +1 -0
- package/dist/module/{RealtimeSubscription.js → RealtimeChannel.js} +102 -24
- package/dist/module/RealtimeChannel.js.map +1 -0
- package/dist/module/RealtimeClient.d.ts +4 -4
- package/dist/module/RealtimeClient.d.ts.map +1 -1
- package/dist/module/RealtimeClient.js +36 -2
- package/dist/module/RealtimeClient.js.map +1 -1
- package/dist/module/RealtimePresence.d.ts +97 -0
- package/dist/module/RealtimePresence.d.ts.map +1 -0
- package/dist/module/RealtimePresence.js +198 -0
- package/dist/module/RealtimePresence.js.map +1 -0
- package/dist/module/index.d.ts +3 -2
- package/dist/module/index.d.ts.map +1 -1
- package/dist/module/index.js +3 -2
- package/dist/module/index.js.map +1 -1
- package/dist/module/lib/push.d.ts +3 -3
- package/dist/module/lib/push.d.ts.map +1 -1
- package/dist/module/lib/push.js +2 -2
- package/dist/module/lib/push.js.map +1 -1
- package/dist/module/lib/version.d.ts +1 -1
- package/dist/module/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/{RealtimeSubscription.ts → RealtimeChannel.ts} +136 -33
- package/src/RealtimeClient.ts +38 -9
- package/src/RealtimePresence.ts +326 -0
- package/src/index.ts +4 -2
- package/src/lib/push.ts +4 -4
- package/src/lib/version.ts +1 -1
- package/dist/main/RealtimeSubscription.d.ts.map +0 -1
- package/dist/main/RealtimeSubscription.js.map +0 -1
- package/dist/module/RealtimeSubscription.d.ts.map +0 -1
- package/dist/module/RealtimeSubscription.js.map +0 -1
|
@@ -2,8 +2,9 @@ import { CHANNEL_EVENTS, CHANNEL_STATES } from './lib/constants'
|
|
|
2
2
|
import Push from './lib/push'
|
|
3
3
|
import RealtimeClient from './RealtimeClient'
|
|
4
4
|
import Timer from './lib/timer'
|
|
5
|
+
import RealtimePresence from './RealtimePresence'
|
|
5
6
|
|
|
6
|
-
export default class
|
|
7
|
+
export default class RealtimeChannel {
|
|
7
8
|
bindings: any[] = []
|
|
8
9
|
timeout: number
|
|
9
10
|
state = CHANNEL_STATES.closed
|
|
@@ -11,6 +12,7 @@ export default class RealtimeSubscription {
|
|
|
11
12
|
joinPush: Push
|
|
12
13
|
rejoinTimer: Timer
|
|
13
14
|
pushBuffer: Push[] = []
|
|
15
|
+
presence: RealtimePresence
|
|
14
16
|
|
|
15
17
|
constructor(
|
|
16
18
|
public topic: string,
|
|
@@ -56,9 +58,38 @@ export default class RealtimeSubscription {
|
|
|
56
58
|
this.state = CHANNEL_STATES.errored
|
|
57
59
|
this.rejoinTimer.scheduleTimeout()
|
|
58
60
|
})
|
|
59
|
-
this.on(CHANNEL_EVENTS.reply, (payload: any, ref: string) => {
|
|
61
|
+
this.on(CHANNEL_EVENTS.reply, {}, (payload: any, ref: string) => {
|
|
60
62
|
this.trigger(this.replyEventName(ref), payload)
|
|
61
63
|
})
|
|
64
|
+
this.presence = new RealtimePresence(this)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
list() {
|
|
68
|
+
return this.presence.list()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async track(
|
|
72
|
+
payload: { [key: string]: any },
|
|
73
|
+
opts: { [key: string]: any } = {}
|
|
74
|
+
) {
|
|
75
|
+
return await this.send(
|
|
76
|
+
{
|
|
77
|
+
type: 'presence',
|
|
78
|
+
event: 'track',
|
|
79
|
+
payload,
|
|
80
|
+
},
|
|
81
|
+
opts
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async untrack(opts: { [key: string]: any } = {}) {
|
|
86
|
+
return await this.send(
|
|
87
|
+
{
|
|
88
|
+
type: 'presence',
|
|
89
|
+
event: 'untrack',
|
|
90
|
+
},
|
|
91
|
+
opts
|
|
92
|
+
)
|
|
62
93
|
}
|
|
63
94
|
|
|
64
95
|
rejoinUntilConnected() {
|
|
@@ -72,29 +103,69 @@ export default class RealtimeSubscription {
|
|
|
72
103
|
if (this.joinedOnce) {
|
|
73
104
|
throw `tried to subscribe multiple times. 'subscribe' can only be called a single time per channel instance`
|
|
74
105
|
} else {
|
|
106
|
+
const configs = this.bindings.reduce(
|
|
107
|
+
(acc, binding: { [key: string]: any }) => {
|
|
108
|
+
const { type } = binding
|
|
109
|
+
if (
|
|
110
|
+
![
|
|
111
|
+
'phx_close',
|
|
112
|
+
'phx_error',
|
|
113
|
+
'phx_reply',
|
|
114
|
+
'presence_diff',
|
|
115
|
+
'presence_state',
|
|
116
|
+
].includes(type)
|
|
117
|
+
) {
|
|
118
|
+
acc[type] = binding
|
|
119
|
+
}
|
|
120
|
+
return acc
|
|
121
|
+
},
|
|
122
|
+
{}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if (Object.keys(configs).length) {
|
|
126
|
+
this.updateJoinPayload({ configs })
|
|
127
|
+
}
|
|
128
|
+
|
|
75
129
|
this.joinedOnce = true
|
|
76
130
|
this.rejoin(timeout)
|
|
77
131
|
return this.joinPush
|
|
78
132
|
}
|
|
79
133
|
}
|
|
80
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Registers a callback that will be executed when the channel closes.
|
|
137
|
+
*/
|
|
81
138
|
onClose(callback: Function) {
|
|
82
|
-
this.on(CHANNEL_EVENTS.close, callback)
|
|
139
|
+
this.on(CHANNEL_EVENTS.close, {}, callback)
|
|
83
140
|
}
|
|
84
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Registers a callback that will be executed when the channel encounteres an error.
|
|
144
|
+
*/
|
|
85
145
|
onError(callback: Function) {
|
|
86
|
-
this.on(CHANNEL_EVENTS.error, (reason: string) => callback(reason))
|
|
146
|
+
this.on(CHANNEL_EVENTS.error, {}, (reason: string) => callback(reason))
|
|
87
147
|
}
|
|
88
148
|
|
|
89
|
-
on(
|
|
90
|
-
this.bindings.push({
|
|
149
|
+
on(type: string, filter?: { [key: string]: string }, callback?: Function) {
|
|
150
|
+
this.bindings.push({
|
|
151
|
+
type,
|
|
152
|
+
filter: filter ?? {},
|
|
153
|
+
callback: callback ?? (() => {}),
|
|
154
|
+
})
|
|
91
155
|
}
|
|
92
156
|
|
|
93
|
-
off(
|
|
94
|
-
this.bindings = this.bindings.filter((bind) =>
|
|
157
|
+
off(type: string, filter: { [key: string]: any }) {
|
|
158
|
+
this.bindings = this.bindings.filter((bind) => {
|
|
159
|
+
return !(
|
|
160
|
+
bind.type === type && RealtimeChannel.isEqual(bind.filter, filter)
|
|
161
|
+
)
|
|
162
|
+
})
|
|
95
163
|
}
|
|
96
164
|
|
|
97
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Returns `true` if the socket is connected and the channel has been joined.
|
|
167
|
+
*/
|
|
168
|
+
canPush(): boolean {
|
|
98
169
|
return this.socket.isConnected() && this.isJoined()
|
|
99
170
|
}
|
|
100
171
|
|
|
@@ -118,7 +189,7 @@ export default class RealtimeSubscription {
|
|
|
118
189
|
}
|
|
119
190
|
|
|
120
191
|
/**
|
|
121
|
-
* Leaves the channel
|
|
192
|
+
* Leaves the channel.
|
|
122
193
|
*
|
|
123
194
|
* Unsubscribes from server events, and instructs channel to terminate on server.
|
|
124
195
|
* Triggers onClose() hooks.
|
|
@@ -126,16 +197,16 @@ export default class RealtimeSubscription {
|
|
|
126
197
|
* To receive leave acknowledgements, use the a `receive` hook to bind to the server ack, ie:
|
|
127
198
|
* channel.unsubscribe().receive("ok", () => alert("left!") )
|
|
128
199
|
*/
|
|
129
|
-
unsubscribe(timeout = this.timeout) {
|
|
200
|
+
unsubscribe(timeout = this.timeout): Push {
|
|
130
201
|
this.state = CHANNEL_STATES.leaving
|
|
131
|
-
|
|
202
|
+
const onClose = () => {
|
|
132
203
|
this.socket.log('channel', `leave ${this.topic}`)
|
|
133
204
|
this.trigger(CHANNEL_EVENTS.close, 'leave', this.joinRef())
|
|
134
205
|
}
|
|
135
206
|
// Destroy joinPush to avoid connection timeouts during unscription phase
|
|
136
207
|
this.joinPush.destroy()
|
|
137
208
|
|
|
138
|
-
|
|
209
|
+
const leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout)
|
|
139
210
|
leavePush.receive('ok', () => onClose()).receive('timeout', () => onClose())
|
|
140
211
|
leavePush.send()
|
|
141
212
|
if (!this.canPush()) {
|
|
@@ -155,15 +226,15 @@ export default class RealtimeSubscription {
|
|
|
155
226
|
return payload
|
|
156
227
|
}
|
|
157
228
|
|
|
158
|
-
isMember(topic: string) {
|
|
229
|
+
isMember(topic: string): boolean {
|
|
159
230
|
return this.topic === topic
|
|
160
231
|
}
|
|
161
232
|
|
|
162
|
-
joinRef() {
|
|
233
|
+
joinRef(): string {
|
|
163
234
|
return this.joinPush.ref
|
|
164
235
|
}
|
|
165
236
|
|
|
166
|
-
rejoin(timeout = this.timeout) {
|
|
237
|
+
rejoin(timeout = this.timeout): void {
|
|
167
238
|
if (this.isLeaving()) {
|
|
168
239
|
return
|
|
169
240
|
}
|
|
@@ -172,46 +243,78 @@ export default class RealtimeSubscription {
|
|
|
172
243
|
this.joinPush.resend(timeout)
|
|
173
244
|
}
|
|
174
245
|
|
|
175
|
-
trigger(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (ref && events.indexOf(
|
|
246
|
+
trigger(type: string, payload?: any, ref?: string) {
|
|
247
|
+
const { close, error, leave, join } = CHANNEL_EVENTS
|
|
248
|
+
const events: string[] = [close, error, leave, join]
|
|
249
|
+
if (ref && events.indexOf(type) >= 0 && ref !== this.joinRef()) {
|
|
179
250
|
return
|
|
180
251
|
}
|
|
181
|
-
|
|
252
|
+
const handledPayload = this.onMessage(type, payload, ref)
|
|
182
253
|
if (payload && !handledPayload) {
|
|
183
254
|
throw 'channel onMessage callbacks must return the payload, modified or unmodified'
|
|
184
255
|
}
|
|
185
256
|
|
|
186
257
|
this.bindings
|
|
187
258
|
.filter((bind) => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
259
|
+
return (
|
|
260
|
+
bind?.type === type &&
|
|
261
|
+
(bind?.filter?.event === '*' ||
|
|
262
|
+
bind?.filter?.event === payload?.event)
|
|
263
|
+
)
|
|
194
264
|
})
|
|
195
265
|
.map((bind) => bind.callback(handledPayload, ref))
|
|
196
266
|
}
|
|
197
267
|
|
|
198
|
-
|
|
268
|
+
send(
|
|
269
|
+
payload: { type: string; [key: string]: any },
|
|
270
|
+
opts: { [key: string]: any } = {}
|
|
271
|
+
) {
|
|
272
|
+
const push = this.push(
|
|
273
|
+
payload.type as any,
|
|
274
|
+
payload,
|
|
275
|
+
opts.timeout ?? this.timeout
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return new Promise((resolve) => {
|
|
279
|
+
push.receive('ok', () => resolve('ok'))
|
|
280
|
+
push.receive('timeout', () => resolve('timeout'))
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
replyEventName(ref: string): string {
|
|
199
285
|
return `chan_reply_${ref}`
|
|
200
286
|
}
|
|
201
287
|
|
|
202
|
-
isClosed() {
|
|
288
|
+
isClosed(): boolean {
|
|
203
289
|
return this.state === CHANNEL_STATES.closed
|
|
204
290
|
}
|
|
205
|
-
isErrored() {
|
|
291
|
+
isErrored(): boolean {
|
|
206
292
|
return this.state === CHANNEL_STATES.errored
|
|
207
293
|
}
|
|
208
|
-
isJoined() {
|
|
294
|
+
isJoined(): boolean {
|
|
209
295
|
return this.state === CHANNEL_STATES.joined
|
|
210
296
|
}
|
|
211
|
-
isJoining() {
|
|
297
|
+
isJoining(): boolean {
|
|
212
298
|
return this.state === CHANNEL_STATES.joining
|
|
213
299
|
}
|
|
214
|
-
isLeaving() {
|
|
300
|
+
isLeaving(): boolean {
|
|
215
301
|
return this.state === CHANNEL_STATES.leaving
|
|
216
302
|
}
|
|
303
|
+
|
|
304
|
+
private static isEqual(
|
|
305
|
+
obj1: { [key: string]: string },
|
|
306
|
+
obj2: { [key: string]: string }
|
|
307
|
+
) {
|
|
308
|
+
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
|
|
309
|
+
return false
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (const k in obj1) {
|
|
313
|
+
if (obj1[k] !== obj2[k]) {
|
|
314
|
+
return false
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return true
|
|
319
|
+
}
|
|
217
320
|
}
|
package/src/RealtimeClient.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from './lib/constants'
|
|
12
12
|
import Timer from './lib/timer'
|
|
13
13
|
import Serializer from './lib/serializer'
|
|
14
|
-
import
|
|
14
|
+
import RealtimeChannel from './RealtimeChannel'
|
|
15
15
|
|
|
16
16
|
export type Options = {
|
|
17
17
|
transport?: WebSocket
|
|
@@ -41,7 +41,7 @@ const noop = () => {}
|
|
|
41
41
|
|
|
42
42
|
export default class RealtimeClient {
|
|
43
43
|
accessToken: string | null = null
|
|
44
|
-
channels:
|
|
44
|
+
channels: RealtimeChannel[] = []
|
|
45
45
|
endPoint: string = ''
|
|
46
46
|
headers?: { [key: string]: string } = DEFAULT_HEADERS
|
|
47
47
|
params?: { [key: string]: string } = {}
|
|
@@ -255,14 +255,43 @@ export default class RealtimeClient {
|
|
|
255
255
|
*
|
|
256
256
|
* @param channel An open subscription.
|
|
257
257
|
*/
|
|
258
|
-
remove(channel:
|
|
258
|
+
remove(channel: RealtimeChannel) {
|
|
259
259
|
this.channels = this.channels.filter(
|
|
260
|
-
(c:
|
|
260
|
+
(c: RealtimeChannel) => c.joinRef() !== channel.joinRef()
|
|
261
261
|
)
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
channel(topic: string, chanParams: ChannelParams = {}):
|
|
265
|
-
const
|
|
264
|
+
channel(topic: string, chanParams: ChannelParams = {}): RealtimeChannel {
|
|
265
|
+
const { selfBroadcast, ...params } = chanParams
|
|
266
|
+
|
|
267
|
+
if (selfBroadcast) {
|
|
268
|
+
params.self_broadcast = selfBroadcast
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const chan = new RealtimeChannel(topic, params, this)
|
|
272
|
+
|
|
273
|
+
chan.presence.onJoin((key, currentPresences, newPresences) => {
|
|
274
|
+
chan.trigger('presence', {
|
|
275
|
+
event: 'join',
|
|
276
|
+
key,
|
|
277
|
+
currentPresences,
|
|
278
|
+
newPresences,
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
chan.presence.onLeave((key, currentPresences, leftPresences) => {
|
|
283
|
+
chan.trigger('presence', {
|
|
284
|
+
event: 'leave',
|
|
285
|
+
key,
|
|
286
|
+
currentPresences,
|
|
287
|
+
leftPresences,
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
chan.presence.onSync(() => {
|
|
292
|
+
chan.trigger('presence', { event: 'sync' })
|
|
293
|
+
})
|
|
294
|
+
|
|
266
295
|
this.channels.push(chan)
|
|
267
296
|
return chan
|
|
268
297
|
}
|
|
@@ -306,8 +335,8 @@ export default class RealtimeClient {
|
|
|
306
335
|
payload
|
|
307
336
|
)
|
|
308
337
|
this.channels
|
|
309
|
-
.filter((channel:
|
|
310
|
-
.forEach((channel:
|
|
338
|
+
.filter((channel: RealtimeChannel) => channel.isMember(topic))
|
|
339
|
+
.forEach((channel: RealtimeChannel) =>
|
|
311
340
|
channel.trigger(event, payload, ref)
|
|
312
341
|
)
|
|
313
342
|
this.stateChangeCallbacks.message.forEach((callback) => callback(msg))
|
|
@@ -395,7 +424,7 @@ export default class RealtimeClient {
|
|
|
395
424
|
}
|
|
396
425
|
|
|
397
426
|
private _triggerChanError() {
|
|
398
|
-
this.channels.forEach((channel:
|
|
427
|
+
this.channels.forEach((channel: RealtimeChannel) =>
|
|
399
428
|
channel.trigger(CHANNEL_EVENTS.error)
|
|
400
429
|
)
|
|
401
430
|
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file draws heavily from https://github.com/phoenixframework/phoenix/blob/d344ec0a732ab4ee204215b31de69cf4be72e3bf/assets/js/phoenix/presence.js
|
|
3
|
+
License: https://github.com/phoenixframework/phoenix/blob/d344ec0a732ab4ee204215b31de69cf4be72e3bf/LICENSE.md
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
PresenceOpts,
|
|
8
|
+
PresenceOnJoinCallback,
|
|
9
|
+
PresenceOnLeaveCallback,
|
|
10
|
+
} from 'phoenix'
|
|
11
|
+
import RealtimeChannel from './RealtimeChannel'
|
|
12
|
+
|
|
13
|
+
type Presence = {
|
|
14
|
+
presence_id: string
|
|
15
|
+
[key: string]: any
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PresenceState = { [key: string]: Presence[] }
|
|
19
|
+
|
|
20
|
+
type PresenceDiff = {
|
|
21
|
+
joins: PresenceState
|
|
22
|
+
leaves: PresenceState
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type RawPresenceState = {
|
|
26
|
+
[key: string]: Record<
|
|
27
|
+
'metas',
|
|
28
|
+
{
|
|
29
|
+
phx_ref?: string
|
|
30
|
+
phx_ref_prev?: string
|
|
31
|
+
[key: string]: any
|
|
32
|
+
}[]
|
|
33
|
+
>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type RawPresenceDiff = {
|
|
37
|
+
joins: RawPresenceState
|
|
38
|
+
leaves: RawPresenceState
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type PresenceChooser<T> = (key: string, presences: any) => T
|
|
42
|
+
|
|
43
|
+
export default class RealtimePresence {
|
|
44
|
+
state: PresenceState = {}
|
|
45
|
+
pendingDiffs: RawPresenceDiff[] = []
|
|
46
|
+
joinRef: string | null = null
|
|
47
|
+
caller: {
|
|
48
|
+
onJoin: PresenceOnJoinCallback
|
|
49
|
+
onLeave: PresenceOnLeaveCallback
|
|
50
|
+
onSync: () => void
|
|
51
|
+
} = {
|
|
52
|
+
onJoin: () => {},
|
|
53
|
+
onLeave: () => {},
|
|
54
|
+
onSync: () => {},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Initializes the Presence.
|
|
59
|
+
*
|
|
60
|
+
* @param channel - The RealtimeChannel
|
|
61
|
+
* @param opts - The options,
|
|
62
|
+
* for example `{events: {state: 'state', diff: 'diff'}}`
|
|
63
|
+
*/
|
|
64
|
+
constructor(public channel: RealtimeChannel, opts?: PresenceOpts) {
|
|
65
|
+
const events = opts?.events || {
|
|
66
|
+
state: 'presence_state',
|
|
67
|
+
diff: 'presence_diff',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.channel.on(events.state, {}, (newState: RawPresenceState) => {
|
|
71
|
+
const { onJoin, onLeave, onSync } = this.caller
|
|
72
|
+
|
|
73
|
+
this.joinRef = this.channel.joinRef()
|
|
74
|
+
|
|
75
|
+
this.state = RealtimePresence.syncState(
|
|
76
|
+
this.state,
|
|
77
|
+
newState,
|
|
78
|
+
onJoin,
|
|
79
|
+
onLeave
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
this.pendingDiffs.forEach((diff) => {
|
|
83
|
+
this.state = RealtimePresence.syncDiff(
|
|
84
|
+
this.state,
|
|
85
|
+
diff,
|
|
86
|
+
onJoin,
|
|
87
|
+
onLeave
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
this.pendingDiffs = []
|
|
92
|
+
|
|
93
|
+
onSync()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
this.channel.on(events.diff, {}, (diff: RawPresenceDiff) => {
|
|
97
|
+
const { onJoin, onLeave, onSync } = this.caller
|
|
98
|
+
|
|
99
|
+
if (this.inPendingSyncState()) {
|
|
100
|
+
this.pendingDiffs.push(diff)
|
|
101
|
+
} else {
|
|
102
|
+
this.state = RealtimePresence.syncDiff(
|
|
103
|
+
this.state,
|
|
104
|
+
diff,
|
|
105
|
+
onJoin,
|
|
106
|
+
onLeave
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
onSync()
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Used to sync the list of presences on the server with the
|
|
116
|
+
* client's state.
|
|
117
|
+
*
|
|
118
|
+
* An optional `onJoin` and `onLeave` callback can be provided to
|
|
119
|
+
* react to changes in the client's local presences across
|
|
120
|
+
* disconnects and reconnects with the server.
|
|
121
|
+
*/
|
|
122
|
+
static syncState(
|
|
123
|
+
currentState: PresenceState,
|
|
124
|
+
newState: RawPresenceState | PresenceState,
|
|
125
|
+
onJoin: PresenceOnJoinCallback,
|
|
126
|
+
onLeave: PresenceOnLeaveCallback
|
|
127
|
+
): PresenceState {
|
|
128
|
+
const state = this.cloneDeep(currentState)
|
|
129
|
+
const transformedState = this.transformState(newState)
|
|
130
|
+
const joins: PresenceState = {}
|
|
131
|
+
const leaves: PresenceState = {}
|
|
132
|
+
|
|
133
|
+
this.map(state, (key: string, presences: Presence[]) => {
|
|
134
|
+
if (!transformedState[key]) {
|
|
135
|
+
leaves[key] = presences
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
this.map(transformedState, (key, newPresences: Presence[]) => {
|
|
140
|
+
const currentPresences: Presence[] = state[key]
|
|
141
|
+
|
|
142
|
+
if (currentPresences) {
|
|
143
|
+
const newPresenceIds = newPresences.map((m: Presence) => m.presence_id)
|
|
144
|
+
const curPresenceIds = currentPresences.map(
|
|
145
|
+
(m: Presence) => m.presence_id
|
|
146
|
+
)
|
|
147
|
+
const joinedPresences: Presence[] = newPresences.filter(
|
|
148
|
+
(m: Presence) => curPresenceIds.indexOf(m.presence_id) < 0
|
|
149
|
+
)
|
|
150
|
+
const leftPresences: Presence[] = currentPresences.filter(
|
|
151
|
+
(m: Presence) => newPresenceIds.indexOf(m.presence_id) < 0
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if (joinedPresences.length > 0) {
|
|
155
|
+
joins[key] = joinedPresences
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (leftPresences.length > 0) {
|
|
159
|
+
leaves[key] = leftPresences
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
joins[key] = newPresences
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
return this.syncDiff(state, { joins, leaves }, onJoin, onLeave)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Used to sync a diff of presence join and leave events from the
|
|
171
|
+
* server, as they happen.
|
|
172
|
+
*
|
|
173
|
+
* Like `syncState`, `syncDiff` accepts optional `onJoin` and
|
|
174
|
+
* `onLeave` callbacks to react to a user joining or leaving from a
|
|
175
|
+
* device.
|
|
176
|
+
*/
|
|
177
|
+
static syncDiff(
|
|
178
|
+
state: PresenceState,
|
|
179
|
+
diff: RawPresenceDiff | PresenceDiff,
|
|
180
|
+
onJoin: PresenceOnJoinCallback,
|
|
181
|
+
onLeave: PresenceOnLeaveCallback
|
|
182
|
+
): PresenceState {
|
|
183
|
+
const { joins, leaves } = {
|
|
184
|
+
joins: this.transformState(diff.joins),
|
|
185
|
+
leaves: this.transformState(diff.leaves),
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!onJoin) {
|
|
189
|
+
onJoin = () => {}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!onLeave) {
|
|
193
|
+
onLeave = () => {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.map(joins, (key, newPresences: Presence[]) => {
|
|
197
|
+
const currentPresences: Presence[] = state[key]
|
|
198
|
+
state[key] = this.cloneDeep(newPresences)
|
|
199
|
+
|
|
200
|
+
if (currentPresences) {
|
|
201
|
+
const joinedPresenceIds = state[key].map((m: Presence) => m.presence_id)
|
|
202
|
+
const curPresences: Presence[] = currentPresences.filter(
|
|
203
|
+
(m: Presence) => joinedPresenceIds.indexOf(m.presence_id) < 0
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
state[key].unshift(...curPresences)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
onJoin(key, currentPresences, newPresences)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
this.map(leaves, (key, leftPresences: Presence[]) => {
|
|
213
|
+
let currentPresences: Presence[] = state[key]
|
|
214
|
+
|
|
215
|
+
if (!currentPresences) return
|
|
216
|
+
|
|
217
|
+
const presenceIdsToRemove = leftPresences.map(
|
|
218
|
+
(m: Presence) => m.presence_id
|
|
219
|
+
)
|
|
220
|
+
currentPresences = currentPresences.filter(
|
|
221
|
+
(m: Presence) => presenceIdsToRemove.indexOf(m.presence_id) < 0
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
state[key] = currentPresences
|
|
225
|
+
|
|
226
|
+
onLeave(key, currentPresences, leftPresences)
|
|
227
|
+
|
|
228
|
+
if (currentPresences.length === 0) delete state[key]
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
return state
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Returns the array of presences, with selected metadata.
|
|
236
|
+
*/
|
|
237
|
+
static list<T = any>(
|
|
238
|
+
presences: PresenceState,
|
|
239
|
+
chooser: PresenceChooser<T> | undefined
|
|
240
|
+
): T[] {
|
|
241
|
+
if (!chooser) {
|
|
242
|
+
chooser = (_key, pres) => pres
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return this.map(presences, (key, presences: Presence[]) =>
|
|
246
|
+
chooser!(key, presences)
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private static map<T = any>(
|
|
251
|
+
obj: PresenceState,
|
|
252
|
+
func: PresenceChooser<T>
|
|
253
|
+
): T[] {
|
|
254
|
+
return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key]))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Remove 'metas' key
|
|
259
|
+
* Change 'phx_ref' to 'presence_id'
|
|
260
|
+
* Remove 'phx_ref' and 'phx_ref_prev'
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* // returns {
|
|
264
|
+
* abc123: [
|
|
265
|
+
* { presence_id: '2', user_id: 1 },
|
|
266
|
+
* { presence_id: '3', user_id: 2 }
|
|
267
|
+
* ]
|
|
268
|
+
* }
|
|
269
|
+
* RealtimePresence.transformState({
|
|
270
|
+
* abc123: {
|
|
271
|
+
* metas: [
|
|
272
|
+
* { phx_ref: '2', phx_ref_prev: '1' user_id: 1 },
|
|
273
|
+
* { phx_ref: '3', user_id: 2 }
|
|
274
|
+
* ]
|
|
275
|
+
* }
|
|
276
|
+
* })
|
|
277
|
+
*/
|
|
278
|
+
private static transformState(
|
|
279
|
+
state: RawPresenceState | PresenceState
|
|
280
|
+
): PresenceState {
|
|
281
|
+
state = this.cloneDeep(state)
|
|
282
|
+
|
|
283
|
+
return Object.getOwnPropertyNames(state).reduce((newState, key) => {
|
|
284
|
+
const presences = state[key]
|
|
285
|
+
|
|
286
|
+
if ('metas' in presences) {
|
|
287
|
+
newState[key] = presences.metas.map((presence) => {
|
|
288
|
+
presence['presence_id'] = presence['phx_ref']
|
|
289
|
+
|
|
290
|
+
delete presence['phx_ref']
|
|
291
|
+
delete presence['phx_ref_prev']
|
|
292
|
+
|
|
293
|
+
return presence
|
|
294
|
+
}) as Presence[]
|
|
295
|
+
} else {
|
|
296
|
+
newState[key] = presences
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return newState
|
|
300
|
+
}, {} as PresenceState)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private static cloneDeep(obj: object) {
|
|
304
|
+
return JSON.parse(JSON.stringify(obj))
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
onJoin(callback: PresenceOnJoinCallback): void {
|
|
308
|
+
this.caller.onJoin = callback
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
onLeave(callback: PresenceOnLeaveCallback): void {
|
|
312
|
+
this.caller.onLeave = callback
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
onSync(callback: () => void): void {
|
|
316
|
+
this.caller.onSync = callback
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
list<T = any>(by?: PresenceChooser<T>): T[] {
|
|
320
|
+
return RealtimePresence.list<T>(this.state, by)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private inPendingSyncState(): boolean {
|
|
324
|
+
return !this.joinRef || this.joinRef !== this.channel.joinRef()
|
|
325
|
+
}
|
|
326
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,13 @@ import * as Transformers from './lib/transformers'
|
|
|
2
2
|
import RealtimeClient, {
|
|
3
3
|
Options as RealtimeClientOptions,
|
|
4
4
|
} from './RealtimeClient'
|
|
5
|
-
import
|
|
5
|
+
import RealtimeChannel from './RealtimeChannel'
|
|
6
|
+
import RealtimePresence from './RealtimePresence'
|
|
6
7
|
|
|
7
8
|
export {
|
|
8
9
|
RealtimeClient,
|
|
9
10
|
RealtimeClientOptions,
|
|
10
|
-
|
|
11
|
+
RealtimeChannel,
|
|
12
|
+
RealtimePresence,
|
|
11
13
|
Transformers,
|
|
12
14
|
}
|