import { clearOpfsStorage, detectOpfsSupport } from './opfs.js';
import { createTreecrdtSqliteWriter, decodeSqliteNodeIds, decodeSqliteOpRefs, decodeSqliteOps, decodeSqliteTreeChildRows, decodeSqliteTreeRows, } from '@treecrdt/interface/sqlite';
import { bytesToHex, nodeIdFromBytes16, nodeIdToBytes16, replicaIdToBytes, } from '@treecrdt/interface/ids';
import { addMaterializationWriteId, createMaterializationDispatcher, createTreecrdtEngineLocal, } from '@treecrdt/interface/engine';
import { dbGetText } from './sql.js';
import { openTreecrdtDb } from './open.js';
export const CLIENT_CLOSED_ERROR = 'TreecrdtClient was closed';
function normalizeStorageOptions(opts) {
    const raw = opts.storage ?? { type: 'auto' };
    if (!raw || typeof raw !== 'object') {
        throw new Error('createTreecrdtClient storage must use object options, e.g. { type: "memory" } or { type: "opfs" }');
    }
    if (raw.type === 'memory') {
        return { type: 'memory', requireOpfs: false, fallback: 'memory' };
    }
    if (raw.type === 'opfs') {
        const fallback = raw.fallback ?? 'throw';
        return {
            type: 'opfs',
            filename: raw.filename,
            requireOpfs: fallback === 'throw',
            fallback,
        };
    }
    if (raw.type !== 'auto') {
        throw new Error('createTreecrdtClient storage.type must be "memory", "opfs", or "auto"');
    }
    const fallback = raw.fallback ?? 'memory';
    return {
        type: 'auto',
        filename: raw.filename,
        requireOpfs: fallback === 'throw',
        fallback,
    };
}
function normalizeRuntimeOptions(opts) {
    return opts.runtime ?? { type: 'auto' };
}
export async function createTreecrdtClient(opts = {}) {
    const storage = normalizeStorageOptions(opts);
    const runtime = normalizeRuntimeOptions(opts);
    const docId = opts.docId ?? 'treecrdt';
    const rawBase = opts.assets?.baseUrl ??
        (typeof import.meta !== 'undefined' && import.meta.env?.BASE_URL
            ? import.meta.env.BASE_URL
            : '/');
    const baseUrl = rawBase.endsWith('/') ? rawBase : `${rawBase}/`;
    const support = detectOpfsSupport();
    const shouldUseOpfs = storage.type === 'opfs' || (storage.type === 'auto' && support.available);
    if (storage.type === 'auto' && !support.available && storage.fallback === 'throw') {
        throw new Error(`OPFS unavailable in this environment: ${support.reason ?? 'unknown reason'}`);
    }
    const resolvedRuntime = resolveRuntimeMode(runtime, shouldUseOpfs);
    // If OPFS is requested, default runtime:auto to a worker path to avoid main-thread sync handles.
    if (shouldUseOpfs && resolvedRuntime === 'direct' && !support.available && storage.requireOpfs) {
        throw new Error(`OPFS unavailable in this environment: ${support.reason ?? 'unknown reason'}`);
    }
    if (resolvedRuntime === 'shared-worker') {
        return createSharedWorkerClient({
            baseUrl,
            filename: storage.filename,
            storage: shouldUseOpfs ? 'opfs' : 'memory',
            requireOpfs: storage.requireOpfs,
            docId,
            workerUrl: runtime.type === 'shared-worker' ? runtime.workerUrl : undefined,
            name: runtime.type === 'shared-worker'
                ? (runtime.name ??
                    defaultSharedWorkerName(docId, shouldUseOpfs ? storage.filename : ':memory:'))
                : defaultSharedWorkerName(docId, shouldUseOpfs ? storage.filename : ':memory:'),
        });
    }
    if (resolvedRuntime === 'dedicated-worker') {
        return createWorkerClient({
            baseUrl,
            filename: storage.filename,
            storage: shouldUseOpfs ? 'opfs' : 'memory',
            requireOpfs: storage.requireOpfs,
            docId,
            workerUrl: runtime.type === 'dedicated-worker' ? runtime.workerUrl : undefined,
        });
    }
    return createDirectClient({
        baseUrl,
        filename: storage.filename,
        storage: shouldUseOpfs ? 'opfs' : 'memory',
        requireOpfs: storage.requireOpfs,
        docId,
    });
}
function resolveRuntimeMode(runtime, shouldUseOpfs) {
    if (runtime.type === 'direct')
        return 'direct';
    if (runtime.type === 'dedicated-worker')
        return 'dedicated-worker';
    if (runtime.type === 'shared-worker') {
        if (typeof SharedWorker === 'undefined') {
            throw new Error('TreeCRDT shared-worker runtime is unavailable in this environment');
        }
        return 'shared-worker';
    }
    if (shouldUseOpfs)
        return 'dedicated-worker';
    return 'direct';
}
function defaultSharedWorkerName(docId, filename) {
    return `treecrdt:${docId}:${filename ?? '/treecrdt.db'}`;
}
// --- Worker client
const CROSS_TAB_MATERIALIZED_MESSAGE = 'treecrdt-materialized-v1';
function createClientMaterializationDispatcher(opts = {}) {
    const dispatcher = createMaterializationDispatcher();
    const clientId = randomClientId();
    let channel = null;
    let scope = null;
    const close = () => {
        channel?.close();
        channel = null;
        scope = null;
    };
    const eventForPeers = (event) => {
        return {
            ...event,
            changes: event.changes.map((change) => {
                if (!change.source?.writeIds)
                    return change;
                const { writeIds: _writeIds, ...source } = change.source;
                if (Object.keys(source).length > 0)
                    return { ...change, source };
                const { source: _source, ...nextChange } = change;
                return nextChange;
            }),
        };
    };
    const broadcast = (event) => {
        if (!channel || !scope || event.changes.length === 0)
            return;
        channel.postMessage({
            type: CROSS_TAB_MATERIALIZED_MESSAGE,
            sourceId: clientId,
            docId: scope.docId,
            filename: scope.filename,
            event: eventForPeers(event),
        });
    };
    const emitEvent = (event) => {
        if (event.changes.length === 0)
            return;
        dispatcher.emitEvent(event);
        opts.broadcast?.(eventForPeers(event));
        broadcast(event);
    };
    const emitOutcome = (outcome, writeId) => {
        if (outcome.changes.length === 0)
            return;
        emitEvent(addMaterializationWriteId(outcome, writeId));
    };
    const enableCrossTab = (nextScope) => {
        if (typeof BroadcastChannel === 'undefined')
            return;
        close();
        scope = nextScope;
        channel = new BroadcastChannel(materializationChannelName(nextScope));
        channel.onmessage = (ev) => {
            const msg = ev.data;
            if (!msg || msg.type !== CROSS_TAB_MATERIALIZED_MESSAGE)
                return;
            if (msg.sourceId === clientId)
                return;
            if (msg.docId !== nextScope.docId || msg.filename !== nextScope.filename)
                return;
            dispatcher.emitEvent(msg.event);
        };
    };
    return {
        emitEvent,
        emitOutcome,
        emitIncomingEvent: dispatcher.emitEvent,
        onMaterialized: dispatcher.onMaterialized,
        enableCrossTab,
        close,
    };
}
function materializationChannelName(scope) {
    return `${CROSS_TAB_MATERIALIZED_MESSAGE}:${scope.docId}:${scope.filename}`;
}
function randomClientId() {
    if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
        return crypto.randomUUID();
    }
    return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}
async function createWorkerClient(opts) {
    const materialized = createClientMaterializationDispatcher();
    // Keep the URL inline so Vite detects and bundles the worker properly.
    const worker = (opts.workerUrl
        ? new Worker(opts.workerUrl, { type: 'module' })
        : new Worker(new URL('./worker.js', import.meta.url), {
            type: 'module',
        }));
    let nextId = 1;
    const pending = new Map();
    let terminalError = null;
    let closed = false;
    let callQueue = Promise.resolve();
    const closedError = new Error(CLIENT_CLOSED_ERROR);
    const settleQueue = (promise) => promise.then(() => undefined, () => undefined);
    const callRaw = (method, params) => {
        if (closed)
            return Promise.reject(closedError);
        const id = nextId++;
        if (terminalError)
            return Promise.reject(terminalError);
        return new Promise((resolve, reject) => {
            pending.set(id, { resolve, reject });
            worker.postMessage({ id, method, params });
        });
    };
    const call = (method, params) => {
        const run = callQueue.then(() => callRaw(method, params));
        callQueue = settleQueue(run);
        return run;
    };
    const onMessage = (ev) => {
        const data = ev.data;
        if ('type' in data && data.type === 'materialized') {
            materialized.emitEvent(data.event);
            return;
        }
        const response = data;
        const handler = pending.get(response.id);
        if (!handler)
            return;
        pending.delete(response.id);
        if (response.ok)
            handler.resolve(response.result);
        else
            handler.reject(new Error(response.error || 'worker error'));
    };
    const onError = (ev) => {
        const err = new Error(ev.message || 'worker error');
        terminalError = err;
        for (const { reject } of pending.values())
            reject(err);
        pending.clear();
    };
    worker.addEventListener('message', onMessage);
    worker.addEventListener('error', onError);
    // init
    const initResult = (await call('init', [
        opts.baseUrl,
        opts.filename,
        opts.storage,
        opts.docId,
    ]));
    const effectiveStorage = initResult?.storage === 'opfs' ? 'opfs' : 'memory';
    const effectiveFilename = initResult?.filename ??
        (effectiveStorage === 'opfs' ? (opts.filename ?? '/treecrdt.db') : ':memory:');
    if (effectiveStorage === 'opfs') {
        materialized.enableCrossTab({ docId: opts.docId, filename: effectiveFilename });
    }
    const cleanup = () => {
        closed = true;
        materialized.close();
        for (const { reject } of pending.values())
            reject(closedError);
        pending.clear();
        worker.removeEventListener('error', onError);
        worker.removeEventListener('message', onMessage);
        worker.terminate();
    };
    if (opts.requireOpfs && effectiveStorage !== 'opfs') {
        const reason = initResult?.opfsError ? `: ${initResult.opfsError}` : '';
        try {
            if (!terminalError)
                await call('close', []);
        }
        catch {
            // ignore close errors on init failure
        }
        finally {
            cleanup();
        }
        throw new Error(`OPFS requested but could not be initialized${reason}`);
    }
    const closeImpl = async () => {
        if (closed)
            return;
        try {
            if (!terminalError)
                await call('close', []);
            cleanup();
        }
        finally {
            // noop: cleanup already handles terminal teardown, and repeated close is idempotent
        }
    };
    const dropImpl = async () => {
        if (closed)
            return;
        try {
            if (!terminalError)
                await call('drop', []);
            cleanup();
        }
        finally {
            // noop: cleanup already handles terminal teardown, and repeated drop is idempotent
        }
    };
    return makeTreecrdtClientFromCall({
        mode: 'worker',
        runtime: 'dedicated-worker',
        storage: effectiveStorage,
        docId: opts.docId,
        call,
        materialized,
        close: closeImpl,
        drop: dropImpl,
    });
}
// --- Shared worker client (one SQLite backend shared by same-origin tabs)
async function createSharedWorkerClient(opts) {
    const sharedWorker = opts.workerUrl
        ? new SharedWorker(opts.workerUrl, { name: opts.name, type: 'module' })
        : await createDefaultSharedWorker(opts.name);
    const port = sharedWorker.port;
    let nextId = 1;
    const pending = new Map();
    let terminalError = null;
    let closed = false;
    let callQueue = Promise.resolve();
    const closedError = new Error(CLIENT_CLOSED_ERROR);
    const settleQueue = (promise) => promise.then(() => undefined, () => undefined);
    const callRaw = (method, params) => {
        if (closed)
            return Promise.reject(closedError);
        const id = nextId++;
        if (terminalError)
            return Promise.reject(terminalError);
        return new Promise((resolve, reject) => {
            pending.set(id, { resolve, reject });
            port.postMessage({ id, method, params });
        });
    };
    const call = (method, params) => {
        const run = callQueue.then(() => callRaw(method, params));
        callQueue = settleQueue(run);
        return run;
    };
    const materialized = createClientMaterializationDispatcher({
        broadcast: (event) => {
            void call('broadcastMaterialized', [event]).catch(() => {
                // Closing tabs can race a final materialization notification.
            });
        },
    });
    const onMessage = (ev) => {
        const data = ev.data;
        if ('type' in data && data.type === 'materialized') {
            materialized.emitIncomingEvent(data.event);
            return;
        }
        const response = data;
        const handler = pending.get(response.id);
        if (!handler)
            return;
        pending.delete(response.id);
        if (response.ok)
            handler.resolve(response.result);
        else
            handler.reject(new Error(response.error || 'shared worker error'));
    };
    const onMessageError = () => {
        const err = new Error('shared worker message error');
        terminalError = err;
        for (const { reject } of pending.values())
            reject(err);
        pending.clear();
    };
    port.addEventListener('message', onMessage);
    port.addEventListener('messageerror', onMessageError);
    port.start();
    const initResult = (await call('init', [
        opts.baseUrl,
        opts.filename,
        opts.storage,
        opts.docId,
    ]));
    const effectiveStorage = initResult?.storage === 'opfs' ? 'opfs' : 'memory';
    const cleanup = () => {
        closed = true;
        materialized.close();
        for (const { reject } of pending.values())
            reject(closedError);
        pending.clear();
        port.removeEventListener('message', onMessage);
        port.removeEventListener('messageerror', onMessageError);
        port.close();
    };
    if (opts.requireOpfs && effectiveStorage !== 'opfs') {
        const reason = initResult?.opfsError ? `: ${initResult.opfsError}` : '';
        try {
            if (!terminalError)
                await call('close', []);
        }
        catch {
            // ignore close errors on init failure
        }
        finally {
            cleanup();
        }
        throw new Error(`OPFS requested but could not be initialized${reason}`);
    }
    const closeImpl = async () => {
        if (closed)
            return;
        try {
            if (!terminalError)
                await call('close', []);
            cleanup();
        }
        finally {
            // noop
        }
    };
    const dropImpl = async () => {
        if (closed)
            return;
        try {
            if (!terminalError)
                await call('drop', []);
            cleanup();
        }
        finally {
            // noop
        }
    };
    return makeTreecrdtClientFromCall({
        mode: 'worker',
        runtime: 'shared-worker',
        storage: effectiveStorage,
        docId: opts.docId,
        call,
        materialized,
        close: closeImpl,
        drop: dropImpl,
    });
}
async function createDefaultSharedWorker(name) {
    return new SharedWorker(new URL('./shared-worker.js', import.meta.url), 
    /* @vite-ignore */ { name, type: 'module' });
}
// --- Direct client (main-thread, used for memory or opt-in opfs)
async function createDirectClient(opts) {
    const materialized = createClientMaterializationDispatcher();
    const { baseUrl, storage, requireOpfs } = opts;
    const opened = await openTreecrdtDb({
        baseUrl,
        filename: opts.filename,
        storage,
        docId: opts.docId,
        requireOpfs,
        // The coop-sync OPFS VFS depends on createSyncAccessHandle, which is only
        // available in workers. Direct browser clients run on the window thread, so
        // OPFS needs the async any-context VFS.
        opfsVfs: storage === 'opfs' ? 'any-context' : undefined,
        onMaterialized: materialized.emitEvent,
    });
    const db = opened.db;
    const finalStorage = opened.storage;
    const filename = opened.filename;
    if (finalStorage === 'opfs') {
        materialized.enableCrossTab({ docId: opts.docId, filename });
    }
    const adapter = opened.api;
    const runner = {
        exec: (sql) => db.exec(sql),
        getText: (sql, params = []) => dbGetText(db, sql, params),
    };
    const wrapError = (stage, err) => new Error(JSON.stringify({
        stage,
        storage: finalStorage,
        filename,
        baseUrl,
        message: err instanceof Error ? err.message : String(err),
    }));
    let closed = false;
    const closedError = new Error(CLIENT_CLOSED_ERROR);
    let callQueue = Promise.resolve();
    const settleQueue = (promise) => promise.then(() => undefined, () => undefined);
    const runDirectCall = async (method, params) => {
        if (closed)
            throw closedError;
        try {
            switch (method) {
                case 'sqlExec': {
                    const [sql] = params;
                    await db.exec(sql);
                    return undefined;
                }
                case 'sqlGetText': {
                    const [sql, rawParams] = params;
                    return dbGetText(db, sql, (rawParams ?? []));
                }
                case 'append': {
                    const [op] = params;
                    return (await adapter.appendOp(op, nodeIdToBytes16, encodeReplica));
                }
                case 'appendMany': {
                    const [ops] = params;
                    return (await adapter.appendOps(ops, nodeIdToBytes16, encodeReplica));
                }
                case 'opsSince': {
                    const [lamport, root] = params;
                    return (await adapter.opsSince(lamport, root));
                }
                case 'opRefsAll':
                    return (await adapter.opRefsAll());
                case 'opRefsChildren': {
                    const [parent] = params;
                    return (await adapter.opRefsChildren(nodeIdToBytes16(parent)));
                }
                case 'opsByOpRefs': {
                    const [opRefs] = params;
                    return (await adapter.opsByOpRefs(opRefs.map((r) => Uint8Array.from(r))));
                }
                case 'treeChildren': {
                    const [parent] = params;
                    return (await adapter.treeChildren(nodeIdToBytes16(parent)));
                }
                case 'treeChildrenPage': {
                    const [parent, cursor, limit] = params;
                    const cursorBytes = cursor
                        ? {
                            orderKey: Uint8Array.from(cursor.orderKey),
                            node: Uint8Array.from(cursor.node),
                        }
                        : null;
                    return (await adapter.treeChildrenPage(nodeIdToBytes16(parent), cursorBytes, limit));
                }
                case 'treeDump':
                    return (await adapter.treeDump());
                case 'treePayload': {
                    const [node] = params;
                    const payload = await adapter.treePayload(nodeIdToBytes16(node));
                    return (payload === null ? null : Array.from(payload));
                }
                case 'treeNodeCount':
                    return (await adapter.treeNodeCount());
                case 'treeParent': {
                    const [node] = params;
                    const result = await adapter.treeParent(nodeIdToBytes16(node));
                    return result ? Array.from(result) : null;
                }
                case 'treeExists': {
                    const [node] = params;
                    return (await adapter.treeExists(nodeIdToBytes16(node)));
                }
                case 'headLamport':
                    return (await adapter.headLamport());
                case 'replicaMaxCounter': {
                    const [rawReplica] = params;
                    return (await adapter.replicaMaxCounter(Uint8Array.from(rawReplica)));
                }
                case 'close': {
                    if (db.close)
                        await db.close();
                    return undefined;
                }
                case 'drop': {
                    if (db.close)
                        await db.close();
                    if (finalStorage === 'opfs') {
                        await clearOpfsStorage(filename);
                    }
                    return undefined;
                }
                default:
                    throw new Error(`unsupported direct method: ${method}`);
            }
        }
        catch (err) {
            throw wrapError(method, err);
        }
    };
    const call = (method, params) => {
        const run = callQueue.then(() => runDirectCall(method, params));
        callQueue = settleQueue(run);
        return run;
    };
    return makeTreecrdtClientFromCall({
        mode: 'direct',
        runtime: 'direct',
        storage: finalStorage,
        docId: opts.docId,
        call,
        materialized,
        close: async () => {
            if (closed)
                return;
            if (db.close)
                await db.close();
            closed = true;
        },
        drop: async () => {
            if (closed)
                return;
            if (db.close)
                await db.close();
            if (finalStorage === 'opfs') {
                await clearOpfsStorage(filename);
            }
            closed = true;
        },
    });
}
// --- helpers
function makeTreecrdtClientFromCall(opts) {
    const call = opts.call;
    const materialized = opts.materialized;
    let closePromise = null;
    const runner = {
        exec: (sql) => call('sqlExec', [sql]).then(() => undefined),
        getText: (sql, params = []) => call('sqlGetText', [sql, params]),
    };
    const localWriters = new Map();
    const localWriterKey = (replica) => bytesToHex(replica);
    const localWriterFor = (replica) => {
        const key = localWriterKey(replica);
        const existing = localWriters.get(key);
        if (existing)
            return existing;
        const next = createTreecrdtSqliteWriter(runner, {
            replica,
            onMaterialized: materialized.emitEvent,
        });
        localWriters.set(key, next);
        return next;
    };
    const opsSinceImpl = async (lamport, root) => {
        const rows = await call('opsSince', [lamport, root]);
        return decodeSqliteOps(rows);
    };
    const opRefsAllImpl = async () => decodeSqliteOpRefs(await call('opRefsAll', []));
    const opRefsChildrenImpl = async (parent) => decodeSqliteOpRefs(await call('opRefsChildren', [parent]));
    const opsByOpRefsImpl = async (opRefs) => decodeSqliteOps(await call('opsByOpRefs', [opRefs.map((r) => Array.from(r))]));
    const treeChildrenImpl = async (parent) => decodeSqliteNodeIds(await call('treeChildren', [parent]));
    const treeChildrenPageImpl = async (parent, cursor, limit) => {
        const rpcCursor = cursor
            ? { orderKey: Array.from(cursor.orderKey), node: Array.from(cursor.node) }
            : null;
        return decodeSqliteTreeChildRows(await call('treeChildrenPage', [parent, rpcCursor, limit]));
    };
    const treeDumpImpl = async () => decodeSqliteTreeRows(await call('treeDump', []));
    const treeNodeCountImpl = async () => Number(await call('treeNodeCount', []));
    const treeParentImpl = async (node) => {
        const result = await call('treeParent', [node]);
        if (result === null)
            return null;
        return nodeIdFromBytes16(Uint8Array.from(result));
    };
    const treeExistsImpl = async (node) => Boolean(await call('treeExists', [node]));
    const treeGetPayloadImpl = async (node) => {
        const result = await call('treePayload', [node]);
        return result === null ? null : Uint8Array.from(result);
    };
    const headLamportImpl = async () => Number(await call('headLamport', []));
    const replicaMaxCounterImpl = async (replica) => Number(await call('replicaMaxCounter', [Array.from(encodeReplica(replica))]));
    const localInsertImpl = async (replica, parent, node, placement, payload, writeOpts) => localWriterFor(replica).insert(parent, node, placement, {
        ...writeOpts,
        ...(payload ? { payload } : {}),
    });
    const localMoveImpl = async (replica, node, newParent, placement, writeOpts) => localWriterFor(replica).move(node, newParent, placement, writeOpts);
    const localDeleteImpl = async (replica, node, writeOpts) => localWriterFor(replica).delete(node, writeOpts);
    const localPayloadImpl = async (replica, node, payload, writeOpts) => localWriterFor(replica).payload(node, payload, writeOpts);
    const local = createTreecrdtEngineLocal({
        insert: localInsertImpl,
        move: localMoveImpl,
        delete: localDeleteImpl,
        payload: localPayloadImpl,
    });
    const closeImpl = async () => {
        if (closePromise)
            return await closePromise;
        closePromise = (async () => {
            try {
                await opts.close();
            }
            catch {
                // Client teardown is best-effort. Fast refresh and overlapping resets can race a prior
                // close, and the underlying sqlite handle may already be gone by the time this runs.
            }
            finally {
                materialized.close();
            }
        })();
        await closePromise;
    };
    let dropPromise = null;
    const dropImpl = async () => {
        if (dropPromise)
            return await dropPromise;
        dropPromise = (async () => {
            try {
                await opts.drop();
            }
            finally {
                materialized.close();
            }
        })();
        await dropPromise;
    };
    return {
        mode: opts.mode,
        runtime: opts.runtime,
        storage: opts.storage,
        docId: opts.docId,
        runner,
        ops: {
            append: async (op, writeOpts) => {
                const outcome = await call('append', [op]);
                materialized.emitOutcome(outcome, writeOpts?.writeId);
            },
            appendMany: async (ops, writeOpts) => {
                const outcome = await call('appendMany', [ops]);
                materialized.emitOutcome(outcome, writeOpts?.writeId);
            },
            all: () => opsSinceImpl(0),
            since: opsSinceImpl,
            children: async (parent) => opsByOpRefsImpl(await opRefsChildrenImpl(parent)),
            get: opsByOpRefsImpl,
        },
        opRefs: { all: opRefsAllImpl, children: opRefsChildrenImpl },
        tree: {
            children: treeChildrenImpl,
            childrenPage: treeChildrenPageImpl,
            dump: treeDumpImpl,
            nodeCount: treeNodeCountImpl,
            parent: treeParentImpl,
            exists: treeExistsImpl,
            getPayload: treeGetPayloadImpl,
        },
        meta: { headLamport: headLamportImpl, replicaMaxCounter: replicaMaxCounterImpl },
        local,
        onMaterialized: materialized.onMaterialized,
        close: closeImpl,
        drop: dropImpl,
    };
}
function encodeReplica(replica) {
    return replicaIdToBytes(replica);
}
