/**
 * ReplicatedRecordEmitter — the GENERIC journal-backed SEND emitter (WS2 send-side).
 *
 * Spec: docs/specs/WS2-SEND-SIDE-EMISSION-SPEC.md §4; the substrate it rides is
 * docs/specs/multi-machine-replicated-store-foundation.md §4 (the envelope +
 * flag-gated emission) and §7.2 (the `observed` last-writer-witness).
 *
 * THE GAP THIS CLOSES: every memory manager already calls an internal
 * `emitter.emitPut(record)` / `emitter.emitDelete(...)` hook on each write, behind a
 * `*ReplicationEmitter | null` seam — but server.ts never constructed the concrete
 * emitter those hooks call (it was deferred to "a later rollout stage" that never
 * came), so the hooks fired into a no-op and nothing reached the journal own-streams.
 * This class IS that concrete emitter; server.ts builds ONE and adapts it to each
 * manager's emit seam (a tiny per-store adapter that names the store's
 * `build*RecordData`).
 *
 * It is store-AGNOSTIC: `emit(store, recordKey, build)` resolves the store's journal
 * kind from the injected `ReplicatedKindRegistry`, gates on
 * `multiMachine.stateSync.<store>.enabled` (dark by default), stamps the HLC, derives
 * the `observed` witness, asks the store's builder for the disclosure-minimized
 * envelope `data`, and appends it via `CoherenceJournal.emitReplicatedRecord`. ONE
 * generic path serves all 7 kinds; the only per-store thing is the builder closure.
 *
 * SAFETY: NEVER throws into the caller (the manager hooks are best-effort). A disabled
 * store, an unregistered store, a degenerate (null) recordKey, a builder that returns
 * null or throws (e.g. a record over its per-entry byte cap) — every one is a counted
 * no-op, never an exception that would break the local write the agent is performing.
 * Emission is SINGLE-ORIGIN by construction: it stamps `origin = this machine` and the
 * journal writes only this machine's own stream (§6.1 anti-forgery holds end-to-end).
 */
import { isStoreEmissionEnabled, } from './ReplicatedRecordEnvelope.js';
export class ReplicatedRecordEmitter {
    seams;
    stats = {
        emitted: 0,
        storeDisabled: 0,
        skipped: 0,
        errors: 0,
    };
    constructor(seams) {
        // Wiring-integrity preconditions: the seams MUST be real, not null/no-op.
        if (!seams)
            throw new Error('ReplicatedRecordEmitter: seams are required');
        if (!seams.journal || typeof seams.journal.emitReplicatedRecord !== 'function') {
            throw new Error('ReplicatedRecordEmitter: journal.emitReplicatedRecord seam must be a function (not a no-op)');
        }
        if (!seams.clock || typeof seams.clock.tick !== 'function') {
            throw new Error('ReplicatedRecordEmitter: clock.tick seam must be a function');
        }
        if (!seams.registry)
            throw new Error('ReplicatedRecordEmitter: registry seam is required (not null)');
        if (typeof seams.origin !== 'string' || seams.origin.length === 0) {
            throw new Error('ReplicatedRecordEmitter: origin must be a non-empty string');
        }
        if (typeof seams.stores !== 'function')
            throw new Error('ReplicatedRecordEmitter: stores seam must be a function');
        if (typeof seams.loadWitness !== 'function')
            throw new Error('ReplicatedRecordEmitter: loadWitness seam must be a function (not a no-op)');
        this.seams = seams;
    }
    /** Read-only stats (for observability + the wiring-integrity assertions). */
    getStats() {
        return { ...this.stats };
    }
    /**
     * Emit one replicated record for (store, recordKey). The single generic path for a
     * put OR a delete — the only difference is the `build` closure (a put builder vs a
     * tombstone builder). Never throws into the caller.
     *
     * Order (§4): dark gate → degenerate guard → witness → tick → build → append.
     */
    emit(store, recordKey, build) {
        try {
            // 1. Dark gate (the default). A disabled store emits NOTHING — a strict no-op.
            if (!isStoreEmissionEnabled(this.seams.stores(), store)) {
                this.stats.storeDisabled++;
                return;
            }
            // 2. Only a registered store has a journal kind to ride.
            const reg = this.seams.registry.getByStore(store);
            if (!reg) {
                this.stats.skipped++;
                return;
            }
            // 3. Degenerate guard — a null/empty recordKey has no stable identity surface.
            if (typeof recordKey !== 'string' || recordKey.length === 0) {
                this.stats.skipped++;
                return;
            }
            // 4. Witness BEFORE the tick — the HLC this machine had already merged for the
            //    key (own prior + applied peers). Best-effort; a witness read fault degrades
            //    to "no witness" (flag-on-conflict, the safe direction), never a throw.
            let observed;
            try {
                observed = this.seams.loadWitness(store, recordKey);
            }
            catch (e) { /* @silent-fallback-ok: a witness read fault degrades to no-witness ⇒ flag-on-conflict (the safe merge direction, §7.2); never blocks the emit. */
                observed = undefined;
                this.log('witness-read-failed', { store, error: e?.message });
            }
            // 5. Tick AFTER the witness so hlc > observed (a clean sequential position).
            const hlc = this.seams.clock.tick();
            // 6. Build the disclosure-minimized envelope data (store-specific).
            const data = build(hlc, this.seams.origin, observed);
            if (data === null || data === undefined) {
                this.stats.skipped++;
                return;
            }
            // 7. Append. The journal validates + op-key-dedupes + enqueues (non-blocking).
            this.seams.journal.emitReplicatedRecord(reg.kind, data);
            this.stats.emitted++;
        }
        catch (e) { /* @silent-fallback-ok: the manager's write must NEVER fail because replication did — a builder throw (e.g. over-cap) / journal fault is a counted no-op, surfaced via stats + the log, never propagated (Structure > Willpower: the safety is structural, not per-caller). */
            this.stats.errors++;
            this.log('emit-failed', { store, error: e?.message });
        }
    }
    log(event, detail) {
        this.seams.log?.(event, detail);
    }
}
//# sourceMappingURL=ReplicatedRecordEmitter.js.map