@bounded-sh/core 0.0.103 → 0.0.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/client/solana-operations.d.ts +15 -0
  2. package/dist/{index-D7dRe0PL.js → index-62gjQH30.js} +55 -26
  3. package/dist/index-62gjQH30.js.map +1 -0
  4. package/dist/{index-CC2WVN43.mjs → index-CLst0IiM.mjs} +55 -26
  5. package/dist/index-CLst0IiM.mjs.map +1 -0
  6. package/dist/index.js +1 -1
  7. package/dist/index.mjs +1 -1
  8. package/dist/{realtime-store-CuFYPLgk.js → realtime-store-9SCt1RPf.js} +2 -2
  9. package/dist/{realtime-store-CuFYPLgk.js.map → realtime-store-9SCt1RPf.js.map} +1 -1
  10. package/dist/{realtime-store-2iWofj5n.mjs → realtime-store-C9AgPg7U.mjs} +2 -2
  11. package/dist/{realtime-store-2iWofj5n.mjs.map → realtime-store-C9AgPg7U.mjs.map} +1 -1
  12. package/dist/{solana-message-DLBiBfnG.js → solana-message-Bf2q_JO1.js} +2 -2
  13. package/dist/{solana-message-DLBiBfnG.js.map → solana-message-Bf2q_JO1.js.map} +1 -1
  14. package/dist/{solana-message-Bx04VMG1.mjs → solana-message-Deru1tF1.mjs} +2 -2
  15. package/dist/{solana-message-Bx04VMG1.mjs.map → solana-message-Deru1tF1.mjs.map} +1 -1
  16. package/dist/{solana-operations-CCFNQIgd.mjs → solana-operations-Cm19J14R.mjs} +34 -5
  17. package/dist/{solana-operations-CCFNQIgd.mjs.map → solana-operations-Cm19J14R.mjs.map} +1 -1
  18. package/dist/{solana-operations-Be0dWtVZ.js → solana-operations-eTiLgak2.js} +34 -3
  19. package/dist/{solana-operations-Be0dWtVZ.js.map → solana-operations-eTiLgak2.js.map} +1 -1
  20. package/dist/{solana-v1-operations-Bbj0F4cp.js → solana-v1-operations-CnmiKiyj.js} +3 -3
  21. package/dist/{solana-v1-operations-Bbj0F4cp.js.map → solana-v1-operations-CnmiKiyj.js.map} +1 -1
  22. package/dist/{solana-v1-operations-C6Qev1fZ.mjs → solana-v1-operations-Dxs4XM_F.mjs} +3 -3
  23. package/dist/{solana-v1-operations-C6Qev1fZ.mjs.map → solana-v1-operations-Dxs4XM_F.mjs.map} +1 -1
  24. package/dist/types.d.ts +16 -1
  25. package/package.json +1 -1
  26. package/dist/index-CC2WVN43.mjs.map +0 -1
  27. package/dist/index-D7dRe0PL.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-D7dRe0PL.js');
3
+ var index = require('./index-62gjQH30.js');
4
4
  require('axios');
5
5
  require('tweetnacl');
6
6
  require('reconnecting-websocket');
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { ac as AUTH_CHANGED_CODE, ad as AUTH_EXPIRED_CODE, L as AppTokenExchangeError, T as BoundedDeclineError, W as DECLINE_EVENT_NAME, Q as DEFAULT_QUERY_POLL_INTERVAL_MS, aH as EFFECT_INTENT_ADDRESS, ay as FunctionInvokeError, I as InsufficientBalanceError, aD as LiveIntentError, R as MIN_QUERY_POLL_INTERVAL_MS, Y as RATE_LIMIT_EVENT_NAME, a7 as ReactNativeSessionManager, as as SOLANA_SIGN_IN_STATEMENT, G as ScopedClientReadOnlyError, ag as ServerSessionManager, aa as SessionEnvironmentError, ab as UNSUPPORTED_ENVIRONMENT_CODE, a6 as WebSessionManager, x as aggregate, ar as browserSignInDomain, O as canonicalizeArgs, P as canonicalizeResult, J as clearAppTokenCache, C as clearCache, N as closeAllQuerySubscriptions, B as closeAllSubscriptions, w as count, K as createAppTokenAuthHeaders, F as createClient, ah as createSessionWithSignature, ao as createSolanaSignInMessageText, an as createSolanaSignInRequest, aG as defineLiveModule, U as deriveDecline, a4 as deriveUserIdentityFromIdToken, V as dispatchDeclineEvent, X as dispatchRateLimitEvent, h as docId, H as exchangeAppToken, aw as functions, al as genAuthNonce, at as genSiweMessage, am as genSolanaMessage, aq as genSolanaSignInMessage, ap as genSolanaSignInRequest, e as get, a8 as getActiveSessionManager, D as getCachedData, g as getConfig, d as getConfiguredAppId, l as getFiles, af as getFreshAuthToken, a3 as getIdToken, f as getMany, a5 as getSessionGeneration, c as getWebhookKeysUrl, Z as hasActiveConnection, _ as increment, i as init, ax as invokeFunction, ae as isAuthExpiredError, aF as isEffectResult, az as live, aA as liveIntent, aB as liveStatus, au as normalizeEvmAddress, a0 as now, a9 as onSessionCleared, q as queryAggregate, y as reconcileSetResult, E as reconnectWithNewAuth, ai as refreshSession, aj as revokeSession, o as runExpression, p as runExpressionMany, r as runQuery, n as runQueryMany, m as search, $ as serverTimestamp, s as set, k as setFile, j as setMany, v as signAndSubmitTransaction, t as signMessage, ak as signSessionCreateMessage, u as signTransaction, A as subscribe, aC as subscribeLiveView, M as subscribeQuery, av as toLowerEvmAddress, a2 as toMillis, a1 as toSeconds, z as waitForSetResult, aE as withEffects } from './index-CC2WVN43.mjs';
1
+ export { ac as AUTH_CHANGED_CODE, ad as AUTH_EXPIRED_CODE, L as AppTokenExchangeError, T as BoundedDeclineError, W as DECLINE_EVENT_NAME, Q as DEFAULT_QUERY_POLL_INTERVAL_MS, aH as EFFECT_INTENT_ADDRESS, ay as FunctionInvokeError, I as InsufficientBalanceError, aD as LiveIntentError, R as MIN_QUERY_POLL_INTERVAL_MS, Y as RATE_LIMIT_EVENT_NAME, a7 as ReactNativeSessionManager, as as SOLANA_SIGN_IN_STATEMENT, G as ScopedClientReadOnlyError, ag as ServerSessionManager, aa as SessionEnvironmentError, ab as UNSUPPORTED_ENVIRONMENT_CODE, a6 as WebSessionManager, x as aggregate, ar as browserSignInDomain, O as canonicalizeArgs, P as canonicalizeResult, J as clearAppTokenCache, C as clearCache, N as closeAllQuerySubscriptions, B as closeAllSubscriptions, w as count, K as createAppTokenAuthHeaders, F as createClient, ah as createSessionWithSignature, ao as createSolanaSignInMessageText, an as createSolanaSignInRequest, aG as defineLiveModule, U as deriveDecline, a4 as deriveUserIdentityFromIdToken, V as dispatchDeclineEvent, X as dispatchRateLimitEvent, h as docId, H as exchangeAppToken, aw as functions, al as genAuthNonce, at as genSiweMessage, am as genSolanaMessage, aq as genSolanaSignInMessage, ap as genSolanaSignInRequest, e as get, a8 as getActiveSessionManager, D as getCachedData, g as getConfig, d as getConfiguredAppId, l as getFiles, af as getFreshAuthToken, a3 as getIdToken, f as getMany, a5 as getSessionGeneration, c as getWebhookKeysUrl, Z as hasActiveConnection, _ as increment, i as init, ax as invokeFunction, ae as isAuthExpiredError, aF as isEffectResult, az as live, aA as liveIntent, aB as liveStatus, au as normalizeEvmAddress, a0 as now, a9 as onSessionCleared, q as queryAggregate, y as reconcileSetResult, E as reconnectWithNewAuth, ai as refreshSession, aj as revokeSession, o as runExpression, p as runExpressionMany, r as runQuery, n as runQueryMany, m as search, $ as serverTimestamp, s as set, k as setFile, j as setMany, v as signAndSubmitTransaction, t as signMessage, ak as signSessionCreateMessage, u as signTransaction, A as subscribe, aC as subscribeLiveView, M as subscribeQuery, av as toLowerEvmAddress, a2 as toMillis, a1 as toSeconds, z as waitForSetResult, aE as withEffects } from './index-CLst0IiM.mjs';
2
2
  import 'axios';
3
3
  import 'tweetnacl';
4
4
  import 'reconnecting-websocket';
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- require('./index-D7dRe0PL.js');
3
+ require('./index-62gjQH30.js');
4
4
  require('axios');
5
5
  require('tweetnacl');
6
6
  require('reconnecting-websocket');
@@ -15,4 +15,4 @@ async function reconnectRealtimeStoreWithNewAuth() {
15
15
  }
16
16
 
17
17
  exports.reconnectRealtimeStoreWithNewAuth = reconnectRealtimeStoreWithNewAuth;
18
- //# sourceMappingURL=realtime-store-CuFYPLgk.js.map
18
+ //# sourceMappingURL=realtime-store-9SCt1RPf.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"realtime-store-CuFYPLgk.js","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 { RECONNECT_BASE_DELAY_MS, RECONNECT_MAX_DELAY_MS, jitteredReconnectDelayMs, reconnectDelayAfterClose } from './reconnect-backoff';\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 = RECONNECT_BASE_DELAY_MS;\n this.maxReconnectDelay = RECONNECT_MAX_DELAY_MS;\n /** When the current socket finished connecting; decides whether a close resets the backoff. */\n this.connectedAtMs = null;\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 // The delay resets only once this connection proves stable (see the\n // close handler), so an accept-then-drop loop still backs off.\n this.connectedAtMs = Date.now();\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 const connectedFor = this.connectedAtMs === null ? null : Date.now() - this.connectedAtMs;\n this.connectedAtMs = null;\n // A socket that never finished connecting is backed off by the\n // failed-connect path in scheduleReconnect instead.\n if (connectedFor !== null) {\n this.reconnectDelay = reconnectDelayAfterClose(this.reconnectDelay, connectedFor, this.maxReconnectDelay);\n }\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 }, jitteredReconnectDelayMs(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;AA2/BO,eAAe,iCAAiC,GAAG;AAI1D;;;;"}
1
+ {"version":3,"file":"realtime-store-9SCt1RPf.js","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 { RECONNECT_BASE_DELAY_MS, RECONNECT_MAX_DELAY_MS, jitteredReconnectDelayMs, reconnectDelayAfterClose } from './reconnect-backoff';\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 = RECONNECT_BASE_DELAY_MS;\n this.maxReconnectDelay = RECONNECT_MAX_DELAY_MS;\n /** When the current socket finished connecting; decides whether a close resets the backoff. */\n this.connectedAtMs = null;\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 // The delay resets only once this connection proves stable (see the\n // close handler), so an accept-then-drop loop still backs off.\n this.connectedAtMs = Date.now();\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 const connectedFor = this.connectedAtMs === null ? null : Date.now() - this.connectedAtMs;\n this.connectedAtMs = null;\n // A socket that never finished connecting is backed off by the\n // failed-connect path in scheduleReconnect instead.\n if (connectedFor !== null) {\n this.reconnectDelay = reconnectDelayAfterClose(this.reconnectDelay, connectedFor, this.maxReconnectDelay);\n }\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 }, jitteredReconnectDelayMs(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;AA2/BO,eAAe,iCAAiC,GAAG;AAI1D;;;;"}
@@ -1,4 +1,4 @@
1
- import './index-CC2WVN43.mjs';
1
+ import './index-CLst0IiM.mjs';
2
2
  import 'axios';
3
3
  import 'tweetnacl';
4
4
  import 'reconnecting-websocket';
@@ -13,4 +13,4 @@ async function reconnectRealtimeStoreWithNewAuth() {
13
13
  }
14
14
 
15
15
  export { reconnectRealtimeStoreWithNewAuth };
16
- //# sourceMappingURL=realtime-store-2iWofj5n.mjs.map
16
+ //# sourceMappingURL=realtime-store-C9AgPg7U.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"realtime-store-2iWofj5n.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 { RECONNECT_BASE_DELAY_MS, RECONNECT_MAX_DELAY_MS, jitteredReconnectDelayMs, reconnectDelayAfterClose } from './reconnect-backoff';\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 = RECONNECT_BASE_DELAY_MS;\n this.maxReconnectDelay = RECONNECT_MAX_DELAY_MS;\n /** When the current socket finished connecting; decides whether a close resets the backoff. */\n this.connectedAtMs = null;\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 // The delay resets only once this connection proves stable (see the\n // close handler), so an accept-then-drop loop still backs off.\n this.connectedAtMs = Date.now();\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 const connectedFor = this.connectedAtMs === null ? null : Date.now() - this.connectedAtMs;\n this.connectedAtMs = null;\n // A socket that never finished connecting is backed off by the\n // failed-connect path in scheduleReconnect instead.\n if (connectedFor !== null) {\n this.reconnectDelay = reconnectDelayAfterClose(this.reconnectDelay, connectedFor, this.maxReconnectDelay);\n }\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 }, jitteredReconnectDelayMs(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;AA2/BO,eAAe,iCAAiC,GAAG;AAI1D;;;;"}
1
+ {"version":3,"file":"realtime-store-C9AgPg7U.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 { RECONNECT_BASE_DELAY_MS, RECONNECT_MAX_DELAY_MS, jitteredReconnectDelayMs, reconnectDelayAfterClose } from './reconnect-backoff';\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 = RECONNECT_BASE_DELAY_MS;\n this.maxReconnectDelay = RECONNECT_MAX_DELAY_MS;\n /** When the current socket finished connecting; decides whether a close resets the backoff. */\n this.connectedAtMs = null;\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 // The delay resets only once this connection proves stable (see the\n // close handler), so an accept-then-drop loop still backs off.\n this.connectedAtMs = Date.now();\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 const connectedFor = this.connectedAtMs === null ? null : Date.now() - this.connectedAtMs;\n this.connectedAtMs = null;\n // A socket that never finished connecting is backed off by the\n // failed-connect path in scheduleReconnect instead.\n if (connectedFor !== null) {\n this.reconnectDelay = reconnectDelayAfterClose(this.reconnectDelay, connectedFor, this.maxReconnectDelay);\n }\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 }, jitteredReconnectDelayMs(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;AA2/BO,eAAe,iCAAiC,GAAG;AAI1D;;;;"}
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-D7dRe0PL.js');
3
+ var index = require('./index-62gjQH30.js');
4
4
  require('axios');
5
5
  require('tweetnacl');
6
6
  require('reconnecting-websocket');
@@ -36,4 +36,4 @@ async function genSolanaMessage(address, nonce, appIdOverride) {
36
36
  }
37
37
 
38
38
  exports.genSolanaMessage = genSolanaMessage;
39
- //# sourceMappingURL=solana-message-DLBiBfnG.js.map
39
+ //# sourceMappingURL=solana-message-Bf2q_JO1.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"solana-message-DLBiBfnG.js","sources":["../.rollup-tmp/utils/sol/solana-message.js"],"sourcesContent":["import { getConfig } from \"../../client/config\";\n// NOTE: this message text must stay BYTE-IDENTICAL to what the wallet-auth\n// verifier reconstructs — any drift breaks every SIWS login.\nexport async function genSolanaMessage(address, nonce, appIdOverride) {\n var _a, _b;\n /* Domain / origin aren't available server-side – fall back to envs\n or simple placeholders. */\n const domain = (typeof process !== 'undefined' && ((_a = process.env) === null || _a === void 0 ? void 0 : _a.SESSION_DOMAIN)) || \"server\";\n const origin = (typeof process !== 'undefined' && ((_b = process.env) === null || _b === void 0 ? void 0 : _b.SESSION_ORIGIN)) || \"server\";\n // SECURITY (audit-4 CRITICAL-1): bind the signed prompt to the target app.\n // A server wallet client passes its snapshotted appId (appIdOverride) so the\n // signed \"App ID\" stays bound to the app the wallet client was created against\n // even after a later global init() (CTA-05); the default path reads the SAME\n // global config that genAuthNonce + /session use, so the signed \"App ID\" is\n // guaranteed to equal the app the session is minted for; the server verifier\n // rejects any mismatch.\n const appId = appIdOverride !== null && appIdOverride !== void 0 ? appIdOverride : (await getConfig()).appId;\n const statement = \"Sign this message to authenticate with our application.\";\n const currentDate = new Date();\n const issuedAt = currentDate.toISOString();\n const expirationTime = new Date(currentDate.getTime() + 5 * 60 * 1000).toISOString();\n return `${statement}\\n\\n` +\n `Wallet address:\\n${address}\\n\\n` +\n `App ID: ${appId}\\n` +\n `Domain: ${domain}\\n` +\n `Origin: ${origin}\\n` +\n `Nonce: ${nonce}\\n` +\n `Issued At: ${issuedAt}\\n` +\n `Expiration Time: ${expirationTime}`;\n}\n"],"names":["getConfig"],"mappings":";;;;;;;AACA;AACA;AACO,eAAe,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE;AACtE,IAAI,IAAI,EAAE,EAAE,EAAE;AACd;AACA;AACA,IAAI,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,KAAK,WAAW,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,KAAK,MAAM,GAAG,MAAM,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,QAAQ;AAC9I,IAAI,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,KAAK,WAAW,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,KAAK,MAAM,GAAG,MAAM,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,QAAQ;AAC9I;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,MAAM,KAAK,GAAG,aAAa,KAAK,IAAI,IAAI,aAAa,KAAK,MAAM,GAAG,aAAa,GAAG,CAAC,MAAMA,eAAS,EAAE,EAAE,KAAK;AAChH,IAAI,MAAM,SAAS,GAAG,yDAAyD;AAC/E,IAAI,MAAM,WAAW,GAAG,IAAI,IAAI,EAAE;AAClC,IAAI,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,EAAE;AAC9C,IAAI,MAAM,cAAc,GAAG,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;AACxF,IAAI,OAAO,CAAC,EAAE,SAAS,CAAC,IAAI,CAAC;AAC7B,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC,IAAI,CAAC;AACzC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;AAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;AAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;AAC7B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;AAC3B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,EAAE,CAAC;AAClC,QAAQ,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;AAC5C;;;;"}
1
+ {"version":3,"file":"solana-message-Bf2q_JO1.js","sources":["../.rollup-tmp/utils/sol/solana-message.js"],"sourcesContent":["import { getConfig } from \"../../client/config\";\n// NOTE: this message text must stay BYTE-IDENTICAL to what the wallet-auth\n// verifier reconstructs — any drift breaks every SIWS login.\nexport async function genSolanaMessage(address, nonce, appIdOverride) {\n var _a, _b;\n /* Domain / origin aren't available server-side – fall back to envs\n or simple placeholders. */\n const domain = (typeof process !== 'undefined' && ((_a = process.env) === null || _a === void 0 ? void 0 : _a.SESSION_DOMAIN)) || \"server\";\n const origin = (typeof process !== 'undefined' && ((_b = process.env) === null || _b === void 0 ? void 0 : _b.SESSION_ORIGIN)) || \"server\";\n // SECURITY (audit-4 CRITICAL-1): bind the signed prompt to the target app.\n // A server wallet client passes its snapshotted appId (appIdOverride) so the\n // signed \"App ID\" stays bound to the app the wallet client was created against\n // even after a later global init() (CTA-05); the default path reads the SAME\n // global config that genAuthNonce + /session use, so the signed \"App ID\" is\n // guaranteed to equal the app the session is minted for; the server verifier\n // rejects any mismatch.\n const appId = appIdOverride !== null && appIdOverride !== void 0 ? appIdOverride : (await getConfig()).appId;\n const statement = \"Sign this message to authenticate with our application.\";\n const currentDate = new Date();\n const issuedAt = currentDate.toISOString();\n const expirationTime = new Date(currentDate.getTime() + 5 * 60 * 1000).toISOString();\n return `${statement}\\n\\n` +\n `Wallet address:\\n${address}\\n\\n` +\n `App ID: ${appId}\\n` +\n `Domain: ${domain}\\n` +\n `Origin: ${origin}\\n` +\n `Nonce: ${nonce}\\n` +\n `Issued At: ${issuedAt}\\n` +\n `Expiration Time: ${expirationTime}`;\n}\n"],"names":["getConfig"],"mappings":";;;;;;;AACA;AACA;AACO,eAAe,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE;AACtE,IAAI,IAAI,EAAE,EAAE,EAAE;AACd;AACA;AACA,IAAI,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,KAAK,WAAW,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,KAAK,MAAM,GAAG,MAAM,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,QAAQ;AAC9I,IAAI,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,KAAK,WAAW,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,KAAK,MAAM,GAAG,MAAM,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,QAAQ;AAC9I;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,MAAM,KAAK,GAAG,aAAa,KAAK,IAAI,IAAI,aAAa,KAAK,MAAM,GAAG,aAAa,GAAG,CAAC,MAAMA,eAAS,EAAE,EAAE,KAAK;AAChH,IAAI,MAAM,SAAS,GAAG,yDAAyD;AAC/E,IAAI,MAAM,WAAW,GAAG,IAAI,IAAI,EAAE;AAClC,IAAI,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,EAAE;AAC9C,IAAI,MAAM,cAAc,GAAG,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;AACxF,IAAI,OAAO,CAAC,EAAE,SAAS,CAAC,IAAI,CAAC;AAC7B,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC,IAAI,CAAC;AACzC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;AAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;AAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;AAC7B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;AAC3B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,EAAE,CAAC;AAClC,QAAQ,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;AAC5C;;;;"}
@@ -1,4 +1,4 @@
1
- import { g as getConfig } from './index-CC2WVN43.mjs';
1
+ import { g as getConfig } from './index-CLst0IiM.mjs';
2
2
  import 'axios';
3
3
  import 'tweetnacl';
4
4
  import 'reconnecting-websocket';
@@ -34,4 +34,4 @@ async function genSolanaMessage(address, nonce, appIdOverride) {
34
34
  }
35
35
 
36
36
  export { genSolanaMessage };
37
- //# sourceMappingURL=solana-message-Bx04VMG1.mjs.map
37
+ //# sourceMappingURL=solana-message-Deru1tF1.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"solana-message-Bx04VMG1.mjs","sources":["../.rollup-tmp/utils/sol/solana-message.js"],"sourcesContent":["import { getConfig } from \"../../client/config\";\n// NOTE: this message text must stay BYTE-IDENTICAL to what the wallet-auth\n// verifier reconstructs — any drift breaks every SIWS login.\nexport async function genSolanaMessage(address, nonce, appIdOverride) {\n var _a, _b;\n /* Domain / origin aren't available server-side – fall back to envs\n or simple placeholders. */\n const domain = (typeof process !== 'undefined' && ((_a = process.env) === null || _a === void 0 ? void 0 : _a.SESSION_DOMAIN)) || \"server\";\n const origin = (typeof process !== 'undefined' && ((_b = process.env) === null || _b === void 0 ? void 0 : _b.SESSION_ORIGIN)) || \"server\";\n // SECURITY (audit-4 CRITICAL-1): bind the signed prompt to the target app.\n // A server wallet client passes its snapshotted appId (appIdOverride) so the\n // signed \"App ID\" stays bound to the app the wallet client was created against\n // even after a later global init() (CTA-05); the default path reads the SAME\n // global config that genAuthNonce + /session use, so the signed \"App ID\" is\n // guaranteed to equal the app the session is minted for; the server verifier\n // rejects any mismatch.\n const appId = appIdOverride !== null && appIdOverride !== void 0 ? appIdOverride : (await getConfig()).appId;\n const statement = \"Sign this message to authenticate with our application.\";\n const currentDate = new Date();\n const issuedAt = currentDate.toISOString();\n const expirationTime = new Date(currentDate.getTime() + 5 * 60 * 1000).toISOString();\n return `${statement}\\n\\n` +\n `Wallet address:\\n${address}\\n\\n` +\n `App ID: ${appId}\\n` +\n `Domain: ${domain}\\n` +\n `Origin: ${origin}\\n` +\n `Nonce: ${nonce}\\n` +\n `Issued At: ${issuedAt}\\n` +\n `Expiration Time: ${expirationTime}`;\n}\n"],"names":[],"mappings":";;;;;AACA;AACA;AACO,eAAe,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE;AACtE,IAAI,IAAI,EAAE,EAAE,EAAE;AACd;AACA;AACA,IAAI,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,KAAK,WAAW,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,KAAK,MAAM,GAAG,MAAM,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,QAAQ;AAC9I,IAAI,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,KAAK,WAAW,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,KAAK,MAAM,GAAG,MAAM,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,QAAQ;AAC9I;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,MAAM,KAAK,GAAG,aAAa,KAAK,IAAI,IAAI,aAAa,KAAK,MAAM,GAAG,aAAa,GAAG,CAAC,MAAM,SAAS,EAAE,EAAE,KAAK;AAChH,IAAI,MAAM,SAAS,GAAG,yDAAyD;AAC/E,IAAI,MAAM,WAAW,GAAG,IAAI,IAAI,EAAE;AAClC,IAAI,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,EAAE;AAC9C,IAAI,MAAM,cAAc,GAAG,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;AACxF,IAAI,OAAO,CAAC,EAAE,SAAS,CAAC,IAAI,CAAC;AAC7B,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC,IAAI,CAAC;AACzC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;AAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;AAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;AAC7B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;AAC3B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,EAAE,CAAC;AAClC,QAAQ,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;AAC5C;;;;"}
1
+ {"version":3,"file":"solana-message-Deru1tF1.mjs","sources":["../.rollup-tmp/utils/sol/solana-message.js"],"sourcesContent":["import { getConfig } from \"../../client/config\";\n// NOTE: this message text must stay BYTE-IDENTICAL to what the wallet-auth\n// verifier reconstructs — any drift breaks every SIWS login.\nexport async function genSolanaMessage(address, nonce, appIdOverride) {\n var _a, _b;\n /* Domain / origin aren't available server-side – fall back to envs\n or simple placeholders. */\n const domain = (typeof process !== 'undefined' && ((_a = process.env) === null || _a === void 0 ? void 0 : _a.SESSION_DOMAIN)) || \"server\";\n const origin = (typeof process !== 'undefined' && ((_b = process.env) === null || _b === void 0 ? void 0 : _b.SESSION_ORIGIN)) || \"server\";\n // SECURITY (audit-4 CRITICAL-1): bind the signed prompt to the target app.\n // A server wallet client passes its snapshotted appId (appIdOverride) so the\n // signed \"App ID\" stays bound to the app the wallet client was created against\n // even after a later global init() (CTA-05); the default path reads the SAME\n // global config that genAuthNonce + /session use, so the signed \"App ID\" is\n // guaranteed to equal the app the session is minted for; the server verifier\n // rejects any mismatch.\n const appId = appIdOverride !== null && appIdOverride !== void 0 ? appIdOverride : (await getConfig()).appId;\n const statement = \"Sign this message to authenticate with our application.\";\n const currentDate = new Date();\n const issuedAt = currentDate.toISOString();\n const expirationTime = new Date(currentDate.getTime() + 5 * 60 * 1000).toISOString();\n return `${statement}\\n\\n` +\n `Wallet address:\\n${address}\\n\\n` +\n `App ID: ${appId}\\n` +\n `Domain: ${domain}\\n` +\n `Origin: ${origin}\\n` +\n `Nonce: ${nonce}\\n` +\n `Issued At: ${issuedAt}\\n` +\n `Expiration Time: ${expirationTime}`;\n}\n"],"names":[],"mappings":";;;;;AACA;AACA;AACO,eAAe,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE;AACtE,IAAI,IAAI,EAAE,EAAE,EAAE;AACd;AACA;AACA,IAAI,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,KAAK,WAAW,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,KAAK,MAAM,GAAG,MAAM,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,QAAQ;AAC9I,IAAI,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,KAAK,WAAW,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,KAAK,MAAM,GAAG,MAAM,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,QAAQ;AAC9I;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,MAAM,KAAK,GAAG,aAAa,KAAK,IAAI,IAAI,aAAa,KAAK,MAAM,GAAG,aAAa,GAAG,CAAC,MAAM,SAAS,EAAE,EAAE,KAAK;AAChH,IAAI,MAAM,SAAS,GAAG,yDAAyD;AAC/E,IAAI,MAAM,WAAW,GAAG,IAAI,IAAI,EAAE;AAClC,IAAI,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,EAAE;AAC9C,IAAI,MAAM,cAAc,GAAG,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;AACxF,IAAI,OAAO,CAAC,EAAE,SAAS,CAAC,IAAI,CAAC;AAC7B,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC,IAAI,CAAC;AACzC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;AAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;AAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;AAC7B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;AAC3B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,EAAE,CAAC;AAClC,QAAQ,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;AAC5C;;;;"}
@@ -1,5 +1,5 @@
1
- import { g as getConfig, b as bufferExports, a as getTransactionJson } from './index-CC2WVN43.mjs';
2
- export { S as SOLANA_MAX_SUPPORTED_TRANSACTION_VERSION } from './index-CC2WVN43.mjs';
1
+ import { g as getConfig, b as bufferExports, a as getTransactionJson } from './index-CLst0IiM.mjs';
2
+ export { S as SOLANA_MAX_SUPPORTED_TRANSACTION_VERSION } from './index-CLst0IiM.mjs';
3
3
  import 'axios';
4
4
  import 'tweetnacl';
5
5
  import 'reconnecting-websocket';
@@ -831,12 +831,41 @@ async function refreshTransactionLifetime(transaction, rpc, serverFence) {
831
831
  transaction.message.recentBlockhash = latest.blockhash;
832
832
  return { blockhash: latest.blockhash, lastValidBlockHeight: latest.lastValidBlockHeight };
833
833
  }
834
+ /**
835
+ * Poofnet lane of the onchain 202. Poofnet has no chain, so there is nothing to
836
+ * submit and no blockhash to refresh: the worker issued an unsendable message
837
+ * (off-curve fee payer) that only proves this wallet approved this write. It is
838
+ * validated exactly like a mainnet transaction before the wallet sees it, signed
839
+ * by the same provider method, and returned for the worker to verify and commit.
840
+ */
841
+ /**
842
+ * The fee payer of every poofnet approval message (realtime's
843
+ * POOFNET_VOID_FEE_PAYER): an off-curve address with no private key, so a message
844
+ * it pays for can never land on any cluster. Pinned here so a 202 from anything
845
+ * else can never get a live, broadcastable transaction signed.
846
+ */
847
+ const POOFNET_VOID_FEE_PAYER = 'CAXES7Tt3KGsSw4A5knQXKKUJZWsrguu6PC7SDBL4oq2';
848
+ async function signPoofnetTransaction(serializedTransaction, authProvider, options, expectedIntent) {
849
+ var _a, _b, _c;
850
+ const { VersionedTransaction, PublicKey } = await import('@solana/web3.js');
851
+ const config = (_b = (_a = options === null || options === void 0 ? void 0 : options._overrides) === null || _a === void 0 ? void 0 : _a._config) !== null && _b !== void 0 ? _b : await getConfig();
852
+ const transaction = deserializeAndValidateServerTransaction(serializedTransaction, { VersionedTransaction, PublicKey }, {
853
+ label: 'Poofnet write transaction', expectedAppId: config.appId, expectedIntent,
854
+ });
855
+ // Only an unsendable approval message may be signed on poofnet: any other fee
856
+ // payer could make these exact bytes a live transaction on a real cluster.
857
+ if (((_c = transaction.message.staticAccountKeys[0]) === null || _c === void 0 ? void 0 : _c.toBase58()) !== POOFNET_VOID_FEE_PAYER) {
858
+ throw new Error('Poofnet write transaction must be paid by the poofnet void fee payer; nothing was signed.');
859
+ }
860
+ const signed = await authProvider.signTransaction(transaction);
861
+ return bufferExports.Buffer.from(signed.serialize()).toString('base64');
862
+ }
834
863
  async function handlePreBuiltTransaction(tx, authProvider, options, expectedIntent) {
835
864
  var _a, _b, _c, _d, _e, _f, _g, _h;
836
865
  // SIMD-0385 v1 wires start with 0x81 (message first, signatures trailing);
837
866
  // web3.js's legacy/v0 classes cannot represent them, so they take their own lane.
838
867
  if (isTransactionV1Base64(tx.serializedTransaction)) {
839
- const { handlePreBuiltV1Transaction } = await import('./solana-v1-operations-C6Qev1fZ.mjs');
868
+ const { handlePreBuiltV1Transaction } = await import('./solana-v1-operations-Dxs4XM_F.mjs');
840
869
  return handlePreBuiltV1Transaction(tx, authProvider, options, expectedIntent);
841
870
  }
842
871
  const { Connection, VersionedTransaction, PublicKey } = await import('@solana/web3.js');
@@ -949,5 +978,5 @@ async function handlePreBuiltTransaction(tx, authProvider, options, expectedInte
949
978
  };
950
979
  }
951
980
 
952
- export { BoundedOnchainRuleRejection, buildExpectedWriteIntent, decodeBase58, deriveTransactionSignature, encodeBase58, errorText, getTransactionJson, handlePreBuiltTransaction, isBoundedOnchainRuleRejection, isDefinitiveSolanaSendFailure, isTransactionV1Base64, validateServerTransactionSemantics };
953
- //# sourceMappingURL=solana-operations-CCFNQIgd.mjs.map
981
+ export { BoundedOnchainRuleRejection, POOFNET_VOID_FEE_PAYER, buildExpectedWriteIntent, decodeBase58, deriveTransactionSignature, encodeBase58, errorText, getTransactionJson, handlePreBuiltTransaction, isBoundedOnchainRuleRejection, isDefinitiveSolanaSendFailure, isTransactionV1Base64, signPoofnetTransaction, validateServerTransactionSemantics };
982
+ //# sourceMappingURL=solana-operations-Cm19J14R.mjs.map