Spaces:
Running
Running
| /* | |
| This file draws heavily from https://github.com/phoenixframework/phoenix/blob/d344ec0a732ab4ee204215b31de69cf4be72e3bf/assets/js/phoenix/presence.js | |
| License: https://github.com/phoenixframework/phoenix/blob/d344ec0a732ab4ee204215b31de69cf4be72e3bf/LICENSE.md | |
| */ | |
| export var REALTIME_PRESENCE_LISTEN_EVENTS; | |
| (function (REALTIME_PRESENCE_LISTEN_EVENTS) { | |
| REALTIME_PRESENCE_LISTEN_EVENTS["SYNC"] = "sync"; | |
| REALTIME_PRESENCE_LISTEN_EVENTS["JOIN"] = "join"; | |
| REALTIME_PRESENCE_LISTEN_EVENTS["LEAVE"] = "leave"; | |
| })(REALTIME_PRESENCE_LISTEN_EVENTS || (REALTIME_PRESENCE_LISTEN_EVENTS = {})); | |
| export default class RealtimePresence { | |
| /** | |
| * Initializes the Presence. | |
| * | |
| * @param channel - The RealtimeChannel | |
| * @param opts - The options, | |
| * for example `{events: {state: 'state', diff: 'diff'}}` | |
| */ | |
| constructor(channel, opts) { | |
| this.channel = channel; | |
| this.state = {}; | |
| this.pendingDiffs = []; | |
| this.joinRef = null; | |
| this.caller = { | |
| onJoin: () => { }, | |
| onLeave: () => { }, | |
| onSync: () => { }, | |
| }; | |
| const events = (opts === null || opts === void 0 ? void 0 : opts.events) || { | |
| state: 'presence_state', | |
| diff: 'presence_diff', | |
| }; | |
| this.channel._on(events.state, {}, (newState) => { | |
| const { onJoin, onLeave, onSync } = this.caller; | |
| this.joinRef = this.channel._joinRef(); | |
| this.state = RealtimePresence.syncState(this.state, newState, onJoin, onLeave); | |
| this.pendingDiffs.forEach((diff) => { | |
| this.state = RealtimePresence.syncDiff(this.state, diff, onJoin, onLeave); | |
| }); | |
| this.pendingDiffs = []; | |
| onSync(); | |
| }); | |
| this.channel._on(events.diff, {}, (diff) => { | |
| const { onJoin, onLeave, onSync } = this.caller; | |
| if (this.inPendingSyncState()) { | |
| this.pendingDiffs.push(diff); | |
| } | |
| else { | |
| this.state = RealtimePresence.syncDiff(this.state, diff, onJoin, onLeave); | |
| onSync(); | |
| } | |
| }); | |
| this.onJoin((key, currentPresences, newPresences) => { | |
| this.channel._trigger('presence', { | |
| event: 'join', | |
| key, | |
| currentPresences, | |
| newPresences, | |
| }); | |
| }); | |
| this.onLeave((key, currentPresences, leftPresences) => { | |
| this.channel._trigger('presence', { | |
| event: 'leave', | |
| key, | |
| currentPresences, | |
| leftPresences, | |
| }); | |
| }); | |
| this.onSync(() => { | |
| this.channel._trigger('presence', { event: 'sync' }); | |
| }); | |
| } | |
| /** | |
| * Used to sync the list of presences on the server with the | |
| * client's state. | |
| * | |
| * An optional `onJoin` and `onLeave` callback can be provided to | |
| * react to changes in the client's local presences across | |
| * disconnects and reconnects with the server. | |
| * | |
| * @internal | |
| */ | |
| static syncState(currentState, newState, onJoin, onLeave) { | |
| const state = this.cloneDeep(currentState); | |
| const transformedState = this.transformState(newState); | |
| const joins = {}; | |
| const leaves = {}; | |
| this.map(state, (key, presences) => { | |
| if (!transformedState[key]) { | |
| leaves[key] = presences; | |
| } | |
| }); | |
| this.map(transformedState, (key, newPresences) => { | |
| const currentPresences = state[key]; | |
| if (currentPresences) { | |
| const newPresenceRefs = newPresences.map((m) => m.presence_ref); | |
| const curPresenceRefs = currentPresences.map((m) => m.presence_ref); | |
| const joinedPresences = newPresences.filter((m) => curPresenceRefs.indexOf(m.presence_ref) < 0); | |
| const leftPresences = currentPresences.filter((m) => newPresenceRefs.indexOf(m.presence_ref) < 0); | |
| if (joinedPresences.length > 0) { | |
| joins[key] = joinedPresences; | |
| } | |
| if (leftPresences.length > 0) { | |
| leaves[key] = leftPresences; | |
| } | |
| } | |
| else { | |
| joins[key] = newPresences; | |
| } | |
| }); | |
| return this.syncDiff(state, { joins, leaves }, onJoin, onLeave); | |
| } | |
| /** | |
| * Used to sync a diff of presence join and leave events from the | |
| * server, as they happen. | |
| * | |
| * Like `syncState`, `syncDiff` accepts optional `onJoin` and | |
| * `onLeave` callbacks to react to a user joining or leaving from a | |
| * device. | |
| * | |
| * @internal | |
| */ | |
| static syncDiff(state, diff, onJoin, onLeave) { | |
| const { joins, leaves } = { | |
| joins: this.transformState(diff.joins), | |
| leaves: this.transformState(diff.leaves), | |
| }; | |
| if (!onJoin) { | |
| onJoin = () => { }; | |
| } | |
| if (!onLeave) { | |
| onLeave = () => { }; | |
| } | |
| this.map(joins, (key, newPresences) => { | |
| var _a; | |
| const currentPresences = (_a = state[key]) !== null && _a !== void 0 ? _a : []; | |
| state[key] = this.cloneDeep(newPresences); | |
| if (currentPresences.length > 0) { | |
| const joinedPresenceRefs = state[key].map((m) => m.presence_ref); | |
| const curPresences = currentPresences.filter((m) => joinedPresenceRefs.indexOf(m.presence_ref) < 0); | |
| state[key].unshift(...curPresences); | |
| } | |
| onJoin(key, currentPresences, newPresences); | |
| }); | |
| this.map(leaves, (key, leftPresences) => { | |
| let currentPresences = state[key]; | |
| if (!currentPresences) | |
| return; | |
| const presenceRefsToRemove = leftPresences.map((m) => m.presence_ref); | |
| currentPresences = currentPresences.filter((m) => presenceRefsToRemove.indexOf(m.presence_ref) < 0); | |
| state[key] = currentPresences; | |
| onLeave(key, currentPresences, leftPresences); | |
| if (currentPresences.length === 0) | |
| delete state[key]; | |
| }); | |
| return state; | |
| } | |
| /** @internal */ | |
| static map(obj, func) { | |
| return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key])); | |
| } | |
| /** | |
| * Remove 'metas' key | |
| * Change 'phx_ref' to 'presence_ref' | |
| * Remove 'phx_ref' and 'phx_ref_prev' | |
| * | |
| * @example | |
| * // returns { | |
| * abc123: [ | |
| * { presence_ref: '2', user_id: 1 }, | |
| * { presence_ref: '3', user_id: 2 } | |
| * ] | |
| * } | |
| * RealtimePresence.transformState({ | |
| * abc123: { | |
| * metas: [ | |
| * { phx_ref: '2', phx_ref_prev: '1' user_id: 1 }, | |
| * { phx_ref: '3', user_id: 2 } | |
| * ] | |
| * } | |
| * }) | |
| * | |
| * @internal | |
| */ | |
| static transformState(state) { | |
| state = this.cloneDeep(state); | |
| return Object.getOwnPropertyNames(state).reduce((newState, key) => { | |
| const presences = state[key]; | |
| if ('metas' in presences) { | |
| newState[key] = presences.metas.map((presence) => { | |
| presence['presence_ref'] = presence['phx_ref']; | |
| delete presence['phx_ref']; | |
| delete presence['phx_ref_prev']; | |
| return presence; | |
| }); | |
| } | |
| else { | |
| newState[key] = presences; | |
| } | |
| return newState; | |
| }, {}); | |
| } | |
| /** @internal */ | |
| static cloneDeep(obj) { | |
| return JSON.parse(JSON.stringify(obj)); | |
| } | |
| /** @internal */ | |
| onJoin(callback) { | |
| this.caller.onJoin = callback; | |
| } | |
| /** @internal */ | |
| onLeave(callback) { | |
| this.caller.onLeave = callback; | |
| } | |
| /** @internal */ | |
| onSync(callback) { | |
| this.caller.onSync = callback; | |
| } | |
| /** @internal */ | |
| inPendingSyncState() { | |
| return !this.joinRef || this.joinRef !== this.channel._joinRef(); | |
| } | |
| } | |
| //# sourceMappingURL=RealtimePresence.js.map |