5ba7717ecd
Critical Fix: - StateStore now uses global Hooks.callAll directly (per spec) - Removed hooks parameter from StateStore constructor - Updated module.js to pass only adapter.settings - Updated tests to stub globalThis.Hooks Minor Cleanup: - Fixed misleading warning in SocketHandler.registerPendingOp - Added clarifying comment for setMatrix _revision behavior Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
82 lines
3.4 KiB
JavaScript
82 lines
3.4 KiB
JavaScript
/**
|
|
* PendingOp contract.
|
|
*
|
|
* A PendingOp tracks an in-flight visibility state change from the moment
|
|
* the GM issues the intent until the authoritative echo is received (or
|
|
* the 3-second timeout fires and triggers a revert).
|
|
*
|
|
* Lifecycle:
|
|
* create → register in Map<opId, PendingOp>
|
|
* echo received → delete from map + clearTimeout(timeoutId)
|
|
* 3s timeout fires → revert to previousState + GM notification
|
|
*
|
|
* @module contracts/pending-op
|
|
*/
|
|
|
|
/** Shape version constant for PendingOp. @type {1} */
|
|
export const PENDING_OP_VERSION = 1;
|
|
|
|
/**
|
|
* @typedef {Object} PendingOp
|
|
* @property {string} opId - Unique operation identifier (non-empty string).
|
|
* @property {string} userId - Target participant userId (non-empty string).
|
|
* @property {string} targetState - Desired VisibilityState (non-empty string).
|
|
* @property {string} previousState - State before this op; used for revert (non-empty string).
|
|
* @property {number} issuedAt - Timestamp (ms) when op was issued — Date.now() integer.
|
|
* @property {number|null} timeoutId - setTimeout handle; null if timeout not yet set.
|
|
*/
|
|
|
|
/**
|
|
* Creates a new PendingOp.
|
|
* @param {string} opId - Unique operation identifier.
|
|
* @param {string} userId - Target participant userId.
|
|
* @param {string} targetState - Desired VisibilityState.
|
|
* @param {string} previousState - State before this op.
|
|
* @param {number} [issuedAt] - Timestamp; defaults to Date.now().
|
|
* @returns {PendingOp}
|
|
*/
|
|
export function createPendingOp(opId, userId, targetState, previousState, issuedAt = Date.now()) {
|
|
return { opId, userId, targetState, previousState, issuedAt, timeoutId: null };
|
|
}
|
|
|
|
/**
|
|
* Validates a PendingOp DTO. Throws TypeError on any violation.
|
|
* @param {unknown} data - Value to validate.
|
|
* @returns {PendingOp} The validated PendingOp.
|
|
* @throws {TypeError} If data fails validation.
|
|
*/
|
|
export function isValidPendingOp(data) {
|
|
if (data === null || typeof data !== "object") {
|
|
throw new TypeError("PendingOp: must be an object");
|
|
}
|
|
const obj = /** @type {Record<string, unknown>} */ (data);
|
|
const { opId, userId, targetState, previousState, issuedAt, timeoutId, ...rest } = obj;
|
|
if (Object.keys(rest).length > 0) {
|
|
throw new TypeError(`PendingOp: unknown keys: ${Object.keys(rest).join(", ")}`);
|
|
}
|
|
if (typeof opId !== "string" || opId.trim().length === 0) {
|
|
throw new TypeError("PendingOp: opId must be a non-empty string");
|
|
}
|
|
if (typeof userId !== "string" || userId.trim().length === 0) {
|
|
throw new TypeError("PendingOp: userId must be a non-empty string");
|
|
}
|
|
if (typeof targetState !== "string" || targetState.trim().length === 0) {
|
|
throw new TypeError("PendingOp: targetState must be a non-empty string");
|
|
}
|
|
if (typeof previousState !== "string" || previousState.trim().length === 0) {
|
|
throw new TypeError("PendingOp: previousState must be a non-empty string");
|
|
}
|
|
if (typeof issuedAt !== "number" || !Number.isFinite(issuedAt) || issuedAt < 0 || !Number.isInteger(issuedAt)) {
|
|
throw new TypeError("PendingOp: issuedAt must be a finite non-negative integer");
|
|
}
|
|
if (timeoutId !== null) {
|
|
if (typeof timeoutId !== "number") {
|
|
throw new TypeError("PendingOp: timeoutId must be a number or null");
|
|
}
|
|
if (!Number.isFinite(timeoutId) || timeoutId < 0) {
|
|
throw new TypeError("PendingOp: timeoutId must be a finite non-negative number");
|
|
}
|
|
}
|
|
return /** @type {PendingOp} */ (data);
|
|
}
|