refactor: complete rolling-hash rollout — composite/srcRel/dstRel keys + key-tracked cache invalidation
CI / test (push) Successful in 6m24s
CI / benchmark (push) Successful in 1m0s
CI / publish (push) Successful in 10s

Completes the rolling-hash rollout (previously only createChainKey hashed):
- createCompositeKey, createSrcRelKey, createDstRelKey, and the
  valueRelationsBySrc/Dst keys now produce 53-bit rolling hashes instead of
  `src|rel|dst` string concatenation. Direction markers keep srcRel vs dstRel
  distinct; the composite/chain keys are exact integers usable as Map keys.
- Direct-check cache invalidation is now key-TRACKED instead of pattern-
  matched: every direct-check result is registered under the checked relation,
  its base relations (reverse dependency index), and the subject/object node
  ids (Arbiter._trackDirectCheckKey). Relation-level invalidation deletes the
  tracked keys for each affected relation (covering config-override checks);
  node-level invalidation (node removal / updateNodeData) deletes by node id.
  This replaces the pipe-delimited-string regex matching that required the old
  key format.
- DecisionCache.invalidateByNodeKey and invalidateAll route through the tracked
  indexes; tracking maps are cleared on full flush.

Tests updated to the tracked contract (register injected keys via
_trackDirectCheckKey); full suite green.
This commit is contained in:
John Dvorak
2026-08-03 16:35:08 -07:00
parent bd9c74fb0e
commit f6e3ae1922
8 changed files with 152 additions and 61 deletions
+6 -26
View File
@@ -174,32 +174,9 @@ export class DecisionCache {
if (!a?.directCheckCache) return;
const nodeId = a.nodeIdByKey?.get(nodeKey);
if (nodeId === undefined) return;
const cache = a.directCheckCache;
// Pattern-based invalidation is the safest path — the cache file
// owns its storage layout and decides how to enumerate keys.
if (typeof cache.invalidateByPattern === 'function') {
// Match a nodeId that appears in any position of the composite
// key. The composite key format is `${srcId}|${rel}|${dstId}` and
// components are pipe-delimited, so a digit-boundary regex is
// safer than a plain substring match.
const escaped = String(nodeId).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(`(?:^|\\|)${escaped}(?:\\||$)`);
cache.invalidateByPattern(pattern);
return;
}
// Fallback: explicit key iteration. HyperbolicLRUCache does not
// expose `keys()`, so this branch is unreachable for that cache
// family. If a future cache implementation exposes iteration,
// we walk it without leaking storage-layout details.
if (typeof cache.keys === 'function') {
for (const key of cache.keys()) {
if (typeof key === 'string' && key.includes(String(nodeId))) {
cache.delete(key);
}
}
}
// Keys are rolling hashes (not pipe-delimited strings), so node-level
// invalidation uses the per-node key index populated at cache-set time.
a._invalidateDirectCheckCacheByNode?.(nodeId);
}
/**
@@ -210,6 +187,9 @@ export class DecisionCache {
const a = this.arbiter;
if (!a) return;
a.directCheckCache?.clear?.();
a.directCheckCacheKeysByRelation?.clear?.();
a.directCheckCacheKeysByNode?.clear?.();
a.directKeyToRelations?.clear?.();
a.invalidateAllRuleResultCache?.();
}
}