@wordpress/core-data
Version:
Access to and manipulation of core WordPress entities.
243 lines (242 loc) • 7.56 kB
JavaScript
// packages/core-data/src/awareness/awareness-state.ts
import { REMOVAL_DELAY_IN_MS } from "./config.mjs";
import { TypedAwareness } from "./typed-awareness.mjs";
import { getTypedKeys, areMapsEqual } from "./utils.mjs";
var AwarenessWithEqualityChecks = class extends TypedAwareness {
/** OVERRIDDEN METHODS */
/**
* Set a local state field on an awareness document. Calling this method may
* trigger rerenders of any subscribed components.
*
* Equality checks are provided by the abstract `equalityFieldChecks` property.
* @param field - The field to set.
* @param value - The value to set.
*/
setLocalStateField(field, value) {
if (this.isFieldEqual(
field,
value,
this.getLocalStateField(field) ?? void 0
)) {
return;
}
super.setLocalStateField(field, value);
}
/** CUSTOM METHODS */
/**
* Determine if a field value has changed using the provided equality checks.
* @param field - The field to check.
* @param value1 - The first value to compare.
* @param value2 - The second value to compare.
*/
isFieldEqual(field, value1, value2) {
if (["clientId", "isConnected", "isMe"].includes(field)) {
return value1 === value2;
}
if (field in this.equalityFieldChecks) {
const fn = this.equalityFieldChecks[field];
return fn(value1, value2);
}
throw new Error(
`No equality check implemented for awareness state field "${field.toString()}".`
);
}
/**
* Determine if two states are equal by comparing each field using the
* provided equality checks.
* @param state1 - The first state to compare.
* @param state2 - The second state to compare.
*/
isStateEqual(state1, state2) {
return [
.../* @__PURE__ */ new Set([
...getTypedKeys(state1),
...getTypedKeys(state2)
])
].every((field) => {
const value1 = state1[field];
const value2 = state2[field];
return this.isFieldEqual(field, value1, value2);
});
}
};
var AwarenessState = class extends AwarenessWithEqualityChecks {
/** CUSTOM PROPERTIES */
/**
* Whether the setUp method has been called, to avoid running it multiple
* times.
*/
hasSetupRun = false;
/**
* We keep track of all seen states during the current session for two reasons:
*
* 1. So that we can represent recently disconnected collaborators in our UI, even
* after they have been removed from the awareness document.
* 2. So that we can provide debug information about all collaborators seen during
* the session.
*/
disconnectedCollaborators = /* @__PURE__ */ new Set();
seenStates = /* @__PURE__ */ new Map();
/**
* Hold a snapshot of the previous awareness state allows us to compare the
* state values and avoid unnecessary updates to subscribers.
*/
previousSnapshot = /* @__PURE__ */ new Map();
stateSubscriptions = [];
/**
* In some cases, we may want to throttle setting local state fields to avoid
* overwhelming the awareness document with rapid updates. At the same time, we
* want to ensure that when we read our own state locally, we get the latest
* value -- even if it hasn't yet been set on the awareness instance.
*/
myThrottledState = {};
throttleTimeouts = /* @__PURE__ */ new Map();
/** CUSTOM METHODS */
/**
* Set up the awareness state. This method is idempotent and will only run
* once. Subclasses should override `onSetUp()` instead of this method to
* add their own setup logic.
*
* This is defined as a readonly arrow function property to prevent
* subclasses from overriding it.
*/
setUp = () => {
if (this.hasSetupRun) {
return;
}
this.hasSetupRun = true;
this.onSetUp();
this.on(
"change",
({ added, removed, updated }) => {
[...added, ...updated].forEach((id) => {
this.disconnectedCollaborators.delete(id);
});
removed.forEach((id) => {
this.disconnectedCollaborators.add(id);
setTimeout(() => {
this.disconnectedCollaborators.delete(id);
this.updateSubscribers(
true
/* force update */
);
}, REMOVAL_DELAY_IN_MS);
});
this.updateSubscribers();
}
);
};
/**
* Get the most recent state from the last processed change event.
*
* @return An array of EnhancedState< State >.
*/
getCurrentState() {
return Array.from(this.previousSnapshot.values());
}
/**
* Get all seen states in this session to enable debug reporting.
*/
getSeenStates() {
return this.seenStates;
}
/**
* Allow external code to subscribe to awareness state changes.
* @param callback - The callback to subscribe to.
*/
onStateChange(callback) {
this.stateSubscriptions.push(callback);
return () => {
this.stateSubscriptions = this.stateSubscriptions.filter(
(cb) => cb !== callback
);
};
}
/**
* Set a local state field on an awareness document with throttle. See caveats
* of this.setLocalStateField.
* @param field - The field to set.
* @param value - The value to set.
* @param wait - The wait time in milliseconds.
*/
setThrottledLocalStateField(field, value, wait) {
this.setLocalStateField(field, value);
this.throttleTimeouts.set(
field,
setTimeout(() => {
this.throttleTimeouts.delete(field);
if (this.myThrottledState[field]) {
this.setLocalStateField(
field,
this.myThrottledState[field]
);
delete this.myThrottledState[field];
}
}, wait)
);
}
/**
* Set the current collaborator's connection status as awareness state.
* @param isConnected - The connection status.
*/
setConnectionStatus(isConnected) {
if (isConnected) {
this.disconnectedCollaborators.delete(this.clientID);
} else {
this.disconnectedCollaborators.add(this.clientID);
}
this.updateSubscribers(
true
/* force update */
);
}
/**
* Update all subscribed listeners with the latest awareness state.
* @param forceUpdate - Whether to force an update.
*/
updateSubscribers(forceUpdate = false) {
if (!this.stateSubscriptions.length) {
return;
}
const states = this.getStates();
this.seenStates = new Map([
...this.seenStates.entries(),
...states.entries()
]);
const updatedStates = new Map(
[...this.disconnectedCollaborators, ...states.keys()].filter((clientId) => {
return Object.keys(this.seenStates.get(clientId) ?? {}).length > 0;
}).map((clientId) => {
const rawState = this.seenStates.get(clientId);
const isConnected = !this.disconnectedCollaborators.has(clientId);
const isMe = clientId === this.clientID;
const myState = isMe ? this.myThrottledState : {};
const state = {
...rawState,
...myState,
clientId,
isConnected,
isMe
};
return [clientId, state];
})
);
if (!forceUpdate) {
if (areMapsEqual(
this.previousSnapshot,
updatedStates,
this.isStateEqual.bind(this)
)) {
return;
}
}
this.previousSnapshot = updatedStates;
this.stateSubscriptions.forEach((callback) => {
callback(Array.from(updatedStates.values()));
});
}
};
export {
AwarenessState
};
//# sourceMappingURL=awareness-state.mjs.map