"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MeshSrpcServer = exports.__ΩMeshSrpcServerOptions = void 0;
function __assignType(fn, args) {
    fn.__type = args;
    return fn;
}
const SrpcServer_1 = require("../../srpc/SrpcServer");
const logger_1 = require("../logger");
const mesh_client_service_1 = require("./mesh-client-service");
const types_1 = require("./types");
const __ΩMeshSrpcServerOptions = ['TMeta', 'TRegistryMeta', 'meshKey', 'MeshServiceOptions', 'meshOptions', 'MeshClientRegistryBackend', 'registryBackend', 'SrpcStream', 'stream', '', 'extractMetadata', 'MeshSrpcServerOptions', 'b!e!!c"P&4#"w$4%8"w&4\'8P"w(2)e#"/*4+8Mw,y'];
exports.__ΩMeshSrpcServerOptions = __ΩMeshSrpcServerOptions;
// --- MeshSrpcServer ---
class MeshSrpcServer extends SrpcServer_1.SrpcServer {
    constructor(options) {
        super(options);
        this.meshLogger = (0, logger_1.createLogger)(this);
        this.connectedCallbacks = (Set.Ω = [['clientId', 'metadata', '', 'P&2!!2"P$$`J/#']], new Set());
        this.disconnectedCallbacks = (Set.Ω = [['clientId', 'metadata', '', 'P&2!!2"P$$`J/#']], new Set());
        this.orphanedCallbacks = (Set.Ω = [['nodeId', 'RegisteredClient', 'clients', '', 'P\'2!"w"F2#P$$`J/$']], new Set());
        // Track metadata for connect/disconnect callbacks.
        this.clientMetadata = (Map.Ω = [['&'], ['!']], new Map());
        this.lifecycleConnectedStreams = (WeakSet.Ω = [['SrpcStream', '"w!']], new WeakSet());
        // Serialize registry mutations per client to prevent race conditions
        // without letting slow user callbacks block reconnects.
        this.clientRegistryChains = (Map.Ω = [['&'], ['$`']], new Map());
        this.clientCallbackChains = (Map.Ω = [['&'], ['$`']], new Map());
        // Microtask-debounced sync tracking
        this.pendingSyncs = (Set.Ω = [['&']], new Set());
        this.extractMetadataFn = options.extractMetadata;
        // Cast needed: MeshClientServiceOptions doesn't carry TBroadcasts,
        // but the broadcast generic only affects registerBroadcastHandler/broadcast
        // which are type-safe at the call site.
        this.meshClientService = new mesh_client_service_1.MeshClientService({
            key: options.meshKey,
            meshOptions: options.meshOptions,
            registryBackend: options.registryBackend,
            clientInvokeFn: __assignType(async (clientId, type, data, timeoutMs) => {
                const stream = this.streamsByClientId.get(clientId);
                if (!stream) {
                    throw new types_1.ClientDisconnectedError(clientId);
                }
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                return super.invoke(stream, type, data, timeoutMs);
            }, ['clientId', 'type', 'data', 'timeoutMs', '', 'P&2!&2"#2#\'2$8#`/%']),
            clientUpdateMetaFn: __assignType((clientId, metadata) => {
                const stream = this.streamsByClientId.get(clientId);
                if (!stream)
                    return false;
                // Merge registry metadata onto stream.meta.
                // The proxy traps fire scheduleSyncStreamMeta for each set,
                // but microtask debounce batches them into a single Redis update.
                const meta = stream.meta;
                const update = metadata;
                for (const key of Object.keys(update)) {
                    meta[key] = update[key];
                }
                return true;
            }, ['clientId', 'metadata', '', 'P&2!!2")/#'])
        });
        // Wire up cross-pod duplicate detection: disconnect local stream when
        // the same client connects on a different node.
        this.meshClientService.onClientSuperseded(__assignType(async (clientId) => {
            const stream = this.getCurrentStreamByClientId(clientId);
            if (stream) {
                this.meshLogger.info('Disconnecting superseded client', { clientId });
                this.cleanupStream(stream, 'supersede');
            }
        }, ['clientId', '', 'P"2!"/"']));
        // Wire up mesh node cleanup callback
        this.meshClientService.onNodeClientsOrphaned(__assignType(async (nodeId, orphaned) => {
            for (const cb of this.orphanedCallbacks) {
                try {
                    await cb(nodeId, orphaned);
                }
                catch (err) {
                    this.meshLogger.warn('orphaned callback error', { err, nodeId });
                }
            }
        }, ['nodeId', 'orphaned', '', 'P"2!"2""/#']));
    }
    ////////////////////////////////////////
    // Post-establish check — reserve mesh ownership before activation
    /**
     * Defers stream activation until mesh reservation succeeds.
     * Installs meta proxy and reserves the client atomically in Redis
     * (respecting allowSupersede for v2), all serialized in the per-client
     * registry chain. Reserved clients remain hidden from lookup/invoke until
     * onStreamActivated promotes them to active.
     *
     * If registration returns a conflict, cleanupStream is called
     * (which fires onStreamDisconnected and drains the queue) so
     * no connection handlers or RPCs ever run on the rejected stream.
     */
    postEstablishCheck(stream) {
        // Install proxy before anything can mutate meta (even though
        // onStreamConnected hasn't fired yet, code may hold a reference).
        this.installMetaProxy(stream);
        const metadata = snapshotMetadata(this.extractMeta(stream));
        this.clientMetadata.set(stream.clientId, metadata);
        const allowSupersede = !(stream.protocolVersion >= 2 && !stream.supersede);
        return this.enqueueClientRegistry(stream.clientId, async () => {
            // Stream cleaned up during queue wait (disconnect / reconnect)
            if (stream.lastPingAt < 0 || !this.isCurrentStream(stream)) {
                return true;
            }
            const registered = await this.meshClientService.reserveClient(stream.clientId, metadata, allowSupersede);
            if (!registered) {
                this.meshLogger.warn('Rejecting stream due to cross-pod conflict', {
                    streamId: stream.id,
                    clientId: stream.clientId
                });
                this.cleanupStream(stream, 'conflict');
                return true;
            }
            return false;
        });
    }
    ////////////////////////////////////////
    // Lifecycle overrides — connection handlers + mesh callbacks
    extractMeta(stream) {
        return this.extractMetadataFn ? this.extractMetadataFn(stream) : stream.meta;
    }
    /**
     * Install a Proxy on stream.meta that schedules a microtask-debounced
     * sync to Redis whenever any property is mutated.
     *
     * This means handler code, connection handlers, and external code
     * (e.g. FreeSwitch controller) can all mutate stream.meta directly
     * and the mesh registry stays in sync — no manual sync calls needed.
     *
     * **Limitation:** Only top-level property mutations are tracked.
     * Nested mutations (e.g. `stream.meta.user.name = 'Bob'`) do NOT
     * trigger a sync. For nested metadata, either reassign the top-level
     * property (`stream.meta.user = { ...stream.meta.user, name: 'Bob' }`)
     * or call `updateClientMetadata()` explicitly.
     */
    installMetaProxy(stream) {
        // Guard against double-proxy (e.g. meshStart backfill after postEstablishCheck)
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        if (stream.meta[MeshSrpcServer.PROXIED])
            return;
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const self = this;
        const clientId = stream.clientId;
        const proxied = new Proxy(stream.meta, {
            get: __assignType(function get(target, prop) {
                if (prop === MeshSrpcServer.PROXIED)
                    return true;
                return target[prop];
            }, ['target', 'prop', 'get', 'P"2!"2""/#']),
            set: __assignType(function set(target, prop, value) {
                target[prop] = value;
                self.scheduleSyncStreamMeta(clientId, stream);
                return true;
            }, ['target', 'prop', 'value', 'set', 'P"2!"2""2#"/$']),
            deleteProperty: __assignType(function deleteProperty(target, prop) {
                delete target[prop];
                self.scheduleSyncStreamMeta(clientId, stream);
                return true;
            }, ['target', 'prop', 'deleteProperty', 'P"2!"2""/#'])
        });
        // Replace meta with proxied version.
        // stream is a plain object, so this is safe despite the readonly type.
        stream.meta = proxied;
    }
    /**
     * Schedule a microtask-debounced sync for a client.
     * Multiple synchronous mutations are batched into a single sync.
     */
    scheduleSyncStreamMeta(clientId, stream) {
        if (this.pendingSyncs.has(clientId))
            return;
        this.pendingSyncs.add(clientId);
        queueMicrotask(() => {
            this.pendingSyncs.delete(clientId);
            // Only sync if this stream is still current for this client.
            if (this.isCurrentStream(stream)) {
                this.syncStreamMeta(stream);
            }
        });
    }
    async onStreamConnected(stream) {
        // Run user-registered connection handlers after the initial ping.
        // They may mutate stream.meta; the proxy will sync those changes.
        await super.onStreamConnected(stream);
    }
    async onStreamActivated(stream) {
        const metadata = snapshotMetadata(this.extractMeta(stream));
        this.clientMetadata.set(stream.clientId, metadata);
        if (this.meshInstanceId !== 0) {
            const activated = await this.enqueueClientRegistry(stream.clientId, async () => {
                if (stream.lastPingAt < 0 || this.streamsByClientId.get(stream.clientId) !== stream) {
                    return false;
                }
                return this.meshClientService.activateClient(stream.clientId, metadata);
            });
            if (!activated) {
                this.meshLogger.warn('client activation missing mesh reservation', {
                    streamId: stream.id,
                    clientId: stream.clientId
                });
                this.cleanupStream(stream, 'disconnect');
                return;
            }
        }
        this.syncStreamMeta(stream);
        await this.enqueueClientCallback(stream.clientId, async () => {
            // Skip stale connection callbacks for streams that disconnected
            // or were replaced before activation finished.
            if (stream.lastPingAt < 0 || this.streamsByClientId.get(stream.clientId) !== stream) {
                return;
            }
            if (!this.clientMetadata.has(stream.clientId)) {
                this.meshLogger.warn('client metadata missing during activation', {
                    streamId: stream.id,
                    clientId: stream.clientId
                });
                return;
            }
            this.lifecycleConnectedStreams.add(stream);
            const metadata = this.clientMetadata.get(stream.clientId);
            for (const cb of this.connectedCallbacks) {
                try {
                    await cb(stream.clientId, metadata);
                }
                catch (err) {
                    this.meshLogger.warn('client connected callback error', { err, clientId: stream.clientId });
                }
            }
        });
    }
    onStreamDisconnected(stream, cause) {
        super.onStreamDisconnected(stream, cause);
        const publishedLifecycle = this.lifecycleConnectedStreams.has(stream);
        this.lifecycleConnectedStreams.delete(stream);
        void this.enqueueClientRegistry(stream.clientId, async () => {
            // If a replacement stream is already connected, this is a stale
            // disconnect (same-node reconnect). Skip unregister and callbacks,
            // and leave clientMetadata intact for the new stream.
            const currentStream = this.getCurrentStreamByClientId(stream.clientId);
            if (currentStream && currentStream !== stream) {
                return;
            }
            const hasMetadata = this.clientMetadata.has(stream.clientId);
            const metadata = this.clientMetadata.get(stream.clientId);
            const removed = await this.meshClientService.unregisterClient(stream.clientId);
            if (removed && hasMetadata && publishedLifecycle) {
                this.clientMetadata.delete(stream.clientId);
                void this.enqueueClientCallback(stream.clientId, async () => {
                    for (const cb of this.disconnectedCallbacks) {
                        try {
                            await cb(stream.clientId, metadata);
                        }
                        catch (err) {
                            this.meshLogger.warn('client disconnected callback error', { err, clientId: stream.clientId });
                        }
                    }
                });
            }
            else if (removed && publishedLifecycle) {
                this.meshLogger.warn('client metadata missing during disconnect cleanup', {
                    streamId: stream.id,
                    clientId: stream.clientId
                });
                this.clientMetadata.delete(stream.clientId);
            }
            else {
                this.clientMetadata.delete(stream.clientId);
            }
        });
    }
    ////////////////////////////////////////
    // Meta sync
    /**
     * Sync the current stream.meta to the mesh registry.
     * Called automatically by the meta proxy's microtask debounce.
     * Routed through enqueueClientRegistry so updates are serialized
     * after initial registration (prevents lost updates if registration
     * hasn't completed yet).
     */
    syncStreamMeta(stream) {
        // Snapshot the current metadata so we compare values, not references.
        // Without this, the default path (no extractMetadataFn) returns the
        // same proxied object stored in clientMetadata, so shallowChanged
        // would always return false.
        const metadata = snapshotMetadata(this.extractMeta(stream));
        const existing = this.clientMetadata.get(stream.clientId);
        if (existing && !shallowChanged(existing, metadata))
            return;
        this.clientMetadata.set(stream.clientId, metadata);
        // Write directly to the registry (always local/owning node).
        // Do NOT route through meshClientService.updateClientMetadata here —
        // that would loop back into clientUpdateMetaFn → stream.meta → proxy.
        void this.enqueueClientRegistry(stream.clientId, async () => {
            await this.clientRegistry.updateMetadata(stream.clientId, metadata);
        });
    }
    ////////////////////////////////////////
    // Client lifecycle serialization
    enqueueClientRegistry(clientId, fn) {
        const prev = this.clientRegistryChains.get(clientId) ?? Promise.resolve();
        const next = prev.then(fn, fn);
        const safeNext = next.then(() => undefined, __assignType(err => {
            this.meshLogger.warn('client registry error', { err, clientId });
        }, ['err', '', 'P"2!"/"']));
        const chain = safeNext.finally(() => {
            // Clean up the chain entry if it's still ours
            if (this.clientRegistryChains.get(clientId) === chain) {
                this.clientRegistryChains.delete(clientId);
            }
        });
        this.clientRegistryChains.set(clientId, chain);
        return next;
    }
    enqueueClientCallback(clientId, fn) {
        const prev = this.clientCallbackChains.get(clientId) ?? Promise.resolve();
        const next = prev.then(fn, fn);
        const safeNext = next.then(() => undefined, __assignType(err => {
            this.meshLogger.warn('client callback error', { err, clientId });
        }, ['err', '', 'P"2!"/"']));
        const chain = safeNext.finally(() => {
            if (this.clientCallbackChains.get(clientId) === chain) {
                this.clientCallbackChains.delete(clientId);
            }
        });
        this.clientCallbackChains.set(clientId, chain);
        return next;
    }
    ////////////////////////////////////////
    // Public API
    get meshInstanceId() {
        return this.meshClientService.instanceId;
    }
    get clientRegistry() {
        return this.meshClientService.clientRegistry;
    }
    /**
     * Update metadata for a client, regardless of which node owns it.
     * Routes through the mesh to the owning node so that stream.meta
     * reflects the change immediately and the proxy auto-syncs to Redis.
     * For local streams, you can also mutate stream.meta directly.
     */
    async updateClientMetadata(clientId, metadata) {
        // Set clientMetadata eagerly so the proxy's deferred syncStreamMeta
        // sees shallowChanged=false and skips the redundant Redis write.
        const previous = this.clientMetadata.get(clientId);
        this.clientMetadata.set(clientId, snapshotMetadata(metadata));
        const updated = await this.meshClientService.updateClientMetadata(clientId, metadata);
        if (!updated) {
            // Restore on failure
            if (previous !== undefined) {
                this.clientMetadata.set(clientId, previous);
            }
            else {
                this.clientMetadata.delete(clientId);
            }
        }
        return updated;
    }
    onClientConnected(handler) {
        this.connectedCallbacks.add(handler);
    }
    onClientDisconnected(handler) {
        this.disconnectedCallbacks.add(handler);
    }
    onNodeClientsOrphaned(handler) {
        this.orphanedCallbacks.add(handler);
    }
    registerBroadcastHandler(type, handler) {
        this.meshClientService.registerBroadcastHandler(type, handler);
    }
    async broadcast(type, data, options) {
        return this.meshClientService.broadcast(type, data, options);
    }
    /**
     * Invoke a client method across any node in the mesh.
     * Overloaded: when called with a stream, delegates to SrpcServer.invoke.
     * When called with a clientId string, routes through the mesh.
     */
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    invoke(streamOrClientId, prefix, data, timeoutMs) {
        if (typeof streamOrClientId === 'string') {
            return this.meshClientService.invoke(streamOrClientId, prefix, data, timeoutMs);
        }
        return super.invoke(streamOrClientId, prefix, data, timeoutMs);
    }
    async meshStart() {
        await this.meshClientService.start();
        // Backfill clients that connected before mesh tracking was running.
        // Route through enqueueClientRegistry so backfill registrations are
        // serialized with any concurrent disconnect for the same clientId.
        const backfillStreams = (Map.Ω = [['&'], ['SrpcStream', '"w!']], new Map());
        for (const [clientId, stream] of this.pendingStreamsByClientId) {
            backfillStreams.set(clientId, stream);
        }
        for (const [clientId, stream] of this.streamsByClientId) {
            if (!backfillStreams.has(clientId)) {
                backfillStreams.set(clientId, stream);
            }
        }
        const backfillPromises = [];
        for (const [clientId, stream] of backfillStreams) {
            // Install proxy if not already proxied (streams that connected before meshStart)
            this.installMetaProxy(stream);
            if (!this.clientMetadata.has(clientId)) {
                const metadata = snapshotMetadata(this.extractMeta(stream));
                this.clientMetadata.set(clientId, metadata);
            }
            const metadata = this.clientMetadata.get(clientId);
            const allowSupersede = !(stream.protocolVersion >= 2 && !stream.supersede);
            void this.enqueueClientRegistry(clientId, async () => {
                // Only backfill the current stream (active or pending).
                const currentStream = this.getCurrentStreamByClientId(clientId);
                if (currentStream !== stream)
                    return;
                const registered = stream.isActivated
                    ? await this.meshClientService.registerClient(clientId, metadata, allowSupersede)
                    : await this.meshClientService.reserveClient(clientId, metadata, allowSupersede);
                if (!registered) {
                    this.meshLogger.warn('Backfill rejected: cross-pod conflict', { clientId });
                    this.cleanupStream(stream, 'conflict');
                }
            });
            const chain = this.clientRegistryChains.get(clientId);
            if (chain)
                backfillPromises.push(chain);
        }
        await Promise.all(backfillPromises);
    }
    async meshStop() {
        await this.meshClientService.stop();
        this.clientMetadata.clear();
    }
}
exports.MeshSrpcServer = MeshSrpcServer;
MeshSrpcServer.PROXIED = Symbol('proxied');
MeshSrpcServer.__type = ['SrpcMeta', 'TMeta', 'BaseMessage', 'TClientOutput', 'TServerOutput', 'TRegistryMeta', 'TBroadcasts', () => SrpcServer_1.SrpcServer, () => mesh_client_service_1.MeshClientService, 'meshClientService', 'meshLogger', function () { return (0, logger_1.createLogger)(this); }, 'SrpcStream', 'stream', '', 'extractMetadataFn', 'connectedCallbacks', function () { return (Set.Ω = [['clientId', 'metadata', '', 'P&2!!2"P$$`J/#']], new Set()); }, 'disconnectedCallbacks', function () { return (Set.Ω = [['clientId', 'metadata', '', 'P&2!!2"P$$`J/#']], new Set()); }, 'orphanedCallbacks', function () { return (Set.Ω = [['nodeId', 'RegisteredClient', 'clients', '', 'P\'2!"w"F2#P$$`J/$']], new Set()); }, 'clientMetadata', function () { return (Map.Ω = [['&'], ['!']], new Map()); }, 'lifecycleConnectedStreams', function () { return (WeakSet.Ω = [['SrpcStream', '"w!']], new WeakSet()); }, 'clientRegistryChains', function () { return (Map.Ω = [['&'], ['$`']], new Map()); }, 'clientCallbackChains', function () { return (Map.Ω = [['&'], ['$`']], new Map()); }, 'pendingSyncs', function () { return (Set.Ω = [['&']], new Set()); }, 'ISrpcServerOptions', () => __ΩMeshSrpcServerOptions, 'options', 'constructor', 'postEstablishCheck', 'extractMeta', 'PROXIED', function () { return Symbol('proxied'); }, 'installMetaProxy', 'clientId', 'scheduleSyncStreamMeta', 'onStreamConnected', 'onStreamActivated', 'SrpcDisconnectCause', 'cause', 'onStreamDisconnected', 'syncStreamMeta', 'fn', 'enqueueClientRegistry', 'enqueueClientCallback', 'metadata', 'updateClientMetadata', 'handler', 'onClientConnected', 'onClientDisconnected', 'nodeId', 'RegisteredClient', 'clients', 'onNodeClientsOrphaned', 'type', 'data', 'senderInstanceId', 'registerBroadcastHandler', 'MeshBroadcastOptions', 'broadcast', 'streamOrClientId', 'prefix', 'timeoutMs', 'invoke', 'meshStart', 'meshStop', 'MeshSrpcServer', '"w!c""w#c$"w#c%e!!c&PMc\'Pe"!e""e"#7(Pe"$e"%7)3*;!3+;>,P"w-2.e"$//308;!31;>2!33;>4!35;>6!37;>8!39;>:!3;;><!3=;>>!3?;>@PP"wAe#!e#$oB#K2C"0DP"w-2.)`0E<P"w-2.e"$0F;+3G9;s>HP"w-2.$0I;P&2J"w-2.$0K;P"w-2.$`0L<P"w-2.$`0M<P"w-2."wN2O$0P<P"w-2.$0Q;P&2JP"`//2R"`0S;P&2JP"`//2R"`0T;!!P&2Je"$2U)`0VPP&2Je#$2UP$$`J//2W$0XPP&2Je#$2UP$$`J//2W$0YPP\'2Z"w[F2\\P$$`J//2W$0]P"2^Pe#%"f2_\'2`P$$`J//2W$0aP"2^e"%"f2_"wb2C8$`0cPP"w-&J2d"2e"2_\'2f8"`0gP$`0hP$`0i5e!!e!"e!#6$wj'];
////////////////////////////////////////
// Helpers
function snapshotMetadata(meta) {
    if (Array.isArray(meta))
        return [...meta];
    if (!isPlainObject(meta))
        return meta;
    return { ...meta };
}
snapshotMetadata.__type = ['meta', 'snapshotMetadata', 'P"2!"/"'];
function isPlainObject(value) {
    if (typeof value !== 'object' || value === null)
        return false;
    const proto = Object.getPrototypeOf(value);
    return proto === Object.prototype || proto === null;
}
isPlainObject.__type = ['value', 'isPlainObject', 'P#2!!/"'];
function shallowChanged(a, b) {
    if (a === b)
        return false;
    if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
        return a !== b;
    const aObj = a;
    const bObj = b;
    const keys = new Set([...Object.keys(aObj), ...Object.keys(bObj)]);
    for (const key of keys) {
        if (aObj[key] !== bObj[key])
            return true;
    }
    return false;
}
shallowChanged.__type = ['a', 'b', 'shallowChanged', 'P#2!#2")/#'];
//# sourceMappingURL=mesh-srpc-server.js.map