/**
 * Realtime gateway for `@actuate-media/realtime` rooms.
 *
 * The gateway is **transport-agnostic by design**: it does not depend on
 * `ws`, `uWebSockets.js`, Cloudflare Workers, or any specific framework.
 * Consumers create a `WebSocketLike` adapter around their server's socket
 * primitive and pass it to `gateway.handleConnection(socket, req)`.
 *
 * Responsibilities:
 *   1. Authenticate the connection (delegated to `options.authenticate`).
 *   2. Look up (or lazily create) the `Room` for the document.
 *   3. Pump bytes between the socket and the room.
 *   4. Persist edits via the configured `DocumentPersistence`.
 *   5. Reap idle rooms (no connections + no pending writes) to bound memory.
 *
 * The companion file `persistence.ts` provides
 * `createPrismaDocumentPersistence(db)` which the gateway uses by default
 * when callers don't supply their own.
 */
import { bindPersistence, createRoom, } from '@actuate-media/realtime';
/**
 * Create a realtime gateway. The returned object is intended to live for the
 * lifetime of the server process — one per node. Calling
 * `shutdown()` is mandatory for graceful exit (it flushes pending writes).
 */
export function createRealtimeGateway(opts) {
    const persistence = opts.persistence ?? createNoopPersistence();
    const idleTimeoutMs = opts.idleTimeoutMs ?? 60_000;
    const onError = opts.onPersistenceError ??
        ((documentId, err) => {
            // eslint-disable-next-line no-console
            console.error(`[cms-core realtime] save failed for ${documentId}:`, err);
        });
    const rooms = new Map();
    let connectionCount = 0;
    let shuttingDown = false;
    async function getOrCreateRoom(documentId) {
        const existing = rooms.get(documentId);
        if (existing) {
            if (existing.reapTimer) {
                clearTimeout(existing.reapTimer);
                existing.reapTimer = null;
            }
            return existing;
        }
        const initial = (await persistence.load(documentId)) ?? undefined;
        const room = createRoom({ documentId, initialState: initial });
        const binding = bindPersistence(room, persistence, {
            debounceMs: opts.persistenceDebounceMs,
            maxIdleMs: opts.persistenceMaxIdleMs,
            onError: (err) => onError(documentId, err),
        });
        const entry = { room, binding, connections: 0, reapTimer: null };
        rooms.set(documentId, entry);
        opts.onRoomOpen?.(documentId);
        return entry;
    }
    function scheduleReap(documentId) {
        const entry = rooms.get(documentId);
        if (!entry)
            return;
        if (entry.connections > 0)
            return;
        if (entry.reapTimer)
            return;
        if (!Number.isFinite(idleTimeoutMs))
            return;
        entry.reapTimer = setTimeout(() => {
            void destroyRoom(documentId);
        }, idleTimeoutMs);
    }
    async function destroyRoom(documentId) {
        const entry = rooms.get(documentId);
        if (!entry)
            return;
        rooms.delete(documentId);
        if (entry.reapTimer) {
            clearTimeout(entry.reapTimer);
            entry.reapTimer = null;
        }
        try {
            await entry.binding.flush();
        }
        catch (err) {
            onError(documentId, err);
        }
        entry.binding.unbind();
        entry.room.destroy();
        opts.onRoomClose?.(documentId);
    }
    async function handleConnection(socket, req) {
        if (shuttingDown) {
            try {
                socket.close(1001, 'server shutting down');
            }
            catch {
                /* ignore */
            }
            return;
        }
        let auth;
        try {
            auth = await opts.authenticate(req);
        }
        catch {
            auth = null;
        }
        if (!auth) {
            try {
                socket.close(4401, 'unauthorized');
            }
            catch {
                /* ignore */
            }
            return;
        }
        const entry = await getOrCreateRoom(auth.documentId);
        if (entry.reapTimer) {
            clearTimeout(entry.reapTimer);
            entry.reapTimer = null;
        }
        const conn = {
            id: auth.connectionId,
            send: (bytes) => {
                try {
                    socket.send(bytes);
                }
                catch {
                    /* surfaced via close handler */
                }
            },
            close: (reason) => {
                try {
                    socket.close(1000, reason ?? 'closed');
                }
                catch {
                    /* ignore */
                }
            },
        };
        try {
            entry.room.addConnection(conn);
        }
        catch {
            // duplicate connection id — close and bail out.
            try {
                socket.close(4409, 'duplicate connection id');
            }
            catch {
                /* ignore */
            }
            return;
        }
        entry.connections += 1;
        connectionCount += 1;
        socket.on('message', (data) => {
            try {
                entry.room.handleMessage(auth.connectionId, data);
            }
            catch {
                // Malformed frame — close the offending peer; other connections continue.
                try {
                    socket.close(1003, 'malformed frame');
                }
                catch {
                    /* ignore */
                }
            }
        });
        socket.on('error', () => {
            try {
                socket.close(1011, 'socket error');
            }
            catch {
                /* ignore */
            }
        });
        socket.on('close', () => {
            entry.room.removeConnection(auth.connectionId);
            entry.connections = Math.max(0, entry.connections - 1);
            connectionCount = Math.max(0, connectionCount - 1);
            if (entry.connections === 0) {
                scheduleReap(auth.documentId);
            }
        });
    }
    async function shutdown() {
        shuttingDown = true;
        const docIds = Array.from(rooms.keys());
        await Promise.all(docIds.map((id) => destroyRoom(id)));
        connectionCount = 0;
    }
    return {
        get roomCount() {
            return rooms.size;
        },
        get connectionCount() {
            return connectionCount;
        },
        handleConnection,
        shutdown,
    };
}
/** Persistence that swallows writes — used when consumers don't pass one. */
function createNoopPersistence() {
    return {
        async load() {
            return null;
        },
        async save() {
            /* discard */
        },
    };
}
//# sourceMappingURL=gateway.js.map