@bounded-sh/core 0.0.118-openapps.0 → 0.0.119
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.
- package/dist/client/operations.d.ts +19 -1
- package/dist/client/reconcile.d.ts +2 -2
- package/dist/client/solana-confirm.d.ts +49 -0
- package/dist/client/solana-operations.d.ts +13 -1
- package/dist/{index-DQ0FXeI8.js → index-D7-5ecyu.js} +356 -237
- package/dist/index-D7-5ecyu.js.map +1 -0
- package/dist/{index-C6ZW5Gt2.mjs → index-D93TEYGf.mjs} +355 -238
- package/dist/index-D93TEYGf.mjs.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{mirror-sync-kt9PzQen.mjs → mirror-sync-DPMtQC3A.mjs} +2 -2
- package/dist/{mirror-sync-kt9PzQen.mjs.map → mirror-sync-DPMtQC3A.mjs.map} +1 -1
- package/dist/{mirror-sync-ChzMq7bH.js → mirror-sync-DzbDh6G_.js} +2 -2
- package/dist/{mirror-sync-ChzMq7bH.js.map → mirror-sync-DzbDh6G_.js.map} +1 -1
- package/dist/{realtime-store-CURbZ68v.js → realtime-store-C2Jz8YM0.js} +2 -2
- package/dist/{realtime-store-CURbZ68v.js.map → realtime-store-C2Jz8YM0.js.map} +1 -1
- package/dist/{realtime-store-FUjGL_Z8.mjs → realtime-store-FObAdw-J.mjs} +2 -2
- package/dist/{realtime-store-FUjGL_Z8.mjs.map → realtime-store-FObAdw-J.mjs.map} +1 -1
- package/dist/{solana-message-F1e5wPKF.js → solana-message-CPotom18.js} +2 -2
- package/dist/{solana-message-F1e5wPKF.js.map → solana-message-CPotom18.js.map} +1 -1
- package/dist/{solana-message-CEjXjnaG.mjs → solana-message-DqKp-y53.mjs} +2 -2
- package/dist/{solana-message-CEjXjnaG.mjs.map → solana-message-DqKp-y53.mjs.map} +1 -1
- package/dist/{solana-operations-CI0sREvl.js → solana-operations-DsT1303T.js} +55 -39
- package/dist/solana-operations-DsT1303T.js.map +1 -0
- package/dist/{solana-operations-5WjFwhnt.mjs → solana-operations-cwcuTNyk.mjs} +54 -40
- package/dist/solana-operations-cwcuTNyk.mjs.map +1 -0
- package/dist/{solana-v1-operations-DB9pih7o.mjs → solana-v1-operations-Jzg3uAot.mjs} +10 -24
- package/dist/solana-v1-operations-Jzg3uAot.mjs.map +1 -0
- package/dist/{solana-v1-operations-oot2R63I.js → solana-v1-operations-qj0WA2tF.js} +10 -24
- package/dist/solana-v1-operations-qj0WA2tF.js.map +1 -0
- package/dist/types.d.ts +22 -15
- package/package.json +1 -1
- package/dist/index-C6ZW5Gt2.mjs.map +0 -1
- package/dist/index-DQ0FXeI8.js.map +0 -1
- package/dist/solana-operations-5WjFwhnt.mjs.map +0 -1
- package/dist/solana-operations-CI0sREvl.js.map +0 -1
- package/dist/solana-v1-operations-DB9pih7o.mjs.map +0 -1
- package/dist/solana-v1-operations-oot2R63I.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"realtime-store-FUjGL_Z8.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-FObAdw-J.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-
|
|
3
|
+
var index = require('./index-D7-5ecyu.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-
|
|
39
|
+
//# sourceMappingURL=solana-message-CPotom18.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"solana-message-
|
|
1
|
+
{"version":3,"file":"solana-message-CPotom18.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-
|
|
1
|
+
import { g as getConfig } from './index-D93TEYGf.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-
|
|
37
|
+
//# sourceMappingURL=solana-message-DqKp-y53.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"solana-message-
|
|
1
|
+
{"version":3,"file":"solana-message-DqKp-y53.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,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var index = require('./index-
|
|
3
|
+
var index = require('./index-D7-5ecyu.js');
|
|
4
4
|
require('axios');
|
|
5
5
|
require('tweetnacl');
|
|
6
6
|
require('reconnecting-websocket');
|
|
@@ -125,6 +125,16 @@ const DEFINITIVE_SEND_FAILURE_MARKERS = [
|
|
|
125
125
|
'insufficient funds for rent',
|
|
126
126
|
'attempt to debit an account but found no record of a prior credit',
|
|
127
127
|
];
|
|
128
|
+
/**
|
|
129
|
+
* The send was refused because the transaction's blockhash is no longer current: the RPC's
|
|
130
|
+
* preflight simulation names it, and no byte entered the network. The one send failure a
|
|
131
|
+
* fresh build cures, since the bytes were right and only their lifetime ran out.
|
|
132
|
+
*/
|
|
133
|
+
function isExpiredBlockhashRejection(error) {
|
|
134
|
+
var _a, _b;
|
|
135
|
+
const message = String((_b = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error) !== null && _b !== void 0 ? _b : '').toLowerCase();
|
|
136
|
+
return message.includes('blockhash not found') || message.includes('block height exceeded');
|
|
137
|
+
}
|
|
128
138
|
function isDefinitiveSolanaSendFailure(error) {
|
|
129
139
|
var _a, _b, _c;
|
|
130
140
|
const err = error;
|
|
@@ -208,6 +218,25 @@ function errorText(error) {
|
|
|
208
218
|
const message = error === null || error === void 0 ? void 0 : error.message;
|
|
209
219
|
return typeof message === 'string' && message.length > 0 ? message : String(error);
|
|
210
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* The tagged failure for a transaction that LANDED and failed on-chain: a Bounded
|
|
223
|
+
* rule/invariant rejection when the logs say so, otherwise the raw error, with the
|
|
224
|
+
* program logs attached whenever the RPC can still read them.
|
|
225
|
+
*/
|
|
226
|
+
async function landedTransactionFailure(rpc, signature, err) {
|
|
227
|
+
var _a, _b;
|
|
228
|
+
let logMessages;
|
|
229
|
+
try {
|
|
230
|
+
logMessages = (_b = (_a = (await getTransactionJson(rpc, signature, 'confirmed'))) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.logMessages;
|
|
231
|
+
}
|
|
232
|
+
catch (_c) {
|
|
233
|
+
// Log retrieval is diagnostic only; the on-chain error is the verdict.
|
|
234
|
+
}
|
|
235
|
+
if (isBoundedOnchainRuleRejection(err, logMessages)) {
|
|
236
|
+
return index.markWriteNotApplied(new BoundedOnchainRuleRejection(err, logMessages), 'failed_onchain');
|
|
237
|
+
}
|
|
238
|
+
return index.markWriteNotApplied(new Error(`Transaction failed: ${logMessages ? JSON.stringify(logMessages) : JSON.stringify(err)}`), 'failed_onchain');
|
|
239
|
+
}
|
|
211
240
|
const BOUNDED_PROGRAM_MAINNET = ['open', 'Tv7fbpYSseNHYmCZFZ1CZgj4r8D9fKNgEz1qo6F'].join('');
|
|
212
241
|
const BOUNDED_PROGRAM_DEVNET = ['open', 'Tv7fbpYSseNHYmCZFZ1CZgj4r8D9fKNgEz1qo6F'].join('');
|
|
213
242
|
const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
|
|
@@ -819,16 +848,18 @@ function serverTransactionIsPartiallySigned(transaction) {
|
|
|
819
848
|
* LF-003: hand the wallet a blockhash that is still alive, and report the
|
|
820
849
|
* lifetime the transaction was ACTUALLY signed against.
|
|
821
850
|
*
|
|
822
|
-
* A Solana blockhash dies 150 blocks
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
851
|
+
* A Solana blockhash dies 150 blocks after it is minted: about 33 seconds on
|
|
852
|
+
* mainnet in late 2026, down from about 60 in mid-2026, and shrinking as slots
|
|
853
|
+
* get faster. The worker mints this one at the START of its build. The
|
|
854
|
+
* remaining builder RPC round trips, the response hop, and - dominating
|
|
855
|
+
* everything - the unbounded human pause in front of the wallet's approval
|
|
856
|
+
* dialog all come out of that one window. So the pre-built lane routinely
|
|
857
|
+
* asks a wallet to sign a transaction that is already expired or expires
|
|
858
|
+
* before it can be broadcast: the send fails `blockhash not found` (a
|
|
859
|
+
* DEFINITIVE failure), and a wallet that substitutes the dead blockhash
|
|
860
|
+
* instead of signing it is refused by the SDK's own message-equality check.
|
|
861
|
+
* Refreshing here, immediately before the signature, gives the whole window
|
|
862
|
+
* to the approval.
|
|
832
863
|
*
|
|
833
864
|
* Returns the fence the caller must confirm and reconcile against - refreshing
|
|
834
865
|
* the transaction without moving BOTH fence fields with it would declare expiry
|
|
@@ -906,11 +937,11 @@ async function signPoofnetTransaction(serializedTransaction, authProvider, optio
|
|
|
906
937
|
return index.bufferExports.Buffer.from(signed.serialize()).toString('base64');
|
|
907
938
|
}
|
|
908
939
|
async function handlePreBuiltTransaction(tx, authProvider, options, expectedIntent, beforeBroadcast) {
|
|
909
|
-
var _a, _b, _c, _d, _e, _f
|
|
940
|
+
var _a, _b, _c, _d, _e, _f;
|
|
910
941
|
// SIMD-0385 v1 wires start with 0x81 (message first, signatures trailing);
|
|
911
942
|
// web3.js's legacy/v0 classes cannot represent them, so they take their own lane.
|
|
912
943
|
if (isTransactionV1Base64(tx.serializedTransaction)) {
|
|
913
|
-
const { handlePreBuiltV1Transaction } = await Promise.resolve().then(function () { return require('./solana-v1-operations-
|
|
944
|
+
const { handlePreBuiltV1Transaction } = await Promise.resolve().then(function () { return require('./solana-v1-operations-qj0WA2tF.js'); });
|
|
914
945
|
return handlePreBuiltV1Transaction(tx, authProvider, options, expectedIntent, beforeBroadcast);
|
|
915
946
|
}
|
|
916
947
|
const { Connection, VersionedTransaction, PublicKey } = await import('@solana/web3.js');
|
|
@@ -981,48 +1012,31 @@ async function handlePreBuiltTransaction(tx, authProvider, options, expectedInte
|
|
|
981
1012
|
reason: `Broadcast response was lost or inconclusive: ${errorText(error)}`,
|
|
982
1013
|
};
|
|
983
1014
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
confirmation
|
|
987
|
-
|
|
988
|
-
}, 'confirmed');
|
|
989
|
-
}
|
|
990
|
-
catch (error) {
|
|
991
|
-
// The bytes were accepted; only the confirmation outcome is unknown.
|
|
1015
|
+
const confirmation = await index.confirmSignature(submitRpc, { signature, lastValidBlockHeight: fence.lastValidBlockHeight });
|
|
1016
|
+
if (!confirmation.landed) {
|
|
1017
|
+
// The bytes were accepted; only the confirmation outcome is unknown. Whether the
|
|
1018
|
+
// transaction can still land is reconcileSetResult's call, from the finalized height.
|
|
992
1019
|
return {
|
|
993
1020
|
outcome: 'submitted',
|
|
994
1021
|
lane: 'solana',
|
|
995
1022
|
transactionSignature: signature,
|
|
996
1023
|
signedTransaction,
|
|
997
1024
|
fence,
|
|
998
|
-
reason: `Confirmation outcome unknown: ${
|
|
1025
|
+
reason: `Confirmation outcome unknown: ${confirmation.reason}`,
|
|
999
1026
|
};
|
|
1000
1027
|
}
|
|
1001
1028
|
// #310: "confirmed" only means the network recorded the transaction, not that it
|
|
1002
1029
|
// succeeded - a landed-but-failed transaction carries an err. Report it as a failure
|
|
1003
1030
|
// (with on-chain logs) instead of a fake success, mirroring the server keypair provider.
|
|
1004
|
-
if (
|
|
1005
|
-
|
|
1006
|
-
try {
|
|
1007
|
-
const txInfo = await getTransactionJson(submitRpc, signature, 'confirmed');
|
|
1008
|
-
logMessages = (_f = txInfo === null || txInfo === void 0 ? void 0 : txInfo.meta) === null || _f === void 0 ? void 0 : _f.logMessages;
|
|
1009
|
-
}
|
|
1010
|
-
catch (_j) {
|
|
1011
|
-
// Log retrieval is diagnostic only; the on-chain error is the verdict.
|
|
1012
|
-
}
|
|
1013
|
-
if (isBoundedOnchainRuleRejection(confirmation.value.err, logMessages)) {
|
|
1014
|
-
throw index.markWriteNotApplied(new BoundedOnchainRuleRejection(confirmation.value.err, logMessages), 'failed_onchain');
|
|
1015
|
-
}
|
|
1016
|
-
const errorMessage = logMessages ? JSON.stringify(logMessages) : JSON.stringify(confirmation.value.err);
|
|
1017
|
-
throw index.markWriteNotApplied(new Error(`Transaction failed: ${errorMessage}`), 'failed_onchain');
|
|
1018
|
-
}
|
|
1031
|
+
if (confirmation.status.err)
|
|
1032
|
+
throw await landedTransactionFailure(submitRpc, signature, confirmation.status.err);
|
|
1019
1033
|
return {
|
|
1020
1034
|
outcome: 'confirmed',
|
|
1021
1035
|
lane: 'solana',
|
|
1022
1036
|
transactionSignature: signature,
|
|
1023
1037
|
signedTransaction,
|
|
1024
1038
|
fence,
|
|
1025
|
-
confirmationContextSlot: (
|
|
1039
|
+
confirmationContextSlot: (_f = (_e = confirmation.status.slot) !== null && _e !== void 0 ? _e : confirmation.contextSlot) !== null && _f !== void 0 ? _f : 0,
|
|
1026
1040
|
};
|
|
1027
1041
|
}
|
|
1028
1042
|
|
|
@@ -1038,7 +1052,9 @@ exports.getTransactionJson = getTransactionJson;
|
|
|
1038
1052
|
exports.handlePreBuiltTransaction = handlePreBuiltTransaction;
|
|
1039
1053
|
exports.isBoundedOnchainRuleRejection = isBoundedOnchainRuleRejection;
|
|
1040
1054
|
exports.isDefinitiveSolanaSendFailure = isDefinitiveSolanaSendFailure;
|
|
1055
|
+
exports.isExpiredBlockhashRejection = isExpiredBlockhashRejection;
|
|
1041
1056
|
exports.isTransactionV1Base64 = isTransactionV1Base64;
|
|
1057
|
+
exports.landedTransactionFailure = landedTransactionFailure;
|
|
1042
1058
|
exports.signPoofnetTransaction = signPoofnetTransaction;
|
|
1043
1059
|
exports.validateServerTransactionSemantics = validateServerTransactionSemantics;
|
|
1044
|
-
//# sourceMappingURL=solana-operations-
|
|
1060
|
+
//# sourceMappingURL=solana-operations-DsT1303T.js.map
|