/** * Visibility Matrix contract. * * Canonical shape: { _version: 1, matrix: { [userId: string]: VisibilityState } } * where VisibilityState ∈ VISIBILITY_STATES. * * StateStore is the sole writer of this structure. All other modules treat it as read-only. * * @module contracts/visibility-matrix */ /** @typedef {'active'|'hidden'|'self-muted'|'offline'|'cam-lost'|'reconnecting'|'never-connected'|'ghost'} VisibilityState */ /** * @typedef {Object} VisibilityMatrix * @property {1} _version - Schema version; always 1 for v1. * @property {Object.} matrix - userId → VisibilityState map. * @property {number} [_revision] - Optional revision counter for optimistic concurrency control. */ export const VISIBILITY_STATES = Object.freeze([ "active", "hidden", "self-muted", "offline", "cam-lost", "reconnecting", "never-connected", "ghost", ]); export const VISIBILITY_MATRIX_VERSION = 1; /** * Creates a new VisibilityMatrix with an optional initial matrix. * @param {Object.} [matrix={}] - Initial userId→state entries. * @returns {VisibilityMatrix} */ export function createVisibilityMatrix(matrix = {}) { return { _version: VISIBILITY_MATRIX_VERSION, matrix: { ...matrix } }; } /** * Validates a VisibilityMatrix DTO. Throws TypeError on any violation. * @param {unknown} data - Value to validate. * @returns {VisibilityMatrix} The validated matrix. * @throws {TypeError} If data fails validation. */ export function isValidVisibilityMatrix(data) { if (data === null || typeof data !== "object") { throw new TypeError("VisibilityMatrix: must be an object"); } const obj = /** @type {Record} */ (data); const { _version, matrix, _revision, ...rest } = obj; if (Object.keys(rest).length > 0) { throw new TypeError(`VisibilityMatrix: unknown keys: ${Object.keys(rest).join(", ")}`); } if (_version !== VISIBILITY_MATRIX_VERSION) { throw new TypeError(`VisibilityMatrix: _version must be ${VISIBILITY_MATRIX_VERSION}, got ${_version}`); } if (matrix === null || typeof matrix !== "object" || Array.isArray(matrix)) { throw new TypeError("VisibilityMatrix: matrix must be a plain object"); } if (_revision !== undefined && (typeof _revision !== "number" || !Number.isFinite(_revision) || _revision < 0)) { throw new TypeError("VisibilityMatrix: _revision must be a finite non-negative number"); } const matrixObj = /** @type {Record} */ (matrix); for (const [userId, state] of Object.entries(matrixObj)) { if (typeof userId !== "string" || userId.length === 0) { throw new TypeError(`VisibilityMatrix: userId must be a non-empty string, got "${userId}"`); } if (!VISIBILITY_STATES.includes(/** @type {any} */ (state))) { throw new TypeError(`VisibilityMatrix: invalid state "${state}" for userId "${userId}"`); } } return /** @type {VisibilityMatrix} */ (data); }