fix(read-state): structured decode for NIP-RS override layer

Replace the flat-map decode path with a parseContexts() function that
returns {frontiers, overrides} — separate namespaces keyed by raw
context ID.  This fixes three correctness defects in the previous
implementation:

- Frontier keys were stored with their esc: prefix intact, so
  isOverrideActive evaluated against the wrong frontier for any raw
  context ID starting with ov_ or esc: (e.g. raw ov_s:evil had its
  frontier stored as esc:ov_s:evil while the override register was
  keyed by ov_s:evil).
- The 256-byte UTF-8 key-length check was applied only to frontier
  entries; ov_* sibling keys were written back unchecked, allowing
  wire keys up to 5 + suffix bytes through.
- extractOverrideRegisters accepted an arbitrary flat map and zero-filled
  missing components, reconstructing partial groups that group-first
  validation had already rejected.

Changes:
- readStateFormat.ts: add ParsedContexts interface and parseContexts();
  make partitionOverrideGroups internal; add 256-byte enforcement to
  every wire key (frontier and override siblings alike); fix the
  sanitizeContexts doc comment.
- readStateSnapshot.ts: add contexts: ParsedContexts to
  ParsedReadStateEvent; parseReadStateEvent populates both blob.contexts
  (flat, for readStateManager backward compat) and contexts (structured);
  mergeReadStateEvents iterates parsed.contexts.frontiers so callers
  receive unescaped raw ctx IDs; remove extractOverrideRegisters; update
  mergeOverrideRegisterMaps to accept ReadonlyMap inputs.
- readStateOverride.test.mjs: rewrite around parseContexts(); add the
  full partial-group rejection matrix (S-only, B-only, S+B, C+B, S+C),
  invalid sibling at each position, uint32 boundary values, 256/257-byte
  key-length cases for live and floor shapes, multibyte UTF-8 boundary,
  and Thufir's exact collision witness (raw ctx ov_s:evil alongside ctx
  evil with its own ov_s:evil counter).  3968/3968 pass.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
2026-07-31 14:15:31 -04:00
parent 72b1d4f368
commit d979427a7b
3 changed files with 660 additions and 258 deletions

View File

@@ -97,8 +97,8 @@ export function unescapeFrontierKey(wireKey: string): string {
*
* Slice 2 (manager) and slice 3 (UI) use this type as the wire-in/wire-out
* contract. Slices MUST NOT construct registers directly — use
* `encodeOverrideGroup` for encoding and rely on `extractOverrideRegisters`
* (snapshot.ts) for decoding.
* `encodeOverrideGroup` for encoding and read `ParsedReadStateEvent.contexts.overrides`
* (via `parseContexts`) for decoding.
*/
export interface OverrideRegister {
/** Set counter S — incremented on each mark-unread. */
@@ -193,90 +193,224 @@ export function encodeOverrideGroup(
}
// ---------------------------------------------------------------------------
// Override group validation — used inside sanitizeContexts
//
// The spec requires that override entries be validated as a complete logical
// group BEFORE any per-entry processing. This function partitions the raw
// contexts map into (a) validated override groups and (b) everything else,
// so that sanitizeContexts can apply group-first rules without mixing the two
// validation paths.
// Structured parse result — separates decoded frontier IDs from override keys
// ---------------------------------------------------------------------------
/**
* Structured result of parsing a `contexts` map.
*
* `frontiers` — Map from **raw** context ID (unescaped) to merged frontier
* value. Keys are never `ov_*` or `esc:` prefixed: all escaping is stripped
* at decode time so the same raw ID is used in both maps.
*
* `overrides` — Map from **raw** context ID to validated `OverrideRegister`.
* Keyed by the same raw IDs as `frontiers`, so `isOverrideActive` can safely
* evaluate `overrides.get(rawCtx)` against `frontiers.get(rawCtx)`.
*/
export interface ParsedContexts {
readonly frontiers: ReadonlyMap<string, number>;
readonly overrides: ReadonlyMap<string, OverrideRegister>;
}
/**
* A validated override group ready for merge.
* `kind === "live"` → three-key group with all counters present.
* `kind === "floor"` → tombstone, only `c` is meaningful (s=0, b=0).
*
* Used internally by `parseContexts` and `sanitizeContexts`.
*/
export type ValidatedOverrideGroup =
| { kind: "live"; reg: OverrideRegister }
| { kind: "floor"; c: number };
/**
* Parse and validate a raw `contexts` object from a decoded blob into
* separate frontier and override namespaces.
*
* This is the single structured decode path for the override layer. It
* applies group-first validation (spec `:114`) before any per-entry
* processing:
*
* 1. Collect all `ov_*` entries by suffix into candidate groups.
* 2. Validate each group as a whole: complete live (S+C+B) or tombstone
* floor (C-only). Any other shape (partial group, invalid value in any
* sibling) silently drops the whole group; the frontier entry for `<ctx>`
* is retained.
* 3. Apply the 256-byte UTF-8 key-length check to EVERY wire key — both
* frontier keys and override sibling keys. An invalid sibling rejects
* its entire group; an invalid frontier key discards only that entry.
* 4. Unescape frontier wire keys (`esc:…` → raw ctx) so both maps share
* the same key domain.
*
* The result contains no `ov_*` or `esc:*` keys in `frontiers`.
* Slices 2–3 use this type; existing callers use `sanitizeContexts` which
* wraps this function.
*/
export function parseContexts(raw: Record<string, unknown>): ParsedContexts {
const enc = new TextEncoder();
const isUint32 = (v: unknown): v is number =>
typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 4294967295;
// ── Pass 1: bucket override candidates by suffix ─────────────────────────
const sWire = new Map<string, string>(); // suffix → wire key (for byte-length check)
const cWire = new Map<string, string>();
const bWire = new Map<string, string>();
const sVal = new Map<string, unknown>();
const cVal = new Map<string, unknown>();
const bVal = new Map<string, unknown>();
const frontierEntries: Array<[string, unknown]> = [];
for (const [key, value] of Object.entries(raw)) {
if (key.startsWith(OV_S_PREFIX)) {
const suffix = key.slice(OV_S_PREFIX.length);
sWire.set(suffix, key);
sVal.set(suffix, value);
} else if (key.startsWith(OV_C_PREFIX)) {
const suffix = key.slice(OV_C_PREFIX.length);
cWire.set(suffix, key);
cVal.set(suffix, value);
} else if (key.startsWith(OV_B_PREFIX)) {
const suffix = key.slice(OV_B_PREFIX.length);
bWire.set(suffix, key);
bVal.set(suffix, value);
} else {
frontierEntries.push([key, value]);
}
}
// ── Pass 2: validate each suffix as a group ───────────────────────────────
const overrides = new Map<string, OverrideRegister>();
const suffixes = new Set([...sWire.keys(), ...cWire.keys(), ...bWire.keys()]);
for (const suffix of suffixes) {
const hasS = sWire.has(suffix);
const hasC = cWire.has(suffix);
const hasB = bWire.has(suffix);
const sv = sVal.get(suffix);
const cv = cVal.get(suffix);
const bv = bVal.get(suffix);
if (!hasS && hasC && !hasB) {
// Tombstone floor: only ov_c: present.
const wireKey = cWire.get(suffix) ?? "";
if (enc.encode(wireKey).length > 256) continue; // key too long → drop group
if (!isUint32(cv)) continue;
overrides.set(suffix, { s: 0, c: cv, b: 0 });
} else if (hasS && hasC && hasB) {
// Complete live group: all three sibling wire keys must be ≤ 256 bytes.
const swk = sWire.get(suffix) ?? "";
const cwk = cWire.get(suffix) ?? "";
const bwk = bWire.get(suffix) ?? "";
if (
enc.encode(swk).length > 256 ||
enc.encode(cwk).length > 256 ||
enc.encode(bwk).length > 256
)
continue; // any sibling key too long → drop group
if (!isUint32(sv) || !isUint32(cv) || !isUint32(bv)) continue;
overrides.set(suffix, { s: sv, c: cv, b: bv });
}
// Any other shape (B-only, S-only, S+B, C+B, S+C): silently dropped.
// The frontier entry for `suffix` may still appear in frontierEntries.
}
// ── Pass 3: validate and unescape frontier entries ────────────────────────
const frontiers = new Map<string, number>();
for (const [wireKey, value] of frontierEntries) {
if (enc.encode(wireKey).length > 256) continue;
if (!isUint32(value)) continue;
// Strip exactly one leading `esc:` to recover the raw context ID.
const rawCtx = unescapeFrontierKey(wireKey);
// max-merge in case a malformed blob has both escaped and unescaped forms.
const existing = frontiers.get(rawCtx);
if (existing === undefined || value > existing) {
frontiers.set(rawCtx, value);
}
}
return { frontiers, overrides };
}
/**
* Partition the raw contexts map into validated override groups and
* non-override entries.
*
* Override entries that fail group validation are silently dropped (per spec:
* "the entire override group is rejected"); the corresponding frontier entry
* for `<ctx>` is retained in `nonOverride`.
* This internal helper is used by `sanitizeContexts` to produce the flat
* `Record<string, number>` stored in `ReadStateBlob.contexts` for backward
* compatibility with `readStateManager`. New callers (slices 2–3) should
* use `parseContexts` directly for the structured result.
*
* Override key-length enforcement mirrors `parseContexts`: an invalid sibling
* wire key rejects the entire group while retaining the frontier entry.
*
* @returns
* `overrides` — Map from raw context ID to validated group.
* `nonOverride` — Raw contexts map with all `ov_*` keys removed (frontier
* entries and escaped keys remain for standard processing).
* `overrides` — Map from raw context suffix to `ValidatedOverrideGroup`.
* `nonOverride` — Raw contexts map with all `ov_*` keys removed.
*/
export function partitionOverrideGroups(contexts: Record<string, unknown>): {
function partitionOverrideGroups(contexts: Record<string, unknown>): {
overrides: Map<string, ValidatedOverrideGroup>;
nonOverride: Record<string, unknown>;
} {
// First pass: collect all ov_* entries by context suffix.
const sMap = new Map<string, unknown>();
const cMap = new Map<string, unknown>();
const bMap = new Map<string, unknown>();
const enc = new TextEncoder();
const isUint32 = (v: unknown): v is number =>
typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 4294967295;
const sWire = new Map<string, string>();
const cWire = new Map<string, string>();
const bWire = new Map<string, string>();
const sVal = new Map<string, unknown>();
const cVal = new Map<string, unknown>();
const bVal = new Map<string, unknown>();
const nonOverride: Record<string, unknown> = {};
for (const [key, value] of Object.entries(contexts)) {
if (key.startsWith(OV_S_PREFIX)) {
sMap.set(key.slice(OV_S_PREFIX.length), value);
const suffix = key.slice(OV_S_PREFIX.length);
sWire.set(suffix, key);
sVal.set(suffix, value);
} else if (key.startsWith(OV_C_PREFIX)) {
cMap.set(key.slice(OV_C_PREFIX.length), value);
const suffix = key.slice(OV_C_PREFIX.length);
cWire.set(suffix, key);
cVal.set(suffix, value);
} else if (key.startsWith(OV_B_PREFIX)) {
bMap.set(key.slice(OV_B_PREFIX.length), value);
const suffix = key.slice(OV_B_PREFIX.length);
bWire.set(suffix, key);
bVal.set(suffix, value);
} else {
nonOverride[key] = value;
}
}
// Second pass: validate each unique ctx suffix as a group.
const ctxSuffixes = new Set([...sMap.keys(), ...cMap.keys(), ...bMap.keys()]);
const suffixes = new Set([...sWire.keys(), ...cWire.keys(), ...bWire.keys()]);
const overrides = new Map<string, ValidatedOverrideGroup>();
for (const ctx of ctxSuffixes) {
const hasS = sMap.has(ctx);
const hasC = cMap.has(ctx);
const hasB = bMap.has(ctx);
const sVal = sMap.get(ctx);
const cVal = cMap.get(ctx);
const bVal = bMap.get(ctx);
const isUint32 = (v: unknown): v is number =>
typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 4294967295;
for (const suffix of suffixes) {
const hasS = sWire.has(suffix);
const hasC = cWire.has(suffix);
const hasB = bWire.has(suffix);
const sv = sVal.get(suffix);
const cv = cVal.get(suffix);
const bv = bVal.get(suffix);
if (!hasS && hasC && !hasB) {
// Tombstone floor: only ov_c: present — must be a valid uint32.
if (!isUint32(cVal)) continue; // invalid — drop group silently
overrides.set(ctx, { kind: "floor", c: cVal });
const wireKey = cWire.get(suffix) ?? "";
if (enc.encode(wireKey).length > 256) continue;
if (!isUint32(cv)) continue;
overrides.set(suffix, { kind: "floor", c: cv });
} else if (hasS && hasC && hasB) {
// Complete live group: all three keys must have valid uint32 values.
if (!isUint32(sVal) || !isUint32(cVal) || !isUint32(bVal)) continue;
overrides.set(ctx, {
kind: "live",
reg: { s: sVal, c: cVal, b: bVal },
});
} else {
// Partial group or any other shape — reject the whole group (spec).
// The frontier entry for this ctx remains in nonOverride; do nothing here.
const swk = sWire.get(suffix) ?? "";
const cwk = cWire.get(suffix) ?? "";
const bwk = bWire.get(suffix) ?? "";
if (
enc.encode(swk).length > 256 ||
enc.encode(cwk).length > 256 ||
enc.encode(bwk).length > 256
)
continue;
if (!isUint32(sv) || !isUint32(cv) || !isUint32(bv)) continue;
overrides.set(suffix, { kind: "live", reg: { s: sv, c: cv, b: bv } });
}
// Any other shape: silently dropped; frontier entry retained in nonOverride.
}
return { overrides, nonOverride };
@@ -346,28 +480,29 @@ export function isValidBlob(obj: unknown): obj is ReadStateBlob {
}
/**
* Sanitize and validate a raw contexts object from a decoded blob.
* Sanitize and validate a raw contexts object from a decoded blob into the
* flat `Record<string, number>` stored in `ReadStateBlob.contexts`.
*
* Override entries (`ov_*` keys) are validated as complete logical groups
* BEFORE any per-entry processing, per the spec's Content Validation section.
* Partial groups are rejected (dropped) while their corresponding frontier
* entry is retained. Validated override groups are re-encoded into the result
* as raw entries so that the rest of the pipeline (merge, liveness) operates
* on a flat integer map — the same type as frontier entries.
*
* Escaped frontier keys (`esc:…`) pass through unchanged; unescaping happens
* at read time in the snapshot layer.
* This is the backward-compatible path for `readStateManager`, which reads
* the flat blob directly. Override entries (`ov_*` keys) are validated as
* complete logical groups BEFORE any per-entry processing; partial groups are
* dropped while their frontier entry is retained. The 256-byte UTF-8
* key-length check is applied to every wire key — including override siblings.
* Escaped frontier keys (`esc:…`) are stored as-is in the flat map (the
* structured path via `parseContexts` applies unescaping; this flat path
* preserves the wire form for the manager).
*/
export function sanitizeContexts(
contexts: Record<string, unknown>,
): Record<string, number> {
const enc = new TextEncoder();
// Step 1: partition override groups from the rest (group-first validation).
const { overrides, nonOverride } = partitionOverrideGroups(contexts);
// Step 2: sanitize non-override entries with the standard per-entry rules.
const result: Record<string, number> = {};
for (const [key, value] of Object.entries(nonOverride)) {
if (new TextEncoder().encode(key).length > 256) continue;
if (enc.encode(key).length > 256) continue;
if (typeof value !== "number" || !Number.isInteger(value)) continue;
if (value < 0 || value > 4294967295) continue;
result[key] = value;

View File

@@ -10,15 +10,12 @@ import {
OV_B_PREFIX,
OV_C_PREFIX,
OV_S_PREFIX,
partitionOverrideGroups,
parseContexts,
sanitizeContexts,
unescapeFrontierKey,
} from "./readStateFormat.ts";
import {
extractOverrideRegisters,
mergeOverrideRegisterMaps,
} from "./readStateSnapshot.ts";
import { mergeOverrideRegisterMaps } from "./readStateSnapshot.ts";
// ---------------------------------------------------------------------------
// escapeFrontierKey / unescapeFrontierKey
@@ -130,6 +127,24 @@ test("isOverrideActive_tombstoneFloor_returnsFalse", () => {
assert.equal(isOverrideActive({ s: 0, c: 5, b: 0 }, 0), false);
});
test("isOverrideActive_uint32Max_liveOverride_returnsTrue", () => {
// Boundary: S at uint32 max, C one less, B at max, F at zero
const UINT32_MAX = 4294967295;
assert.equal(
isOverrideActive({ s: UINT32_MAX, c: UINT32_MAX - 1, b: UINT32_MAX }, 0),
true,
);
});
test("isOverrideActive_uint32Max_zeroFrontier_tile_returnsFalse", () => {
// S == C at max → clear-wins
const UINT32_MAX = 4294967295;
assert.equal(
isOverrideActive({ s: UINT32_MAX, c: UINT32_MAX, b: 1 }, 0),
false,
);
});
// ---------------------------------------------------------------------------
// mergeOverrideRegisters — componentwise max()
// ---------------------------------------------------------------------------
@@ -161,6 +176,29 @@ test("mergeOverrideRegisters_associative", () => {
);
});
test("mergeOverrideRegisters_zeroBoundary_idempotent", () => {
const zero = { s: 0, c: 0, b: 0 };
assert.deepEqual(mergeOverrideRegisters(zero, zero), zero);
});
test("mergeOverrideRegisters_uint32Max_idempotent", () => {
const UINT32_MAX = 4294967295;
const max = { s: UINT32_MAX, c: UINT32_MAX, b: UINT32_MAX };
assert.deepEqual(mergeOverrideRegisters(max, max), max);
});
test("mergeOverrideRegisters_uint32Max_commutative", () => {
const UINT32_MAX = 4294967295;
const a = { s: UINT32_MAX, c: 0, b: UINT32_MAX };
const b = { s: 0, c: UINT32_MAX, b: 0 };
assert.deepEqual(mergeOverrideRegisters(a, b), mergeOverrideRegisters(b, a));
assert.deepEqual(mergeOverrideRegisters(a, b), {
s: UINT32_MAX,
c: UINT32_MAX,
b: UINT32_MAX,
});
});
// ---------------------------------------------------------------------------
// encodeOverrideGroup — canonical wire encoding
// ---------------------------------------------------------------------------
@@ -204,116 +242,373 @@ test("encodeOverrideGroup_tombstoneFloorShape_returnsOnlyOvC", () => {
});
// ---------------------------------------------------------------------------
// partitionOverrideGroups — group-first validation
// parseContexts — structured decode (group-first validation, unescape, 256-byte)
// ---------------------------------------------------------------------------
test("partitionOverrideGroups_liveGroup_validates", () => {
// ── accepted shapes ─────────────────────────────────────────────────────────
test("parseContexts_liveGroup_populatesBothMaps", () => {
const raw = {
"channel:x": 1000,
"ov_s:channel:x": 1,
"ov_c:channel:x": 0,
"ov_b:channel:x": 900,
};
const { overrides, nonOverride } = partitionOverrideGroups(raw);
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("channel:x"), 1000);
assert.deepEqual(overrides.get("channel:x"), { s: 1, c: 0, b: 900 });
assert.equal(overrides.size, 1);
const group = overrides.get("channel:x");
assert.equal(group?.kind, "live");
assert.deepEqual(group?.reg, { s: 1, c: 0, b: 900 });
assert.deepEqual(nonOverride, { "channel:x": 1000 });
// No ov_* keys in frontiers
assert.equal(frontiers.has("ov_s:channel:x"), false);
});
test("partitionOverrideGroups_tombstoneFloor_validates", () => {
test("parseContexts_tombstoneFloor_populatesBothMaps", () => {
const raw = {
"channel:x": 1000,
"ov_c:channel:x": 5,
};
const { overrides, nonOverride } = partitionOverrideGroups(raw);
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("channel:x"), 1000);
assert.deepEqual(overrides.get("channel:x"), { s: 0, c: 5, b: 0 });
assert.equal(overrides.size, 1);
const group = overrides.get("channel:x");
assert.equal(group?.kind, "floor");
assert.equal(group?.c, 5);
assert.deepEqual(nonOverride, { "channel:x": 1000 });
});
test("partitionOverrideGroups_partialGroup_droppedFrontierRetained", () => {
// Only ov_s without ov_c and ov_b → partial group → rejected
test("parseContexts_normalFrontierOnly_noOverrides", () => {
const raw = {
"channel:x": 1000,
"ov_s:channel:x": 1,
"ch:normal": 5000,
"msg:abc": 3000,
"thread:def": 2000,
};
const { overrides, nonOverride } = partitionOverrideGroups(raw);
assert.equal(overrides.size, 0); // group rejected
assert.deepEqual(nonOverride, { "channel:x": 1000 }); // frontier retained
});
test("partitionOverrideGroups_sConly_droppedFrontierRetained", () => {
// ov_s + ov_c but no ov_b → partial group
const raw = {
"channel:x": 1000,
"ov_s:channel:x": 1,
"ov_c:channel:x": 0,
};
const { overrides, nonOverride } = partitionOverrideGroups(raw);
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:normal"), 5000);
assert.equal(frontiers.get("msg:abc"), 3000);
assert.equal(frontiers.get("thread:def"), 2000);
assert.equal(overrides.size, 0);
assert.deepEqual(nonOverride, { "channel:x": 1000 });
});
test("partitionOverrideGroups_invalidValue_groupDropped", () => {
// ov_s has non-integer value → invalid → whole group dropped
const raw = {
"channel:x": 1000,
"ov_s:channel:x": "not-a-number",
"ov_c:channel:x": 0,
"ov_b:channel:x": 900,
};
const { overrides, nonOverride } = partitionOverrideGroups(raw);
test("parseContexts_escapedFrontierKey_unescapedInFrontiers", () => {
// Wire key esc:ov_s:evil → raw ctx ov_s:evil
const raw = { "esc:ov_s:evil": 777 };
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ov_s:evil"), 777);
assert.equal(frontiers.has("esc:ov_s:evil"), false);
assert.equal(overrides.size, 0);
assert.deepEqual(nonOverride, { "channel:x": 1000 });
});
test("partitionOverrideGroups_negativeValue_groupDropped", () => {
test("parseContexts_doubleEscapedFrontierKey_stripsOneEsc", () => {
// Wire key esc:esc:foo → raw ctx esc:foo
const raw = { "esc:esc:foo": 888 };
const { frontiers } = parseContexts(raw);
assert.equal(frontiers.get("esc:foo"), 888);
assert.equal(frontiers.has("esc:esc:foo"), false);
});
// ── partial-group rejection matrix (every illegal subset) ───────────────────
test("parseContexts_sOnly_droppedFrontierRetained", () => {
const raw = { "ch:x": 100, "ov_s:ch:x": 1 };
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:x"), 100);
assert.equal(overrides.has("ch:x"), false);
});
test("parseContexts_bOnly_droppedFrontierRetained", () => {
const raw = { "ch:x": 100, "ov_b:ch:x": 50 };
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:x"), 100);
assert.equal(overrides.has("ch:x"), false);
});
test("parseContexts_sConly_droppedFrontierRetained", () => {
const raw = { "ch:x": 100, "ov_s:ch:x": 1, "ov_c:ch:x": 0 };
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:x"), 100);
assert.equal(overrides.has("ch:x"), false);
});
test("parseContexts_sBonly_droppedFrontierRetained", () => {
const raw = { "ch:x": 100, "ov_s:ch:x": 1, "ov_b:ch:x": 50 };
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:x"), 100);
assert.equal(overrides.has("ch:x"), false);
});
test("parseContexts_cBonly_droppedFrontierRetained", () => {
const raw = { "ch:x": 100, "ov_c:ch:x": 2, "ov_b:ch:x": 50 };
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:x"), 100);
assert.equal(overrides.has("ch:x"), false);
});
// ── invalid sibling at each position ────────────────────────────────────────
test("parseContexts_invalidS_liveGroupDropped", () => {
const raw = {
"channel:x": 1000,
"ov_s:channel:x": -1,
"ov_c:channel:x": 0,
"ov_b:channel:x": 900,
"ch:x": 100,
"ov_s:ch:x": "not-a-number",
"ov_c:ch:x": 0,
"ov_b:ch:x": 50,
};
const { overrides, nonOverride } = partitionOverrideGroups(raw);
assert.equal(overrides.size, 0);
assert.deepEqual(nonOverride, { "channel:x": 1000 });
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:x"), 100);
assert.equal(overrides.has("ch:x"), false);
});
test("partitionOverrideGroups_ovCInvalidValue_tombstoneDropped", () => {
// Tombstone floor with non-integer c
test("parseContexts_invalidC_liveGroupDropped", () => {
const raw = {
"channel:x": 1000,
"ov_c:channel:x": 1.5,
"ch:x": 100,
"ov_s:ch:x": 1,
"ov_c:ch:x": 1.5,
"ov_b:ch:x": 50,
};
const { overrides, nonOverride } = partitionOverrideGroups(raw);
assert.equal(overrides.size, 0);
assert.deepEqual(nonOverride, { "channel:x": 1000 });
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:x"), 100);
assert.equal(overrides.has("ch:x"), false);
});
test("partitionOverrideGroups_multipleContexts_allValidated", () => {
test("parseContexts_invalidB_liveGroupDropped", () => {
const raw = { "ch:x": 100, "ov_s:ch:x": 1, "ov_c:ch:x": 0, "ov_b:ch:x": -1 };
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:x"), 100);
assert.equal(overrides.has("ch:x"), false);
});
test("parseContexts_uint32Overflow_liveGroupDropped", () => {
const raw = {
"ch:x": 100,
"ov_s:ch:x": 4294967296,
"ov_c:ch:x": 0,
"ov_b:ch:x": 50,
};
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:x"), 100);
assert.equal(overrides.has("ch:x"), false);
});
test("parseContexts_invalidC_tombstoneDropped", () => {
const raw = { "ch:x": 100, "ov_c:ch:x": -5 };
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:x"), 100);
assert.equal(overrides.has("ch:x"), false);
});
// ── uint32 boundary values accepted ─────────────────────────────────────────
test("parseContexts_uint32Zero_acceptedInLiveGroup", () => {
const raw = { "ch:x": 0, "ov_s:ch:x": 0, "ov_c:ch:x": 0, "ov_b:ch:x": 0 };
const { overrides } = parseContexts(raw);
assert.deepEqual(overrides.get("ch:x"), { s: 0, c: 0, b: 0 });
});
test("parseContexts_uint32Max_acceptedInLiveGroup", () => {
const UINT32_MAX = 4294967295;
const raw = {
"ch:x": UINT32_MAX,
"ov_s:ch:x": UINT32_MAX,
"ov_c:ch:x": UINT32_MAX,
"ov_b:ch:x": UINT32_MAX,
};
const { frontiers, overrides } = parseContexts(raw);
assert.equal(frontiers.get("ch:x"), UINT32_MAX);
assert.deepEqual(overrides.get("ch:x"), {
s: UINT32_MAX,
c: UINT32_MAX,
b: UINT32_MAX,
});
});
test("parseContexts_uint32Max_acceptedInTombstone", () => {
const UINT32_MAX = 4294967295;
const raw = { "ch:x": 0, "ov_c:ch:x": UINT32_MAX };
const { overrides } = parseContexts(raw);
assert.deepEqual(overrides.get("ch:x"), { s: 0, c: UINT32_MAX, b: 0 });
});
// ── 256-byte key-length enforcement ─────────────────────────────────────────
// Build a string of exactly N UTF-8 bytes (ASCII, so length == byte count).
function asciiOfBytes(n) {
return "a".repeat(n);
}
test("parseContexts_frontierKey256Bytes_accepted", () => {
// ov_s: prefix is 5 bytes; a 251-byte suffix yields a 256-byte wire key.
const suffix251 = asciiOfBytes(251);
const raw = { [suffix251]: 42 };
const { frontiers } = parseContexts(raw);
assert.equal(frontiers.get(suffix251), 42);
});
test("parseContexts_frontierKey257Bytes_dropped", () => {
const suffix257 = asciiOfBytes(257);
const raw = { [suffix257]: 42 }; // 257 bytes — exceeds 256
const { frontiers } = parseContexts(raw);
assert.equal(frontiers.has(suffix257), false);
});
test("parseContexts_liveGroupSiblingKey256Bytes_accepted", () => {
// ov_s: is 5 bytes; suffix of 251 bytes → wire key exactly 256 bytes.
const suffix251 = asciiOfBytes(251);
const raw = {
[suffix251]: 10,
[`${OV_S_PREFIX}${suffix251}`]: 1,
[`${OV_C_PREFIX}${suffix251}`]: 0,
[`${OV_B_PREFIX}${suffix251}`]: 9,
};
const { overrides } = parseContexts(raw);
assert.deepEqual(overrides.get(suffix251), { s: 1, c: 0, b: 9 });
});
test("parseContexts_liveGroupSiblingKey257Bytes_groupDropped", () => {
// ov_s: is 5 bytes; suffix of 252 bytes → wire key 257 bytes — drops group.
const suffix252 = asciiOfBytes(252);
const raw = {
[suffix252]: 10, // frontier key also 252 bytes — also dropped
[`${OV_S_PREFIX}${suffix252}`]: 1,
[`${OV_C_PREFIX}${suffix252}`]: 0,
[`${OV_B_PREFIX}${suffix252}`]: 9,
};
const { overrides } = parseContexts(raw);
assert.equal(overrides.has(suffix252), false);
});
test("parseContexts_tombstoneFloorKey256Bytes_accepted", () => {
// ov_c: is 5 bytes; suffix of 251 bytes → wire key exactly 256 bytes.
const suffix251 = asciiOfBytes(251);
const raw = {
[suffix251]: 0,
[`${OV_C_PREFIX}${suffix251}`]: 3,
};
const { overrides } = parseContexts(raw);
assert.deepEqual(overrides.get(suffix251), { s: 0, c: 3, b: 0 });
});
test("parseContexts_tombstoneFloorKey257Bytes_groupDropped", () => {
// ov_c: is 5 bytes; suffix of 252 bytes → wire key 257 bytes — drops group.
const suffix252 = asciiOfBytes(252);
const raw = {
[suffix252]: 0,
[`${OV_C_PREFIX}${suffix252}`]: 3,
};
const { overrides } = parseContexts(raw);
assert.equal(overrides.has(suffix252), false);
});
test("parseContexts_multibyteSuffix_keyLengthCheckedInBytes", () => {
// A 2-byte UTF-8 char (e.g. 'é') counts as 2 bytes toward the 256 limit.
// suffix of 127 × 'é' = 254 bytes; ov_c: (5) + 254 = 259 → over limit.
const multiSuffix = "\u00e9".repeat(127); // 127 × 2 bytes = 254 bytes
const raw = {
[multiSuffix]: 0,
[`${OV_C_PREFIX}${multiSuffix}`]: 7,
};
const { overrides } = parseContexts(raw);
assert.equal(overrides.has(multiSuffix), false);
});
test("parseContexts_multibyteSuffix_justUnder256_accepted", () => {
// 'é' × 125 = 250 bytes; ov_c: (5) + 250 = 255 → under limit.
const multiSuffix = "\u00e9".repeat(125); // 125 × 2 = 250 bytes
const raw = {
[multiSuffix]: 0,
[`${OV_C_PREFIX}${multiSuffix}`]: 7,
};
const { overrides } = parseContexts(raw);
assert.deepEqual(overrides.get(multiSuffix), { s: 0, c: 7, b: 0 });
});
// ── reserved-ID collision witness (Thufir finding 1) ────────────────────────
test("parseContexts_reservedIdCollision_frontierAndOverrideUseDistinctNamespaces", () => {
// Raw context `ov_s:evil` has its frontier published under `esc:ov_s:evil`
// (as a publisher-aware client would do). It also has a live register for
// raw context `ov_s:evil` (suffix = `evil` for the ov_* keys).
// Raw context `evil` independently has its own frontier and override.
const raw = {
"esc:ov_s:evil": 7, // frontier for raw ctx `ov_s:evil`
"ov_s:evil": 3, // set counter for raw ctx `evil`
"ov_c:evil": 1,
"ov_b:evil": 6,
evil: 100, // frontier for raw ctx `evil`
"ov_s:regular": 2, // set counter for raw ctx `regular`
"ov_c:regular": 0,
"ov_b:regular": 50,
regular: 40,
};
const { frontiers, overrides } = parseContexts(raw);
// frontier for raw ctx `ov_s:evil` must be 7 (unescaped from `esc:ov_s:evil`)
assert.equal(
frontiers.get("ov_s:evil"),
7,
"frontier for ctx ov_s:evil must be 7",
);
// frontier for raw ctx `evil` must be 100
assert.equal(frontiers.get("evil"), 100, "frontier for ctx evil must be 100");
// override for raw ctx `evil` comes from `ov_s:evil`/`ov_c:evil`/`ov_b:evil` (suffix=`evil`)
assert.deepEqual(
overrides.get("evil"),
{ s: 3, c: 1, b: 6 },
"override for ctx evil",
);
// override for raw ctx `regular`
assert.deepEqual(
overrides.get("regular"),
{ s: 2, c: 0, b: 50 },
"override for ctx regular",
);
// No `ov_s:evil` in frontiers as an override key — but `ov_s:evil` IS a raw ctx ID with frontier 7
assert.equal(
frontiers.get("ov_s:evil"),
7,
"ov_s:evil raw ctx has frontier 7",
);
// No `esc:ov_s:evil` in frontiers (was unescaped)
assert.equal(
frontiers.has("esc:ov_s:evil"),
false,
"escaped wire key must not appear in frontiers",
);
// liveness evaluation uses consistent namespaces
const evilReg = overrides.get("evil");
const evilFrontier = frontiers.get("evil") ?? 0;
// s=3, c=1, b=6, f=100 → F(100) > B(6) → inactive
assert.equal(
isOverrideActive(evilReg, evilFrontier),
false,
"evil override inactive because frontier(100) > baseline(6)",
);
const regularReg = overrides.get("regular");
const regularFrontier = frontiers.get("regular") ?? 0;
// s=2, c=0, b=50, f=40 → S>0 ∧ F(40)<=B(50) ∧ S(2)>C(0) → active
assert.equal(
isOverrideActive(regularReg, regularFrontier),
true,
"regular override active: F(40) <= B(50), S(2) > C(0)",
);
});
// ── multiple contexts in one blob ────────────────────────────────────────────
test("parseContexts_multipleContexts_allProcessedCorrectly", () => {
const raw = {
"ch:a": 100,
"ov_s:ch:a": 1,
"ov_c:ch:a": 0,
"ov_b:ch:a": 90,
"ch:b": 200,
"ov_c:ch:b": 3, // tombstone floor for ch:b
"ov_c:ch:b": 3, // tombstone floor
"ch:c": 300,
"ov_s:ch:c": 1, // partial — only s, no c/b → rejected
};
const { overrides, nonOverride } = partitionOverrideGroups(raw);
assert.equal(overrides.size, 2); // ch:a and ch:b, ch:c rejected
assert.equal(overrides.get("ch:a")?.kind, "live");
assert.equal(overrides.get("ch:b")?.kind, "floor");
const { frontiers, overrides } = parseContexts(raw);
assert.equal(overrides.get("ch:a")?.s, 1);
assert.deepEqual(overrides.get("ch:b"), { s: 0, c: 3, b: 0 });
assert.equal(overrides.has("ch:c"), false);
// Frontier entries all retained
assert.equal(nonOverride["ch:a"], 100);
assert.equal(nonOverride["ch:b"], 200);
assert.equal(nonOverride["ch:c"], 300);
assert.equal(frontiers.get("ch:a"), 100);
assert.equal(frontiers.get("ch:b"), 200);
assert.equal(frontiers.get("ch:c"), 300);
});
// ---------------------------------------------------------------------------
@@ -357,6 +652,27 @@ test("sanitizeContexts_partialGroup_droppedFrontierRetained", () => {
assert.equal(result[`${OV_S_PREFIX}channel:x`], undefined);
});
test("sanitizeContexts_liveGroupSiblingKeyTooLong_groupDropped", () => {
// ov_s: (5 bytes) + suffix of 252 bytes = 257 bytes wire key → drop group
const suffix252 = asciiOfBytes(252);
const raw = {
[suffix252]: 10,
[`${OV_S_PREFIX}${suffix252}`]: 1,
[`${OV_C_PREFIX}${suffix252}`]: 0,
[`${OV_B_PREFIX}${suffix252}`]: 9,
};
const result = sanitizeContexts(raw);
assert.equal(result[`${OV_S_PREFIX}${suffix252}`], undefined);
assert.equal(result[`${OV_C_PREFIX}${suffix252}`], undefined);
});
test("sanitizeContexts_tombstoneKeyTooLong_groupDropped", () => {
const suffix252 = asciiOfBytes(252);
const raw = { [suffix252]: 0, [`${OV_C_PREFIX}${suffix252}`]: 3 };
const result = sanitizeContexts(raw);
assert.equal(result[`${OV_C_PREFIX}${suffix252}`], undefined);
});
test("sanitizeContexts_normalEntriesUnaffected", () => {
const raw = {
"ch:normal": 5000,
@@ -380,56 +696,12 @@ test("sanitizeContexts_invalidFrontierEntry_dropped", () => {
});
test("sanitizeContexts_escapedFrontierKey_passesThrough", () => {
// esc: prefixed frontier keys must pass through unchanged
// esc: prefixed frontier keys must pass through unchanged in the flat map
const raw = { "esc:ov_s:suspicious-ctx": 1000 };
const result = sanitizeContexts(raw);
assert.equal(result["esc:ov_s:suspicious-ctx"], 1000);
});
// ---------------------------------------------------------------------------
// extractOverrideRegisters — from sanitized flat map
// ---------------------------------------------------------------------------
test("extractOverrideRegisters_liveGroup_reconstructsRegister", () => {
const contexts = new Map([
["channel:x", 1000],
[`${OV_S_PREFIX}channel:x`, 2],
[`${OV_C_PREFIX}channel:x`, 1],
[`${OV_B_PREFIX}channel:x`, 900],
]);
const regs = extractOverrideRegisters(contexts);
assert.equal(regs.size, 1);
assert.deepEqual(regs.get("channel:x"), { s: 2, c: 1, b: 900 });
});
test("extractOverrideRegisters_tombstoneFloor_reconstructsRegister", () => {
const contexts = new Map([
["channel:x", 1000],
[`${OV_C_PREFIX}channel:x`, 5],
]);
const regs = extractOverrideRegisters(contexts);
assert.equal(regs.size, 1);
// Floor shape: s=0, c=5, b=0
assert.deepEqual(regs.get("channel:x"), { s: 0, c: 5, b: 0 });
});
test("extractOverrideRegisters_noOverrideKeys_returnsEmptyMap", () => {
const contexts = new Map([["channel:x", 1000]]);
const regs = extractOverrideRegisters(contexts);
assert.equal(regs.size, 0);
});
test("extractOverrideRegisters_acceptsPlainRecord", () => {
const contexts = {
"channel:x": 1000,
[`${OV_S_PREFIX}channel:x`]: 1,
[`${OV_C_PREFIX}channel:x`]: 0,
[`${OV_B_PREFIX}channel:x`]: 900,
};
const regs = extractOverrideRegisters(contexts);
assert.deepEqual(regs.get("channel:x"), { s: 1, c: 0, b: 900 });
});
// ---------------------------------------------------------------------------
// mergeOverrideRegisterMaps — across multiple blobs
// ---------------------------------------------------------------------------
@@ -469,51 +741,87 @@ test("mergeOverrideRegisterMaps_threeWayMerge_takesGlobalMax", () => {
assert.deepEqual(merged.get("ctx"), { s: 5, c: 3, b: 30 });
});
test("mergeOverrideRegisterMaps_uint32Max_commutative", () => {
const UINT32_MAX = 4294967295;
const a = new Map([["ctx", { s: UINT32_MAX, c: 0, b: UINT32_MAX }]]);
const b = new Map([["ctx", { s: 0, c: UINT32_MAX, b: 0 }]]);
const ab = mergeOverrideRegisterMaps(a, b);
const ba = mergeOverrideRegisterMaps(b, a);
assert.deepEqual(ab.get("ctx"), ba.get("ctx"));
assert.deepEqual(ab.get("ctx"), {
s: UINT32_MAX,
c: UINT32_MAX,
b: UINT32_MAX,
});
});
// ---------------------------------------------------------------------------
// Round-trip: encode → sanitize → extract produces original register
// Round-trip: encodeOverrideGroup → parseContexts produces original register
// ---------------------------------------------------------------------------
test("roundTrip_liveRegister_survivesEncodeAndExtract", () => {
test("roundTrip_liveRegister_survivesEncodeAndParse", () => {
const reg = { s: 2, c: 1, b: 100 };
const frontier = 50;
const rawCtx = "channel-uuid-1234";
// Encode (as a publisher would)
const patch = encodeOverrideGroup(rawCtx, reg, frontier);
// Simulate inclusion in a blob's contexts map (add frontier entry too)
const rawContexts = { [rawCtx]: frontier, ...patch };
// Sanitize (as a receiver would)
const sanitized = sanitizeContexts(rawContexts);
// Extract registers
const regs = extractOverrideRegisters(sanitized);
const { frontiers, overrides } = parseContexts(rawContexts);
assert.deepEqual(regs.get(rawCtx), reg);
assert.equal(frontiers.get(rawCtx), frontier);
assert.deepEqual(overrides.get(rawCtx), reg);
});
test("roundTrip_tombstoneFloor_survivesEncodeAndExtract", () => {
// A dead register compacts to tombstone floor on encode
const reg = { s: 0, c: 5, b: 0 }; // already a floor shape
test("roundTrip_tombstoneFloor_survivesEncodeAndParse", () => {
const reg = { s: 0, c: 5, b: 0 };
const frontier = 0;
const rawCtx = "channel-uuid-5678";
const patch = encodeOverrideGroup(rawCtx, reg, frontier);
const rawContexts = { [rawCtx]: frontier, ...patch };
const sanitized = sanitizeContexts(rawContexts);
const regs = extractOverrideRegisters(sanitized);
const { frontiers, overrides } = parseContexts(rawContexts);
// Floor is s=0, c=5, b=0
assert.deepEqual(regs.get(rawCtx), { s: 0, c: 5, b: 0 });
assert.equal(frontiers.get(rawCtx), frontier);
assert.deepEqual(overrides.get(rawCtx), { s: 0, c: 5, b: 0 });
});
test("roundTrip_virginRegister_omittedFromWire", () => {
const reg = { s: 0, c: 0, b: 0 };
const frontier = 0;
const rawCtx = "channel-uuid-virgin";
const patch = encodeOverrideGroup(rawCtx, reg, frontier);
assert.deepEqual(patch, {}); // nothing emitted
const rawContexts = { [rawCtx]: frontier };
const sanitized = sanitizeContexts(rawContexts);
const regs = extractOverrideRegisters(sanitized);
assert.equal(regs.has(rawCtx), false);
const patch = encodeOverrideGroup(rawCtx, reg, 0);
assert.deepEqual(patch, {});
const rawContexts = { [rawCtx]: 0 };
const { frontiers, overrides } = parseContexts(rawContexts);
assert.equal(frontiers.get(rawCtx), 0);
assert.equal(overrides.has(rawCtx), false);
});
test("roundTrip_escapedRawCtx_survivesEncodeAndParse", () => {
// A raw context ID that starts with ov_: publisher escapes frontier key.
// encodeOverrideGroup does NOT escape the raw ctx suffix in ov_* keys —
// the spec says ov_* keys use the raw suffix. The frontier key is
// published escaped (esc:ov_s:raw) by the publisher — simulate that here.
const rawCtx = "ov_s:tricky";
const reg = { s: 1, c: 0, b: 10 };
const frontier = 8;
// Publisher encodes: override keys use raw suffix, frontier key is escaped.
const patch = encodeOverrideGroup(rawCtx, reg, frontier);
// patch has ov_s:ov_s:tricky, ov_c:ov_s:tricky, ov_b:ov_s:tricky
const rawContexts = {
[`esc:${rawCtx}`]: frontier, // frontier published escaped
...patch,
};
const { frontiers, overrides } = parseContexts(rawContexts);
// After unescape: frontier keyed by raw ctx `ov_s:tricky`
assert.equal(frontiers.get(rawCtx), frontier);
// Override keyed by suffix after `ov_s:` → `ov_s:tricky`
assert.deepEqual(overrides.get(rawCtx), reg);
// liveness evaluation uses the same raw key for both
const ov = overrides.get(rawCtx);
const f = frontiers.get(rawCtx) ?? 0;
// s=1, c=0, b=10, f=8 → S>0 ∧ F(8)<=B(10) ∧ S(1)>C(0) → active
assert.equal(isOverrideActive(ov, f), true);
});

View File

@@ -4,11 +4,10 @@ import {
isValidBlob,
isValidReadStateDTag,
mergeOverrideRegisters,
OV_B_PREFIX,
OV_C_PREFIX,
OV_S_PREFIX,
parseContexts,
sanitizeContexts,
type OverrideRegister,
type ParsedContexts,
type ReadStateBlob,
} from "@/features/channels/readState/readStateFormat";
@@ -17,6 +16,8 @@ export type ReadStateDecrypt = (ciphertext: string) => Promise<string>;
export type ParsedReadStateEvent = {
dTag: string;
blob: ReadStateBlob;
/** Structured decode: frontiers keyed by raw ctx ID, overrides by raw ctx ID. */
contexts: ParsedContexts;
createdAt: number;
};
@@ -41,13 +42,15 @@ export async function parseReadStateEvent(
const plaintext = await decrypt(event.content);
const parsed = JSON.parse(plaintext);
if (!isValidBlob(parsed)) return null;
const sanitized = sanitizeContexts(parsed.contexts);
return {
dTag,
blob: {
v: 1,
client_id: parsed.client_id,
contexts: sanitizeContexts(parsed.contexts),
contexts: sanitized,
},
contexts: parseContexts(parsed.contexts),
createdAt: event.created_at,
};
} catch (error) {
@@ -60,61 +63,15 @@ export async function parseReadStateEvent(
}
/**
* Extract all override registers from a sanitized contexts map.
*
* Returns a Map from raw context ID to `OverrideRegister`. A tombstone floor
* (`ov_c:` only) is returned as `{ s: 0, c: <floor>, b: 0 }` — the same
* shape as a dead register, which is correct: the floor's sole purpose is to
* block counter reuse, and the liveness predicate evaluates to false for it.
*
* This is the read-side counterpart of `encodeOverrideGroup`: it reconstructs
* the register from the flat integer map that `sanitizeContexts` produced.
* The input MUST already have been through `sanitizeContexts` (so partial
* groups are absent — they were rejected at validation time).
*/
export function extractOverrideRegisters(
contexts: ReadonlyMap<string, number> | Record<string, number>,
): Map<string, OverrideRegister> {
// Collect s/c/b values per context suffix.
const sVals = new Map<string, number>();
const cVals = new Map<string, number>();
const bVals = new Map<string, number>();
const iterate =
contexts instanceof Map
? [...contexts.entries()]
: Object.entries(contexts);
for (const [key, value] of iterate) {
if (key.startsWith(OV_S_PREFIX))
sVals.set(key.slice(OV_S_PREFIX.length), value);
else if (key.startsWith(OV_C_PREFIX))
cVals.set(key.slice(OV_C_PREFIX.length), value);
else if (key.startsWith(OV_B_PREFIX))
bVals.set(key.slice(OV_B_PREFIX.length), value);
}
const result = new Map<string, OverrideRegister>();
const ctxSet = new Set([...sVals.keys(), ...cVals.keys(), ...bVals.keys()]);
for (const ctx of ctxSet) {
result.set(ctx, {
s: sVals.get(ctx) ?? 0,
c: cVals.get(ctx) ?? 0,
b: bVals.get(ctx) ?? 0,
});
}
return result;
}
/**
* Merge override registers from multiple sanitized contexts maps.
* Merge override registers from multiple register maps.
*
* Applies componentwise `max()` across all sources for each context suffix,
* returning a single Map from raw context ID to merged `OverrideRegister`.
* This is the register-level analogue of the frontier `max()` merge.
* Slices 2–3 call this after collecting `ParsedReadStateEvent.contexts.overrides`
* maps from each parsed event.
*/
export function mergeOverrideRegisterMaps(
...maps: Array<Map<string, OverrideRegister>>
...maps: Array<ReadonlyMap<string, OverrideRegister>>
): Map<string, OverrideRegister> {
const result = new Map<string, OverrideRegister>();
for (const source of maps) {
@@ -131,21 +88,23 @@ export async function mergeReadStateEvents(
pubkey: string,
decrypt?: ReadStateDecrypt,
): Promise<Map<string, number>> {
const contexts = new Map<string, number>();
const frontiers = new Map<string, number>();
for (const event of events) {
const parsed = await parseReadStateEvent(event, pubkey, decrypt);
if (!parsed) continue;
for (const [contextId, timestamp] of Object.entries(parsed.blob.contexts)) {
const current = contexts.get(contextId) ?? 0;
if (timestamp > current) {
contexts.set(contextId, timestamp);
// Use the structured frontiers (keyed by raw ctx ID, unescaped) rather
// than the flat blob.contexts, which retains wire keys for readStateManager.
for (const [rawCtx, ts] of parsed.contexts.frontiers) {
const current = frontiers.get(rawCtx) ?? 0;
if (ts > current) {
frontiers.set(rawCtx, ts);
}
}
}
return contexts;
return frontiers;
}
export function getSnapshotReadTimestamp(