UNPKG

@supabase/realtime-js

Version:
229 lines 8.36 kB
"use strict"; /* 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 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.REALTIME_PRESENCE_LISTEN_EVENTS = void 0; 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 || (exports.REALTIME_PRESENCE_LISTEN_EVENTS = REALTIME_PRESENCE_LISTEN_EVENTS = {})); 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.enabled = false; 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(); } } exports.default = RealtimePresence; //# sourceMappingURL=RealtimePresence.js.map