Fix Story 1.3: StateStore spec compliance and minor cleanup

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>
This commit is contained in:
2026-05-22 11:38:45 +02:00
parent 110b295a7b
commit 5ba7717ecd
17 changed files with 2391 additions and 55 deletions
-2
View File
@@ -13,8 +13,6 @@
* @module contracts/pending-op
*/
/** @typedef {Object} PendingOp */
/** Shape version constant for PendingOp. @type {1} */
export const PENDING_OP_VERSION = 1;
+9 -4
View File
@@ -2,7 +2,7 @@
* Socket message contract.
*
* Two message directions:
* Intent (GM → all): scrying-pool.visibility.set { opId, userId, targetState }
* Intent (GM → all): scrying-pool.visibility.set { opId, userId, targetState, baseRevision }
* Echo (all ← GM): scrying-pool.visibility.updated { opId, userId, state, revision }
*
* Validated at both send and receive. Payload ≥ 4096 bytes → throw before emit.
@@ -17,6 +17,7 @@
* @property {string} opId - Unique operation ID (non-empty string).
* @property {string} userId - Target participant userId (non-empty string).
* @property {string} targetState - Desired VisibilityState.
* @property {number} baseRevision - Revision counter when the GM issued the intent (for latest-revision-wins guard).
*/
/**
@@ -47,12 +48,13 @@ export const MAX_PAYLOAD_BYTES = 4096;
* @param {string} opId - Unique operation ID.
* @param {string} userId - Target participant userId.
* @param {string} targetState - Desired VisibilityState.
* @param {number} baseRevision - Revision counter at time of intent.
* @returns {SocketMessage}
*/
export function createSocketIntentMessage(opId, userId, targetState) {
export function createSocketIntentMessage(opId, userId, targetState, baseRevision) {
return {
event: SOCKET_EVENTS.VISIBILITY_SET,
payload: { opId, userId, targetState },
payload: { opId, userId, targetState, baseRevision },
};
}
@@ -95,7 +97,7 @@ export function isValidSocketMessage(data) {
const p = /** @type {Record<string, unknown>} */ (payload);
// Validate intent payload
if (event === SOCKET_EVENTS.VISIBILITY_SET) {
const { opId, userId, targetState, ...payloadRest } = p;
const { opId, userId, targetState, baseRevision, ...payloadRest } = p;
if (Object.keys(payloadRest).length > 0) {
throw new TypeError(`SocketMessage intent: unknown payload keys: ${Object.keys(payloadRest).join(", ")}`);
}
@@ -108,6 +110,9 @@ export function isValidSocketMessage(data) {
if (typeof targetState !== "string" || targetState.length === 0) {
throw new TypeError("SocketMessage: targetState must be a non-empty string");
}
if (typeof baseRevision !== "number" || !Number.isFinite(baseRevision) || baseRevision < 0) {
throw new TypeError("SocketMessage: baseRevision must be a finite non-negative number");
}
}
// Validate echo payload
if (event === SOCKET_EVENTS.VISIBILITY_UPDATED) {
+5 -1
View File
@@ -15,6 +15,7 @@
* @typedef {Object} VisibilityMatrix
* @property {1} _version - Schema version; always 1 for v1.
* @property {Object.<string, VisibilityState>} matrix - userId → VisibilityState map.
* @property {number} [_revision] - Optional revision counter for optimistic concurrency control.
*/
export const VISIBILITY_STATES = Object.freeze([
@@ -50,7 +51,7 @@ export function isValidVisibilityMatrix(data) {
throw new TypeError("VisibilityMatrix: must be an object");
}
const obj = /** @type {Record<string, unknown>} */ (data);
const { _version, matrix, ...rest } = obj;
const { _version, matrix, _revision, ...rest } = obj;
if (Object.keys(rest).length > 0) {
throw new TypeError(`VisibilityMatrix: unknown keys: ${Object.keys(rest).join(", ")}`);
}
@@ -60,6 +61,9 @@ export function isValidVisibilityMatrix(data) {
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<string, unknown>} */ (matrix);
for (const [userId, state] of Object.entries(matrixObj)) {
if (typeof userId !== "string" || userId.length === 0) {