feat(readState): add NIP-RS override layer format and parse (slice 1)

Implements the manual-unread override register algebra from the merged
NIP-RS spec (docs/nips/NIP-RS.md, merge commit 209536ade) as pure
functions in the format and snapshot layers. No manager or UI wiring —
slice 1 only; slices 2 and 3 depend on the types exported here.

Changes in readStateFormat.ts:
- escapeFrontierKey / unescapeFrontierKey: bijection for raw context IDs
  that begin with ov_ or esc: (Reserved Namespace spec rule).
- isOverrideKey: classifies ov_s:, ov_c:, ov_b: wire keys.
- OverrideRegister interface: the (S, C, B) triple for one context.
- OverrideLiveness interface: evaluation result for slice 3 consumers.
- isOverrideActive: liveness predicate S>0 AND F<=B AND S>C (clear-wins,
  mandatory per spec Tie Policy section).
- mergeOverrideRegisters: componentwise max() — commutative, associative,
  idempotent; matches model.py::merge_reg_b semantics exactly.
- encodeOverrideGroup: canonical wire encoding (live three-key / tombstone
  floor / virgin-omit rules per Mandatory Canonical Publication section).
- partitionOverrideGroups: group-first override validation required by
  Content Validation — collects complete logical groups before any
  per-entry processing; partial groups are dropped, frontier retained.
- ValidatedOverrideGroup discriminated union: live | floor.
- sanitizeContexts: extended to run group-first validation (step 1),
  then per-entry sanitization on non-override keys (step 2), then fold
  validated groups back as integers (step 3). Existing callers unaffected.

Changes in readStateSnapshot.ts:
- extractOverrideRegisters: reconstructs register map from a sanitized
  flat contexts map (read-side counterpart of encodeOverrideGroup).
- mergeOverrideRegisterMaps: merges register maps from multiple blobs via
  componentwise max() (register-level analogue of frontier merge).

New file readStateOverride.test.mjs (58 tests):
- escape/unescape bijection including double-escape and round-trip cases.
- isOverrideKey coverage for all three prefixes and negative cases.
- isOverrideActive: live, virgin, frontier-dominated, tie (clear-wins),
  clear-exceeds-set, and tombstone-floor cases.
- mergeOverrideRegisters: componentwise max, commutativity, idempotence,
  associativity.
- encodeOverrideGroup: all canonical shapes (live, dead, tie, frontier-
  dominated, pre-compacted floor, virgin).
- partitionOverrideGroups: live group, tombstone floor, partial group
  (s-only, s+c only), invalid value, negative value, invalid floor c,
  multi-context with mixed validity.
- sanitizeContexts: live group, tombstone floor, partial-group dropped
  with frontier retained, normal entries, escaped frontier key pass-through.
- extractOverrideRegisters: live group, floor, no overrides, plain record.
- mergeOverrideRegisterMaps: two-map, disjoint, empty, single, three-way.
- Round-trip: live register, tombstone floor, and virgin register.

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 13:31:10 -04:00
parent 209536ade6
commit 72b1d4f368
3 changed files with 872 additions and 1 deletions

View File

@@ -35,6 +35,257 @@ export const READ_STATE_MAX_SLOTS = 8;
export const MSG_PREFIX = "msg:";
export const THREAD_PREFIX = "thread:";
// ---------------------------------------------------------------------------
// Reserved namespace — override key prefixes and escape marker
//
// NIP-RS spec (Reserved Namespace section): the `ov_` stem and `esc:` marker
// are reserved for the manual-unread override layer. Any raw context ID that
// starts with either prefix MUST be escaped before use as a frontier key.
// Override counter keys (`ov_s:`, `ov_c:`, `ov_b:`) are NEVER escaped — they
// are their own wire namespace and are not legitimate frontier keys.
// ---------------------------------------------------------------------------
export const OV_S_PREFIX = "ov_s:" as const;
export const OV_C_PREFIX = "ov_c:" as const;
export const OV_B_PREFIX = "ov_b:" as const;
const OV_STEM = "ov_" as const;
const ESC_PREFIX = "esc:" as const;
/** True for wire keys that are override counters (`ov_s:`, `ov_c:`, `ov_b:`). */
export function isOverrideKey(key: string): boolean {
return (
key.startsWith(OV_S_PREFIX) ||
key.startsWith(OV_C_PREFIX) ||
key.startsWith(OV_B_PREFIX)
);
}
/**
* Escape a raw context ID for use as a frontier key.
* Prepends `esc:` when the raw ID begins with `ov_` or `esc:`.
* No-op for the context shapes Buzz generates (UUID, msg:hex64, thread:hex64).
*/
export function escapeFrontierKey(rawCtx: string): string {
if (rawCtx.startsWith(OV_STEM) || rawCtx.startsWith(ESC_PREFIX)) {
return ESC_PREFIX + rawCtx;
}
return rawCtx;
}
/**
* Recover the raw context ID from a frontier wire key.
* Strips exactly one leading `esc:` if present; never strips more.
*/
export function unescapeFrontierKey(wireKey: string): string {
if (wireKey.startsWith(ESC_PREFIX)) {
return wireKey.slice(ESC_PREFIX.length);
}
return wireKey;
}
// ---------------------------------------------------------------------------
// Override register — the (S, C, B) triple for a single context
// ---------------------------------------------------------------------------
/**
* A fully-validated override register for one context.
*
* A live override (`isOverrideActive === true`) carries all three counters.
* A tombstone floor carries only `c` (S=0, B=0).
* A virgin register (never activated) is represented by the absence of this
* type entirely — it is omitted from the wire.
*
* 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.
*/
export interface OverrideRegister {
/** Set counter S — incremented on each mark-unread. */
readonly s: number;
/** Clear counter C — incremented on each mark-read. */
readonly c: number;
/** Baseline B — effective frontier at the time of the most recent mark-unread. */
readonly b: number;
}
/**
* Result of evaluating whether a context has an active manual-unread override.
*
* `active` means: `S > 0 AND F <= B AND S > C` (clear-wins tie policy).
* `frontier` is the merged effective frontier for the context (used by the
* unread verdict in the UI layer).
*/
export interface OverrideLiveness {
readonly active: boolean;
/** The merged effective frontier value passed to `isOverrideActive`. */
readonly frontier: number;
}
/**
* Evaluate the liveness predicate from the spec (clear-wins, mandatory).
*
* override_active(S, C, B, F) = S > 0 AND F <= B AND S > C
*
* F is the merged effective frontier for the context — typically
* `contexts.get(rawCtxId) ?? 0` after applying the hierarchical frontier rule.
*/
export function isOverrideActive(
reg: OverrideRegister,
frontier: number,
): boolean {
return reg.s > 0 && frontier <= reg.b && reg.s > reg.c;
}
/**
* Componentwise `max()` merge of two override registers.
* Commutative, associative, idempotent — same as the frontier max() merge.
*/
export function mergeOverrideRegisters(
a: OverrideRegister,
b: OverrideRegister,
): OverrideRegister {
return {
s: Math.max(a.s, b.s),
c: Math.max(a.c, b.c),
b: Math.max(a.b, b.b),
};
}
/**
* Canonical wire encoding of an override register for a given context.
*
* Applies the tombstone-floor / virgin-omit rules from the spec
* (Mandatory Canonical Publication section) so publishers always emit
* a correctly-shaped group.
*
* Returns a partial `contexts` patch to be merged into the blob before
* serialization. Caller is responsible for including the frontier entry
* for `rawCtxId` separately.
*
* @param rawCtxId Raw (unescaped) context ID — e.g. `"abc-channel-uuid"`.
* @param reg Override register to encode.
* @param frontier Current merged effective frontier for this context.
*/
export function encodeOverrideGroup(
rawCtxId: string,
reg: OverrideRegister,
frontier: number,
): Record<string, number> {
const virgin = reg.s === 0 && reg.c === 0;
if (virgin) {
// Virgin registers are omitted entirely from the wire.
return {};
}
const active = isOverrideActive(reg, frontier);
if (active) {
// Live override: publish all three keys.
return {
[`${OV_S_PREFIX}${rawCtxId}`]: reg.s,
[`${OV_C_PREFIX}${rawCtxId}`]: reg.c,
[`${OV_B_PREFIX}${rawCtxId}`]: reg.b,
};
}
// Dead override: tombstone floor — only ov_c: with value max(S, C).
return {
[`${OV_C_PREFIX}${rawCtxId}`]: Math.max(reg.s, reg.c),
};
}
// ---------------------------------------------------------------------------
// 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.
// ---------------------------------------------------------------------------
/**
* 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).
*/
export type ValidatedOverrideGroup =
| { kind: "live"; reg: OverrideRegister }
| { kind: "floor"; c: number };
/**
* 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`.
*
* @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).
*/
export 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 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);
} else if (key.startsWith(OV_C_PREFIX)) {
cMap.set(key.slice(OV_C_PREFIX.length), value);
} else if (key.startsWith(OV_B_PREFIX)) {
bMap.set(key.slice(OV_B_PREFIX.length), 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 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;
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 });
} 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.
}
}
return { overrides, nonOverride };
}
// ---------------------------------------------------------------------------
// Core validation and sanitization
// ---------------------------------------------------------------------------
const EVENT_ID_PATTERN = /^[0-9a-f]{64}$/;
export function maxReadAt(...markers: Array<number | null>): number | null {
@@ -94,16 +345,45 @@ export function isValidBlob(obj: unknown): obj is ReadStateBlob {
return true;
}
/**
* Sanitize and validate a raw contexts object from a decoded blob.
*
* 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.
*/
export function sanitizeContexts(
contexts: Record<string, unknown>,
): Record<string, number> {
// 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(contexts)) {
for (const [key, value] of Object.entries(nonOverride)) {
if (new TextEncoder().encode(key).length > 256) continue;
if (typeof value !== "number" || !Number.isInteger(value)) continue;
if (value < 0 || value > 4294967295) continue;
result[key] = value;
}
// Step 3: fold validated override groups back as integer entries.
for (const [ctx, group] of overrides) {
if (group.kind === "floor") {
result[`${OV_C_PREFIX}${ctx}`] = group.c;
} else {
result[`${OV_S_PREFIX}${ctx}`] = group.reg.s;
result[`${OV_C_PREFIX}${ctx}`] = group.reg.c;
result[`${OV_B_PREFIX}${ctx}`] = group.reg.b;
}
}
return result;
}

View File

@@ -0,0 +1,519 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
encodeOverrideGroup,
escapeFrontierKey,
isOverrideActive,
isOverrideKey,
mergeOverrideRegisters,
OV_B_PREFIX,
OV_C_PREFIX,
OV_S_PREFIX,
partitionOverrideGroups,
sanitizeContexts,
unescapeFrontierKey,
} from "./readStateFormat.ts";
import {
extractOverrideRegisters,
mergeOverrideRegisterMaps,
} from "./readStateSnapshot.ts";
// ---------------------------------------------------------------------------
// escapeFrontierKey / unescapeFrontierKey
// ---------------------------------------------------------------------------
test("escapeFrontierKey_normalCtx_passesThrough", () => {
assert.equal(escapeFrontierKey("some-channel-uuid"), "some-channel-uuid");
});
test("escapeFrontierKey_ovPrefix_prepentsEsc", () => {
assert.equal(escapeFrontierKey("ov_s:evil"), "esc:ov_s:evil");
});
test("escapeFrontierKey_escPrefix_prepentsEsc", () => {
assert.equal(escapeFrontierKey("esc:foo"), "esc:esc:foo");
});
test("escapeFrontierKey_ovStem_prepentsEsc", () => {
assert.equal(escapeFrontierKey("ov_c:something"), "esc:ov_c:something");
});
test("unescapeFrontierKey_noEscPrefix_passesThrough", () => {
assert.equal(unescapeFrontierKey("some-channel-uuid"), "some-channel-uuid");
});
test("unescapeFrontierKey_singleEscPrefix_strips", () => {
assert.equal(unescapeFrontierKey("esc:ov_s:evil"), "ov_s:evil");
});
test("unescapeFrontierKey_doubleEscPrefix_stripsExactlyOne", () => {
// Must NOT strip more than one esc: prefix per spec.
assert.equal(unescapeFrontierKey("esc:esc:foo"), "esc:foo");
});
test("escape_then_unescape_isIdentity", () => {
const raw = "ov_s:tricky";
assert.equal(unescapeFrontierKey(escapeFrontierKey(raw)), raw);
});
test("escape_then_unescape_isIdentity_escPrefixed", () => {
const raw = "esc:already-escaped";
assert.equal(unescapeFrontierKey(escapeFrontierKey(raw)), raw);
});
test("escape_then_unescape_isIdentity_normalCtx", () => {
const raw = "msg:deadbeef";
assert.equal(unescapeFrontierKey(escapeFrontierKey(raw)), raw);
});
// ---------------------------------------------------------------------------
// isOverrideKey
// ---------------------------------------------------------------------------
test("isOverrideKey_ovS_returnsTrue", () => {
assert.equal(isOverrideKey("ov_s:ctx"), true);
});
test("isOverrideKey_ovC_returnsTrue", () => {
assert.equal(isOverrideKey("ov_c:ctx"), true);
});
test("isOverrideKey_ovB_returnsTrue", () => {
assert.equal(isOverrideKey("ov_b:ctx"), true);
});
test("isOverrideKey_frontierKey_returnsFalse", () => {
assert.equal(isOverrideKey("some-channel"), false);
});
test("isOverrideKey_escapedKey_returnsFalse", () => {
// esc:ov_s:… is a FRONTIER key, not an override key.
assert.equal(isOverrideKey("esc:ov_s:ctx"), false);
});
// ---------------------------------------------------------------------------
// isOverrideActive — liveness predicate
// ---------------------------------------------------------------------------
test("isOverrideActive_liveOverride_returnsTrue", () => {
// S=2, C=1, B=100, F=50 → S>0 ∧ F<=B ∧ S>C
assert.equal(isOverrideActive({ s: 2, c: 1, b: 100 }, 50), true);
});
test("isOverrideActive_virginRegister_returnsFalse", () => {
assert.equal(isOverrideActive({ s: 0, c: 0, b: 0 }, 0), false);
});
test("isOverrideActive_frontierExceedsBaseline_returnsFalse", () => {
// Frontier advance dominates: F=200 > B=100
assert.equal(isOverrideActive({ s: 1, c: 0, b: 100 }, 200), false);
});
test("isOverrideActive_frontierEqualsBaseline_returnsTrue", () => {
// F <= B (equals case): still active
assert.equal(isOverrideActive({ s: 1, c: 0, b: 100 }, 100), true);
});
test("isOverrideActive_clearWinsTie_returnsFalse", () => {
// S == C with S > 0 → clear-wins: inactive
assert.equal(isOverrideActive({ s: 1, c: 1, b: 100 }, 50), false);
});
test("isOverrideActive_clearCounter_exceedsSet_returnsFalse", () => {
assert.equal(isOverrideActive({ s: 1, c: 2, b: 100 }, 50), false);
});
test("isOverrideActive_tombstoneFloor_returnsFalse", () => {
// Tombstone floor shape: s=0, c=k, b=0
assert.equal(isOverrideActive({ s: 0, c: 5, b: 0 }, 0), false);
});
// ---------------------------------------------------------------------------
// mergeOverrideRegisters — componentwise max()
// ---------------------------------------------------------------------------
test("mergeOverrideRegisters_takesMaxPerComponent", () => {
const a = { s: 3, c: 1, b: 50 };
const b = { s: 1, c: 4, b: 80 };
assert.deepEqual(mergeOverrideRegisters(a, b), { s: 3, c: 4, b: 80 });
});
test("mergeOverrideRegisters_commutative", () => {
const a = { s: 5, c: 2, b: 10 };
const b = { s: 2, c: 7, b: 30 };
assert.deepEqual(mergeOverrideRegisters(a, b), mergeOverrideRegisters(b, a));
});
test("mergeOverrideRegisters_idempotent", () => {
const a = { s: 3, c: 3, b: 100 };
assert.deepEqual(mergeOverrideRegisters(a, a), a);
});
test("mergeOverrideRegisters_associative", () => {
const a = { s: 1, c: 0, b: 10 };
const b = { s: 0, c: 2, b: 20 };
const c = { s: 3, c: 1, b: 5 };
assert.deepEqual(
mergeOverrideRegisters(mergeOverrideRegisters(a, b), c),
mergeOverrideRegisters(a, mergeOverrideRegisters(b, c)),
);
});
// ---------------------------------------------------------------------------
// encodeOverrideGroup — canonical wire encoding
// ---------------------------------------------------------------------------
test("encodeOverrideGroup_virginRegister_returnsEmpty", () => {
const patch = encodeOverrideGroup("ctx", { s: 0, c: 0, b: 0 }, 0);
assert.deepEqual(patch, {});
});
test("encodeOverrideGroup_liveOverride_returnsThreeKeys", () => {
const patch = encodeOverrideGroup("ctx", { s: 2, c: 1, b: 100 }, 50);
assert.deepEqual(patch, {
[`${OV_S_PREFIX}ctx`]: 2,
[`${OV_C_PREFIX}ctx`]: 1,
[`${OV_B_PREFIX}ctx`]: 100,
});
});
test("encodeOverrideGroup_deadOverride_returnsOnlyOvC", () => {
// Dead: S=1, C=2 (clear-wins) → tombstone floor ov_c = max(1,2) = 2
const patch = encodeOverrideGroup("ctx", { s: 1, c: 2, b: 100 }, 50);
assert.deepEqual(patch, { [`${OV_C_PREFIX}ctx`]: 2 });
});
test("encodeOverrideGroup_tieRegister_returnsOnlyOvC", () => {
// Tie S==C → clear-wins → tombstone floor
const patch = encodeOverrideGroup("ctx", { s: 3, c: 3, b: 100 }, 50);
assert.deepEqual(patch, { [`${OV_C_PREFIX}ctx`]: 3 });
});
test("encodeOverrideGroup_frontierDominated_returnsOnlyOvC", () => {
// Frontier past baseline: live rule fails → tombstone floor
const patch = encodeOverrideGroup("ctx", { s: 1, c: 0, b: 100 }, 200);
assert.deepEqual(patch, { [`${OV_C_PREFIX}ctx`]: 1 });
});
test("encodeOverrideGroup_tombstoneFloorShape_returnsOnlyOvC", () => {
// Pre-compacted floor: s=0, c=5, b=0 → dead → tombstone max(0,5)=5
const patch = encodeOverrideGroup("ctx", { s: 0, c: 5, b: 0 }, 0);
assert.deepEqual(patch, { [`${OV_C_PREFIX}ctx`]: 5 });
});
// ---------------------------------------------------------------------------
// partitionOverrideGroups — group-first validation
// ---------------------------------------------------------------------------
test("partitionOverrideGroups_liveGroup_validates", () => {
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);
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 });
});
test("partitionOverrideGroups_tombstoneFloor_validates", () => {
const raw = {
"channel:x": 1000,
"ov_c:channel:x": 5,
};
const { overrides, nonOverride } = partitionOverrideGroups(raw);
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
const raw = {
"channel:x": 1000,
"ov_s:channel:x": 1,
};
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);
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);
assert.equal(overrides.size, 0);
assert.deepEqual(nonOverride, { "channel:x": 1000 });
});
test("partitionOverrideGroups_negativeValue_groupDropped", () => {
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);
assert.equal(overrides.size, 0);
assert.deepEqual(nonOverride, { "channel:x": 1000 });
});
test("partitionOverrideGroups_ovCInvalidValue_tombstoneDropped", () => {
// Tombstone floor with non-integer c
const raw = {
"channel:x": 1000,
"ov_c:channel:x": 1.5,
};
const { overrides, nonOverride } = partitionOverrideGroups(raw);
assert.equal(overrides.size, 0);
assert.deepEqual(nonOverride, { "channel:x": 1000 });
});
test("partitionOverrideGroups_multipleContexts_allValidated", () => {
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
"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");
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);
});
// ---------------------------------------------------------------------------
// sanitizeContexts — end-to-end with override group validation
// ---------------------------------------------------------------------------
test("sanitizeContexts_liveGroup_passesThrough", () => {
const raw = {
"channel:x": 1000,
"ov_s:channel:x": 1,
"ov_c:channel:x": 0,
"ov_b:channel:x": 900,
};
const result = sanitizeContexts(raw);
assert.equal(result["channel:x"], 1000);
assert.equal(result[`${OV_S_PREFIX}channel:x`], 1);
assert.equal(result[`${OV_C_PREFIX}channel:x`], 0);
assert.equal(result[`${OV_B_PREFIX}channel:x`], 900);
});
test("sanitizeContexts_tombstoneFloor_passesThrough", () => {
const raw = {
"channel:x": 1000,
"ov_c:channel:x": 5,
};
const result = sanitizeContexts(raw);
assert.equal(result["channel:x"], 1000);
assert.equal(result[`${OV_C_PREFIX}channel:x`], 5);
assert.equal(result[`${OV_S_PREFIX}channel:x`], undefined);
assert.equal(result[`${OV_B_PREFIX}channel:x`], undefined);
});
test("sanitizeContexts_partialGroup_droppedFrontierRetained", () => {
// Partial group rejected; frontier entry kept
const raw = {
"channel:x": 1000,
"ov_s:channel:x": 1,
};
const result = sanitizeContexts(raw);
assert.equal(result["channel:x"], 1000);
assert.equal(result[`${OV_S_PREFIX}channel:x`], undefined);
});
test("sanitizeContexts_normalEntriesUnaffected", () => {
const raw = {
"ch:normal": 5000,
"msg:abc": 3000,
"thread:def": 2000,
};
const result = sanitizeContexts(raw);
assert.equal(result["ch:normal"], 5000);
assert.equal(result["msg:abc"], 3000);
assert.equal(result["thread:def"], 2000);
});
test("sanitizeContexts_invalidFrontierEntry_dropped", () => {
const raw = {
"good:key": 100,
"bad:key": "not-a-number",
};
const result = sanitizeContexts(raw);
assert.equal(result["good:key"], 100);
assert.equal(result["bad:key"], undefined);
});
test("sanitizeContexts_escapedFrontierKey_passesThrough", () => {
// esc: prefixed frontier keys must pass through unchanged
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
// ---------------------------------------------------------------------------
test("mergeOverrideRegisterMaps_twoMaps_takesComponentwiseMax", () => {
const a = new Map([["ctx", { s: 3, c: 1, b: 50 }]]);
const b = new Map([["ctx", { s: 1, c: 4, b: 80 }]]);
const merged = mergeOverrideRegisterMaps(a, b);
assert.deepEqual(merged.get("ctx"), { s: 3, c: 4, b: 80 });
});
test("mergeOverrideRegisterMaps_disjointContexts_bothPresent", () => {
const a = new Map([["ctx:A", { s: 1, c: 0, b: 10 }]]);
const b = new Map([["ctx:B", { s: 0, c: 2, b: 0 }]]);
const merged = mergeOverrideRegisterMaps(a, b);
assert.equal(merged.size, 2);
assert.deepEqual(merged.get("ctx:A"), { s: 1, c: 0, b: 10 });
assert.deepEqual(merged.get("ctx:B"), { s: 0, c: 2, b: 0 });
});
test("mergeOverrideRegisterMaps_emptyMaps_returnsEmpty", () => {
const merged = mergeOverrideRegisterMaps(new Map(), new Map());
assert.equal(merged.size, 0);
});
test("mergeOverrideRegisterMaps_singleMap_isIdentity", () => {
const m = new Map([["ctx", { s: 2, c: 1, b: 50 }]]);
const merged = mergeOverrideRegisterMaps(m);
assert.deepEqual(merged.get("ctx"), { s: 2, c: 1, b: 50 });
});
test("mergeOverrideRegisterMaps_threeWayMerge_takesGlobalMax", () => {
const a = new Map([["ctx", { s: 5, c: 0, b: 10 }]]);
const b = new Map([["ctx", { s: 1, c: 3, b: 20 }]]);
const c = new Map([["ctx", { s: 2, c: 1, b: 30 }]]);
const merged = mergeOverrideRegisterMaps(a, b, c);
assert.deepEqual(merged.get("ctx"), { s: 5, c: 3, b: 30 });
});
// ---------------------------------------------------------------------------
// Round-trip: encode → sanitize → extract produces original register
// ---------------------------------------------------------------------------
test("roundTrip_liveRegister_survivesEncodeAndExtract", () => {
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);
assert.deepEqual(regs.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
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);
// Floor is s=0, c=5, b=0
assert.deepEqual(regs.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);
});

View File

@@ -3,7 +3,12 @@ import type { RelayEvent } from "@/shared/api/types";
import {
isValidBlob,
isValidReadStateDTag,
mergeOverrideRegisters,
OV_B_PREFIX,
OV_C_PREFIX,
OV_S_PREFIX,
sanitizeContexts,
type OverrideRegister,
type ReadStateBlob,
} from "@/features/channels/readState/readStateFormat";
@@ -54,6 +59,73 @@ 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.
*
* 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.
*/
export function mergeOverrideRegisterMaps(
...maps: Array<Map<string, OverrideRegister>>
): Map<string, OverrideRegister> {
const result = new Map<string, OverrideRegister>();
for (const source of maps) {
for (const [ctx, reg] of source) {
const existing = result.get(ctx);
result.set(ctx, existing ? mergeOverrideRegisters(existing, reg) : reg);
}
}
return result;
}
export async function mergeReadStateEvents(
events: RelayEvent[],
pubkey: string,