{"version":3,"file":"realtime-store-CeDT7vJF.mjs","sources":["../.rollup-tmp/client/realtime-store.js"],"sourcesContent":["// ---------------------------------------------------------------------------\n// realtime-store.ts — Client-side state manager for realtime apps.\n//\n// Manages: WS connection, in-memory state, IDB persistence, optimistic\n// writes, delta accumulation, loading states, ephemeral/durable tiers.\n// ---------------------------------------------------------------------------\nimport { getConfig, isBoundedNetwork } from './config';\n// ---------------------------------------------------------------------------\n// IDB helpers (lazy-loaded, non-blocking)\n// ---------------------------------------------------------------------------\nconst IDB_NAME = 'bounded-realtime';\nconst IDB_STORE = 'subscriptions';\nconst IDB_VERSION = 1;\nlet idbPromise = null;\nfunction getIDB() {\n    if (idbPromise)\n        return idbPromise;\n    if (typeof indexedDB === 'undefined') {\n        return Promise.reject(new Error('IndexedDB not available'));\n    }\n    idbPromise = new Promise((resolve, reject) => {\n        const req = indexedDB.open(IDB_NAME, IDB_VERSION);\n        req.onupgradeneeded = () => {\n            const db = req.result;\n            if (!db.objectStoreNames.contains(IDB_STORE)) {\n                db.createObjectStore(IDB_STORE);\n            }\n        };\n        req.onsuccess = () => resolve(req.result);\n        req.onerror = () => reject(req.error);\n    });\n    return idbPromise;\n}\nasync function idbGet(key) {\n    try {\n        const db = await getIDB();\n        return new Promise((resolve) => {\n            const tx = db.transaction(IDB_STORE, 'readonly');\n            const store = tx.objectStore(IDB_STORE);\n            const req = store.get(key);\n            req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };\n            req.onerror = () => resolve(null);\n        });\n    }\n    catch (_a) {\n        return null;\n    }\n}\nasync function idbSet(key, value) {\n    try {\n        const db = await getIDB();\n        return new Promise((resolve) => {\n            const tx = db.transaction(IDB_STORE, 'readwrite');\n            const store = tx.objectStore(IDB_STORE);\n            store.put(value, key);\n            tx.oncomplete = () => resolve();\n            tx.onerror = () => resolve();\n        });\n    }\n    catch (_a) {\n        // Best-effort persistence\n    }\n}\nasync function idbDelete(key) {\n    try {\n        const db = await getIDB();\n        return new Promise((resolve) => {\n            const tx = db.transaction(IDB_STORE, 'readwrite');\n            const store = tx.objectStore(IDB_STORE);\n            store.delete(key);\n            tx.oncomplete = () => resolve();\n            tx.onerror = () => resolve();\n        });\n    }\n    catch (_a) {\n        // Best-effort\n    }\n}\n// ---------------------------------------------------------------------------\n// RealtimeStore\n// ---------------------------------------------------------------------------\nlet nextRequestId = 1;\nfunction hashForKey(value) {\n    let h = 5381;\n    for (let i = 0; i < value.length; i++) {\n        h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;\n    }\n    return h.toString(36);\n}\nfunction principalFromToken(token) {\n    return token ? `t${hashForKey(token)}` : 'anon';\n}\nexport class RealtimeStore {\n    constructor() {\n        this.ws = null;\n        this.wsUrl = '';\n        this.appId = '';\n        this.subscriptions = new Map();\n        this.pendingRequests = new Map();\n        this.connectPromise = null;\n        this.reconnectTimer = null;\n        this.reconnectDelay = 1000;\n        this.maxReconnectDelay = 30000;\n        this.idbFlushTimer = null;\n        this.idbDirtyKeys = new Set();\n        this.closed = false;\n        this.authToken = null;\n        this.authPrincipalKey = 'anon';\n        this.authenticating = false;\n        this.suppressNextReconnect = false;\n        this.isServer = false;\n        this.tokenRefreshTimer = null;\n        // -----------------------------------------------------------------------\n        // WebSocket connection\n        // -----------------------------------------------------------------------\n        this.initPromise = null;\n    }\n    // -----------------------------------------------------------------------\n    // Initialization\n    // -----------------------------------------------------------------------\n    async init() {\n        const config = await getConfig();\n        this.appId = config.appId;\n        this.wsUrl = config.wsApiUrl;\n        this.isServer = config.isServer;\n        await this.refreshToken();\n        this.startTokenRefresh();\n    }\n    async refreshToken() {\n        let token = null;\n        try {\n            const { getIdToken } = await import('../utils/utils');\n            token = await getIdToken(this.isServer);\n        }\n        catch ( /* no auth available */_a) { /* no auth available */ }\n        this.authToken = token !== null && token !== void 0 ? token : null;\n        this.authPrincipalKey = principalFromToken(this.authToken);\n    }\n    startTokenRefresh() {\n        if (this.tokenRefreshTimer)\n            return;\n        this.tokenRefreshTimer = setInterval(async () => {\n            const prevPrincipal = this.authPrincipalKey;\n            await this.refreshToken();\n            if (this.authPrincipalKey !== prevPrincipal) {\n                await this.applyAuthPrincipalChange();\n                if (this.subscriptions.size > 0) {\n                    await this.ensureConnected().catch(() => {\n                        this.setAllSubscriptionStatus('error');\n                    });\n                }\n            }\n        }, 5 * 60 * 1000); // Check every 5 minutes\n    }\n    async ensureInitialized() {\n        if (this.appId)\n            return;\n        if (!this.initPromise)\n            this.initPromise = this.init();\n        await this.initPromise;\n    }\n    async ensureCurrentAuth() {\n        await this.ensureInitialized();\n        const prevPrincipal = this.authPrincipalKey;\n        await this.refreshToken();\n        if (this.authPrincipalKey !== prevPrincipal) {\n            await this.applyAuthPrincipalChange();\n        }\n    }\n    rekeySubscriptionsForPrincipal() {\n        const subs = Array.from(this.subscriptions.values());\n        this.subscriptions.clear();\n        for (const sub of subs) {\n            this.subscriptions.set(this.getSubKey(sub.path, sub.options), sub);\n        }\n    }\n    async applyAuthPrincipalChange() {\n        if (this.idbFlushTimer) {\n            clearTimeout(this.idbFlushTimer);\n            this.idbFlushTimer = null;\n        }\n        this.idbDirtyKeys.clear();\n        this.rekeySubscriptionsForPrincipal();\n        for (const sub of this.subscriptions.values()) {\n            sub.docs.clear();\n            sub.ref.current = sub.docs;\n            sub.error = null;\n            sub.isStale = false;\n            let loaded = false;\n            if (this.shouldUseIdb(sub.tier, sub.options)) {\n                const cached = await idbGet(this.idbKey(sub.path));\n                if (cached && Array.isArray(cached)) {\n                    for (const doc of cached) {\n                        if (doc && doc._id)\n                            sub.docs.set(doc._id, doc);\n                    }\n                    sub.ref.current = sub.docs;\n                    loaded = sub.docs.size > 0;\n                }\n            }\n            sub.status = loaded ? 'cached' : 'loading';\n            sub.isStale = loaded;\n            if (loaded)\n                this.notifySubscription(sub);\n            else\n                this.notifyState(sub);\n        }\n        if (this.ws) {\n            const ws = this.ws;\n            this.ws = null;\n            this.connectPromise = null;\n            this.suppressNextReconnect = true;\n            try {\n                ws.close(1000, 'Auth changed');\n            }\n            catch ( /* ignore */_a) { /* ignore */ }\n        }\n    }\n    async ensureConnected() {\n        var _a;\n        await this.ensureCurrentAuth();\n        if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)\n            return;\n        if (this.connectPromise)\n            return this.connectPromise;\n        this.connectPromise = this.connect();\n        return this.connectPromise;\n    }\n    connect() {\n        return new Promise((resolve, reject) => {\n            if (this.closed) {\n                reject(new Error('Store closed'));\n                return;\n            }\n            const params = new URLSearchParams();\n            params.set('appId', this.appId);\n            const url = `${this.wsUrl}?${params.toString()}`;\n            const ws = new WebSocket(url);\n            this.ws = ws;\n            let authTimer = null;\n            const finishConnected = () => {\n                if (authTimer) {\n                    clearTimeout(authTimer);\n                    authTimer = null;\n                }\n                this.authenticating = false;\n                ws.removeEventListener('error', onError);\n                this.reconnectDelay = 1000;\n                this.connectPromise = null;\n                this.resubscribeAll();\n                resolve();\n            };\n            const onOpen = () => {\n                if (!this.authToken) {\n                    finishConnected();\n                    return;\n                }\n                this.authenticating = true;\n                authTimer = setTimeout(() => {\n                    this.authenticating = false;\n                    this.connectPromise = null;\n                    try {\n                        ws.close(1008, 'Authentication timeout');\n                    }\n                    catch ( /* ignore */_a) { /* ignore */ }\n                    reject(new Error('WebSocket authentication timeout'));\n                }, 10000);\n                try {\n                    ws.send(JSON.stringify({ type: 'auth', token: this.authToken }));\n                }\n                catch (e) {\n                    if (authTimer)\n                        clearTimeout(authTimer);\n                    this.authenticating = false;\n                    this.connectPromise = null;\n                    reject(e);\n                }\n            };\n            const onError = (e) => {\n                if (authTimer)\n                    clearTimeout(authTimer);\n                this.authenticating = false;\n                ws.removeEventListener('open', onOpen);\n                this.connectPromise = null;\n                reject(new Error('WebSocket connection failed'));\n            };\n            ws.addEventListener('open', onOpen, { once: true });\n            ws.addEventListener('error', onError, { once: true });\n            ws.addEventListener('message', (event) => {\n                if (this.authenticating) {\n                    try {\n                        const msg = JSON.parse(typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data));\n                        if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'authenticated') {\n                            finishConnected();\n                            return;\n                        }\n                    }\n                    catch ( /* fall through to normal handling */_a) { /* fall through to normal handling */ }\n                }\n                this.handleMessage(event.data);\n            });\n            ws.addEventListener('close', () => {\n                if (authTimer)\n                    clearTimeout(authTimer);\n                if (this.ws !== ws) {\n                    if (this.suppressNextReconnect)\n                        this.suppressNextReconnect = false;\n                    return;\n                }\n                this.authenticating = false;\n                this.ws = null;\n                this.connectPromise = null;\n                this.rejectAllPending('WebSocket closed');\n                this.setAllSubscriptionStatus('reconnecting');\n                if (this.suppressNextReconnect) {\n                    this.suppressNextReconnect = false;\n                    return;\n                }\n                this.scheduleReconnect();\n            });\n        });\n    }\n    scheduleReconnect() {\n        if (this.closed)\n            return;\n        if (this.reconnectTimer)\n            clearTimeout(this.reconnectTimer);\n        this.reconnectTimer = setTimeout(() => {\n            this.ensureConnected().catch(() => {\n                this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);\n                this.scheduleReconnect();\n            });\n        }, this.reconnectDelay);\n    }\n    resubscribeAll() {\n        for (const sub of this.subscriptions.values()) {\n            this.sendSubscribe(sub);\n        }\n    }\n    // -----------------------------------------------------------------------\n    // Message handling\n    // -----------------------------------------------------------------------\n    handleMessage(raw) {\n        const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);\n        let msg;\n        try {\n            msg = JSON.parse(text);\n        }\n        catch (_a) {\n            return;\n        }\n        switch (msg.type) {\n            case 'snapshot':\n                this.handleSnapshot(msg);\n                break;\n            case 'delta':\n                this.handleDelta(msg);\n                break;\n            case 'result':\n                this.handleResult(msg);\n                break;\n            case 'error':\n                this.handleError(msg);\n                break;\n            case 'pong':\n                break;\n            case 'authenticated':\n                break;\n            // v1 compat: handle legacy message types during transition\n            case 'subscribed':\n                this.handleSnapshot(Object.assign(Object.assign({}, msg), { type: 'snapshot', docs: msg.data }));\n                break;\n            case 'data':\n                // Legacy full-snapshot delta — treat as snapshot replacement\n                this.handleLegacyData(msg);\n                break;\n            case 'response':\n                this.handleResult(Object.assign(Object.assign({}, msg), { type: 'result', ok: msg.status === 200, doc: msg.data }));\n                break;\n        }\n    }\n    handleSnapshot(msg) {\n        var _a, _b, _c;\n        const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;\n        if (!subId)\n            return;\n        const sub = this.findSubscriptionById(subId);\n        if (!sub)\n            return;\n        const docs = (_c = (_b = msg.docs) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : [];\n        const docsArray = Array.isArray(docs) ? docs : [docs];\n        sub.docs.clear();\n        for (const doc of docsArray) {\n            if (doc && doc._id) {\n                sub.docs.set(doc._id, doc);\n            }\n        }\n        sub.ref.current = sub.docs;\n        sub.status = 'live';\n        sub.isStale = false;\n        sub.error = null;\n        this.notifySubscription(sub);\n        this.markIdbDirty(sub.path);\n    }\n    handleDelta(msg) {\n        var _a, _b;\n        const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;\n        if (!subId)\n            return;\n        const sub = this.findSubscriptionById(subId);\n        if (!sub)\n            return;\n        if (sub.tier === 'ephemeral') {\n            // Ephemeral: just overwrite, no accumulation logic\n            if (msg.change === 'removed' && msg.docId) {\n                sub.docs.delete(msg.docId);\n            }\n            else if (msg.doc && msg.doc._id) {\n                sub.docs.set(msg.doc._id, msg.doc);\n            }\n            sub.ref.current = sub.docs;\n            if (sub.options.mode !== 'ref') {\n                this.notifySubscription(sub);\n            }\n            return;\n        }\n        // Durable/checkpointed: full delta handling\n        switch (msg.change) {\n            case 'added':\n            case 'modified':\n                if (msg.doc && msg.doc._id) {\n                    sub.docs.set(msg.doc._id, msg.doc);\n                }\n                break;\n            case 'removed':\n                if (msg.docId) {\n                    sub.docs.delete(msg.docId);\n                }\n                else if ((_b = msg.doc) === null || _b === void 0 ? void 0 : _b._id) {\n                    sub.docs.delete(msg.doc._id);\n                }\n                break;\n        }\n        sub.ref.current = sub.docs;\n        this.notifySubscription(sub);\n        this.markIdbDirty(sub.path);\n    }\n    handleLegacyData(msg) {\n        // Legacy v1 format: 'data' message with full snapshot or single doc\n        const subId = msg.subscriptionId;\n        if (!subId)\n            return;\n        const sub = this.findSubscriptionById(subId);\n        if (!sub)\n            return;\n        if (Array.isArray(msg.data)) {\n            // Full snapshot replacement\n            sub.docs.clear();\n            for (const doc of msg.data) {\n                if (doc && doc._id)\n                    sub.docs.set(doc._id, doc);\n            }\n        }\n        else if (msg.data && msg.data._id) {\n            // Single doc update\n            sub.docs.set(msg.data._id, msg.data);\n        }\n        else if (msg.data === null) {\n            // Removal — but we don't know which doc. Re-fetch needed.\n        }\n        sub.ref.current = sub.docs;\n        sub.status = 'live';\n        sub.isStale = false;\n        this.notifySubscription(sub);\n        this.markIdbDirty(sub.path);\n    }\n    handleResult(msg) {\n        var _a, _b, _c, _d;\n        const requestId = msg.requestId;\n        if (!requestId)\n            return;\n        const pending = this.pendingRequests.get(requestId);\n        if (!pending)\n            return;\n        this.pendingRequests.delete(requestId);\n        clearTimeout(pending.timeout);\n        const ok = (_a = msg.ok) !== null && _a !== void 0 ? _a : (msg.status === 200);\n        if (ok) {\n            pending.resolve((_c = (_b = msg.doc) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : true);\n        }\n        else {\n            pending.reject(new Error((_d = msg.error) !== null && _d !== void 0 ? _d : 'Operation failed'));\n        }\n    }\n    handleError(msg) {\n        var _a, _b, _c;\n        const error = new Error((_a = msg.message) !== null && _a !== void 0 ? _a : (msg.code ? `${msg.code}: Server error` : 'Server error'));\n        if (msg.code)\n            error.code = msg.code;\n        if (msg.subscriptionId || msg.id)\n            error.subscriptionId = (_b = msg.subscriptionId) !== null && _b !== void 0 ? _b : msg.id;\n        const requestId = msg.requestId;\n        if (requestId) {\n            const pending = this.pendingRequests.get(requestId);\n            if (pending) {\n                this.pendingRequests.delete(requestId);\n                clearTimeout(pending.timeout);\n                pending.reject(error);\n            }\n        }\n        const subId = (_c = msg.subscriptionId) !== null && _c !== void 0 ? _c : msg.id;\n        if (subId) {\n            const sub = this.findSubscriptionById(subId);\n            if (sub) {\n                sub.status = 'error';\n                sub.error = error;\n                this.notifyState(sub);\n                for (const callback of Array.from(sub.errorCallbacks)) {\n                    try {\n                        callback(error);\n                    }\n                    catch ( /* swallow */_d) { /* swallow */ }\n                }\n            }\n        }\n    }\n    // -----------------------------------------------------------------------\n    // Subscribe\n    // -----------------------------------------------------------------------\n    async subscribe(path, opts = {}) {\n        var _a;\n        await this.ensureCurrentAuth();\n        const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';\n        const subKey = this.getSubKey(path, opts);\n        let sub = this.subscriptions.get(subKey);\n        if (sub) {\n            // Existing subscription — add callback\n            if (opts.onData)\n                sub.callbacks.add(opts.onData);\n            if (opts.onState)\n                sub.stateCallbacks.add(opts.onState);\n            if (opts.onError)\n                sub.errorCallbacks.add(opts.onError);\n            if (opts.cache && !sub.options.cache) {\n                sub.options = Object.assign(Object.assign({}, sub.options), { cache: true });\n            }\n            // Immediately deliver current state\n            if (opts.onData && sub.docs.size > 0) {\n                opts.onData(this.docsToArray(sub));\n            }\n            if (opts.onState) {\n                opts.onState(this.getState(sub));\n            }\n            return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);\n        }\n        // New subscription\n        const subId = `sub_${nextRequestId++}`;\n        sub = {\n            id: subId,\n            path,\n            tier,\n            options: opts,\n            docs: new Map(),\n            status: 'idle',\n            isStale: false,\n            error: null,\n            callbacks: new Set(opts.onData ? [opts.onData] : []),\n            stateCallbacks: new Set(opts.onState ? [opts.onState] : []),\n            errorCallbacks: new Set(opts.onError ? [opts.onError] : []),\n            ref: { current: new Map() },\n        };\n        this.subscriptions.set(subKey, sub);\n        // Step 1: Load from IDB only when explicitly enabled and principal-bound.\n        if (this.shouldUseIdb(tier, opts)) {\n            const cached = await idbGet(this.idbKey(path));\n            if (cached && Array.isArray(cached)) {\n                for (const doc of cached) {\n                    if (doc && doc._id)\n                        sub.docs.set(doc._id, doc);\n                }\n                sub.ref.current = sub.docs;\n                sub.status = 'cached';\n                sub.isStale = true;\n                this.notifySubscription(sub);\n            }\n        }\n        // Step 2: Connect and subscribe via WS\n        sub.status = sub.docs.size > 0 ? 'cached' : 'loading';\n        this.notifyState(sub);\n        try {\n            await this.ensureConnected();\n            this.sendSubscribe(sub);\n        }\n        catch (_b) {\n            sub.status = 'error';\n            sub.error = new Error('Connection failed');\n            this.notifyState(sub);\n        }\n        return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);\n    }\n    getRef(path, opts = {}) {\n        var _a;\n        const subKey = this.getSubKey(path, opts);\n        const sub = this.subscriptions.get(subKey);\n        if (sub)\n            return sub.ref;\n        // Auto-subscribe in ref mode\n        const ref = { current: new Map() };\n        this.subscribe(path, Object.assign(Object.assign({}, opts), { mode: 'ref', tier: 'ephemeral' })).catch(() => { });\n        const newSub = this.subscriptions.get(this.getSubKey(path, Object.assign(Object.assign({}, opts), { tier: 'ephemeral' })));\n        return (_a = newSub === null || newSub === void 0 ? void 0 : newSub.ref) !== null && _a !== void 0 ? _a : ref;\n    }\n    // -----------------------------------------------------------------------\n    // CRUD operations\n    // -----------------------------------------------------------------------\n    async set(path, doc) {\n        var _a;\n        await this.ensureConnected();\n        // Resolve operations (Increment, Time.Now) client-side for optimistic update\n        const resolvedDoc = this.resolveOperations(doc, path);\n        // Optimistic update: apply to local state immediately\n        const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n        const collectionPath = this.getCollectionPath(normalizedPath);\n        const optimisticDoc = Object.assign(Object.assign({ _id: normalizedPath, pathId: normalizedPath }, resolvedDoc), { \n            // System timestamp field name: the Bounded worker stamps the neutral\n            // `_updatedAt`; the underscore-prefixed `_updated_at` metadata mirror.\n            // Match it so the optimistic doc lines up with the server's confirmation.\n            [isBoundedNetwork() ? '_updatedAt' : '_updated_at']: Date.now() });\n        const sub = this.findSubscriptionByPath(collectionPath);\n        let prevDoc = null;\n        if (sub) {\n            prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;\n            sub.docs.set(normalizedPath, optimisticDoc);\n            sub.ref.current = sub.docs;\n            this.notifySubscription(sub);\n        }\n        // Send to server\n        const requestId = `r_${nextRequestId++}`;\n        try {\n            const result = await this.sendRequest(requestId, {\n                type: 'set',\n                requestId,\n                documents: [{ destinationPath: normalizedPath, document: doc }],\n            });\n            // Replace optimistic doc with server-confirmed version\n            if (sub && result && typeof result === 'object') {\n                const serverDoc = Array.isArray(result) ? result[0] : result;\n                if (serverDoc && serverDoc._id) {\n                    sub.docs.set(serverDoc._id, serverDoc);\n                    sub.ref.current = sub.docs;\n                    this.notifySubscription(sub);\n                    this.markIdbDirty(collectionPath);\n                }\n            }\n            return Array.isArray(result) ? result[0] : result;\n        }\n        catch (err) {\n            // Revert optimistic update\n            if (sub) {\n                if (prevDoc) {\n                    sub.docs.set(normalizedPath, prevDoc);\n                }\n                else {\n                    sub.docs.delete(normalizedPath);\n                }\n                sub.ref.current = sub.docs;\n                this.notifySubscription(sub);\n            }\n            throw err;\n        }\n    }\n    async get(path) {\n        await this.ensureCurrentAuth();\n        const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n        // Check local subscriptions first\n        const collectionPath = this.getCollectionPath(normalizedPath);\n        const sub = this.findSubscriptionByPath(collectionPath);\n        if (sub && sub.status === 'live') {\n            const doc = sub.docs.get(normalizedPath);\n            return doc !== null && doc !== void 0 ? doc : null;\n        }\n        // One-shot WS fetch\n        await this.ensureConnected();\n        const requestId = `r_${nextRequestId++}`;\n        return this.sendRequest(requestId, {\n            type: 'get',\n            requestId,\n            path: normalizedPath,\n        });\n    }\n    async getMany(paths) {\n        await this.ensureConnected();\n        const normalizedPaths = paths.map(p => p.startsWith('/') ? p.slice(1) : p);\n        const requestId = `r_${nextRequestId++}`;\n        return this.sendRequest(requestId, {\n            type: 'getMany',\n            requestId,\n            paths: normalizedPaths,\n        });\n    }\n    async delete(path) {\n        var _a;\n        await this.ensureConnected();\n        const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n        // Optimistic: remove from local state\n        const collectionPath = this.getCollectionPath(normalizedPath);\n        const sub = this.findSubscriptionByPath(collectionPath);\n        let prevDoc = null;\n        if (sub) {\n            prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;\n            sub.docs.delete(normalizedPath);\n            sub.ref.current = sub.docs;\n            this.notifySubscription(sub);\n        }\n        const requestId = `r_${nextRequestId++}`;\n        try {\n            await this.sendRequest(requestId, {\n                type: 'delete',\n                requestId,\n                path: normalizedPath,\n            });\n            if (sub)\n                this.markIdbDirty(collectionPath);\n        }\n        catch (err) {\n            // Revert\n            if (sub && prevDoc) {\n                sub.docs.set(normalizedPath, prevDoc);\n                sub.ref.current = sub.docs;\n                this.notifySubscription(sub);\n            }\n            throw err;\n        }\n    }\n    async query(path, opts) {\n        await this.ensureConnected();\n        const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n        const requestId = `r_${nextRequestId++}`;\n        return this.sendRequest(requestId, Object.assign(Object.assign(Object.assign(Object.assign({ type: 'query', requestId, path: normalizedPath }, ((opts === null || opts === void 0 ? void 0 : opts.filter) ? { filter: opts.filter } : {})), ((opts === null || opts === void 0 ? void 0 : opts.sort) ? { sort: opts.sort } : {})), ((opts === null || opts === void 0 ? void 0 : opts.limit) !== undefined ? { limit: opts.limit } : {})), ((opts === null || opts === void 0 ? void 0 : opts.includeSubPaths) ? { includeSubPaths: true } : {})));\n    }\n    async count(path) {\n        var _a;\n        await this.ensureConnected();\n        const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n        const requestId = `r_${nextRequestId++}`;\n        const result = await this.sendRequest(requestId, {\n            type: 'count',\n            requestId,\n            path: normalizedPath,\n        });\n        return typeof result === 'number' ? result : ((_a = result === null || result === void 0 ? void 0 : result.value) !== null && _a !== void 0 ? _a : 0);\n    }\n    async aggregate(path, operation, opts) {\n        await this.ensureConnected();\n        const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n        const requestId = `r_${nextRequestId++}`;\n        return this.sendRequest(requestId, Object.assign({ type: 'aggregate', requestId, path: normalizedPath, operation }, ((opts === null || opts === void 0 ? void 0 : opts.field) ? { field: opts.field } : {})));\n    }\n    // -----------------------------------------------------------------------\n    // Helpers\n    // -----------------------------------------------------------------------\n    sendSubscribe(sub) {\n        if (!this.ws || this.ws.readyState !== WebSocket.OPEN)\n            return;\n        const msg = {\n            type: 'subscribe',\n            subscriptionId: sub.id,\n            path: sub.path,\n        };\n        if (sub.options.filter)\n            msg.filter = sub.options.filter;\n        if (sub.options.includeSubPaths)\n            msg.includeSubPaths = true;\n        if (sub.options.limit)\n            msg.limit = sub.options.limit;\n        if (sub.options.prompt)\n            msg.prompt = sub.options.prompt;\n        this.ws.send(JSON.stringify(msg));\n    }\n    sendRequest(requestId, msg) {\n        return new Promise((resolve, reject) => {\n            const timeout = setTimeout(() => {\n                this.pendingRequests.delete(requestId);\n                reject(new Error('Request timed out'));\n            }, 30000);\n            this.pendingRequests.set(requestId, { resolve, reject, timeout });\n            if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n                this.ws.send(JSON.stringify(msg));\n            }\n            else {\n                this.pendingRequests.delete(requestId);\n                clearTimeout(timeout);\n                reject(new Error('WebSocket not connected'));\n            }\n        });\n    }\n    notifySubscription(sub) {\n        const data = this.docsToArray(sub);\n        const callbacks = Array.from(sub.callbacks);\n        for (const cb of callbacks) {\n            try {\n                cb(data);\n            }\n            catch ( /* swallow callback errors */_a) { /* swallow callback errors */ }\n        }\n        this.notifyState(sub);\n    }\n    notifyState(sub) {\n        const state = this.getState(sub);\n        const callbacks = Array.from(sub.stateCallbacks);\n        for (const cb of callbacks) {\n            try {\n                cb(state);\n            }\n            catch ( /* swallow */_a) { /* swallow */ }\n        }\n    }\n    getState(sub) {\n        return {\n            data: this.docsToArray(sub),\n            status: sub.status,\n            isStale: sub.isStale,\n            error: sub.error,\n        };\n    }\n    docsToArray(sub) {\n        return Array.from(sub.docs.values());\n    }\n    findSubscriptionById(id) {\n        for (const sub of this.subscriptions.values()) {\n            if (sub.id === id)\n                return sub;\n        }\n        return undefined;\n    }\n    findSubscriptionByPath(collectionPath) {\n        for (const sub of this.subscriptions.values()) {\n            const subPath = sub.path.startsWith('/') ? sub.path.slice(1) : sub.path;\n            if (subPath === collectionPath)\n                return sub;\n            if (collectionPath.startsWith(subPath + '/'))\n                return sub;\n        }\n        return undefined;\n    }\n    getCollectionPath(docPath) {\n        const segments = docPath.split('/');\n        if (segments.length % 2 === 0) {\n            return segments.slice(0, -1).join('/');\n        }\n        return docPath;\n    }\n    getSubKey(path, opts) {\n        const parts = [this.appId, this.authPrincipalKey, path];\n        if (opts.filter)\n            parts.push(JSON.stringify(opts.filter));\n        if (opts.prompt)\n            parts.push(opts.prompt);\n        if (opts.tier)\n            parts.push(opts.tier);\n        return parts.join('::');\n    }\n    idbKey(path) {\n        return `${this.appId}:${this.authPrincipalKey}:${path}`;\n    }\n    shouldUseIdb(tier, opts) {\n        return opts.cache === true && tier !== 'ephemeral' && this.authPrincipalKey !== 'anon';\n    }\n    markIdbDirty(path) {\n        const sub = this.findSubscriptionByPath(path);\n        if (!sub || !this.shouldUseIdb(sub.tier, sub.options))\n            return;\n        this.idbDirtyKeys.add(path);\n        if (!this.idbFlushTimer) {\n            this.idbFlushTimer = setTimeout(() => {\n                this.flushIdb();\n                this.idbFlushTimer = null;\n            }, 500);\n        }\n    }\n    async flushIdb() {\n        const keys = Array.from(this.idbDirtyKeys);\n        this.idbDirtyKeys.clear();\n        for (const path of keys) {\n            const sub = this.findSubscriptionByPath(path);\n            if (sub && this.shouldUseIdb(sub.tier, sub.options)) {\n                const docs = this.docsToArray(sub);\n                await idbSet(this.idbKey(path), docs);\n            }\n        }\n    }\n    createUnsubscribe(subKey, subId, onData, onState, onError) {\n        return async () => {\n            var _a;\n            const sub = (_a = this.subscriptions.get(subKey)) !== null && _a !== void 0 ? _a : this.findSubscriptionById(subId);\n            if (!sub)\n                return;\n            const currentSubKey = this.getSubKey(sub.path, sub.options);\n            if (onData)\n                sub.callbacks.delete(onData);\n            if (onState)\n                sub.stateCallbacks.delete(onState);\n            if (onError)\n                sub.errorCallbacks.delete(onError);\n            // If no more callbacks, unsubscribe entirely\n            if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {\n                this.subscriptions.delete(subKey);\n                this.subscriptions.delete(currentSubKey);\n                if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n                    this.ws.send(JSON.stringify({\n                        type: 'unsubscribe',\n                        subscriptionId: sub.id,\n                    }));\n                }\n            }\n        };\n    }\n    resolveOperations(doc, path) {\n        var _a;\n        if (!doc || typeof doc !== 'object')\n            return doc;\n        const resolved = {};\n        for (const [key, value] of Object.entries(doc)) {\n            if (value && typeof value === 'object' && !Array.isArray(value) && value.operation) {\n                const op = value;\n                if (op.operation === 'time' && op.value === 'now') {\n                    resolved[key] = Math.floor(Date.now() / 1000);\n                }\n                else if (op.operation === 'increment') {\n                    // For optimistic: get current value and add\n                    const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n                    const collectionPath = this.getCollectionPath(normalizedPath);\n                    const sub = this.findSubscriptionByPath(collectionPath);\n                    const existing = sub === null || sub === void 0 ? void 0 : sub.docs.get(normalizedPath);\n                    const current = (_a = existing === null || existing === void 0 ? void 0 : existing[key]) !== null && _a !== void 0 ? _a : 0;\n                    resolved[key] = (typeof current === 'number' ? current : 0) + op.value;\n                }\n                else {\n                    resolved[key] = value;\n                }\n            }\n            else {\n                resolved[key] = value;\n            }\n        }\n        return resolved;\n    }\n    rejectAllPending(reason) {\n        for (const [requestId, pending] of this.pendingRequests) {\n            clearTimeout(pending.timeout);\n            pending.reject(new Error(reason));\n        }\n        this.pendingRequests.clear();\n    }\n    setAllSubscriptionStatus(status) {\n        for (const sub of this.subscriptions.values()) {\n            sub.status = status;\n            this.notifyState(sub);\n        }\n    }\n    // -----------------------------------------------------------------------\n    // Lifecycle\n    // -----------------------------------------------------------------------\n    close() {\n        this.closed = true;\n        if (this.reconnectTimer)\n            clearTimeout(this.reconnectTimer);\n        if (this.idbFlushTimer)\n            clearTimeout(this.idbFlushTimer);\n        if (this.tokenRefreshTimer)\n            clearInterval(this.tokenRefreshTimer);\n        this.flushIdb();\n        if (this.ws) {\n            this.ws.close(1000, 'Store closed');\n            this.ws = null;\n        }\n        this.rejectAllPending('Store closed');\n        this.subscriptions.clear();\n    }\n    async reconnectWithNewAuth() {\n        if (this.closed)\n            return;\n        await this.ensureInitialized();\n        await this.refreshToken();\n        await this.applyAuthPrincipalChange();\n        if (this.subscriptions.size > 0) {\n            await this.ensureConnected().catch((error) => {\n                this.setAllSubscriptionStatus('error');\n                for (const sub of this.subscriptions.values()) {\n                    sub.error = error instanceof Error ? error : new Error(String(error));\n                    this.notifyState(sub);\n                }\n            });\n        }\n    }\n}\n// ---------------------------------------------------------------------------\n// Singleton instance\n// ---------------------------------------------------------------------------\nlet storeInstance = null;\nexport function getRealtimeStore() {\n    if (!storeInstance) {\n        storeInstance = new RealtimeStore();\n    }\n    return storeInstance;\n}\nexport function resetRealtimeStore() {\n    if (storeInstance) {\n        storeInstance.close();\n        storeInstance = null;\n    }\n}\nexport async function reconnectRealtimeStoreWithNewAuth() {\n    if (storeInstance) {\n        await storeInstance.reconnectWithNewAuth();\n    }\n}\n"],"names":[],"mappings":";;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AA++BO,eAAe,iCAAiC,GAAG;AAI1D;;;;"}