"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Flock = void 0;
exports.encodeVersionVector = encodeVersionVector;
exports.decodeVersionVector = decodeVersionVector;
const wasm_1 = require("./wasm");
const event_batcher_1 = require("./event-batcher");
function encodeVersionVector(vector) {
    return encodeVersionVectorBinary(vector);
}
function decodeVersionVector(bytes) {
    return decodeVersionVectorBinary(bytes);
}
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
function utf8ByteLength(value) {
    return textEncoder.encode(value).length;
}
function isValidPeerId(peerId) {
    return typeof peerId === "string" && utf8ByteLength(peerId) < 128;
}
function createRandomPeerId() {
    const id = new Uint8Array(32);
    if (typeof crypto !== "undefined" &&
        typeof crypto.getRandomValues === "function") {
        crypto.getRandomValues(id);
    }
    else {
        for (let i = 0; i < 32; i += 1) {
            id[i] = Math.floor(Math.random() * 256);
        }
    }
    return Array.from(id, (byte) => byte.toString(16).padStart(2, "0")).join("");
}
function normalizePeerId(peerId) {
    if (peerId === undefined) {
        return createRandomPeerId();
    }
    if (!isValidPeerId(peerId)) {
        throw new TypeError("peerId must be a UTF-8 string under 128 bytes.");
    }
    return peerId;
}
function comparePeerBytes(a, b) {
    if (a === b) {
        return 0;
    }
    const limit = Math.min(a.length, b.length);
    for (let i = 0; i < limit; i += 1) {
        const diff = a[i] - b[i];
        if (diff !== 0) {
            return diff;
        }
    }
    return a.length - b.length;
}
function collectEncodableVersionVectorEntries(vv) {
    if (!vv || typeof vv !== "object") {
        return [];
    }
    const entries = [];
    for (const [peer, entry] of Object.entries(vv)) {
        if (!entry || !isValidPeerId(peer)) {
            continue;
        }
        const { physicalTime, logicalCounter } = entry;
        if (typeof physicalTime !== "number" ||
            !Number.isFinite(physicalTime) ||
            typeof logicalCounter !== "number" ||
            !Number.isFinite(logicalCounter)) {
            continue;
        }
        const peerBytes = textEncoder.encode(peer);
        entries.push({
            peer,
            peerBytes,
            timestamp: Math.trunc(physicalTime),
            counter: Math.max(0, Math.trunc(logicalCounter)),
        });
    }
    entries.sort((a, b) => {
        if (a.timestamp !== b.timestamp) {
            return a.timestamp - b.timestamp;
        }
        const peerCmp = comparePeerBytes(a.peerBytes, b.peerBytes);
        if (peerCmp !== 0) {
            return peerCmp;
        }
        return a.counter - b.counter;
    });
    return entries;
}
function writeUnsignedLeb128(value, out) {
    if (!Number.isFinite(value) || value < 0) {
        throw new TypeError("leb128 values must be finite and non-negative");
    }
    let remaining = Math.trunc(value);
    if (remaining === 0) {
        out.push(0);
        return;
    }
    while (remaining > 0) {
        const byte = remaining % 0x80;
        remaining = Math.floor(remaining / 0x80);
        out.push(remaining > 0 ? byte | 0x80 : byte);
    }
}
function writeVarStringBytes(bytes, out) {
    writeUnsignedLeb128(bytes.length, out);
    for (let i = 0; i < bytes.length; i += 1) {
        out.push(bytes[i]);
    }
}
const VERSION_VECTOR_MAGIC = new Uint8Array([86, 69, 86, 69]); // "VEVE"
function encodeVersionVectorBinary(vv) {
    const entries = collectEncodableVersionVectorEntries(vv);
    const buffer = Array.from(VERSION_VECTOR_MAGIC);
    if (entries.length === 0) {
        return Uint8Array.from(buffer);
    }
    let lastTimestamp = 0;
    for (let i = 0; i < entries.length; i += 1) {
        const entry = entries[i];
        if (entry.timestamp < 0) {
            throw new TypeError("timestamp must be non-negative");
        }
        if (i === 0) {
            writeUnsignedLeb128(entry.timestamp, buffer);
            lastTimestamp = entry.timestamp;
        }
        else {
            const delta = entry.timestamp - lastTimestamp;
            if (delta < 0) {
                throw new TypeError("version vector timestamps must be non-decreasing");
            }
            writeUnsignedLeb128(delta, buffer);
            lastTimestamp = entry.timestamp;
        }
        writeUnsignedLeb128(entry.counter, buffer);
        writeVarStringBytes(entry.peerBytes, buffer);
    }
    return Uint8Array.from(buffer);
}
function decodeUnsignedLeb128(bytes, offset) {
    let result = 0;
    let multiplier = 1;
    let consumed = 0;
    while (offset + consumed < bytes.length) {
        const byte = bytes[offset + consumed];
        consumed += 1;
        // Use arithmetic instead of bitwise operations to avoid 32-bit overflow.
        // JavaScript bitwise operators convert to 32-bit signed integers,
        // which breaks for values >= 2^31.
        result += (byte & 0x7f) * multiplier;
        if ((byte & 0x80) === 0) {
            break;
        }
        multiplier *= 128;
    }
    return [result, consumed];
}
function decodeVarString(bytes, offset) {
    const [length, used] = decodeUnsignedLeb128(bytes, offset);
    const start = offset + used;
    const end = start + length;
    if (end > bytes.length) {
        throw new TypeError("varString length exceeds buffer");
    }
    const slice = bytes.subarray(start, end);
    return [textDecoder.decode(slice), used + length];
}
function hasMagic(bytes) {
    return (bytes.length >= 4 &&
        bytes[0] === VERSION_VECTOR_MAGIC[0] &&
        bytes[1] === VERSION_VECTOR_MAGIC[1] &&
        bytes[2] === VERSION_VECTOR_MAGIC[2] &&
        bytes[3] === VERSION_VECTOR_MAGIC[3]);
}
function decodeLegacyVersionVector(bytes) {
    let offset = 0;
    const [count, usedCount] = decodeUnsignedLeb128(bytes, offset);
    offset += usedCount;
    const [baseTimestamp, usedBase] = decodeUnsignedLeb128(bytes, offset);
    offset += usedBase;
    const vv = {};
    for (let i = 0; i < count; i += 1) {
        const [peer, usedPeer] = decodeVarString(bytes, offset);
        offset += usedPeer;
        if (!isValidPeerId(peer)) {
            throw new TypeError("invalid peer id in encoded version vector");
        }
        const [delta, usedDelta] = decodeUnsignedLeb128(bytes, offset);
        offset += usedDelta;
        const [counter, usedCounter] = decodeUnsignedLeb128(bytes, offset);
        offset += usedCounter;
        vv[peer] = {
            physicalTime: baseTimestamp + delta,
            logicalCounter: counter,
        };
    }
    return vv;
}
function decodeNewVersionVector(bytes) {
    let offset = 4;
    const vv = {};
    if (offset === bytes.length) {
        return vv;
    }
    const [firstTimestamp, usedTs] = decodeUnsignedLeb128(bytes, offset);
    offset += usedTs;
    const [firstCounter, usedCounter] = decodeUnsignedLeb128(bytes, offset);
    offset += usedCounter;
    const [firstPeer, usedPeer] = decodeVarString(bytes, offset);
    offset += usedPeer;
    if (!isValidPeerId(firstPeer)) {
        throw new TypeError("invalid peer id in encoded version vector");
    }
    vv[firstPeer] = {
        physicalTime: firstTimestamp,
        logicalCounter: firstCounter,
    };
    let lastTimestamp = firstTimestamp;
    while (offset < bytes.length) {
        const [delta, usedDelta] = decodeUnsignedLeb128(bytes, offset);
        offset += usedDelta;
        const [counter, usedCtr] = decodeUnsignedLeb128(bytes, offset);
        offset += usedCtr;
        const [peer, usedPeerLen] = decodeVarString(bytes, offset);
        offset += usedPeerLen;
        if (!isValidPeerId(peer)) {
            throw new TypeError("invalid peer id in encoded version vector");
        }
        const timestamp = lastTimestamp + delta;
        if (timestamp < lastTimestamp) {
            throw new TypeError("version vector timestamps must be non-decreasing");
        }
        vv[peer] = { physicalTime: timestamp, logicalCounter: counter };
        lastTimestamp = timestamp;
    }
    return vv;
}
function decodeVersionVectorBinary(bytes) {
    if (hasMagic(bytes)) {
        return decodeNewVersionVector(bytes);
    }
    return decodeLegacyVersionVector(bytes);
}
function encodeVersionVectorForFfi(vv) {
    if (!vv) {
        return undefined;
    }
    const raw = {};
    for (const entry of collectEncodableVersionVectorEntries(vv)) {
        raw[entry.peer] = [entry.timestamp, entry.counter];
    }
    return raw;
}
function normalizePruneBefore(pruneTombstonesBefore) {
    if (pruneTombstonesBefore === undefined) {
        return undefined;
    }
    if (typeof pruneTombstonesBefore !== "number" ||
        !Number.isFinite(pruneTombstonesBefore)) {
        return undefined;
    }
    return pruneTombstonesBefore;
}
function decodeVersionVectorFromRaw(raw) {
    if (raw === null || typeof raw !== "object") {
        return {};
    }
    const result = {};
    for (const [peer, value] of Object.entries(raw)) {
        if (!Array.isArray(value) || value.length < 2) {
            continue;
        }
        if (!isValidPeerId(peer)) {
            continue;
        }
        const [physicalTime, logicalCounter] = value;
        if (typeof physicalTime !== "number" || !Number.isFinite(physicalTime)) {
            continue;
        }
        if (typeof logicalCounter !== "number" ||
            !Number.isFinite(logicalCounter)) {
            continue;
        }
        result[peer] = {
            physicalTime,
            logicalCounter: Math.trunc(logicalCounter),
        };
    }
    return result;
}
function encodeBound(bound) {
    if (!bound) {
        return undefined;
    }
    if (bound.kind === "unbounded") {
        return { kind: "unbounded" };
    }
    return { kind: bound.kind, key: bound.key.slice() };
}
function decodeEntryInfo(raw) {
    if (!raw || typeof raw !== "object") {
        return undefined;
    }
    const info = raw;
    const clock = normalizeEntryClock(info.clock);
    if (!clock) {
        return undefined;
    }
    const metadata = normalizeMetadataMap(info.metadata);
    const result = { metadata, clock };
    if ("data" in info) {
        result.data = cloneJson(info.data);
    }
    return result;
}
function decodeEventBatch(raw) {
    if (!raw || typeof raw !== "object") {
        return { source: "local", events: [] };
    }
    const batch = raw;
    const source = typeof batch.source === "string" ? batch.source : "local";
    const eventsRaw = Array.isArray(batch.events) ? batch.events : [];
    const events = eventsRaw
        .filter((entry) => Boolean(entry))
        .map((entry) => buildEvent(entry));
    return { source, events };
}
function buildEvent(entry) {
    const key = Array.isArray(entry.key) ? entry.key : [];
    const payload = buildEventPayload(entry);
    return {
        key,
        value: payload.data,
        metadata: cloneMetadata(payload.metadata),
        payload,
    };
}
function buildEventPayload(entry) {
    const base = {};
    if ("value" in entry) {
        base.data = cloneJson(entry.value);
    }
    const entryMetadata = cloneMetadata(entry.metadata);
    if (entryMetadata !== undefined) {
        base.metadata = entryMetadata;
    }
    const update = normalizeRawEventPayload(entry.payload);
    return mergePayload(base, update);
}
function normalizeRawEventPayload(payload) {
    if (!payload || typeof payload !== "object") {
        return undefined;
    }
    const result = {};
    if ("data" in payload) {
        result.data = cloneJson(payload.data);
    }
    const metadata = cloneMetadata(payload.metadata);
    if (metadata !== undefined) {
        result.metadata = metadata;
    }
    return result;
}
const structuredCloneFn = globalThis.structuredClone;
function cloneJson(value) {
    if (value === undefined) {
        return value;
    }
    if (structuredCloneFn) {
        return structuredCloneFn(value);
    }
    return JSON.parse(JSON.stringify(value));
}
function parseKeyString(key) {
    try {
        const parsed = JSON.parse(key);
        return Array.isArray(parsed) ? parsed : [];
    }
    catch {
        return [];
    }
}
function cloneMetadata(metadata) {
    if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
        return undefined;
    }
    return cloneJson(metadata);
}
function normalizeMetadataMap(metadata) {
    const cloned = cloneMetadata(metadata);
    return cloned ?? {};
}
function decodeClock(record) {
    const rawClock = typeof record.c === "string" ? record.c : "";
    const firstComma = rawClock.indexOf(",");
    const secondComma = firstComma === -1 ? -1 : rawClock.indexOf(",", firstComma + 1);
    if (firstComma === -1 || secondComma === -1) {
        return { physicalTime: 0, logicalCounter: 0, peerId: "" };
    }
    const physicalTime = Number(rawClock.slice(0, firstComma));
    const logicalCounter = Number(rawClock.slice(firstComma + 1, secondComma));
    const peerIdRaw = rawClock.slice(secondComma + 1);
    const peerId = isValidPeerId(peerIdRaw) ? peerIdRaw : "";
    return {
        physicalTime: Number.isFinite(physicalTime) ? physicalTime : 0,
        logicalCounter: Number.isFinite(logicalCounter)
            ? Math.trunc(logicalCounter)
            : 0,
        peerId,
    };
}
function normalizeEntryClock(clock) {
    if (!clock || typeof clock !== "object") {
        return undefined;
    }
    const { physicalTime, logicalCounter, peerId } = clock;
    if (typeof physicalTime !== "number" || !Number.isFinite(physicalTime)) {
        return undefined;
    }
    if (typeof logicalCounter !== "number" || !Number.isFinite(logicalCounter)) {
        return undefined;
    }
    if (!isValidPeerId(peerId)) {
        return undefined;
    }
    return {
        physicalTime,
        logicalCounter: Math.trunc(logicalCounter),
        peerId,
    };
}
function createExportPayload(record) {
    const payload = {};
    if (record.d !== undefined) {
        payload.data = cloneJson(record.d);
    }
    const metadata = cloneMetadata(record.m);
    if (metadata !== undefined) {
        payload.metadata = metadata;
    }
    return payload;
}
function createPutPayload(value, metadata) {
    const payload = { data: cloneJson(value) };
    const cleanMetadata = cloneMetadata(metadata);
    if (cleanMetadata !== undefined) {
        payload.metadata = cleanMetadata;
    }
    return payload;
}
function assignPayload(target, source) {
    if (!source || typeof source !== "object") {
        return;
    }
    if ("data" in source) {
        const value = source.data;
        target.data = value === undefined ? undefined : cloneJson(value);
    }
    if ("metadata" in source) {
        target.metadata = cloneMetadata(source.metadata);
    }
}
function clonePayload(payload) {
    const result = {};
    assignPayload(result, payload);
    return result;
}
function mergePayload(base, update) {
    const result = clonePayload(base);
    assignPayload(result, update);
    return result;
}
function buildRecord(clock, payload) {
    const record = { c: clock };
    if (payload.data !== undefined) {
        record.d = cloneJson(payload.data);
    }
    const metadata = cloneMetadata(payload.metadata);
    if (metadata !== undefined) {
        record.m = metadata;
    }
    return record;
}
function cloneRecord(record) {
    return buildRecord(record.c, createExportPayload(record));
}
function buildContext(key, record) {
    return {
        key: parseKeyString(key),
        clock: decodeClock(record),
        raw: cloneRecord(record),
    };
}
function normalizeImportDecision(decision) {
    if (!decision || typeof decision !== "object") {
        return { accept: true };
    }
    if ("accept" in decision) {
        if (!decision.accept) {
            return { accept: false, reason: decision.reason ?? "rejected" };
        }
        return { accept: true };
    }
    return { accept: true };
}
function decodeImportReport(raw) {
    if (!raw || typeof raw !== "object") {
        return { accepted: 0, skipped: [] };
    }
    const report = raw;
    const accepted = typeof report.accepted === "number" ? report.accepted : 0;
    const skippedRaw = Array.isArray(report.skipped) ? report.skipped : [];
    const skipped = skippedRaw.map((entry) => {
        const key = entry && Array.isArray(entry.key) ? entry.key : [];
        const reason = entry && typeof entry.reason === "string" ? entry.reason : "unknown";
        return { key, reason };
    });
    return { accepted, skipped };
}
function cloneBundle(bundle) {
    const next = { version: bundle.version, entries: {} };
    for (const [key, record] of Object.entries(bundle.entries)) {
        next.entries[key] = cloneRecord(record);
    }
    return next;
}
function isExportOptions(value) {
    return (typeof value === "object" &&
        value !== null &&
        (Object.prototype.hasOwnProperty.call(value, "hooks") ||
            Object.prototype.hasOwnProperty.call(value, "from") ||
            Object.prototype.hasOwnProperty.call(value, "pruneTombstonesBefore") ||
            Object.prototype.hasOwnProperty.call(value, "peerId")));
}
function isImportOptions(value) {
    return (typeof value === "object" &&
        value !== null &&
        Object.prototype.hasOwnProperty.call(value, "bundle"));
}
const defaultEventBatcherRuntime = {
    now: () => Date.now(),
    setTimeout: (fn, ms) => setTimeout(fn, ms),
    clearTimeout: (handle) => clearTimeout(handle),
};
class Flock {
    inner;
    listeners = new Set();
    nativeSubscriberId;
    eventBatcher;
    constructor(peerId) {
        this.inner = new wasm_1.RawFlock(normalizePeerId(peerId));
        this.eventBatcher = new event_batcher_1.EventBatcher({
            runtime: defaultEventBatcherRuntime,
            emit: (source, events) => {
                this.deliverBatch({ source, events });
            },
        });
    }
    static fromInner(inner) {
        const flock = Object.create(Flock.prototype);
        flock.inner = inner;
        flock.listeners = new Set();
        flock.nativeSubscriberId = undefined;
        flock.eventBatcher = new event_batcher_1.EventBatcher({
            runtime: defaultEventBatcherRuntime,
            emit: (source, events) => {
                flock.deliverBatch({ source, events });
            },
        });
        return flock;
    }
    static fromJson(bundle, peerId) {
        const inner = wasm_1.RawFlock.fromJson(bundle, normalizePeerId(peerId));
        return Flock.fromInner(inner);
    }
    static fromFile(bytes, peerId) {
        const inner = wasm_1.RawFlock.fromFile(normalizePeerId(peerId), bytes);
        return Flock.fromInner(inner);
    }
    checkInvariants() {
        void this.version();
        void this.inclusiveVersion();
        void this.scan();
    }
    setPeerId(peerId) {
        this.inner.setPeerId(normalizePeerId(peerId));
    }
    putWithMetaInternal(key, value, metadata, now) {
        const metadataClone = cloneMetadata(metadata);
        this.inner.putWithMeta(key, cloneJson(value), metadataClone, now);
    }
    async putWithMetaWithHooks(key, value, options) {
        const basePayload = createPutPayload(value, options.metadata);
        const transform = options.hooks?.transform;
        if (!transform) {
            this.putWithMetaInternal(key, value, options.metadata, options.now);
            return;
        }
        const workingPayload = clonePayload(basePayload);
        const transformed = await transform({ key: key.slice(), now: options.now }, workingPayload);
        const finalPayload = mergePayload(basePayload, transformed ?? workingPayload);
        const finalValue = finalPayload.data;
        if (finalValue === undefined) {
            throw new TypeError("putWithMeta requires a data value");
        }
        this.putWithMetaInternal(key, finalValue, finalPayload.metadata, options.now);
    }
    /**
     * Put a value into the flock. If the given entry already exists, this insert will be skipped.
     * @param key
     * @param value
     * @param now
     */
    put(key, value, now) {
        this.inner.put(key, cloneJson(value), now);
    }
    putWithMeta(key, value, options) {
        const opts = options ?? {};
        if (opts.hooks?.transform) {
            return this.putWithMetaWithHooks(key, value, opts);
        }
        this.putWithMetaInternal(key, value, opts.metadata, opts.now);
    }
    set(key, value, now) {
        this.put(key, value, now);
    }
    /**
     * Delete a value from the flock. If the given entry does not exist, this delete will be skipped.
     * @param key
     * @param now
     */
    delete(key, now) {
        this.inner.delete(key, now);
    }
    get(key) {
        return this.inner.get(key);
    }
    /**
     * Returns the full entry payload (data, metadata, and clock) for a key.
     *
     * Unlike `get`, this distinguishes between a missing key (`undefined`) and a
     * tombstone (returns the clock and metadata with `data` omitted). Metadata is
     * cloned and defaults to `{}` when absent.
     */
    getEntry(key) {
        const raw = this.inner.getEntry(key);
        return decodeEntryInfo(raw);
    }
    merge(other) {
        this.eventBatcher.beforeImport();
        void this.inner.merge(other.inner);
    }
    /**
     * Returns the exclusive version vector, which only includes peers that have
     * at least one entry in the current state. This is consistent with the state
     * after export and re-import.
     *
     * Use this version when sending to other peers for incremental sync.
     */
    version() {
        return decodeVersionVectorFromRaw(this.inner.version());
    }
    /**
     * Returns the inclusive version vector, which includes all peers ever seen,
     * even if their entries have been overridden by other peers.
     *
     * Use this version when checking if you have received all data from another peer.
     */
    inclusiveVersion() {
        return decodeVersionVectorFromRaw(this.inner.inclusiveVersion());
    }
    exportJsonInternal(from, pruneTombstonesBefore, peerId) {
        const pruneBefore = normalizePruneBefore(pruneTombstonesBefore);
        const normalizedPeerId = peerId !== undefined && isValidPeerId(peerId) ? peerId : undefined;
        return this.inner.exportJson(encodeVersionVectorForFfi(from), pruneBefore, normalizedPeerId);
    }
    async exportJsonWithHooks(options) {
        const base = this.exportJsonInternal(options.from, options.pruneTombstonesBefore, options.peerId);
        const transform = options.hooks?.transform;
        if (!transform) {
            return base;
        }
        const result = { version: base.version, entries: {} };
        for (const [key, record] of Object.entries(base.entries)) {
            const context = buildContext(key, record);
            const basePayload = createExportPayload(record);
            const workingPayload = clonePayload(basePayload);
            const transformed = await transform(context, workingPayload);
            const finalPayload = mergePayload(basePayload, transformed ?? workingPayload);
            result.entries[key] = buildRecord(record.c, finalPayload);
        }
        return result;
    }
    exportJson(arg, pruneTombstonesBefore) {
        if (arg === undefined) {
            return this.exportJsonInternal(undefined, pruneTombstonesBefore);
        }
        if (isExportOptions(arg)) {
            return this.exportJsonWithHooks(arg);
        }
        return this.exportJsonInternal(arg, pruneTombstonesBefore);
    }
    importJsonInternal(bundle) {
        this.eventBatcher.beforeImport();
        const report = this.inner.importJson(bundle);
        return decodeImportReport(report);
    }
    async importJsonWithHooks(options) {
        const preprocess = options.hooks?.preprocess;
        const working = preprocess ? cloneBundle(options.bundle) : options.bundle;
        const skippedByHooks = [];
        if (preprocess) {
            for (const key of Object.keys(working.entries)) {
                const record = working.entries[key];
                if (!record) {
                    continue;
                }
                const context = buildContext(key, record);
                const basePayload = createExportPayload(record);
                const decision = await preprocess(context, clonePayload(basePayload));
                const normalized = normalizeImportDecision(decision);
                if (!normalized.accept) {
                    skippedByHooks.push({ key: context.key, reason: normalized.reason });
                    delete working.entries[key];
                    continue;
                }
                working.entries[key] = buildRecord(record.c, basePayload);
            }
        }
        const coreReport = this.importJsonInternal(working);
        return {
            accepted: coreReport.accepted,
            skipped: skippedByHooks.concat(coreReport.skipped),
        };
    }
    importJson(arg) {
        if (isImportOptions(arg)) {
            return this.importJsonWithHooks(arg);
        }
        return this.importJsonInternal(arg);
    }
    importJsonStr(bundle) {
        const parsed = JSON.parse(bundle);
        return this.importJson(parsed);
    }
    getMaxPhysicalTime() {
        return Number(this.inner.getMaxPhysicalTime());
    }
    peerId() {
        const id = this.inner.peerId();
        if (typeof id !== "string") {
            throw new TypeError("peerId ffi returned unexpected value");
        }
        if (!isValidPeerId(id)) {
            throw new TypeError("peerId ffi returned an invalid string");
        }
        return id;
    }
    kvToJson() {
        return this.exportJson();
    }
    putMvr(key, value, now) {
        if (value === null || Array.isArray(value) || typeof value === "object") {
            throw new TypeError("putMvr only accepts scalar JSON values");
        }
        const rows = this.scan({ prefix: key.slice() });
        for (const row of rows) {
            if (row.value !== true) {
                continue;
            }
            this.delete(row.key, now);
        }
        const keyWithValue = key.slice();
        keyWithValue.push(value);
        this.put(keyWithValue, true, now);
    }
    getMvr(key) {
        const rows = this.scan({ prefix: key.slice() });
        const out = [];
        for (const row of rows) {
            if (row.key.length !== key.length + 1) {
                continue;
            }
            if (row.value !== true) {
                continue;
            }
            out.push(row.key[key.length]);
        }
        return out;
    }
    exportFile() {
        return this.inner.exportFile();
    }
    scan(options = {}) {
        const start = encodeBound(options.start);
        const end = encodeBound(options.end);
        const prefix = options.prefix ? options.prefix.slice() : undefined;
        const rows = this.inner.scan(start, end, prefix);
        if (!Array.isArray(rows)) {
            return [];
        }
        return rows
            .filter((row) => Boolean(row))
            .map((row) => ({
            key: Array.isArray(row.key) ? row.key : [],
            raw: row.raw,
            value: row.value,
        }));
    }
    ensureNativeSubscription() {
        if (this.nativeSubscriberId !== undefined) {
            return;
        }
        this.nativeSubscriberId = this.inner.subscribe((payload) => {
            (0, wasm_1.callPendingEvents)();
            const batch = decodeEventBatch(payload);
            this.handleBatch(batch);
        });
    }
    handleBatch(batch) {
        const bufferable = batch.source === "local";
        this.eventBatcher.handleCommitEvents(batch.source, batch.events, bufferable);
    }
    deliverBatch(batch) {
        if (this.listeners.size === 0) {
            return;
        }
        const listeners = Array.from(this.listeners);
        for (const listener of listeners) {
            try {
                listener(batch);
            }
            catch (error) {
                void error;
            }
        }
    }
    subscribe(listener) {
        this.listeners.add(listener);
        this.ensureNativeSubscription();
        return () => {
            this.listeners.delete(listener);
            // Optionally clean up native subscription when no listeners remain
            if (this.listeners.size === 0 && this.nativeSubscriberId !== undefined) {
                this.inner.unsubscribe(this.nativeSubscriberId);
                this.nativeSubscriberId = undefined;
            }
        };
    }
    /**
     * Enable auto-debounce mode. Events will be accumulated and emitted after
     * the specified timeout of inactivity. Each new operation resets the timer.
     *
     * Use `commit()` to force immediate emission of pending events.
     * Use `disableAutoDebounceCommit()` to disable and emit pending events.
     *
     * @param timeout - Debounce timeout in milliseconds
     * @param options - Optional configuration object with maxDebounceTime (default: 10000ms)
     * @throws Error if called while a transaction is active
     * @throws Error if autoDebounceCommit is already active
     *
     * @example
     * ```ts
     * flock.autoDebounceCommit(100);
     * flock.put(["a"], 1);
     * flock.put(["b"], 2);
     * // No events emitted yet...
     * // After 100ms of inactivity, subscribers receive single EventBatch
     * // If operations keep coming, commit happens after maxDebounceTime (10s default)
     * ```
     */
    autoDebounceCommit(timeout, options) {
        if (this.isInTxn()) {
            throw new Error("Cannot enable autoDebounceCommit while transaction is active");
        }
        this.eventBatcher.autoDebounceCommit(timeout, options);
    }
    /**
     * Disable auto-debounce mode and emit any pending events immediately.
     * No-op if autoDebounceCommit is not active.
     */
    disableAutoDebounceCommit() {
        this.eventBatcher.disableAutoDebounceCommit();
    }
    /**
     * Force immediate emission of any pending debounced events.
     * Does not disable auto-debounce mode - new operations will continue to be debounced.
     * No-op if autoDebounceCommit is not active or no events are pending.
     */
    commit() {
        this.eventBatcher.commit();
    }
    /**
     * Check if auto-debounce mode is currently active.
     */
    isAutoDebounceActive() {
        return this.eventBatcher.isAutoDebounceActive();
    }
    /**
     * Execute operations within a transaction. All put/delete operations inside
     * the callback will be batched and emitted as a single EventBatch when the
     * transaction commits successfully.
     *
     * If the callback throws an error, the transaction is rolled back and no
     * events are emitted. Note: Data changes are NOT rolled back - only event
     * emission is affected.
     *
     * The callback must be synchronous. For async operations, use FlockSQLite.
     *
     * @param callback - Synchronous function containing put/delete operations
     * @returns The return value of the callback
     * @throws Error if nested transaction attempted
     * @throws Error if import is called during the transaction (auto-commits first)
     * @throws Error if called while autoDebounceCommit is active
     *
     * @example
     * ```ts
     * flock.txn(() => {
     *   flock.put(["a"], 1);
     *   flock.put(["b"], 2);
     *   flock.put(["c"], 3);
     * });
     * // Subscribers receive a single EventBatch with 3 events
     * ```
     */
    txn(callback) {
        if (this.eventBatcher.isAutoDebounceActive()) {
            throw new Error("Cannot start transaction while autoDebounceCommit is active");
        }
        this.inner.txnBegin();
        try {
            const result = callback();
            this.inner.txnCommit();
            return result;
        }
        catch (e) {
            // Only rollback if transaction is still active.
            // import_json auto-commits the transaction before throwing,
            // so we must check before attempting rollback.
            if (this.inner.isInTxn()) {
                this.inner.txnRollback();
            }
            throw e;
        }
    }
    /**
     * Check if a transaction is currently active.
     */
    isInTxn() {
        return this.inner.isInTxn();
    }
}
exports.Flock = Flock;
const CALL_PENDING_EVENTS_WRAPPED = Symbol("flock.callPendingEventsWrapped");
function decorateMethod(prototype, method) {
    const descriptor = Object.getOwnPropertyDescriptor(prototype, method);
    if (!descriptor || typeof descriptor.value !== "function") {
        return;
    }
    const original = descriptor.value;
    if (original[CALL_PENDING_EVENTS_WRAPPED]) {
        return;
    }
    const wrapped = function (...args) {
        let result;
        try {
            result = original.apply(this, args);
            return result;
        }
        finally {
            if (result && typeof result.then === "function") {
                void result
                    .finally(() => {
                    (0, wasm_1.callPendingEvents)();
                })
                    .catch(() => {
                    // Ignore: this promise is only for triggering pending event draining.
                    // The original promise is returned to the caller and should surface errors there.
                });
            }
            else {
                (0, wasm_1.callPendingEvents)();
            }
        }
    };
    wrapped[CALL_PENDING_EVENTS_WRAPPED] = true;
    Object.defineProperty(wrapped, "length", { value: original.length });
    Object.defineProperty(prototype, method, {
        ...descriptor,
        value: wrapped,
    });
}
function decorateAllPrototypeMethods(prototype) {
    const visited = new Set();
    let current = prototype;
    while (current && current !== Object.prototype && current !== Function.prototype) {
        for (const property of Object.getOwnPropertyNames(current)) {
            if (property === "constructor" || visited.has(property)) {
                continue;
            }
            visited.add(property);
            decorateMethod(current, property);
        }
        for (const symbol of Object.getOwnPropertySymbols(current)) {
            if (visited.has(symbol)) {
                continue;
            }
            visited.add(symbol);
            decorateMethod(current, symbol);
        }
        current = Object.getPrototypeOf(current);
    }
}
decorateAllPrototypeMethods(Flock.prototype);
