/**
 * LocalLeaseStore — a git-less LeaseStore for LeaseCoordinator.
 *
 * The git-backed GitLeaseStore is the durable CAS substrate WHEN a machine has
 * git-sync available. But two real deployments have no git medium yet still need
 * lease coordination:
 *   1. A credential-less standby (the HTTP-transport "Build B" design): it joins
 *      the pool and must learn the holder WITHOUT pulling/pushing git.
 *   2. An agent whose home IS the instar source tree (e.g. the instar developer's
 *      agent home), where the SourceTreeGuard refuses GitSyncManager — so the
 *      git medium is unavailable even though gitBackup is enabled.
 *
 * Without lease coordination, a machine's `coordinator.leaseHolder` is null, so
 * MeshRpc rejects the holder's router-only commands (deliverMessage/place/
 * transfer) as `not-router` — which blocks cross-machine session transfer
 * entirely. (Found live, 2026-05-31: the lease block was nested inside the
 * git-gated try, so a gitSync throw skipped the HTTP lease transport too.)
 *
 * This store persists THIS machine's own view of the lease to a local JSON file
 * (durable across restarts) and implements the LeaseStore CAS locally. It is NOT
 * a shared substrate: cross-machine propagation is carried by HttpLeaseTransport
 * (broadcast/observe), which LeaseCoordinator folds into its effectiveView. The
 * fenced-lease acquire/observe rules prevent split-brain over the tunnel (a
 * machine that observes a valid peer lease declines to acquire). When git-sync
 * IS available, GitLeaseStore remains the stronger shared-CAS substrate.
 */
import fs from 'node:fs';
import path from 'node:path';
export class LocalLeaseStore {
    d;
    cached = null;
    loaded = false;
    constructor(deps) {
        this.d = deps;
    }
    log(m) {
        this.d.logger?.(`[local-lease] ${m}`);
    }
    load() {
        if (this.loaded)
            return;
        try {
            if (fs.existsSync(this.d.filePath)) {
                const raw = JSON.parse(fs.readFileSync(this.d.filePath, 'utf-8'));
                const lease = raw?.lease;
                if (lease && typeof lease.epoch === 'number' && typeof lease.holder === 'string') {
                    this.cached = lease;
                }
            }
        }
        catch {
            // @silent-fallback-ok — corrupt local lease file: start clean. The lease is
            // re-acquired (holder) or re-observed over the tunnel (standby), so a lost
            // local copy is self-healing, never authoritative-loss.
            this.cached = null;
        }
        this.loaded = true;
    }
    persist(lease) {
        this.cached = lease;
        try {
            const dir = path.dirname(this.d.filePath);
            if (!fs.existsSync(dir))
                fs.mkdirSync(dir, { recursive: true });
            const tmp = `${this.d.filePath}.${process.pid}.tmp`;
            fs.writeFileSync(tmp, JSON.stringify({ lease }, null, 2));
            fs.renameSync(tmp, this.d.filePath); // atomic swap
        }
        catch (err) {
            this.log(`persist failed: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    read() {
        this.load();
        return { lease: this.cached, epoch: this.cached?.epoch ?? 0 };
    }
    casWrite(candidate) {
        this.load();
        const committedEpoch = this.cached?.epoch ?? 0;
        // Only a strict advance over the locally-committed epoch is a valid CAS —
        // mirrors GitLeaseStore so LeaseCoordinator's acquire/yield logic is identical
        // regardless of which store backs it.
        if (candidate.epoch <= committedEpoch) {
            this.log(`CAS pre-check failed: candidate epoch ${candidate.epoch} <= committed ${committedEpoch}`);
            return { ok: false, observed: { lease: this.cached, epoch: committedEpoch } };
        }
        this.persist(candidate);
        this.log(`lease epoch ${candidate.epoch} committed locally → ${candidate.holder}`);
        return { ok: true, observed: { lease: candidate, epoch: candidate.epoch } };
    }
    refresh(lease) {
        this.load();
        const committedEpoch = this.cached?.epoch ?? 0;
        // Don't overwrite a higher epoch with a same-epoch refresh (superseded).
        if (committedEpoch > lease.epoch) {
            this.log(`refresh declined: superseded (committed ${committedEpoch} > ${lease.epoch})`);
            return false;
        }
        this.persist(lease); // same epoch, fresh expiry + nonce
        return true;
    }
    /**
     * Relinquish THIS machine's local self-lease to break a same-epoch contested
     * tie (spec §Problem A). The deterministic loser forces its persisted lease to
     * read as EXPIRED — so it stops being a "live holder at epoch N" (the winner's
     * canAcquire() unblocks; holdsLease() returns false) — while KEEPING the
     * committed epoch as the CAS/replay floor. (Clearing to epoch 0 would drop the
     * floor and let a replayed stale lease win; expiring-in-place preserves it.)
     * The winner then advances to N+1, which the loser adopts via the strict-`>`
     * tunnel fold in effectiveView(). No-op if there is no local lease.
     */
    forceLocalExpiry() {
        this.load();
        if (!this.cached)
            return;
        const expired = { ...this.cached, expiresAt: new Date(0).toISOString() };
        this.persist(expired); // keep epoch + holder as the floor; expiresAt deep-past
        this.log(`local self-lease forced expired (epoch ${expired.epoch} retained as floor) → relinquished`);
    }
}
//# sourceMappingURL=LocalLeaseStore.js.map