/**
* In-band schema-version guard (#52 Phase 4.4 hardening) — Keet's pattern.
*
* Keet stamps the schema/ABI version IN the replicated oplog and a reader that
* sees ops newer than it supports INTERRUPTs (unsupportedABI) rather than
* mis-reading/serving stale data. Our schema version was a per-store FILE
* (schema-version.json) — purely local, so an under-versioned MIRROR had no
* in-band signal that the origin moved to a newer schema and would keep
* serving stale data. This puts the version in the replicated OrgEventLog so
* every node learns the canonical schema version by replication, and a node
* running OLDER code than the declared version can refuse to serve.
*
* Wire-safe: reuses the existing OrgEventLog entry — a NEW `op` value
* (SCHEMA_VERSION_OP) with the version embedded in the `name` string. No new
* struct field, so old decoders read it cleanly and ignore it (op != 2 and no
* orgBootstrapKey → OrgPuller skips it). Protects FUTURE migrations: once all
* nodes run vN code, a later vN+1 origin's marker makes the vN nodes detect
* they're behind. (The already-deployed pre-guard code can't check it — the
* current node→bare cutover still relies on coordinated rollout.)
*/
export const SCHEMA_VERSION_OP = 3 // 0=created, 2=deleted, 3=schema-version marker
const PREFIX = '@schema-version:'
export function isSchemaVersionEntry (e) {
return !!(e && e.op === SCHEMA_VERSION_OP && typeof e.name === 'string' && e.name.startsWith(PREFIX))
}
/** @returns {number|null} the version a marker entry declares, or null. */
export function parseSchemaVersionEntry (e) {
if (!isSchemaVersionEntry(e)) return null
const v = parseInt(e.name.slice(PREFIX.length), 10)
return Number.isFinite(v) ? v : null
}
/**
* Highest schema version declared in the replicated log (0 if none yet).
* @param {OrgEventLog} orgEventLog
*/
export async function readDeclaredSchemaVersion (orgEventLog) {
let max = 0
for await (const e of orgEventLog.entries({ live: false, start: 0 })) {
const v = parseSchemaVersionEntry(e)
if (v !== null && v > max) max = v
}
return max
}
/**
* Origin-only: stamp the canonical schema version into the replicated log.
* Idempotent — no-op if the log already declares >= version. A mirror's log
* is read-only (not writable) so this is a no-op there.
* @returns {Promise<boolean>} true if a marker was appended.
*/
export async function announceSchemaVersion (orgEventLog, version) {
if (!orgEventLog || !orgEventLog.writable) return false
const current = await readDeclaredSchemaVersion(orgEventLog)
if (current >= version) return false
await orgEventLog.append({ op: SCHEMA_VERSION_OP, name: PREFIX + version })
return true
}
/**
* Is this node behind the replicated schema? `behind` ⇒ the log declares a
* version newer than this code supports → the node must NOT serve stale data
* (Keet's interrupt). Point-in-time: on a mirror the marker may not have
* replicated yet — pair with the live check in OrgPuller for ops that arrive
* while running.
* @returns {Promise<{declared:number, supported:number, behind:boolean}>}
*/
export async function checkSchemaGuard (orgEventLog, supported) {
const declared = await readDeclaredSchemaVersion(orgEventLog)
return { declared, supported, behind: declared > supported }
}