@bounded-sh/core 0.0.17 → 0.0.19
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/config.d.ts +0 -4
- package/dist/client/functions.d.ts +2 -3
- package/dist/client/live.d.ts +2 -5
- package/dist/client/operations.d.ts +8 -4
- package/dist/client/realtime-store.d.ts +6 -0
- package/dist/client/subscription-v2.d.ts +0 -13
- package/dist/index.d.ts +1 -3
- package/dist/index.js +501 -1573
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +502 -1566
- package/dist/index.mjs.map +1 -1
- package/dist/realtime-store-Ck_VgTcv.js +21 -0
- package/dist/realtime-store-Ck_VgTcv.js.map +1 -0
- package/dist/realtime-store-D3t7PyZl.mjs +19 -0
- package/dist/realtime-store-D3t7PyZl.mjs.map +1 -0
- package/dist/types.d.ts +11 -9
- package/dist/utils/auth-api.d.ts +6 -2
- package/dist/utils/server-session-manager.d.ts +2 -2
- package/dist/utils/utils.d.ts +2 -4
- package/package.json +2 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('./index.js');
|
|
4
|
+
require('axios');
|
|
5
|
+
require('tweetnacl');
|
|
6
|
+
require('@solana/web3.js');
|
|
7
|
+
require('@coral-xyz/anchor');
|
|
8
|
+
require('bn.js');
|
|
9
|
+
require('reconnecting-websocket');
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// realtime-store.ts — Client-side state manager for realtime apps.
|
|
13
|
+
//
|
|
14
|
+
// Manages: WS connection, in-memory state, IDB persistence, optimistic
|
|
15
|
+
// writes, delta accumulation, loading states, ephemeral/durable tiers.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
async function reconnectRealtimeStoreWithNewAuth() {
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
exports.reconnectRealtimeStoreWithNewAuth = reconnectRealtimeStoreWithNewAuth;
|
|
21
|
+
//# sourceMappingURL=realtime-store-Ck_VgTcv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-store-Ck_VgTcv.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 { getConfig, isBoundedNetwork } from './config';\n// ---------------------------------------------------------------------------\n// IDB helpers (lazy-loaded, non-blocking)\n// ---------------------------------------------------------------------------\nconst IDB_NAME = 'bounded-realtime';\nconst IDB_STORE = 'subscriptions';\nconst IDB_VERSION = 1;\nlet idbPromise = null;\nfunction getIDB() {\n if (idbPromise)\n return idbPromise;\n if (typeof indexedDB === 'undefined') {\n return Promise.reject(new Error('IndexedDB not available'));\n }\n idbPromise = new Promise((resolve, reject) => {\n const req = indexedDB.open(IDB_NAME, IDB_VERSION);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(IDB_STORE)) {\n db.createObjectStore(IDB_STORE);\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n return idbPromise;\n}\nasync function idbGet(key) {\n try {\n const db = await getIDB();\n return new Promise((resolve) => {\n const tx = db.transaction(IDB_STORE, 'readonly');\n const store = tx.objectStore(IDB_STORE);\n const req = store.get(key);\n req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };\n req.onerror = () => resolve(null);\n });\n }\n catch (_a) {\n return null;\n }\n}\nasync function idbSet(key, value) {\n try {\n const db = await getIDB();\n return new Promise((resolve) => {\n const tx = db.transaction(IDB_STORE, 'readwrite');\n const store = tx.objectStore(IDB_STORE);\n store.put(value, key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => resolve();\n });\n }\n catch (_a) {\n // Best-effort persistence\n }\n}\nasync function idbDelete(key) {\n try {\n const db = await getIDB();\n return new Promise((resolve) => {\n const tx = db.transaction(IDB_STORE, 'readwrite');\n const store = tx.objectStore(IDB_STORE);\n store.delete(key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => resolve();\n });\n }\n catch (_a) {\n // Best-effort\n }\n}\n// ---------------------------------------------------------------------------\n// RealtimeStore\n// ---------------------------------------------------------------------------\nlet nextRequestId = 1;\nfunction hashForKey(value) {\n let h = 5381;\n for (let i = 0; i < value.length; i++) {\n h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;\n }\n return h.toString(36);\n}\nfunction principalFromToken(token) {\n return token ? `t${hashForKey(token)}` : 'anon';\n}\nexport class RealtimeStore {\n constructor() {\n this.ws = null;\n this.wsUrl = '';\n this.appId = '';\n this.subscriptions = new Map();\n this.pendingRequests = new Map();\n this.connectPromise = null;\n this.reconnectTimer = null;\n this.reconnectDelay = 1000;\n this.maxReconnectDelay = 30000;\n this.idbFlushTimer = null;\n this.idbDirtyKeys = new Set();\n this.closed = false;\n this.authToken = null;\n this.authPrincipalKey = 'anon';\n this.authenticating = false;\n this.suppressNextReconnect = false;\n this.isServer = false;\n this.tokenRefreshTimer = null;\n // -----------------------------------------------------------------------\n // WebSocket connection\n // -----------------------------------------------------------------------\n this.initPromise = null;\n }\n // -----------------------------------------------------------------------\n // Initialization\n // -----------------------------------------------------------------------\n async init() {\n const config = await getConfig();\n this.appId = config.appId;\n this.wsUrl = config.wsApiUrl;\n this.isServer = config.isServer;\n await this.refreshToken();\n this.startTokenRefresh();\n }\n async refreshToken() {\n let token = null;\n try {\n const { getIdToken } = await import('../utils/utils');\n token = await getIdToken(this.isServer);\n }\n catch ( /* no auth available */_a) { /* no auth available */ }\n this.authToken = token !== null && token !== void 0 ? token : null;\n this.authPrincipalKey = principalFromToken(this.authToken);\n }\n startTokenRefresh() {\n if (this.tokenRefreshTimer)\n return;\n this.tokenRefreshTimer = setInterval(async () => {\n const prevPrincipal = this.authPrincipalKey;\n await this.refreshToken();\n if (this.authPrincipalKey !== prevPrincipal) {\n await this.applyAuthPrincipalChange();\n if (this.subscriptions.size > 0) {\n await this.ensureConnected().catch(() => {\n this.setAllSubscriptionStatus('error');\n });\n }\n }\n }, 5 * 60 * 1000); // Check every 5 minutes\n }\n async ensureInitialized() {\n if (this.appId)\n return;\n if (!this.initPromise)\n this.initPromise = this.init();\n await this.initPromise;\n }\n async ensureCurrentAuth() {\n await this.ensureInitialized();\n const prevPrincipal = this.authPrincipalKey;\n await this.refreshToken();\n if (this.authPrincipalKey !== prevPrincipal) {\n await this.applyAuthPrincipalChange();\n }\n }\n rekeySubscriptionsForPrincipal() {\n const subs = Array.from(this.subscriptions.values());\n this.subscriptions.clear();\n for (const sub of subs) {\n this.subscriptions.set(this.getSubKey(sub.path, sub.options), sub);\n }\n }\n async applyAuthPrincipalChange() {\n if (this.idbFlushTimer) {\n clearTimeout(this.idbFlushTimer);\n this.idbFlushTimer = null;\n }\n this.idbDirtyKeys.clear();\n this.rekeySubscriptionsForPrincipal();\n for (const sub of this.subscriptions.values()) {\n sub.docs.clear();\n sub.ref.current = sub.docs;\n sub.error = null;\n sub.isStale = false;\n let loaded = false;\n if (this.shouldUseIdb(sub.tier, sub.options)) {\n const cached = await idbGet(this.idbKey(sub.path));\n if (cached && Array.isArray(cached)) {\n for (const doc of cached) {\n if (doc && doc._id)\n sub.docs.set(doc._id, doc);\n }\n sub.ref.current = sub.docs;\n loaded = sub.docs.size > 0;\n }\n }\n sub.status = loaded ? 'cached' : 'loading';\n sub.isStale = loaded;\n if (loaded)\n this.notifySubscription(sub);\n else\n this.notifyState(sub);\n }\n if (this.ws) {\n const ws = this.ws;\n this.ws = null;\n this.connectPromise = null;\n this.suppressNextReconnect = true;\n try {\n ws.close(1000, 'Auth changed');\n }\n catch ( /* ignore */_a) { /* ignore */ }\n }\n }\n async ensureConnected() {\n var _a;\n await this.ensureCurrentAuth();\n if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)\n return;\n if (this.connectPromise)\n return this.connectPromise;\n this.connectPromise = this.connect();\n return this.connectPromise;\n }\n connect() {\n return new Promise((resolve, reject) => {\n if (this.closed) {\n reject(new Error('Store closed'));\n return;\n }\n const params = new URLSearchParams();\n params.set('appId', this.appId);\n const url = `${this.wsUrl}?${params.toString()}`;\n const ws = new WebSocket(url);\n this.ws = ws;\n let authTimer = null;\n const finishConnected = () => {\n if (authTimer) {\n clearTimeout(authTimer);\n authTimer = null;\n }\n this.authenticating = false;\n ws.removeEventListener('error', onError);\n this.reconnectDelay = 1000;\n this.connectPromise = null;\n this.resubscribeAll();\n resolve();\n };\n const onOpen = () => {\n if (!this.authToken) {\n finishConnected();\n return;\n }\n this.authenticating = true;\n authTimer = setTimeout(() => {\n this.authenticating = false;\n this.connectPromise = null;\n try {\n ws.close(1008, 'Authentication timeout');\n }\n catch ( /* ignore */_a) { /* ignore */ }\n reject(new Error('WebSocket authentication timeout'));\n }, 10000);\n try {\n ws.send(JSON.stringify({ type: 'auth', token: this.authToken }));\n }\n catch (e) {\n if (authTimer)\n clearTimeout(authTimer);\n this.authenticating = false;\n this.connectPromise = null;\n reject(e);\n }\n };\n const onError = (e) => {\n if (authTimer)\n clearTimeout(authTimer);\n this.authenticating = false;\n ws.removeEventListener('open', onOpen);\n this.connectPromise = null;\n reject(new Error('WebSocket connection failed'));\n };\n ws.addEventListener('open', onOpen, { once: true });\n ws.addEventListener('error', onError, { once: true });\n ws.addEventListener('message', (event) => {\n if (this.authenticating) {\n try {\n const msg = JSON.parse(typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data));\n if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'authenticated') {\n finishConnected();\n return;\n }\n }\n catch ( /* fall through to normal handling */_a) { /* fall through to normal handling */ }\n }\n this.handleMessage(event.data);\n });\n ws.addEventListener('close', () => {\n if (authTimer)\n clearTimeout(authTimer);\n if (this.ws !== ws) {\n if (this.suppressNextReconnect)\n this.suppressNextReconnect = false;\n return;\n }\n this.authenticating = false;\n this.ws = null;\n this.connectPromise = null;\n this.rejectAllPending('WebSocket closed');\n this.setAllSubscriptionStatus('reconnecting');\n if (this.suppressNextReconnect) {\n this.suppressNextReconnect = false;\n return;\n }\n this.scheduleReconnect();\n });\n });\n }\n scheduleReconnect() {\n if (this.closed)\n return;\n if (this.reconnectTimer)\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = setTimeout(() => {\n this.ensureConnected().catch(() => {\n this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);\n this.scheduleReconnect();\n });\n }, this.reconnectDelay);\n }\n resubscribeAll() {\n for (const sub of this.subscriptions.values()) {\n this.sendSubscribe(sub);\n }\n }\n // -----------------------------------------------------------------------\n // Message handling\n // -----------------------------------------------------------------------\n handleMessage(raw) {\n const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);\n let msg;\n try {\n msg = JSON.parse(text);\n }\n catch (_a) {\n return;\n }\n switch (msg.type) {\n case 'snapshot':\n this.handleSnapshot(msg);\n break;\n case 'delta':\n this.handleDelta(msg);\n break;\n case 'result':\n this.handleResult(msg);\n break;\n case 'error':\n this.handleError(msg);\n break;\n case 'pong':\n break;\n case 'authenticated':\n break;\n // v1 compat: handle legacy message types during transition\n case 'subscribed':\n this.handleSnapshot(Object.assign(Object.assign({}, msg), { type: 'snapshot', docs: msg.data }));\n break;\n case 'data':\n // Legacy full-snapshot delta — treat as snapshot replacement\n this.handleLegacyData(msg);\n break;\n case 'response':\n this.handleResult(Object.assign(Object.assign({}, msg), { type: 'result', ok: msg.status === 200, doc: msg.data }));\n break;\n }\n }\n handleSnapshot(msg) {\n var _a, _b, _c;\n const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;\n if (!subId)\n return;\n const sub = this.findSubscriptionById(subId);\n if (!sub)\n return;\n const docs = (_c = (_b = msg.docs) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : [];\n const docsArray = Array.isArray(docs) ? docs : [docs];\n sub.docs.clear();\n for (const doc of docsArray) {\n if (doc && doc._id) {\n sub.docs.set(doc._id, doc);\n }\n }\n sub.ref.current = sub.docs;\n sub.status = 'live';\n sub.isStale = false;\n sub.error = null;\n this.notifySubscription(sub);\n this.markIdbDirty(sub.path);\n }\n handleDelta(msg) {\n var _a, _b;\n const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;\n if (!subId)\n return;\n const sub = this.findSubscriptionById(subId);\n if (!sub)\n return;\n if (sub.tier === 'ephemeral') {\n // Ephemeral: just overwrite, no accumulation logic\n if (msg.change === 'removed' && msg.docId) {\n sub.docs.delete(msg.docId);\n }\n else if (msg.doc && msg.doc._id) {\n sub.docs.set(msg.doc._id, msg.doc);\n }\n sub.ref.current = sub.docs;\n if (sub.options.mode !== 'ref') {\n this.notifySubscription(sub);\n }\n return;\n }\n // Durable/checkpointed: full delta handling\n switch (msg.change) {\n case 'added':\n case 'modified':\n if (msg.doc && msg.doc._id) {\n sub.docs.set(msg.doc._id, msg.doc);\n }\n break;\n case 'removed':\n if (msg.docId) {\n sub.docs.delete(msg.docId);\n }\n else if ((_b = msg.doc) === null || _b === void 0 ? void 0 : _b._id) {\n sub.docs.delete(msg.doc._id);\n }\n break;\n }\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n this.markIdbDirty(sub.path);\n }\n handleLegacyData(msg) {\n // Legacy v1 format: 'data' message with full snapshot or single doc\n const subId = msg.subscriptionId;\n if (!subId)\n return;\n const sub = this.findSubscriptionById(subId);\n if (!sub)\n return;\n if (Array.isArray(msg.data)) {\n // Full snapshot replacement\n sub.docs.clear();\n for (const doc of msg.data) {\n if (doc && doc._id)\n sub.docs.set(doc._id, doc);\n }\n }\n else if (msg.data && msg.data._id) {\n // Single doc update\n sub.docs.set(msg.data._id, msg.data);\n }\n else if (msg.data === null) {\n // Removal — but we don't know which doc. Re-fetch needed.\n }\n sub.ref.current = sub.docs;\n sub.status = 'live';\n sub.isStale = false;\n this.notifySubscription(sub);\n this.markIdbDirty(sub.path);\n }\n handleResult(msg) {\n var _a, _b, _c, _d;\n const requestId = msg.requestId;\n if (!requestId)\n return;\n const pending = this.pendingRequests.get(requestId);\n if (!pending)\n return;\n this.pendingRequests.delete(requestId);\n clearTimeout(pending.timeout);\n const ok = (_a = msg.ok) !== null && _a !== void 0 ? _a : (msg.status === 200);\n if (ok) {\n pending.resolve((_c = (_b = msg.doc) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : true);\n }\n else {\n pending.reject(new Error((_d = msg.error) !== null && _d !== void 0 ? _d : 'Operation failed'));\n }\n }\n handleError(msg) {\n var _a, _b, _c;\n const error = new Error((_a = msg.message) !== null && _a !== void 0 ? _a : (msg.code ? `${msg.code}: Server error` : 'Server error'));\n if (msg.code)\n error.code = msg.code;\n if (msg.subscriptionId || msg.id)\n error.subscriptionId = (_b = msg.subscriptionId) !== null && _b !== void 0 ? _b : msg.id;\n const requestId = msg.requestId;\n if (requestId) {\n const pending = this.pendingRequests.get(requestId);\n if (pending) {\n this.pendingRequests.delete(requestId);\n clearTimeout(pending.timeout);\n pending.reject(error);\n }\n }\n const subId = (_c = msg.subscriptionId) !== null && _c !== void 0 ? _c : msg.id;\n if (subId) {\n const sub = this.findSubscriptionById(subId);\n if (sub) {\n sub.status = 'error';\n sub.error = error;\n this.notifyState(sub);\n for (const callback of Array.from(sub.errorCallbacks)) {\n try {\n callback(error);\n }\n catch ( /* swallow */_d) { /* swallow */ }\n }\n }\n }\n }\n // -----------------------------------------------------------------------\n // Subscribe\n // -----------------------------------------------------------------------\n async subscribe(path, opts = {}) {\n var _a;\n await this.ensureCurrentAuth();\n const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';\n const subKey = this.getSubKey(path, opts);\n let sub = this.subscriptions.get(subKey);\n if (sub) {\n // Existing subscription — add callback\n if (opts.onData)\n sub.callbacks.add(opts.onData);\n if (opts.onState)\n sub.stateCallbacks.add(opts.onState);\n if (opts.onError)\n sub.errorCallbacks.add(opts.onError);\n if (opts.cache && !sub.options.cache) {\n sub.options = Object.assign(Object.assign({}, sub.options), { cache: true });\n }\n // Immediately deliver current state\n if (opts.onData && sub.docs.size > 0) {\n opts.onData(this.docsToArray(sub));\n }\n if (opts.onState) {\n opts.onState(this.getState(sub));\n }\n return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);\n }\n // New subscription\n const subId = `sub_${nextRequestId++}`;\n sub = {\n id: subId,\n path,\n tier,\n options: opts,\n docs: new Map(),\n status: 'idle',\n isStale: false,\n error: null,\n callbacks: new Set(opts.onData ? [opts.onData] : []),\n stateCallbacks: new Set(opts.onState ? [opts.onState] : []),\n errorCallbacks: new Set(opts.onError ? [opts.onError] : []),\n ref: { current: new Map() },\n };\n this.subscriptions.set(subKey, sub);\n // Step 1: Load from IDB only when explicitly enabled and principal-bound.\n if (this.shouldUseIdb(tier, opts)) {\n const cached = await idbGet(this.idbKey(path));\n if (cached && Array.isArray(cached)) {\n for (const doc of cached) {\n if (doc && doc._id)\n sub.docs.set(doc._id, doc);\n }\n sub.ref.current = sub.docs;\n sub.status = 'cached';\n sub.isStale = true;\n this.notifySubscription(sub);\n }\n }\n // Step 2: Connect and subscribe via WS\n sub.status = sub.docs.size > 0 ? 'cached' : 'loading';\n this.notifyState(sub);\n try {\n await this.ensureConnected();\n this.sendSubscribe(sub);\n }\n catch (_b) {\n sub.status = 'error';\n sub.error = new Error('Connection failed');\n this.notifyState(sub);\n }\n return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);\n }\n getRef(path, opts = {}) {\n var _a;\n const subKey = this.getSubKey(path, opts);\n const sub = this.subscriptions.get(subKey);\n if (sub)\n return sub.ref;\n // Auto-subscribe in ref mode\n const ref = { current: new Map() };\n this.subscribe(path, Object.assign(Object.assign({}, opts), { mode: 'ref', tier: 'ephemeral' })).catch(() => { });\n const newSub = this.subscriptions.get(this.getSubKey(path, Object.assign(Object.assign({}, opts), { tier: 'ephemeral' })));\n return (_a = newSub === null || newSub === void 0 ? void 0 : newSub.ref) !== null && _a !== void 0 ? _a : ref;\n }\n // -----------------------------------------------------------------------\n // CRUD operations\n // -----------------------------------------------------------------------\n async set(path, doc) {\n var _a;\n await this.ensureConnected();\n // Resolve operations (Increment, Time.Now) client-side for optimistic update\n const resolvedDoc = this.resolveOperations(doc, path);\n // Optimistic update: apply to local state immediately\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n const collectionPath = this.getCollectionPath(normalizedPath);\n const optimisticDoc = Object.assign(Object.assign({ _id: normalizedPath, pathId: normalizedPath }, resolvedDoc), { \n // System timestamp field name: the Bounded worker stamps the neutral\n // `_updatedAt`; the underscore-prefixed `_updated_at` metadata mirror.\n // Match it so the optimistic doc lines up with the server's confirmation.\n [isBoundedNetwork() ? '_updatedAt' : '_updated_at']: Date.now() });\n const sub = this.findSubscriptionByPath(collectionPath);\n let prevDoc = null;\n if (sub) {\n prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;\n sub.docs.set(normalizedPath, optimisticDoc);\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n }\n // Send to server\n const requestId = `r_${nextRequestId++}`;\n try {\n const result = await this.sendRequest(requestId, {\n type: 'set',\n requestId,\n documents: [{ destinationPath: normalizedPath, document: doc }],\n });\n // Replace optimistic doc with server-confirmed version\n if (sub && result && typeof result === 'object') {\n const serverDoc = Array.isArray(result) ? result[0] : result;\n if (serverDoc && serverDoc._id) {\n sub.docs.set(serverDoc._id, serverDoc);\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n this.markIdbDirty(collectionPath);\n }\n }\n return Array.isArray(result) ? result[0] : result;\n }\n catch (err) {\n // Revert optimistic update\n if (sub) {\n if (prevDoc) {\n sub.docs.set(normalizedPath, prevDoc);\n }\n else {\n sub.docs.delete(normalizedPath);\n }\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n }\n throw err;\n }\n }\n async get(path) {\n await this.ensureCurrentAuth();\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n // Check local subscriptions first\n const collectionPath = this.getCollectionPath(normalizedPath);\n const sub = this.findSubscriptionByPath(collectionPath);\n if (sub && sub.status === 'live') {\n const doc = sub.docs.get(normalizedPath);\n return doc !== null && doc !== void 0 ? doc : null;\n }\n // One-shot WS fetch\n await this.ensureConnected();\n const requestId = `r_${nextRequestId++}`;\n return this.sendRequest(requestId, {\n type: 'get',\n requestId,\n path: normalizedPath,\n });\n }\n async getMany(paths) {\n await this.ensureConnected();\n const normalizedPaths = paths.map(p => p.startsWith('/') ? p.slice(1) : p);\n const requestId = `r_${nextRequestId++}`;\n return this.sendRequest(requestId, {\n type: 'getMany',\n requestId,\n paths: normalizedPaths,\n });\n }\n async delete(path) {\n var _a;\n await this.ensureConnected();\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n // Optimistic: remove from local state\n const collectionPath = this.getCollectionPath(normalizedPath);\n const sub = this.findSubscriptionByPath(collectionPath);\n let prevDoc = null;\n if (sub) {\n prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;\n sub.docs.delete(normalizedPath);\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n }\n const requestId = `r_${nextRequestId++}`;\n try {\n await this.sendRequest(requestId, {\n type: 'delete',\n requestId,\n path: normalizedPath,\n });\n if (sub)\n this.markIdbDirty(collectionPath);\n }\n catch (err) {\n // Revert\n if (sub && prevDoc) {\n sub.docs.set(normalizedPath, prevDoc);\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n }\n throw err;\n }\n }\n async query(path, opts) {\n await this.ensureConnected();\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n const requestId = `r_${nextRequestId++}`;\n return this.sendRequest(requestId, Object.assign(Object.assign(Object.assign(Object.assign({ type: 'query', requestId, path: normalizedPath }, ((opts === null || opts === void 0 ? void 0 : opts.filter) ? { filter: opts.filter } : {})), ((opts === null || opts === void 0 ? void 0 : opts.sort) ? { sort: opts.sort } : {})), ((opts === null || opts === void 0 ? void 0 : opts.limit) !== undefined ? { limit: opts.limit } : {})), ((opts === null || opts === void 0 ? void 0 : opts.includeSubPaths) ? { includeSubPaths: true } : {})));\n }\n async count(path) {\n var _a;\n await this.ensureConnected();\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n const requestId = `r_${nextRequestId++}`;\n const result = await this.sendRequest(requestId, {\n type: 'count',\n requestId,\n path: normalizedPath,\n });\n return typeof result === 'number' ? result : ((_a = result === null || result === void 0 ? void 0 : result.value) !== null && _a !== void 0 ? _a : 0);\n }\n async aggregate(path, operation, opts) {\n await this.ensureConnected();\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n const requestId = `r_${nextRequestId++}`;\n return this.sendRequest(requestId, Object.assign({ type: 'aggregate', requestId, path: normalizedPath, operation }, ((opts === null || opts === void 0 ? void 0 : opts.field) ? { field: opts.field } : {})));\n }\n // -----------------------------------------------------------------------\n // Helpers\n // -----------------------------------------------------------------------\n sendSubscribe(sub) {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN)\n return;\n const msg = {\n type: 'subscribe',\n subscriptionId: sub.id,\n path: sub.path,\n };\n if (sub.options.filter)\n msg.filter = sub.options.filter;\n if (sub.options.includeSubPaths)\n msg.includeSubPaths = true;\n if (sub.options.limit)\n msg.limit = sub.options.limit;\n if (sub.options.prompt)\n msg.prompt = sub.options.prompt;\n this.ws.send(JSON.stringify(msg));\n }\n sendRequest(requestId, msg) {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n this.pendingRequests.delete(requestId);\n reject(new Error('Request timed out'));\n }, 30000);\n this.pendingRequests.set(requestId, { resolve, reject, timeout });\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n else {\n this.pendingRequests.delete(requestId);\n clearTimeout(timeout);\n reject(new Error('WebSocket not connected'));\n }\n });\n }\n notifySubscription(sub) {\n const data = this.docsToArray(sub);\n const callbacks = Array.from(sub.callbacks);\n for (const cb of callbacks) {\n try {\n cb(data);\n }\n catch ( /* swallow callback errors */_a) { /* swallow callback errors */ }\n }\n this.notifyState(sub);\n }\n notifyState(sub) {\n const state = this.getState(sub);\n const callbacks = Array.from(sub.stateCallbacks);\n for (const cb of callbacks) {\n try {\n cb(state);\n }\n catch ( /* swallow */_a) { /* swallow */ }\n }\n }\n getState(sub) {\n return {\n data: this.docsToArray(sub),\n status: sub.status,\n isStale: sub.isStale,\n error: sub.error,\n };\n }\n docsToArray(sub) {\n return Array.from(sub.docs.values());\n }\n findSubscriptionById(id) {\n for (const sub of this.subscriptions.values()) {\n if (sub.id === id)\n return sub;\n }\n return undefined;\n }\n findSubscriptionByPath(collectionPath) {\n for (const sub of this.subscriptions.values()) {\n const subPath = sub.path.startsWith('/') ? sub.path.slice(1) : sub.path;\n if (subPath === collectionPath)\n return sub;\n if (collectionPath.startsWith(subPath + '/'))\n return sub;\n }\n return undefined;\n }\n getCollectionPath(docPath) {\n const segments = docPath.split('/');\n if (segments.length % 2 === 0) {\n return segments.slice(0, -1).join('/');\n }\n return docPath;\n }\n getSubKey(path, opts) {\n const parts = [this.appId, this.authPrincipalKey, path];\n if (opts.filter)\n parts.push(JSON.stringify(opts.filter));\n if (opts.prompt)\n parts.push(opts.prompt);\n if (opts.tier)\n parts.push(opts.tier);\n return parts.join('::');\n }\n idbKey(path) {\n return `${this.appId}:${this.authPrincipalKey}:${path}`;\n }\n shouldUseIdb(tier, opts) {\n return opts.cache === true && tier !== 'ephemeral' && this.authPrincipalKey !== 'anon';\n }\n markIdbDirty(path) {\n const sub = this.findSubscriptionByPath(path);\n if (!sub || !this.shouldUseIdb(sub.tier, sub.options))\n return;\n this.idbDirtyKeys.add(path);\n if (!this.idbFlushTimer) {\n this.idbFlushTimer = setTimeout(() => {\n this.flushIdb();\n this.idbFlushTimer = null;\n }, 500);\n }\n }\n async flushIdb() {\n const keys = Array.from(this.idbDirtyKeys);\n this.idbDirtyKeys.clear();\n for (const path of keys) {\n const sub = this.findSubscriptionByPath(path);\n if (sub && this.shouldUseIdb(sub.tier, sub.options)) {\n const docs = this.docsToArray(sub);\n await idbSet(this.idbKey(path), docs);\n }\n }\n }\n createUnsubscribe(subKey, subId, onData, onState, onError) {\n return async () => {\n var _a;\n const sub = (_a = this.subscriptions.get(subKey)) !== null && _a !== void 0 ? _a : this.findSubscriptionById(subId);\n if (!sub)\n return;\n const currentSubKey = this.getSubKey(sub.path, sub.options);\n if (onData)\n sub.callbacks.delete(onData);\n if (onState)\n sub.stateCallbacks.delete(onState);\n if (onError)\n sub.errorCallbacks.delete(onError);\n // If no more callbacks, unsubscribe entirely\n if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {\n this.subscriptions.delete(subKey);\n this.subscriptions.delete(currentSubKey);\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify({\n type: 'unsubscribe',\n subscriptionId: sub.id,\n }));\n }\n }\n };\n }\n resolveOperations(doc, path) {\n var _a;\n if (!doc || typeof doc !== 'object')\n return doc;\n const resolved = {};\n for (const [key, value] of Object.entries(doc)) {\n if (value && typeof value === 'object' && !Array.isArray(value) && value.operation) {\n const op = value;\n if (op.operation === 'time' && op.value === 'now') {\n resolved[key] = Math.floor(Date.now() / 1000);\n }\n else if (op.operation === 'increment') {\n // For optimistic: get current value and add\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n const collectionPath = this.getCollectionPath(normalizedPath);\n const sub = this.findSubscriptionByPath(collectionPath);\n const existing = sub === null || sub === void 0 ? void 0 : sub.docs.get(normalizedPath);\n const current = (_a = existing === null || existing === void 0 ? void 0 : existing[key]) !== null && _a !== void 0 ? _a : 0;\n resolved[key] = (typeof current === 'number' ? current : 0) + op.value;\n }\n else {\n resolved[key] = value;\n }\n }\n else {\n resolved[key] = value;\n }\n }\n return resolved;\n }\n rejectAllPending(reason) {\n for (const [requestId, pending] of this.pendingRequests) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(reason));\n }\n this.pendingRequests.clear();\n }\n setAllSubscriptionStatus(status) {\n for (const sub of this.subscriptions.values()) {\n sub.status = status;\n this.notifyState(sub);\n }\n }\n // -----------------------------------------------------------------------\n // Lifecycle\n // -----------------------------------------------------------------------\n close() {\n this.closed = true;\n if (this.reconnectTimer)\n clearTimeout(this.reconnectTimer);\n if (this.idbFlushTimer)\n clearTimeout(this.idbFlushTimer);\n if (this.tokenRefreshTimer)\n clearInterval(this.tokenRefreshTimer);\n this.flushIdb();\n if (this.ws) {\n this.ws.close(1000, 'Store closed');\n this.ws = null;\n }\n this.rejectAllPending('Store closed');\n this.subscriptions.clear();\n }\n async reconnectWithNewAuth() {\n if (this.closed)\n return;\n await this.ensureInitialized();\n await this.refreshToken();\n await this.applyAuthPrincipalChange();\n if (this.subscriptions.size > 0) {\n await this.ensureConnected().catch((error) => {\n this.setAllSubscriptionStatus('error');\n for (const sub of this.subscriptions.values()) {\n sub.error = error instanceof Error ? error : new Error(String(error));\n this.notifyState(sub);\n }\n });\n }\n }\n}\n// ---------------------------------------------------------------------------\n// Singleton instance\n// ---------------------------------------------------------------------------\nlet storeInstance = null;\nexport function getRealtimeStore() {\n if (!storeInstance) {\n storeInstance = new RealtimeStore();\n }\n return storeInstance;\n}\nexport function resetRealtimeStore() {\n if (storeInstance) {\n storeInstance.close();\n storeInstance = null;\n }\n}\nexport async function reconnectRealtimeStoreWithNewAuth() {\n if (storeInstance) {\n await storeInstance.reconnectWithNewAuth();\n }\n}\n"],"names":[],"mappings":";;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AA++BO,eAAe,iCAAiC,GAAG;AAI1D;;;;"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import './index.mjs';
|
|
2
|
+
import 'axios';
|
|
3
|
+
import 'tweetnacl';
|
|
4
|
+
import '@solana/web3.js';
|
|
5
|
+
import '@coral-xyz/anchor';
|
|
6
|
+
import 'bn.js';
|
|
7
|
+
import 'reconnecting-websocket';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// realtime-store.ts — Client-side state manager for realtime apps.
|
|
11
|
+
//
|
|
12
|
+
// Manages: WS connection, in-memory state, IDB persistence, optimistic
|
|
13
|
+
// writes, delta accumulation, loading states, ephemeral/durable tiers.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
async function reconnectRealtimeStoreWithNewAuth() {
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { reconnectRealtimeStoreWithNewAuth };
|
|
19
|
+
//# sourceMappingURL=realtime-store-D3t7PyZl.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-store-D3t7PyZl.mjs","sources":["../.rollup-tmp/client/realtime-store.js"],"sourcesContent":["// ---------------------------------------------------------------------------\n// realtime-store.ts — Client-side state manager for realtime apps.\n//\n// Manages: WS connection, in-memory state, IDB persistence, optimistic\n// writes, delta accumulation, loading states, ephemeral/durable tiers.\n// ---------------------------------------------------------------------------\nimport { getConfig, isBoundedNetwork } from './config';\n// ---------------------------------------------------------------------------\n// IDB helpers (lazy-loaded, non-blocking)\n// ---------------------------------------------------------------------------\nconst IDB_NAME = 'bounded-realtime';\nconst IDB_STORE = 'subscriptions';\nconst IDB_VERSION = 1;\nlet idbPromise = null;\nfunction getIDB() {\n if (idbPromise)\n return idbPromise;\n if (typeof indexedDB === 'undefined') {\n return Promise.reject(new Error('IndexedDB not available'));\n }\n idbPromise = new Promise((resolve, reject) => {\n const req = indexedDB.open(IDB_NAME, IDB_VERSION);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(IDB_STORE)) {\n db.createObjectStore(IDB_STORE);\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n return idbPromise;\n}\nasync function idbGet(key) {\n try {\n const db = await getIDB();\n return new Promise((resolve) => {\n const tx = db.transaction(IDB_STORE, 'readonly');\n const store = tx.objectStore(IDB_STORE);\n const req = store.get(key);\n req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };\n req.onerror = () => resolve(null);\n });\n }\n catch (_a) {\n return null;\n }\n}\nasync function idbSet(key, value) {\n try {\n const db = await getIDB();\n return new Promise((resolve) => {\n const tx = db.transaction(IDB_STORE, 'readwrite');\n const store = tx.objectStore(IDB_STORE);\n store.put(value, key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => resolve();\n });\n }\n catch (_a) {\n // Best-effort persistence\n }\n}\nasync function idbDelete(key) {\n try {\n const db = await getIDB();\n return new Promise((resolve) => {\n const tx = db.transaction(IDB_STORE, 'readwrite');\n const store = tx.objectStore(IDB_STORE);\n store.delete(key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => resolve();\n });\n }\n catch (_a) {\n // Best-effort\n }\n}\n// ---------------------------------------------------------------------------\n// RealtimeStore\n// ---------------------------------------------------------------------------\nlet nextRequestId = 1;\nfunction hashForKey(value) {\n let h = 5381;\n for (let i = 0; i < value.length; i++) {\n h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;\n }\n return h.toString(36);\n}\nfunction principalFromToken(token) {\n return token ? `t${hashForKey(token)}` : 'anon';\n}\nexport class RealtimeStore {\n constructor() {\n this.ws = null;\n this.wsUrl = '';\n this.appId = '';\n this.subscriptions = new Map();\n this.pendingRequests = new Map();\n this.connectPromise = null;\n this.reconnectTimer = null;\n this.reconnectDelay = 1000;\n this.maxReconnectDelay = 30000;\n this.idbFlushTimer = null;\n this.idbDirtyKeys = new Set();\n this.closed = false;\n this.authToken = null;\n this.authPrincipalKey = 'anon';\n this.authenticating = false;\n this.suppressNextReconnect = false;\n this.isServer = false;\n this.tokenRefreshTimer = null;\n // -----------------------------------------------------------------------\n // WebSocket connection\n // -----------------------------------------------------------------------\n this.initPromise = null;\n }\n // -----------------------------------------------------------------------\n // Initialization\n // -----------------------------------------------------------------------\n async init() {\n const config = await getConfig();\n this.appId = config.appId;\n this.wsUrl = config.wsApiUrl;\n this.isServer = config.isServer;\n await this.refreshToken();\n this.startTokenRefresh();\n }\n async refreshToken() {\n let token = null;\n try {\n const { getIdToken } = await import('../utils/utils');\n token = await getIdToken(this.isServer);\n }\n catch ( /* no auth available */_a) { /* no auth available */ }\n this.authToken = token !== null && token !== void 0 ? token : null;\n this.authPrincipalKey = principalFromToken(this.authToken);\n }\n startTokenRefresh() {\n if (this.tokenRefreshTimer)\n return;\n this.tokenRefreshTimer = setInterval(async () => {\n const prevPrincipal = this.authPrincipalKey;\n await this.refreshToken();\n if (this.authPrincipalKey !== prevPrincipal) {\n await this.applyAuthPrincipalChange();\n if (this.subscriptions.size > 0) {\n await this.ensureConnected().catch(() => {\n this.setAllSubscriptionStatus('error');\n });\n }\n }\n }, 5 * 60 * 1000); // Check every 5 minutes\n }\n async ensureInitialized() {\n if (this.appId)\n return;\n if (!this.initPromise)\n this.initPromise = this.init();\n await this.initPromise;\n }\n async ensureCurrentAuth() {\n await this.ensureInitialized();\n const prevPrincipal = this.authPrincipalKey;\n await this.refreshToken();\n if (this.authPrincipalKey !== prevPrincipal) {\n await this.applyAuthPrincipalChange();\n }\n }\n rekeySubscriptionsForPrincipal() {\n const subs = Array.from(this.subscriptions.values());\n this.subscriptions.clear();\n for (const sub of subs) {\n this.subscriptions.set(this.getSubKey(sub.path, sub.options), sub);\n }\n }\n async applyAuthPrincipalChange() {\n if (this.idbFlushTimer) {\n clearTimeout(this.idbFlushTimer);\n this.idbFlushTimer = null;\n }\n this.idbDirtyKeys.clear();\n this.rekeySubscriptionsForPrincipal();\n for (const sub of this.subscriptions.values()) {\n sub.docs.clear();\n sub.ref.current = sub.docs;\n sub.error = null;\n sub.isStale = false;\n let loaded = false;\n if (this.shouldUseIdb(sub.tier, sub.options)) {\n const cached = await idbGet(this.idbKey(sub.path));\n if (cached && Array.isArray(cached)) {\n for (const doc of cached) {\n if (doc && doc._id)\n sub.docs.set(doc._id, doc);\n }\n sub.ref.current = sub.docs;\n loaded = sub.docs.size > 0;\n }\n }\n sub.status = loaded ? 'cached' : 'loading';\n sub.isStale = loaded;\n if (loaded)\n this.notifySubscription(sub);\n else\n this.notifyState(sub);\n }\n if (this.ws) {\n const ws = this.ws;\n this.ws = null;\n this.connectPromise = null;\n this.suppressNextReconnect = true;\n try {\n ws.close(1000, 'Auth changed');\n }\n catch ( /* ignore */_a) { /* ignore */ }\n }\n }\n async ensureConnected() {\n var _a;\n await this.ensureCurrentAuth();\n if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)\n return;\n if (this.connectPromise)\n return this.connectPromise;\n this.connectPromise = this.connect();\n return this.connectPromise;\n }\n connect() {\n return new Promise((resolve, reject) => {\n if (this.closed) {\n reject(new Error('Store closed'));\n return;\n }\n const params = new URLSearchParams();\n params.set('appId', this.appId);\n const url = `${this.wsUrl}?${params.toString()}`;\n const ws = new WebSocket(url);\n this.ws = ws;\n let authTimer = null;\n const finishConnected = () => {\n if (authTimer) {\n clearTimeout(authTimer);\n authTimer = null;\n }\n this.authenticating = false;\n ws.removeEventListener('error', onError);\n this.reconnectDelay = 1000;\n this.connectPromise = null;\n this.resubscribeAll();\n resolve();\n };\n const onOpen = () => {\n if (!this.authToken) {\n finishConnected();\n return;\n }\n this.authenticating = true;\n authTimer = setTimeout(() => {\n this.authenticating = false;\n this.connectPromise = null;\n try {\n ws.close(1008, 'Authentication timeout');\n }\n catch ( /* ignore */_a) { /* ignore */ }\n reject(new Error('WebSocket authentication timeout'));\n }, 10000);\n try {\n ws.send(JSON.stringify({ type: 'auth', token: this.authToken }));\n }\n catch (e) {\n if (authTimer)\n clearTimeout(authTimer);\n this.authenticating = false;\n this.connectPromise = null;\n reject(e);\n }\n };\n const onError = (e) => {\n if (authTimer)\n clearTimeout(authTimer);\n this.authenticating = false;\n ws.removeEventListener('open', onOpen);\n this.connectPromise = null;\n reject(new Error('WebSocket connection failed'));\n };\n ws.addEventListener('open', onOpen, { once: true });\n ws.addEventListener('error', onError, { once: true });\n ws.addEventListener('message', (event) => {\n if (this.authenticating) {\n try {\n const msg = JSON.parse(typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data));\n if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'authenticated') {\n finishConnected();\n return;\n }\n }\n catch ( /* fall through to normal handling */_a) { /* fall through to normal handling */ }\n }\n this.handleMessage(event.data);\n });\n ws.addEventListener('close', () => {\n if (authTimer)\n clearTimeout(authTimer);\n if (this.ws !== ws) {\n if (this.suppressNextReconnect)\n this.suppressNextReconnect = false;\n return;\n }\n this.authenticating = false;\n this.ws = null;\n this.connectPromise = null;\n this.rejectAllPending('WebSocket closed');\n this.setAllSubscriptionStatus('reconnecting');\n if (this.suppressNextReconnect) {\n this.suppressNextReconnect = false;\n return;\n }\n this.scheduleReconnect();\n });\n });\n }\n scheduleReconnect() {\n if (this.closed)\n return;\n if (this.reconnectTimer)\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = setTimeout(() => {\n this.ensureConnected().catch(() => {\n this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);\n this.scheduleReconnect();\n });\n }, this.reconnectDelay);\n }\n resubscribeAll() {\n for (const sub of this.subscriptions.values()) {\n this.sendSubscribe(sub);\n }\n }\n // -----------------------------------------------------------------------\n // Message handling\n // -----------------------------------------------------------------------\n handleMessage(raw) {\n const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);\n let msg;\n try {\n msg = JSON.parse(text);\n }\n catch (_a) {\n return;\n }\n switch (msg.type) {\n case 'snapshot':\n this.handleSnapshot(msg);\n break;\n case 'delta':\n this.handleDelta(msg);\n break;\n case 'result':\n this.handleResult(msg);\n break;\n case 'error':\n this.handleError(msg);\n break;\n case 'pong':\n break;\n case 'authenticated':\n break;\n // v1 compat: handle legacy message types during transition\n case 'subscribed':\n this.handleSnapshot(Object.assign(Object.assign({}, msg), { type: 'snapshot', docs: msg.data }));\n break;\n case 'data':\n // Legacy full-snapshot delta — treat as snapshot replacement\n this.handleLegacyData(msg);\n break;\n case 'response':\n this.handleResult(Object.assign(Object.assign({}, msg), { type: 'result', ok: msg.status === 200, doc: msg.data }));\n break;\n }\n }\n handleSnapshot(msg) {\n var _a, _b, _c;\n const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;\n if (!subId)\n return;\n const sub = this.findSubscriptionById(subId);\n if (!sub)\n return;\n const docs = (_c = (_b = msg.docs) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : [];\n const docsArray = Array.isArray(docs) ? docs : [docs];\n sub.docs.clear();\n for (const doc of docsArray) {\n if (doc && doc._id) {\n sub.docs.set(doc._id, doc);\n }\n }\n sub.ref.current = sub.docs;\n sub.status = 'live';\n sub.isStale = false;\n sub.error = null;\n this.notifySubscription(sub);\n this.markIdbDirty(sub.path);\n }\n handleDelta(msg) {\n var _a, _b;\n const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;\n if (!subId)\n return;\n const sub = this.findSubscriptionById(subId);\n if (!sub)\n return;\n if (sub.tier === 'ephemeral') {\n // Ephemeral: just overwrite, no accumulation logic\n if (msg.change === 'removed' && msg.docId) {\n sub.docs.delete(msg.docId);\n }\n else if (msg.doc && msg.doc._id) {\n sub.docs.set(msg.doc._id, msg.doc);\n }\n sub.ref.current = sub.docs;\n if (sub.options.mode !== 'ref') {\n this.notifySubscription(sub);\n }\n return;\n }\n // Durable/checkpointed: full delta handling\n switch (msg.change) {\n case 'added':\n case 'modified':\n if (msg.doc && msg.doc._id) {\n sub.docs.set(msg.doc._id, msg.doc);\n }\n break;\n case 'removed':\n if (msg.docId) {\n sub.docs.delete(msg.docId);\n }\n else if ((_b = msg.doc) === null || _b === void 0 ? void 0 : _b._id) {\n sub.docs.delete(msg.doc._id);\n }\n break;\n }\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n this.markIdbDirty(sub.path);\n }\n handleLegacyData(msg) {\n // Legacy v1 format: 'data' message with full snapshot or single doc\n const subId = msg.subscriptionId;\n if (!subId)\n return;\n const sub = this.findSubscriptionById(subId);\n if (!sub)\n return;\n if (Array.isArray(msg.data)) {\n // Full snapshot replacement\n sub.docs.clear();\n for (const doc of msg.data) {\n if (doc && doc._id)\n sub.docs.set(doc._id, doc);\n }\n }\n else if (msg.data && msg.data._id) {\n // Single doc update\n sub.docs.set(msg.data._id, msg.data);\n }\n else if (msg.data === null) {\n // Removal — but we don't know which doc. Re-fetch needed.\n }\n sub.ref.current = sub.docs;\n sub.status = 'live';\n sub.isStale = false;\n this.notifySubscription(sub);\n this.markIdbDirty(sub.path);\n }\n handleResult(msg) {\n var _a, _b, _c, _d;\n const requestId = msg.requestId;\n if (!requestId)\n return;\n const pending = this.pendingRequests.get(requestId);\n if (!pending)\n return;\n this.pendingRequests.delete(requestId);\n clearTimeout(pending.timeout);\n const ok = (_a = msg.ok) !== null && _a !== void 0 ? _a : (msg.status === 200);\n if (ok) {\n pending.resolve((_c = (_b = msg.doc) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : true);\n }\n else {\n pending.reject(new Error((_d = msg.error) !== null && _d !== void 0 ? _d : 'Operation failed'));\n }\n }\n handleError(msg) {\n var _a, _b, _c;\n const error = new Error((_a = msg.message) !== null && _a !== void 0 ? _a : (msg.code ? `${msg.code}: Server error` : 'Server error'));\n if (msg.code)\n error.code = msg.code;\n if (msg.subscriptionId || msg.id)\n error.subscriptionId = (_b = msg.subscriptionId) !== null && _b !== void 0 ? _b : msg.id;\n const requestId = msg.requestId;\n if (requestId) {\n const pending = this.pendingRequests.get(requestId);\n if (pending) {\n this.pendingRequests.delete(requestId);\n clearTimeout(pending.timeout);\n pending.reject(error);\n }\n }\n const subId = (_c = msg.subscriptionId) !== null && _c !== void 0 ? _c : msg.id;\n if (subId) {\n const sub = this.findSubscriptionById(subId);\n if (sub) {\n sub.status = 'error';\n sub.error = error;\n this.notifyState(sub);\n for (const callback of Array.from(sub.errorCallbacks)) {\n try {\n callback(error);\n }\n catch ( /* swallow */_d) { /* swallow */ }\n }\n }\n }\n }\n // -----------------------------------------------------------------------\n // Subscribe\n // -----------------------------------------------------------------------\n async subscribe(path, opts = {}) {\n var _a;\n await this.ensureCurrentAuth();\n const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';\n const subKey = this.getSubKey(path, opts);\n let sub = this.subscriptions.get(subKey);\n if (sub) {\n // Existing subscription — add callback\n if (opts.onData)\n sub.callbacks.add(opts.onData);\n if (opts.onState)\n sub.stateCallbacks.add(opts.onState);\n if (opts.onError)\n sub.errorCallbacks.add(opts.onError);\n if (opts.cache && !sub.options.cache) {\n sub.options = Object.assign(Object.assign({}, sub.options), { cache: true });\n }\n // Immediately deliver current state\n if (opts.onData && sub.docs.size > 0) {\n opts.onData(this.docsToArray(sub));\n }\n if (opts.onState) {\n opts.onState(this.getState(sub));\n }\n return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);\n }\n // New subscription\n const subId = `sub_${nextRequestId++}`;\n sub = {\n id: subId,\n path,\n tier,\n options: opts,\n docs: new Map(),\n status: 'idle',\n isStale: false,\n error: null,\n callbacks: new Set(opts.onData ? [opts.onData] : []),\n stateCallbacks: new Set(opts.onState ? [opts.onState] : []),\n errorCallbacks: new Set(opts.onError ? [opts.onError] : []),\n ref: { current: new Map() },\n };\n this.subscriptions.set(subKey, sub);\n // Step 1: Load from IDB only when explicitly enabled and principal-bound.\n if (this.shouldUseIdb(tier, opts)) {\n const cached = await idbGet(this.idbKey(path));\n if (cached && Array.isArray(cached)) {\n for (const doc of cached) {\n if (doc && doc._id)\n sub.docs.set(doc._id, doc);\n }\n sub.ref.current = sub.docs;\n sub.status = 'cached';\n sub.isStale = true;\n this.notifySubscription(sub);\n }\n }\n // Step 2: Connect and subscribe via WS\n sub.status = sub.docs.size > 0 ? 'cached' : 'loading';\n this.notifyState(sub);\n try {\n await this.ensureConnected();\n this.sendSubscribe(sub);\n }\n catch (_b) {\n sub.status = 'error';\n sub.error = new Error('Connection failed');\n this.notifyState(sub);\n }\n return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);\n }\n getRef(path, opts = {}) {\n var _a;\n const subKey = this.getSubKey(path, opts);\n const sub = this.subscriptions.get(subKey);\n if (sub)\n return sub.ref;\n // Auto-subscribe in ref mode\n const ref = { current: new Map() };\n this.subscribe(path, Object.assign(Object.assign({}, opts), { mode: 'ref', tier: 'ephemeral' })).catch(() => { });\n const newSub = this.subscriptions.get(this.getSubKey(path, Object.assign(Object.assign({}, opts), { tier: 'ephemeral' })));\n return (_a = newSub === null || newSub === void 0 ? void 0 : newSub.ref) !== null && _a !== void 0 ? _a : ref;\n }\n // -----------------------------------------------------------------------\n // CRUD operations\n // -----------------------------------------------------------------------\n async set(path, doc) {\n var _a;\n await this.ensureConnected();\n // Resolve operations (Increment, Time.Now) client-side for optimistic update\n const resolvedDoc = this.resolveOperations(doc, path);\n // Optimistic update: apply to local state immediately\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n const collectionPath = this.getCollectionPath(normalizedPath);\n const optimisticDoc = Object.assign(Object.assign({ _id: normalizedPath, pathId: normalizedPath }, resolvedDoc), { \n // System timestamp field name: the Bounded worker stamps the neutral\n // `_updatedAt`; the underscore-prefixed `_updated_at` metadata mirror.\n // Match it so the optimistic doc lines up with the server's confirmation.\n [isBoundedNetwork() ? '_updatedAt' : '_updated_at']: Date.now() });\n const sub = this.findSubscriptionByPath(collectionPath);\n let prevDoc = null;\n if (sub) {\n prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;\n sub.docs.set(normalizedPath, optimisticDoc);\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n }\n // Send to server\n const requestId = `r_${nextRequestId++}`;\n try {\n const result = await this.sendRequest(requestId, {\n type: 'set',\n requestId,\n documents: [{ destinationPath: normalizedPath, document: doc }],\n });\n // Replace optimistic doc with server-confirmed version\n if (sub && result && typeof result === 'object') {\n const serverDoc = Array.isArray(result) ? result[0] : result;\n if (serverDoc && serverDoc._id) {\n sub.docs.set(serverDoc._id, serverDoc);\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n this.markIdbDirty(collectionPath);\n }\n }\n return Array.isArray(result) ? result[0] : result;\n }\n catch (err) {\n // Revert optimistic update\n if (sub) {\n if (prevDoc) {\n sub.docs.set(normalizedPath, prevDoc);\n }\n else {\n sub.docs.delete(normalizedPath);\n }\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n }\n throw err;\n }\n }\n async get(path) {\n await this.ensureCurrentAuth();\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n // Check local subscriptions first\n const collectionPath = this.getCollectionPath(normalizedPath);\n const sub = this.findSubscriptionByPath(collectionPath);\n if (sub && sub.status === 'live') {\n const doc = sub.docs.get(normalizedPath);\n return doc !== null && doc !== void 0 ? doc : null;\n }\n // One-shot WS fetch\n await this.ensureConnected();\n const requestId = `r_${nextRequestId++}`;\n return this.sendRequest(requestId, {\n type: 'get',\n requestId,\n path: normalizedPath,\n });\n }\n async getMany(paths) {\n await this.ensureConnected();\n const normalizedPaths = paths.map(p => p.startsWith('/') ? p.slice(1) : p);\n const requestId = `r_${nextRequestId++}`;\n return this.sendRequest(requestId, {\n type: 'getMany',\n requestId,\n paths: normalizedPaths,\n });\n }\n async delete(path) {\n var _a;\n await this.ensureConnected();\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n // Optimistic: remove from local state\n const collectionPath = this.getCollectionPath(normalizedPath);\n const sub = this.findSubscriptionByPath(collectionPath);\n let prevDoc = null;\n if (sub) {\n prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;\n sub.docs.delete(normalizedPath);\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n }\n const requestId = `r_${nextRequestId++}`;\n try {\n await this.sendRequest(requestId, {\n type: 'delete',\n requestId,\n path: normalizedPath,\n });\n if (sub)\n this.markIdbDirty(collectionPath);\n }\n catch (err) {\n // Revert\n if (sub && prevDoc) {\n sub.docs.set(normalizedPath, prevDoc);\n sub.ref.current = sub.docs;\n this.notifySubscription(sub);\n }\n throw err;\n }\n }\n async query(path, opts) {\n await this.ensureConnected();\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n const requestId = `r_${nextRequestId++}`;\n return this.sendRequest(requestId, Object.assign(Object.assign(Object.assign(Object.assign({ type: 'query', requestId, path: normalizedPath }, ((opts === null || opts === void 0 ? void 0 : opts.filter) ? { filter: opts.filter } : {})), ((opts === null || opts === void 0 ? void 0 : opts.sort) ? { sort: opts.sort } : {})), ((opts === null || opts === void 0 ? void 0 : opts.limit) !== undefined ? { limit: opts.limit } : {})), ((opts === null || opts === void 0 ? void 0 : opts.includeSubPaths) ? { includeSubPaths: true } : {})));\n }\n async count(path) {\n var _a;\n await this.ensureConnected();\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n const requestId = `r_${nextRequestId++}`;\n const result = await this.sendRequest(requestId, {\n type: 'count',\n requestId,\n path: normalizedPath,\n });\n return typeof result === 'number' ? result : ((_a = result === null || result === void 0 ? void 0 : result.value) !== null && _a !== void 0 ? _a : 0);\n }\n async aggregate(path, operation, opts) {\n await this.ensureConnected();\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n const requestId = `r_${nextRequestId++}`;\n return this.sendRequest(requestId, Object.assign({ type: 'aggregate', requestId, path: normalizedPath, operation }, ((opts === null || opts === void 0 ? void 0 : opts.field) ? { field: opts.field } : {})));\n }\n // -----------------------------------------------------------------------\n // Helpers\n // -----------------------------------------------------------------------\n sendSubscribe(sub) {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN)\n return;\n const msg = {\n type: 'subscribe',\n subscriptionId: sub.id,\n path: sub.path,\n };\n if (sub.options.filter)\n msg.filter = sub.options.filter;\n if (sub.options.includeSubPaths)\n msg.includeSubPaths = true;\n if (sub.options.limit)\n msg.limit = sub.options.limit;\n if (sub.options.prompt)\n msg.prompt = sub.options.prompt;\n this.ws.send(JSON.stringify(msg));\n }\n sendRequest(requestId, msg) {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n this.pendingRequests.delete(requestId);\n reject(new Error('Request timed out'));\n }, 30000);\n this.pendingRequests.set(requestId, { resolve, reject, timeout });\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n else {\n this.pendingRequests.delete(requestId);\n clearTimeout(timeout);\n reject(new Error('WebSocket not connected'));\n }\n });\n }\n notifySubscription(sub) {\n const data = this.docsToArray(sub);\n const callbacks = Array.from(sub.callbacks);\n for (const cb of callbacks) {\n try {\n cb(data);\n }\n catch ( /* swallow callback errors */_a) { /* swallow callback errors */ }\n }\n this.notifyState(sub);\n }\n notifyState(sub) {\n const state = this.getState(sub);\n const callbacks = Array.from(sub.stateCallbacks);\n for (const cb of callbacks) {\n try {\n cb(state);\n }\n catch ( /* swallow */_a) { /* swallow */ }\n }\n }\n getState(sub) {\n return {\n data: this.docsToArray(sub),\n status: sub.status,\n isStale: sub.isStale,\n error: sub.error,\n };\n }\n docsToArray(sub) {\n return Array.from(sub.docs.values());\n }\n findSubscriptionById(id) {\n for (const sub of this.subscriptions.values()) {\n if (sub.id === id)\n return sub;\n }\n return undefined;\n }\n findSubscriptionByPath(collectionPath) {\n for (const sub of this.subscriptions.values()) {\n const subPath = sub.path.startsWith('/') ? sub.path.slice(1) : sub.path;\n if (subPath === collectionPath)\n return sub;\n if (collectionPath.startsWith(subPath + '/'))\n return sub;\n }\n return undefined;\n }\n getCollectionPath(docPath) {\n const segments = docPath.split('/');\n if (segments.length % 2 === 0) {\n return segments.slice(0, -1).join('/');\n }\n return docPath;\n }\n getSubKey(path, opts) {\n const parts = [this.appId, this.authPrincipalKey, path];\n if (opts.filter)\n parts.push(JSON.stringify(opts.filter));\n if (opts.prompt)\n parts.push(opts.prompt);\n if (opts.tier)\n parts.push(opts.tier);\n return parts.join('::');\n }\n idbKey(path) {\n return `${this.appId}:${this.authPrincipalKey}:${path}`;\n }\n shouldUseIdb(tier, opts) {\n return opts.cache === true && tier !== 'ephemeral' && this.authPrincipalKey !== 'anon';\n }\n markIdbDirty(path) {\n const sub = this.findSubscriptionByPath(path);\n if (!sub || !this.shouldUseIdb(sub.tier, sub.options))\n return;\n this.idbDirtyKeys.add(path);\n if (!this.idbFlushTimer) {\n this.idbFlushTimer = setTimeout(() => {\n this.flushIdb();\n this.idbFlushTimer = null;\n }, 500);\n }\n }\n async flushIdb() {\n const keys = Array.from(this.idbDirtyKeys);\n this.idbDirtyKeys.clear();\n for (const path of keys) {\n const sub = this.findSubscriptionByPath(path);\n if (sub && this.shouldUseIdb(sub.tier, sub.options)) {\n const docs = this.docsToArray(sub);\n await idbSet(this.idbKey(path), docs);\n }\n }\n }\n createUnsubscribe(subKey, subId, onData, onState, onError) {\n return async () => {\n var _a;\n const sub = (_a = this.subscriptions.get(subKey)) !== null && _a !== void 0 ? _a : this.findSubscriptionById(subId);\n if (!sub)\n return;\n const currentSubKey = this.getSubKey(sub.path, sub.options);\n if (onData)\n sub.callbacks.delete(onData);\n if (onState)\n sub.stateCallbacks.delete(onState);\n if (onError)\n sub.errorCallbacks.delete(onError);\n // If no more callbacks, unsubscribe entirely\n if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {\n this.subscriptions.delete(subKey);\n this.subscriptions.delete(currentSubKey);\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify({\n type: 'unsubscribe',\n subscriptionId: sub.id,\n }));\n }\n }\n };\n }\n resolveOperations(doc, path) {\n var _a;\n if (!doc || typeof doc !== 'object')\n return doc;\n const resolved = {};\n for (const [key, value] of Object.entries(doc)) {\n if (value && typeof value === 'object' && !Array.isArray(value) && value.operation) {\n const op = value;\n if (op.operation === 'time' && op.value === 'now') {\n resolved[key] = Math.floor(Date.now() / 1000);\n }\n else if (op.operation === 'increment') {\n // For optimistic: get current value and add\n const normalizedPath = path.startsWith('/') ? path.slice(1) : path;\n const collectionPath = this.getCollectionPath(normalizedPath);\n const sub = this.findSubscriptionByPath(collectionPath);\n const existing = sub === null || sub === void 0 ? void 0 : sub.docs.get(normalizedPath);\n const current = (_a = existing === null || existing === void 0 ? void 0 : existing[key]) !== null && _a !== void 0 ? _a : 0;\n resolved[key] = (typeof current === 'number' ? current : 0) + op.value;\n }\n else {\n resolved[key] = value;\n }\n }\n else {\n resolved[key] = value;\n }\n }\n return resolved;\n }\n rejectAllPending(reason) {\n for (const [requestId, pending] of this.pendingRequests) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(reason));\n }\n this.pendingRequests.clear();\n }\n setAllSubscriptionStatus(status) {\n for (const sub of this.subscriptions.values()) {\n sub.status = status;\n this.notifyState(sub);\n }\n }\n // -----------------------------------------------------------------------\n // Lifecycle\n // -----------------------------------------------------------------------\n close() {\n this.closed = true;\n if (this.reconnectTimer)\n clearTimeout(this.reconnectTimer);\n if (this.idbFlushTimer)\n clearTimeout(this.idbFlushTimer);\n if (this.tokenRefreshTimer)\n clearInterval(this.tokenRefreshTimer);\n this.flushIdb();\n if (this.ws) {\n this.ws.close(1000, 'Store closed');\n this.ws = null;\n }\n this.rejectAllPending('Store closed');\n this.subscriptions.clear();\n }\n async reconnectWithNewAuth() {\n if (this.closed)\n return;\n await this.ensureInitialized();\n await this.refreshToken();\n await this.applyAuthPrincipalChange();\n if (this.subscriptions.size > 0) {\n await this.ensureConnected().catch((error) => {\n this.setAllSubscriptionStatus('error');\n for (const sub of this.subscriptions.values()) {\n sub.error = error instanceof Error ? error : new Error(String(error));\n this.notifyState(sub);\n }\n });\n }\n }\n}\n// ---------------------------------------------------------------------------\n// Singleton instance\n// ---------------------------------------------------------------------------\nlet storeInstance = null;\nexport function getRealtimeStore() {\n if (!storeInstance) {\n storeInstance = new RealtimeStore();\n }\n return storeInstance;\n}\nexport function resetRealtimeStore() {\n if (storeInstance) {\n storeInstance.close();\n storeInstance = null;\n }\n}\nexport async function reconnectRealtimeStoreWithNewAuth() {\n if (storeInstance) {\n await storeInstance.reconnectWithNewAuth();\n }\n}\n"],"names":[],"mappings":";;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AA++BO,eAAe,iCAAiC,GAAG;AAI1D;;;;"}
|
package/dist/types.d.ts
CHANGED
|
@@ -29,13 +29,10 @@ export interface User {
|
|
|
29
29
|
* Universal stable identity of the user (resolves @user.id). For wallet/SIWS
|
|
30
30
|
* logins this EQUALS `address`; for email/social (Bounded Better Auth) logins
|
|
31
31
|
* it is the account identity (and `address` may be null). Derived from the
|
|
32
|
-
* idToken's `custom:userId` claim
|
|
33
|
-
* tokens minted before the claim-split. Used for ownership/membership/identity.
|
|
32
|
+
* idToken's `custom:userId` claim. Used for ownership/membership/identity.
|
|
34
33
|
*
|
|
35
|
-
* Optional for
|
|
36
|
-
*
|
|
37
|
-
* the principal. Always present for a session derived from a real idToken;
|
|
38
|
-
* may be undefined only for legacy/manually-constructed User objects.
|
|
34
|
+
* Optional only for manually constructed users or invalid legacy tokens. New
|
|
35
|
+
* code should prefer `user.id` as the principal.
|
|
39
36
|
*/
|
|
40
37
|
id?: string;
|
|
41
38
|
/**
|
|
@@ -115,6 +112,12 @@ export interface SubscriptionOptions {
|
|
|
115
112
|
limit?: number;
|
|
116
113
|
/** Opaque cursor for cursor-based pagination (used with limit) */
|
|
117
114
|
cursor?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Opt into immediate delivery of a short-lived cached subscription snapshot.
|
|
117
|
+
* Cache entries are scoped to the opaque authenticated principal. Anonymous
|
|
118
|
+
* subscriptions do not populate or read the response cache.
|
|
119
|
+
*/
|
|
120
|
+
cache?: boolean;
|
|
118
121
|
/** Override the app ID for this subscription (instead of the configured default) */
|
|
119
122
|
appId?: string;
|
|
120
123
|
onData?: (data: any) => void;
|
|
@@ -122,9 +125,8 @@ export interface SubscriptionOptions {
|
|
|
122
125
|
/**
|
|
123
126
|
* @internal Per-subscription auth override. The server `WalletClient` sets this
|
|
124
127
|
* (via `subscribe()`) so the WS connection authenticates as that wallet's
|
|
125
|
-
* identity
|
|
126
|
-
*
|
|
127
|
-
* never sets this — it's threaded internally.
|
|
128
|
+
* explicit identity. Mirrors `RequestOverrides`; only the fields the subscribe
|
|
129
|
+
* path consumes. App code never sets this — it's threaded internally.
|
|
128
130
|
*/
|
|
129
131
|
_overrides?: {
|
|
130
132
|
_getAuthHeaders?: () => Promise<Record<string, string>>;
|
package/dist/utils/auth-api.d.ts
CHANGED
|
@@ -2,9 +2,13 @@ export declare function genNonce(): Promise<any>;
|
|
|
2
2
|
export declare function genAuthNonce(): Promise<any>;
|
|
3
3
|
export declare function createSessionWithSignature(address: string, message: string, signature: string): Promise<any>;
|
|
4
4
|
export declare function refreshSession(refreshToken: string, issuer?: string): Promise<any>;
|
|
5
|
+
export declare class SessionRevokeError extends Error {
|
|
6
|
+
readonly cause?: unknown;
|
|
7
|
+
constructor(message: string, cause?: unknown);
|
|
8
|
+
}
|
|
5
9
|
/**
|
|
6
|
-
* Revoke a session's refresh-token family server-side (logout).
|
|
7
|
-
*
|
|
10
|
+
* Revoke a session's refresh-token family server-side (logout). Routes to the
|
|
11
|
+
* minting issuer and rejects if the revoke request fails.
|
|
8
12
|
*/
|
|
9
13
|
export declare function revokeSession(refreshToken: string, issuer?: string): Promise<void>;
|
|
10
14
|
export declare function signSessionCreateMessage(_signMessageFunction: (message: string) => Promise<string>): Promise<void>;
|
|
@@ -2,8 +2,8 @@ import { Keypair } from "@solana/web3.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Server-side SessionManager
|
|
4
4
|
* --------------------------
|
|
5
|
-
* •
|
|
6
|
-
* • Per-keypair instances via `ServerSessionManager.forKeypair(kp)
|
|
5
|
+
* • No process-global signer.
|
|
6
|
+
* • Per-keypair instances via `ServerSessionManager.forKeypair(kp)`.
|
|
7
7
|
* • Keeps session data in-memory for the life of the instance
|
|
8
8
|
* • If `getSession()` finds no cached session it calls `createSession()`
|
|
9
9
|
* to obtain fresh tokens.
|
package/dist/utils/utils.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare function getUserInfo(isServer: boolean): Promise<any>;
|
|
|
9
9
|
* the same { id, address, email } the backend authenticates as.
|
|
10
10
|
*/
|
|
11
11
|
export interface UserIdentity {
|
|
12
|
-
/** @user.id — universal stable identity (custom:userId
|
|
12
|
+
/** @user.id — universal stable identity (custom:userId only). */
|
|
13
13
|
id: string | null;
|
|
14
14
|
/** @user.address — REAL wallet; null for email-only logins. */
|
|
15
15
|
address: string | null;
|
|
@@ -21,9 +21,7 @@ export interface UserIdentity {
|
|
|
21
21
|
*
|
|
22
22
|
* Mirrors the realtime-worker auth.ts identity resolution so the client-side
|
|
23
23
|
* `user` object matches what the backend authenticates as:
|
|
24
|
-
* - id = custom:userId
|
|
25
|
-
* keeps wallet/SIWS tokens — which omit userId — AND legacy Better Auth
|
|
26
|
-
* tokens — which put the account id in custom:walletAddress — working).
|
|
24
|
+
* - id = custom:userId only.
|
|
27
25
|
* - address = custom:walletAddress only (a REAL wallet). NEVER falls back to the
|
|
28
26
|
* identity: an opaque id is not a spendable onchain address. null for
|
|
29
27
|
* email-only sessions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bounded-sh/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "Core functionality for Poof SDK",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "rm -rf ./dist ./.rollup-tmp && tsc -p tsconfig.build.json --outDir .rollup-tmp --declarationDir .rollup-tmp --emitDeclarationOnly false && rollup -c && tsc -p tsconfig.build.json --emitDeclarationOnly --declarationDir dist && rm -rf ./.rollup-tmp",
|
|
17
17
|
"watch": "rollup -c -w",
|
|
18
|
-
"test": "node src/client/run-read-principal-test.mjs && node src/client/run-row-shape-test.mjs && node src/client/run-transport-test.mjs && node src/client/run-live-auth-test.mjs && node src/client/run-subscription-auth-error-test.mjs && node src/client/websocket-auth-url.test.mjs",
|
|
18
|
+
"test": "node src/client/run-read-principal-test.mjs && node src/client/run-row-shape-test.mjs && node src/client/run-transport-test.mjs && node src/client/run-public-export-surface-test.mjs && node src/client/run-live-auth-test.mjs && node src/client/run-live-view-principal-test.mjs && node src/client/run-subscription-auth-error-test.mjs && node src/client/run-onchain-validation-test.mjs && node src/client/websocket-auth-url.test.mjs",
|
|
19
19
|
"prepare": "npm run build"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|