224 lines
8.0 KiB
JavaScript
224 lines
8.0 KiB
JavaScript
/*
|
|
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
|