import { getLogger } from "../../../util/logger.js";
const DEFAULT_MAX_QUEUE_SIZE = 10_000;
/**
 * Detects the transport-close rejection shape used by `ISocket` when a
 * pending request is rejected because the WebSocket closed mid-flight. Both
 * the plain-object rejection (`{code: 8}`) and the `SocketErrorException`
 * (`code: 10`) paths surface the same message substring.
 */
function isTransportCloseError(err) {
    if (!err || typeof err !== "object" || !("message" in err))
        return false;
    const message = err.message;
    return (typeof message === "string" &&
        message.includes("[internal] Connection closed"));
}
/**
 * Identity-stable wrapper around the editor `EditorClient`.
 *
 * The underlying socket client is swapped on every WebSocket reconnect.
 * Callers that cache a reference (directly or via a closure) would otherwise
 * keep pushing to the dead pre-reconnect instance. StablePeer keeps the
 * reference stable: `clark.context.peer` is always the same object, and
 * `setInner` only swaps what's behind it.
 *
 * Calls made while disconnected are queued and replayed in order when the
 * next peer is bound, up to `maxQueueSize`. The queue lives in process
 * memory; durability across server restarts is a separate concern.
 */
export class StablePeer {
    inner;
    _peerId;
    queue = [];
    maxQueueSize;
    call;
    constructor(options = {}) {
        this.maxQueueSize = options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
        this.call = this.buildCallProxy([]);
    }
    get peerId() {
        return this._peerId;
    }
    /**
     * Bind or clear the backing peer and its identity token atomically.
     * Binding flushes any queued calls in order to the new peer.
     */
    setInner(binding) {
        if (binding.peer) {
            this.inner = binding.peer;
            this._peerId = binding.peerId;
            this.flush(binding.peer);
        }
        else {
            this.inner = undefined;
            this._peerId = undefined;
        }
    }
    close() {
        this.inner?.close();
    }
    /** Not used in practice; throws if exercised so new callers are caught in review. */
    async requestWithTracing() {
        throw new Error("StablePeer.requestWithTracing is not supported — use call.* instead");
    }
    /**
     * Memoized per-path so `peer.call.aiPushMessage` returns the same callable
     * on every property access. Streaming handlers invoke `peer.call.X(...)` in
     * tight loops; without memoization each access would allocate a fresh
     * array, nested Proxy, and closure.
     */
    buildCallProxy(path) {
        const children = new Map();
        return new Proxy({}, {
            get: (_target, prop) => {
                if (typeof prop !== "string")
                    return undefined;
                const cached = children.get(prop);
                if (cached !== undefined)
                    return cached;
                // Routes leaf invocations through `dispatch`; nested property
                // access recurses so `call.a.b(...)` works if the method tree
                // ever nests beyond a single level.
                const nextPath = [...path, prop];
                const invoke = (...args) => this.dispatch(nextPath, args);
                const nested = this.buildCallProxy(nextPath);
                const routed = new Proxy(invoke, {
                    get: (_t, nestedProp) => nested[nestedProp],
                });
                children.set(prop, routed);
                return routed;
            },
        });
    }
    dispatch(path, args) {
        const inner = this.inner;
        if (!inner)
            return this.enqueue(path, args);
        return this.invokeOnInner(inner, path, args).catch((err) => {
            // Observability: in-flight calls on a live inner can still be rejected
            // if the transport closes between send and ack. StablePeer does not
            // retry these today; log so production rates are visible in traces.
            if (isTransportCloseError(err)) {
                getLogger().warn(`[stable-peer] in-flight call to ${path.join(".")} dropped by transport close (not retried)`);
            }
            throw err;
        });
    }
    invokeOnInner(inner, path, args) {
        let target = inner.call;
        for (const segment of path) {
            target = target[segment];
        }
        if (typeof target !== "function") {
            return Promise.reject(new Error(`StablePeer: inner peer has no method at path '${path.join(".")}'`));
        }
        return target(...args);
    }
    enqueue(path, args) {
        return new Promise((resolve, reject) => {
            if (this.queue.length >= this.maxQueueSize) {
                const evicted = this.queue.shift();
                evicted?.reject(new Error("peer_queue_overflow"));
                getLogger().warn(`[stable-peer] queue size cap (${this.maxQueueSize}) hit; evicted oldest call to ${evicted?.path.join(".") ?? "unknown"}`);
            }
            this.queue.push({ path, args, resolve, reject });
        });
    }
    flush(inner) {
        if (this.queue.length === 0)
            return;
        const pending = this.queue;
        this.queue = [];
        getLogger().info(`[stable-peer] flushing ${pending.length} queued call(s) to reconnected peer`);
        for (const item of pending) {
            this.invokeOnInner(inner, item.path, item.args).then(item.resolve, (err) => {
                getLogger().warn(`[stable-peer] flushed call to ${item.path.join(".")} failed`, err);
                item.reject(err);
            });
        }
    }
}
//# sourceMappingURL=stable-peer.js.map