@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.
Files changed (55) hide show
  1. package/dist/main/{RealtimeSubscription.d.ts → RealtimeChannel.d.ts} +37 -6
  2. package/dist/main/RealtimeChannel.d.ts.map +1 -0
  3. package/dist/main/{RealtimeSubscription.js → RealtimeChannel.js} +103 -25
  4. package/dist/main/RealtimeChannel.js.map +1 -0
  5. package/dist/main/RealtimeClient.d.ts +4 -4
  6. package/dist/main/RealtimeClient.d.ts.map +1 -1
  7. package/dist/main/RealtimeClient.js +36 -2
  8. package/dist/main/RealtimeClient.js.map +1 -1
  9. package/dist/main/RealtimePresence.d.ts +97 -0
  10. package/dist/main/RealtimePresence.d.ts.map +1 -0
  11. package/dist/main/RealtimePresence.js +201 -0
  12. package/dist/main/RealtimePresence.js.map +1 -0
  13. package/dist/main/index.d.ts +3 -2
  14. package/dist/main/index.d.ts.map +1 -1
  15. package/dist/main/index.js +5 -3
  16. package/dist/main/index.js.map +1 -1
  17. package/dist/main/lib/push.d.ts +3 -3
  18. package/dist/main/lib/push.d.ts.map +1 -1
  19. package/dist/main/lib/push.js +2 -2
  20. package/dist/main/lib/push.js.map +1 -1
  21. package/dist/main/lib/version.d.ts +1 -1
  22. package/dist/main/lib/version.js +1 -1
  23. package/dist/module/{RealtimeSubscription.d.ts → RealtimeChannel.d.ts} +37 -6
  24. package/dist/module/RealtimeChannel.d.ts.map +1 -0
  25. package/dist/module/{RealtimeSubscription.js → RealtimeChannel.js} +102 -24
  26. package/dist/module/RealtimeChannel.js.map +1 -0
  27. package/dist/module/RealtimeClient.d.ts +4 -4
  28. package/dist/module/RealtimeClient.d.ts.map +1 -1
  29. package/dist/module/RealtimeClient.js +36 -2
  30. package/dist/module/RealtimeClient.js.map +1 -1
  31. package/dist/module/RealtimePresence.d.ts +97 -0
  32. package/dist/module/RealtimePresence.d.ts.map +1 -0
  33. package/dist/module/RealtimePresence.js +198 -0
  34. package/dist/module/RealtimePresence.js.map +1 -0
  35. package/dist/module/index.d.ts +3 -2
  36. package/dist/module/index.d.ts.map +1 -1
  37. package/dist/module/index.js +3 -2
  38. package/dist/module/index.js.map +1 -1
  39. package/dist/module/lib/push.d.ts +3 -3
  40. package/dist/module/lib/push.d.ts.map +1 -1
  41. package/dist/module/lib/push.js +2 -2
  42. package/dist/module/lib/push.js.map +1 -1
  43. package/dist/module/lib/version.d.ts +1 -1
  44. package/dist/module/lib/version.js +1 -1
  45. package/package.json +1 -1
  46. package/src/{RealtimeSubscription.ts → RealtimeChannel.ts} +136 -33
  47. package/src/RealtimeClient.ts +38 -9
  48. package/src/RealtimePresence.ts +326 -0
  49. package/src/index.ts +4 -2
  50. package/src/lib/push.ts +4 -4
  51. package/src/lib/version.ts +1 -1
  52. package/dist/main/RealtimeSubscription.d.ts.map +0 -1
  53. package/dist/main/RealtimeSubscription.js.map +0 -1
  54. package/dist/module/RealtimeSubscription.d.ts.map +0 -1
  55. 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 RealtimeSubscription {
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(event: string, callback: Function) {
90
- this.bindings.push({ event, callback })
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(event: string) {
94
- this.bindings = this.bindings.filter((bind) => bind.event !== event)
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
- canPush() {
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
- let onClose = () => {
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
- let leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout)
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(event: string, payload?: any, ref?: string) {
176
- let { close, error, leave, join } = CHANNEL_EVENTS
177
- let events: string[] = [close, error, leave, join]
178
- if (ref && events.indexOf(event) >= 0 && ref !== this.joinRef()) {
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
- let handledPayload = this.onMessage(event, payload, ref)
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
- // Bind all events if the user specifies a wildcard.
189
- if (bind.event === '*') {
190
- return event === payload?.type
191
- } else {
192
- return bind.event === event
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
- replyEventName(ref: string) {
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
  }
@@ -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 RealtimeSubscription from './RealtimeSubscription'
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: RealtimeSubscription[] = []
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: RealtimeSubscription) {
258
+ remove(channel: RealtimeChannel) {
259
259
  this.channels = this.channels.filter(
260
- (c: RealtimeSubscription) => c.joinRef() !== channel.joinRef()
260
+ (c: RealtimeChannel) => c.joinRef() !== channel.joinRef()
261
261
  )
262
262
  }
263
263
 
264
- channel(topic: string, chanParams: ChannelParams = {}): RealtimeSubscription {
265
- const chan = new RealtimeSubscription(topic, chanParams, this)
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: RealtimeSubscription) => channel.isMember(topic))
310
- .forEach((channel: RealtimeSubscription) =>
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: RealtimeSubscription) =>
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 RealtimeSubscription from './RealtimeSubscription'
5
+ import RealtimeChannel from './RealtimeChannel'
6
+ import RealtimePresence from './RealtimePresence'
6
7
 
7
8
  export {
8
9
  RealtimeClient,
9
10
  RealtimeClientOptions,
10
- RealtimeSubscription,
11
+ RealtimeChannel,
12
+ RealtimePresence,
11
13
  Transformers,
12
14
  }