@pooflabs/core 0.0.48 → 0.0.91
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/realtime-store.d.ts +84 -0
- package/dist/client/subscription-v2.d.ts +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +994 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +986 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4009,9 +4009,40 @@ async function get(path, opts = {}) {
|
|
|
4009
4009
|
}
|
|
4010
4010
|
// Create a new request promise and store it
|
|
4011
4011
|
const requestPromise = (async () => {
|
|
4012
|
+
var _a;
|
|
4012
4013
|
try {
|
|
4013
|
-
//
|
|
4014
|
+
// For realtime chains, prefer WebSocket reads (lower latency, already connected)
|
|
4015
|
+
const config = await getConfig();
|
|
4014
4016
|
const pathIsDocument = normalizedPath.split("/").length % 2 === 0;
|
|
4017
|
+
if (((_a = config.chain) === null || _a === void 0 ? void 0 : _a.startsWith('realtime_')) && !config.isServer && !opts.prompt && !opts.shape && !opts.cursor) {
|
|
4018
|
+
try {
|
|
4019
|
+
const { wsGet, wsQuery, hasActiveConnection } = await Promise.resolve().then(function () { return subscriptionV2; });
|
|
4020
|
+
if (hasActiveConnection()) {
|
|
4021
|
+
if (pathIsDocument) {
|
|
4022
|
+
const wsResult = await wsGet(normalizedPath);
|
|
4023
|
+
const responseData = wsResult;
|
|
4024
|
+
if (!opts.bypassCache) {
|
|
4025
|
+
getCache[cacheKey] = { data: responseData, expiresAt: now + GET_CACHE_TTL };
|
|
4026
|
+
}
|
|
4027
|
+
return responseData;
|
|
4028
|
+
}
|
|
4029
|
+
else if (!opts.limit) {
|
|
4030
|
+
const wsResult = await wsQuery(normalizedPath, {
|
|
4031
|
+
filter: undefined,
|
|
4032
|
+
sort: undefined,
|
|
4033
|
+
includeSubPaths: opts.includeSubPaths,
|
|
4034
|
+
});
|
|
4035
|
+
const responseData = wsResult;
|
|
4036
|
+
if (!opts.bypassCache) {
|
|
4037
|
+
getCache[cacheKey] = { data: responseData, expiresAt: now + GET_CACHE_TTL };
|
|
4038
|
+
}
|
|
4039
|
+
return responseData;
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
catch ( /* fall through to HTTP */_b) { /* fall through to HTTP */ }
|
|
4044
|
+
}
|
|
4045
|
+
// Cache miss or bypass - proceed with HTTP API request
|
|
4015
4046
|
let response;
|
|
4016
4047
|
// Build common query params
|
|
4017
4048
|
const includeSubPathsParam = opts.includeSubPaths ? '&includeSubPaths=true' : '';
|
|
@@ -4020,7 +4051,6 @@ async function get(path, opts = {}) {
|
|
|
4020
4051
|
const cursorParam = opts.cursor ? `&cursor=${encodeURIComponent(opts.cursor)}` : '';
|
|
4021
4052
|
if (pathIsDocument) {
|
|
4022
4053
|
const itemId = encodeURIComponent(normalizedPath);
|
|
4023
|
-
// For documents, query params go after the path
|
|
4024
4054
|
const queryParams = [includeSubPathsParam, shapeParam].filter(p => p).join('');
|
|
4025
4055
|
const apiPath = queryParams ? `items/${itemId}?${queryParams.substring(1)}` : `items/${itemId}`;
|
|
4026
4056
|
response = await makeApiRequest('GET', apiPath, null, opts._overrides);
|
|
@@ -4215,7 +4245,7 @@ async function set(path, document, options) {
|
|
|
4215
4245
|
return result;
|
|
4216
4246
|
}
|
|
4217
4247
|
async function setMany(many, options) {
|
|
4218
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
4248
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
4219
4249
|
// Returns the data that was set, or undefined if the document was already set.
|
|
4220
4250
|
try {
|
|
4221
4251
|
const config = await getConfig();
|
|
@@ -4252,13 +4282,28 @@ async function setMany(many, options) {
|
|
|
4252
4282
|
}
|
|
4253
4283
|
let setResponse;
|
|
4254
4284
|
try {
|
|
4255
|
-
|
|
4285
|
+
// For realtime chains, prefer WebSocket if a connection is active (lower latency)
|
|
4286
|
+
const useWs = ((_c = config.chain) === null || _c === void 0 ? void 0 : _c.startsWith('realtime_')) && !config.isServer;
|
|
4287
|
+
if (useWs) {
|
|
4288
|
+
try {
|
|
4289
|
+
const { wsSet, hasActiveConnection } = await Promise.resolve().then(function () { return subscriptionV2; });
|
|
4290
|
+
if (hasActiveConnection()) {
|
|
4291
|
+
const wsResult = await wsSet(documents);
|
|
4292
|
+
// Normalize to same shape as HTTP 200 response so downstream handling is identical
|
|
4293
|
+
setResponse = { data: wsResult, status: 200 };
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
catch ( /* fall through to HTTP */_j) { /* fall through to HTTP */ }
|
|
4297
|
+
}
|
|
4298
|
+
if (!setResponse) {
|
|
4299
|
+
setResponse = await makeApiRequest('PUT', `items`, { documents }, options === null || options === void 0 ? void 0 : options._overrides);
|
|
4300
|
+
}
|
|
4256
4301
|
}
|
|
4257
4302
|
catch (error) {
|
|
4258
4303
|
if ((error === null || error === void 0 ? void 0 : error.statusCode) === 402 && (error === null || error === void 0 ? void 0 : error.error) === 'INSUFFICIENT_BALANCE') {
|
|
4259
|
-
const deficitLamports = Number((
|
|
4260
|
-
const deficitSol = Number((
|
|
4261
|
-
throw new InsufficientBalanceError(String((
|
|
4304
|
+
const deficitLamports = Number((_d = error.deficitLamports) !== null && _d !== void 0 ? _d : 0);
|
|
4305
|
+
const deficitSol = Number((_e = error.deficitSol) !== null && _e !== void 0 ? _e : deficitLamports / 1000000000);
|
|
4306
|
+
throw new InsufficientBalanceError(String((_f = error.address) !== null && _f !== void 0 ? _f : ''), Number((_g = error.balanceLamports) !== null && _g !== void 0 ? _g : 0), Number((_h = error.estimatedCostLamports) !== null && _h !== void 0 ? _h : 0), deficitLamports, deficitSol);
|
|
4262
4307
|
}
|
|
4263
4308
|
throw error;
|
|
4264
4309
|
}
|
|
@@ -4302,7 +4347,7 @@ async function setMany(many, options) {
|
|
|
4302
4347
|
else if (setResponse.data &&
|
|
4303
4348
|
typeof setResponse.data === 'object' &&
|
|
4304
4349
|
setResponse.data.success === true) {
|
|
4305
|
-
const
|
|
4350
|
+
const _k = setResponse.data, { success: _success } = _k, rest = __rest(_k, ["success"]);
|
|
4306
4351
|
return Object.assign(Object.assign(Object.assign({}, documents.map(d => d.document)), rest), { transactionId: null });
|
|
4307
4352
|
}
|
|
4308
4353
|
else {
|
|
@@ -4764,6 +4809,7 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4764
4809
|
subscriptions: new Map(),
|
|
4765
4810
|
pendingSubscriptions: new Map(),
|
|
4766
4811
|
pendingUnsubscriptions: new Map(),
|
|
4812
|
+
pendingRequests: new Map(),
|
|
4767
4813
|
isConnecting: false,
|
|
4768
4814
|
isConnected: false,
|
|
4769
4815
|
appId,
|
|
@@ -4862,11 +4908,17 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4862
4908
|
clearInterval(connection.tokenRefreshTimer);
|
|
4863
4909
|
connection.tokenRefreshTimer = null;
|
|
4864
4910
|
}
|
|
4911
|
+
// Reject all pending WS CRUD requests on disconnect (fail fast)
|
|
4912
|
+
for (const [id, pending] of connection.pendingRequests) {
|
|
4913
|
+
clearTimeout(pending.timer);
|
|
4914
|
+
pending.reject(new Error('WebSocket disconnected'));
|
|
4915
|
+
}
|
|
4916
|
+
connection.pendingRequests.clear();
|
|
4865
4917
|
});
|
|
4866
4918
|
return connection;
|
|
4867
4919
|
}
|
|
4868
4920
|
function handleServerMessage(connection, message) {
|
|
4869
|
-
var _a;
|
|
4921
|
+
var _a, _b;
|
|
4870
4922
|
switch (message.type) {
|
|
4871
4923
|
case 'subscribed': {
|
|
4872
4924
|
const subscription = connection.subscriptions.get(message.subscriptionId);
|
|
@@ -4911,8 +4963,45 @@ function handleServerMessage(connection, message) {
|
|
|
4911
4963
|
}
|
|
4912
4964
|
break;
|
|
4913
4965
|
}
|
|
4966
|
+
case 'response': {
|
|
4967
|
+
const pendingReq = connection.pendingRequests.get(message.requestId);
|
|
4968
|
+
if (pendingReq) {
|
|
4969
|
+
connection.pendingRequests.delete(message.requestId);
|
|
4970
|
+
clearTimeout(pendingReq.timer);
|
|
4971
|
+
if (message.status >= 400) {
|
|
4972
|
+
pendingReq.reject(new Error(`Request failed with status ${message.status}`));
|
|
4973
|
+
}
|
|
4974
|
+
else {
|
|
4975
|
+
pendingReq.resolve(message.data);
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
break;
|
|
4979
|
+
}
|
|
4980
|
+
case 'setResponse': {
|
|
4981
|
+
const pendingSet = connection.pendingRequests.get(message.requestId);
|
|
4982
|
+
if (pendingSet) {
|
|
4983
|
+
connection.pendingRequests.delete(message.requestId);
|
|
4984
|
+
clearTimeout(pendingSet.timer);
|
|
4985
|
+
if (message.statusCode >= 400) {
|
|
4986
|
+
pendingSet.reject(new Error(((_a = message.body) === null || _a === void 0 ? void 0 : _a.message) || `Set failed with status ${message.statusCode}`));
|
|
4987
|
+
}
|
|
4988
|
+
else {
|
|
4989
|
+
pendingSet.resolve(message.body);
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
break;
|
|
4993
|
+
}
|
|
4914
4994
|
case 'error': {
|
|
4915
4995
|
console.error('[WS v2] Server error:', message.code, message.message);
|
|
4996
|
+
// Handle CRUD request errors (requestId present)
|
|
4997
|
+
if (message.requestId) {
|
|
4998
|
+
const pendingReq = connection.pendingRequests.get(message.requestId);
|
|
4999
|
+
if (pendingReq) {
|
|
5000
|
+
connection.pendingRequests.delete(message.requestId);
|
|
5001
|
+
clearTimeout(pendingReq.timer);
|
|
5002
|
+
pendingReq.reject(new Error(`${message.code}: ${message.message}`));
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
4916
5005
|
if (message.subscriptionId) {
|
|
4917
5006
|
// Reject pending subscription if this is a subscription error
|
|
4918
5007
|
const pending = connection.pendingSubscriptions.get(message.subscriptionId);
|
|
@@ -4924,7 +5013,7 @@ function handleServerMessage(connection, message) {
|
|
|
4924
5013
|
const subscription = connection.subscriptions.get(message.subscriptionId);
|
|
4925
5014
|
if (subscription) {
|
|
4926
5015
|
for (const callback of subscription.callbacks) {
|
|
4927
|
-
(
|
|
5016
|
+
(_b = callback.onError) === null || _b === void 0 ? void 0 : _b.call(callback, new Error(`${message.code}: ${message.message}`));
|
|
4928
5017
|
}
|
|
4929
5018
|
}
|
|
4930
5019
|
}
|
|
@@ -5213,6 +5302,112 @@ async function doReconnectWithNewAuth() {
|
|
|
5213
5302
|
}
|
|
5214
5303
|
}
|
|
5215
5304
|
}
|
|
5305
|
+
// ============ CRUD over WebSocket ============
|
|
5306
|
+
const WS_REQUEST_TIMEOUT_MS = 30000;
|
|
5307
|
+
function generateRequestId() {
|
|
5308
|
+
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
5309
|
+
}
|
|
5310
|
+
/**
|
|
5311
|
+
* Returns true if there is an active v2 WebSocket connection open.
|
|
5312
|
+
*/
|
|
5313
|
+
function hasActiveConnection() {
|
|
5314
|
+
for (const connection of connections.values()) {
|
|
5315
|
+
if (connection.ws && connection.isConnected) {
|
|
5316
|
+
return true;
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
return false;
|
|
5320
|
+
}
|
|
5321
|
+
async function sendRequest(msgBuilder) {
|
|
5322
|
+
const config = await getConfig();
|
|
5323
|
+
const appId = config.appId;
|
|
5324
|
+
const connection = await getOrCreateConnection(appId, config.isServer);
|
|
5325
|
+
// Wait for the connection to be open (getOrCreateConnection may return
|
|
5326
|
+
// while still connecting).
|
|
5327
|
+
if (!connection.isConnected && connection.ws) {
|
|
5328
|
+
await new Promise((resolve, reject) => {
|
|
5329
|
+
const timeout = setTimeout(() => {
|
|
5330
|
+
var _a;
|
|
5331
|
+
(_a = connection.ws) === null || _a === void 0 ? void 0 : _a.removeEventListener('open', onOpen);
|
|
5332
|
+
reject(new Error('WebSocket connection timeout'));
|
|
5333
|
+
}, 10000);
|
|
5334
|
+
const onOpen = () => { clearTimeout(timeout); resolve(); };
|
|
5335
|
+
if (connection.isConnected) {
|
|
5336
|
+
clearTimeout(timeout);
|
|
5337
|
+
resolve();
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
5340
|
+
connection.ws.addEventListener('open', onOpen);
|
|
5341
|
+
});
|
|
5342
|
+
}
|
|
5343
|
+
if (!connection.ws || !connection.isConnected) {
|
|
5344
|
+
throw new Error('WebSocket connection not available');
|
|
5345
|
+
}
|
|
5346
|
+
const requestId = generateRequestId();
|
|
5347
|
+
const message = msgBuilder(requestId);
|
|
5348
|
+
return new Promise((resolve, reject) => {
|
|
5349
|
+
const timer = setTimeout(() => {
|
|
5350
|
+
connection.pendingRequests.delete(requestId);
|
|
5351
|
+
reject(new Error(`WebSocket request timed out after ${WS_REQUEST_TIMEOUT_MS}ms`));
|
|
5352
|
+
}, WS_REQUEST_TIMEOUT_MS);
|
|
5353
|
+
connection.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
5354
|
+
try {
|
|
5355
|
+
connection.ws.send(JSON.stringify(message));
|
|
5356
|
+
}
|
|
5357
|
+
catch (error) {
|
|
5358
|
+
connection.pendingRequests.delete(requestId);
|
|
5359
|
+
clearTimeout(timer);
|
|
5360
|
+
reject(error);
|
|
5361
|
+
}
|
|
5362
|
+
});
|
|
5363
|
+
}
|
|
5364
|
+
async function wsGet(path) {
|
|
5365
|
+
return sendRequest((requestId) => ({
|
|
5366
|
+
type: 'get',
|
|
5367
|
+
requestId,
|
|
5368
|
+
path,
|
|
5369
|
+
}));
|
|
5370
|
+
}
|
|
5371
|
+
async function wsSet(documents) {
|
|
5372
|
+
return sendRequest((requestId) => ({
|
|
5373
|
+
type: 'set',
|
|
5374
|
+
requestId,
|
|
5375
|
+
documents,
|
|
5376
|
+
}));
|
|
5377
|
+
}
|
|
5378
|
+
async function wsQuery(path, opts) {
|
|
5379
|
+
return sendRequest((requestId) => (Object.assign(Object.assign(Object.assign(Object.assign({ type: 'query', requestId,
|
|
5380
|
+
path }, ((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: opts.includeSubPaths } : {}))));
|
|
5381
|
+
}
|
|
5382
|
+
async function wsDelete(path) {
|
|
5383
|
+
return sendRequest((requestId) => ({
|
|
5384
|
+
type: 'delete',
|
|
5385
|
+
requestId,
|
|
5386
|
+
path,
|
|
5387
|
+
}));
|
|
5388
|
+
}
|
|
5389
|
+
async function wsGetMany(paths) {
|
|
5390
|
+
return sendRequest((requestId) => ({
|
|
5391
|
+
type: 'getMany',
|
|
5392
|
+
requestId,
|
|
5393
|
+
paths,
|
|
5394
|
+
}));
|
|
5395
|
+
}
|
|
5396
|
+
|
|
5397
|
+
var subscriptionV2 = /*#__PURE__*/Object.freeze({
|
|
5398
|
+
__proto__: null,
|
|
5399
|
+
clearCacheV2: clearCacheV2,
|
|
5400
|
+
closeAllSubscriptionsV2: closeAllSubscriptionsV2,
|
|
5401
|
+
getCachedDataV2: getCachedDataV2,
|
|
5402
|
+
hasActiveConnection: hasActiveConnection,
|
|
5403
|
+
reconnectWithNewAuthV2: reconnectWithNewAuthV2,
|
|
5404
|
+
subscribeV2: subscribeV2,
|
|
5405
|
+
wsDelete: wsDelete,
|
|
5406
|
+
wsGet: wsGet,
|
|
5407
|
+
wsGetMany: wsGetMany,
|
|
5408
|
+
wsQuery: wsQuery,
|
|
5409
|
+
wsSet: wsSet
|
|
5410
|
+
});
|
|
5216
5411
|
|
|
5217
5412
|
/**
|
|
5218
5413
|
* WebSocket Subscription Module
|
|
@@ -5467,8 +5662,789 @@ class ReactNativeSessionManager {
|
|
|
5467
5662
|
}
|
|
5468
5663
|
ReactNativeSessionManager.TAROBASE_SESSION_STORAGE_KEY = "tarobase_session_storage";
|
|
5469
5664
|
|
|
5665
|
+
// ---------------------------------------------------------------------------
|
|
5666
|
+
// realtime-store.ts — Client-side state manager for realtime apps.
|
|
5667
|
+
//
|
|
5668
|
+
// Manages: WS connection, in-memory state, IDB persistence, optimistic
|
|
5669
|
+
// writes, delta accumulation, loading states, ephemeral/durable tiers.
|
|
5670
|
+
// ---------------------------------------------------------------------------
|
|
5671
|
+
// ---------------------------------------------------------------------------
|
|
5672
|
+
// IDB helpers (lazy-loaded, non-blocking)
|
|
5673
|
+
// ---------------------------------------------------------------------------
|
|
5674
|
+
const IDB_NAME = 'tarobase-realtime';
|
|
5675
|
+
const IDB_STORE = 'subscriptions';
|
|
5676
|
+
const IDB_VERSION = 1;
|
|
5677
|
+
let idbPromise = null;
|
|
5678
|
+
function getIDB() {
|
|
5679
|
+
if (idbPromise)
|
|
5680
|
+
return idbPromise;
|
|
5681
|
+
if (typeof indexedDB === 'undefined') {
|
|
5682
|
+
return Promise.reject(new Error('IndexedDB not available'));
|
|
5683
|
+
}
|
|
5684
|
+
idbPromise = new Promise((resolve, reject) => {
|
|
5685
|
+
const req = indexedDB.open(IDB_NAME, IDB_VERSION);
|
|
5686
|
+
req.onupgradeneeded = () => {
|
|
5687
|
+
const db = req.result;
|
|
5688
|
+
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
|
5689
|
+
db.createObjectStore(IDB_STORE);
|
|
5690
|
+
}
|
|
5691
|
+
};
|
|
5692
|
+
req.onsuccess = () => resolve(req.result);
|
|
5693
|
+
req.onerror = () => reject(req.error);
|
|
5694
|
+
});
|
|
5695
|
+
return idbPromise;
|
|
5696
|
+
}
|
|
5697
|
+
async function idbGet(key) {
|
|
5698
|
+
try {
|
|
5699
|
+
const db = await getIDB();
|
|
5700
|
+
return new Promise((resolve) => {
|
|
5701
|
+
const tx = db.transaction(IDB_STORE, 'readonly');
|
|
5702
|
+
const store = tx.objectStore(IDB_STORE);
|
|
5703
|
+
const req = store.get(key);
|
|
5704
|
+
req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };
|
|
5705
|
+
req.onerror = () => resolve(null);
|
|
5706
|
+
});
|
|
5707
|
+
}
|
|
5708
|
+
catch (_a) {
|
|
5709
|
+
return null;
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
async function idbSet(key, value) {
|
|
5713
|
+
try {
|
|
5714
|
+
const db = await getIDB();
|
|
5715
|
+
return new Promise((resolve) => {
|
|
5716
|
+
const tx = db.transaction(IDB_STORE, 'readwrite');
|
|
5717
|
+
const store = tx.objectStore(IDB_STORE);
|
|
5718
|
+
store.put(value, key);
|
|
5719
|
+
tx.oncomplete = () => resolve();
|
|
5720
|
+
tx.onerror = () => resolve();
|
|
5721
|
+
});
|
|
5722
|
+
}
|
|
5723
|
+
catch (_a) {
|
|
5724
|
+
// Best-effort persistence
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
// ---------------------------------------------------------------------------
|
|
5728
|
+
// RealtimeStore
|
|
5729
|
+
// ---------------------------------------------------------------------------
|
|
5730
|
+
let nextRequestId = 1;
|
|
5731
|
+
class RealtimeStore {
|
|
5732
|
+
constructor() {
|
|
5733
|
+
this.ws = null;
|
|
5734
|
+
this.wsUrl = '';
|
|
5735
|
+
this.appId = '';
|
|
5736
|
+
this.subscriptions = new Map();
|
|
5737
|
+
this.pendingRequests = new Map();
|
|
5738
|
+
this.connectPromise = null;
|
|
5739
|
+
this.reconnectTimer = null;
|
|
5740
|
+
this.reconnectDelay = 1000;
|
|
5741
|
+
this.maxReconnectDelay = 30000;
|
|
5742
|
+
this.idbFlushTimer = null;
|
|
5743
|
+
this.idbDirtyKeys = new Set();
|
|
5744
|
+
this.closed = false;
|
|
5745
|
+
this.authToken = null;
|
|
5746
|
+
}
|
|
5747
|
+
// -----------------------------------------------------------------------
|
|
5748
|
+
// Initialization
|
|
5749
|
+
// -----------------------------------------------------------------------
|
|
5750
|
+
async init() {
|
|
5751
|
+
var _a, _b;
|
|
5752
|
+
const config = await getConfig();
|
|
5753
|
+
this.appId = config.appId;
|
|
5754
|
+
this.wsUrl = config.wsApiUrl;
|
|
5755
|
+
if (config.authProvider) {
|
|
5756
|
+
try {
|
|
5757
|
+
const headers = await ((_b = (_a = config.authProvider).getAuthHeaders) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
5758
|
+
if (headers === null || headers === void 0 ? void 0 : headers.Authorization) {
|
|
5759
|
+
this.authToken = headers.Authorization.replace('Bearer ', '');
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
catch ( /* no auth */_c) { /* no auth */ }
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
// -----------------------------------------------------------------------
|
|
5766
|
+
// WebSocket connection
|
|
5767
|
+
// -----------------------------------------------------------------------
|
|
5768
|
+
async ensureConnected() {
|
|
5769
|
+
var _a;
|
|
5770
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN)
|
|
5771
|
+
return;
|
|
5772
|
+
if (this.connectPromise)
|
|
5773
|
+
return this.connectPromise;
|
|
5774
|
+
this.connectPromise = this.connect();
|
|
5775
|
+
return this.connectPromise;
|
|
5776
|
+
}
|
|
5777
|
+
connect() {
|
|
5778
|
+
return new Promise((resolve, reject) => {
|
|
5779
|
+
if (this.closed) {
|
|
5780
|
+
reject(new Error('Store closed'));
|
|
5781
|
+
return;
|
|
5782
|
+
}
|
|
5783
|
+
const params = new URLSearchParams();
|
|
5784
|
+
params.set('apiKey', this.appId);
|
|
5785
|
+
// Auth token sent via subprotocol to avoid leaking in URL/logs
|
|
5786
|
+
const url = `${this.wsUrl}?${params.toString()}`;
|
|
5787
|
+
const protocols = this.authToken ? [`bearer-${this.authToken}`] : undefined;
|
|
5788
|
+
const ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url);
|
|
5789
|
+
this.ws = ws;
|
|
5790
|
+
const onOpen = () => {
|
|
5791
|
+
ws.removeEventListener('error', onError);
|
|
5792
|
+
this.reconnectDelay = 1000;
|
|
5793
|
+
this.connectPromise = null;
|
|
5794
|
+
this.resubscribeAll();
|
|
5795
|
+
resolve();
|
|
5796
|
+
};
|
|
5797
|
+
const onError = (e) => {
|
|
5798
|
+
ws.removeEventListener('open', onOpen);
|
|
5799
|
+
this.connectPromise = null;
|
|
5800
|
+
reject(new Error('WebSocket connection failed'));
|
|
5801
|
+
};
|
|
5802
|
+
ws.addEventListener('open', onOpen, { once: true });
|
|
5803
|
+
ws.addEventListener('error', onError, { once: true });
|
|
5804
|
+
ws.addEventListener('message', (event) => {
|
|
5805
|
+
this.handleMessage(event.data);
|
|
5806
|
+
});
|
|
5807
|
+
ws.addEventListener('close', () => {
|
|
5808
|
+
this.ws = null;
|
|
5809
|
+
this.connectPromise = null;
|
|
5810
|
+
this.rejectAllPending('WebSocket closed');
|
|
5811
|
+
this.setAllSubscriptionStatus('reconnecting');
|
|
5812
|
+
this.scheduleReconnect();
|
|
5813
|
+
});
|
|
5814
|
+
});
|
|
5815
|
+
}
|
|
5816
|
+
scheduleReconnect() {
|
|
5817
|
+
if (this.closed)
|
|
5818
|
+
return;
|
|
5819
|
+
if (this.reconnectTimer)
|
|
5820
|
+
clearTimeout(this.reconnectTimer);
|
|
5821
|
+
this.reconnectTimer = setTimeout(() => {
|
|
5822
|
+
this.ensureConnected().catch(() => {
|
|
5823
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
|
|
5824
|
+
this.scheduleReconnect();
|
|
5825
|
+
});
|
|
5826
|
+
}, this.reconnectDelay);
|
|
5827
|
+
}
|
|
5828
|
+
resubscribeAll() {
|
|
5829
|
+
for (const sub of this.subscriptions.values()) {
|
|
5830
|
+
this.sendSubscribe(sub);
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5833
|
+
// -----------------------------------------------------------------------
|
|
5834
|
+
// Message handling
|
|
5835
|
+
// -----------------------------------------------------------------------
|
|
5836
|
+
handleMessage(raw) {
|
|
5837
|
+
const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
|
|
5838
|
+
let msg;
|
|
5839
|
+
try {
|
|
5840
|
+
msg = JSON.parse(text);
|
|
5841
|
+
}
|
|
5842
|
+
catch (_a) {
|
|
5843
|
+
return;
|
|
5844
|
+
}
|
|
5845
|
+
switch (msg.type) {
|
|
5846
|
+
case 'snapshot':
|
|
5847
|
+
this.handleSnapshot(msg);
|
|
5848
|
+
break;
|
|
5849
|
+
case 'delta':
|
|
5850
|
+
this.handleDelta(msg);
|
|
5851
|
+
break;
|
|
5852
|
+
case 'result':
|
|
5853
|
+
this.handleResult(msg);
|
|
5854
|
+
break;
|
|
5855
|
+
case 'error':
|
|
5856
|
+
this.handleError(msg);
|
|
5857
|
+
break;
|
|
5858
|
+
case 'pong':
|
|
5859
|
+
break;
|
|
5860
|
+
// v1 compat: handle legacy message types during transition
|
|
5861
|
+
case 'subscribed':
|
|
5862
|
+
this.handleSnapshot(Object.assign(Object.assign({}, msg), { type: 'snapshot', docs: msg.data }));
|
|
5863
|
+
break;
|
|
5864
|
+
case 'data':
|
|
5865
|
+
// Legacy full-snapshot delta — treat as snapshot replacement
|
|
5866
|
+
this.handleLegacyData(msg);
|
|
5867
|
+
break;
|
|
5868
|
+
case 'response':
|
|
5869
|
+
this.handleResult(Object.assign(Object.assign({}, msg), { type: 'result', ok: msg.status === 200, doc: msg.data }));
|
|
5870
|
+
break;
|
|
5871
|
+
}
|
|
5872
|
+
}
|
|
5873
|
+
handleSnapshot(msg) {
|
|
5874
|
+
var _a, _b, _c;
|
|
5875
|
+
const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;
|
|
5876
|
+
if (!subId)
|
|
5877
|
+
return;
|
|
5878
|
+
const sub = this.findSubscriptionById(subId);
|
|
5879
|
+
if (!sub)
|
|
5880
|
+
return;
|
|
5881
|
+
const docs = (_c = (_b = msg.docs) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : [];
|
|
5882
|
+
const docsArray = Array.isArray(docs) ? docs : [docs];
|
|
5883
|
+
sub.docs.clear();
|
|
5884
|
+
for (const doc of docsArray) {
|
|
5885
|
+
if (doc && doc._id) {
|
|
5886
|
+
sub.docs.set(doc._id, doc);
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
sub.ref.current = sub.docs;
|
|
5890
|
+
sub.status = 'live';
|
|
5891
|
+
sub.isStale = false;
|
|
5892
|
+
sub.error = null;
|
|
5893
|
+
this.notifySubscription(sub);
|
|
5894
|
+
this.markIdbDirty(sub.path);
|
|
5895
|
+
}
|
|
5896
|
+
handleDelta(msg) {
|
|
5897
|
+
var _a, _b;
|
|
5898
|
+
const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;
|
|
5899
|
+
if (!subId)
|
|
5900
|
+
return;
|
|
5901
|
+
const sub = this.findSubscriptionById(subId);
|
|
5902
|
+
if (!sub)
|
|
5903
|
+
return;
|
|
5904
|
+
if (sub.tier === 'ephemeral') {
|
|
5905
|
+
// Ephemeral: just overwrite, no accumulation logic
|
|
5906
|
+
if (msg.change === 'removed' && msg.docId) {
|
|
5907
|
+
sub.docs.delete(msg.docId);
|
|
5908
|
+
}
|
|
5909
|
+
else if (msg.doc && msg.doc._id) {
|
|
5910
|
+
sub.docs.set(msg.doc._id, msg.doc);
|
|
5911
|
+
}
|
|
5912
|
+
sub.ref.current = sub.docs;
|
|
5913
|
+
if (sub.options.mode !== 'ref') {
|
|
5914
|
+
this.notifySubscription(sub);
|
|
5915
|
+
}
|
|
5916
|
+
return;
|
|
5917
|
+
}
|
|
5918
|
+
// Durable/checkpointed: full delta handling
|
|
5919
|
+
switch (msg.change) {
|
|
5920
|
+
case 'added':
|
|
5921
|
+
case 'modified':
|
|
5922
|
+
if (msg.doc && msg.doc._id) {
|
|
5923
|
+
sub.docs.set(msg.doc._id, msg.doc);
|
|
5924
|
+
}
|
|
5925
|
+
break;
|
|
5926
|
+
case 'removed':
|
|
5927
|
+
if (msg.docId) {
|
|
5928
|
+
sub.docs.delete(msg.docId);
|
|
5929
|
+
}
|
|
5930
|
+
else if ((_b = msg.doc) === null || _b === void 0 ? void 0 : _b._id) {
|
|
5931
|
+
sub.docs.delete(msg.doc._id);
|
|
5932
|
+
}
|
|
5933
|
+
break;
|
|
5934
|
+
}
|
|
5935
|
+
sub.ref.current = sub.docs;
|
|
5936
|
+
this.notifySubscription(sub);
|
|
5937
|
+
this.markIdbDirty(sub.path);
|
|
5938
|
+
}
|
|
5939
|
+
handleLegacyData(msg) {
|
|
5940
|
+
// Legacy v1 format: 'data' message with full snapshot or single doc
|
|
5941
|
+
const subId = msg.subscriptionId;
|
|
5942
|
+
if (!subId)
|
|
5943
|
+
return;
|
|
5944
|
+
const sub = this.findSubscriptionById(subId);
|
|
5945
|
+
if (!sub)
|
|
5946
|
+
return;
|
|
5947
|
+
if (Array.isArray(msg.data)) {
|
|
5948
|
+
// Full snapshot replacement
|
|
5949
|
+
sub.docs.clear();
|
|
5950
|
+
for (const doc of msg.data) {
|
|
5951
|
+
if (doc && doc._id)
|
|
5952
|
+
sub.docs.set(doc._id, doc);
|
|
5953
|
+
}
|
|
5954
|
+
}
|
|
5955
|
+
else if (msg.data && msg.data._id) {
|
|
5956
|
+
// Single doc update
|
|
5957
|
+
sub.docs.set(msg.data._id, msg.data);
|
|
5958
|
+
}
|
|
5959
|
+
else if (msg.data === null) ;
|
|
5960
|
+
sub.ref.current = sub.docs;
|
|
5961
|
+
sub.status = 'live';
|
|
5962
|
+
sub.isStale = false;
|
|
5963
|
+
this.notifySubscription(sub);
|
|
5964
|
+
this.markIdbDirty(sub.path);
|
|
5965
|
+
}
|
|
5966
|
+
handleResult(msg) {
|
|
5967
|
+
var _a, _b, _c, _d;
|
|
5968
|
+
const requestId = msg.requestId;
|
|
5969
|
+
if (!requestId)
|
|
5970
|
+
return;
|
|
5971
|
+
const pending = this.pendingRequests.get(requestId);
|
|
5972
|
+
if (!pending)
|
|
5973
|
+
return;
|
|
5974
|
+
this.pendingRequests.delete(requestId);
|
|
5975
|
+
clearTimeout(pending.timeout);
|
|
5976
|
+
const ok = (_a = msg.ok) !== null && _a !== void 0 ? _a : (msg.status === 200);
|
|
5977
|
+
if (ok) {
|
|
5978
|
+
pending.resolve((_c = (_b = msg.doc) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : true);
|
|
5979
|
+
}
|
|
5980
|
+
else {
|
|
5981
|
+
pending.reject(new Error((_d = msg.error) !== null && _d !== void 0 ? _d : 'Operation failed'));
|
|
5982
|
+
}
|
|
5983
|
+
}
|
|
5984
|
+
handleError(msg) {
|
|
5985
|
+
var _a;
|
|
5986
|
+
const requestId = msg.requestId;
|
|
5987
|
+
if (requestId) {
|
|
5988
|
+
const pending = this.pendingRequests.get(requestId);
|
|
5989
|
+
if (pending) {
|
|
5990
|
+
this.pendingRequests.delete(requestId);
|
|
5991
|
+
clearTimeout(pending.timeout);
|
|
5992
|
+
pending.reject(new Error((_a = msg.message) !== null && _a !== void 0 ? _a : 'Server error'));
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
// -----------------------------------------------------------------------
|
|
5997
|
+
// Subscribe
|
|
5998
|
+
// -----------------------------------------------------------------------
|
|
5999
|
+
async subscribe(path, opts = {}) {
|
|
6000
|
+
var _a;
|
|
6001
|
+
const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';
|
|
6002
|
+
const subKey = this.getSubKey(path, opts);
|
|
6003
|
+
let sub = this.subscriptions.get(subKey);
|
|
6004
|
+
if (sub) {
|
|
6005
|
+
// Existing subscription — add callback
|
|
6006
|
+
if (opts.onData)
|
|
6007
|
+
sub.callbacks.add(opts.onData);
|
|
6008
|
+
if (opts.onState)
|
|
6009
|
+
sub.stateCallbacks.add(opts.onState);
|
|
6010
|
+
// Immediately deliver current state
|
|
6011
|
+
if (opts.onData && sub.docs.size > 0) {
|
|
6012
|
+
opts.onData(this.docsToArray(sub));
|
|
6013
|
+
}
|
|
6014
|
+
if (opts.onState) {
|
|
6015
|
+
opts.onState(this.getState(sub));
|
|
6016
|
+
}
|
|
6017
|
+
return this.createUnsubscribe(subKey, opts.onData, opts.onState);
|
|
6018
|
+
}
|
|
6019
|
+
// New subscription
|
|
6020
|
+
const subId = `sub_${nextRequestId++}`;
|
|
6021
|
+
sub = {
|
|
6022
|
+
id: subId,
|
|
6023
|
+
path,
|
|
6024
|
+
tier,
|
|
6025
|
+
options: opts,
|
|
6026
|
+
docs: new Map(),
|
|
6027
|
+
status: 'idle',
|
|
6028
|
+
isStale: false,
|
|
6029
|
+
error: null,
|
|
6030
|
+
callbacks: new Set(opts.onData ? [opts.onData] : []),
|
|
6031
|
+
stateCallbacks: new Set(opts.onState ? [opts.onState] : []),
|
|
6032
|
+
ref: { current: new Map() },
|
|
6033
|
+
};
|
|
6034
|
+
this.subscriptions.set(subKey, sub);
|
|
6035
|
+
// Step 1: Load from IDB (durable/checkpointed only)
|
|
6036
|
+
if (tier !== 'ephemeral') {
|
|
6037
|
+
const cached = await idbGet(this.idbKey(path));
|
|
6038
|
+
if (cached && Array.isArray(cached)) {
|
|
6039
|
+
for (const doc of cached) {
|
|
6040
|
+
if (doc && doc._id)
|
|
6041
|
+
sub.docs.set(doc._id, doc);
|
|
6042
|
+
}
|
|
6043
|
+
sub.ref.current = sub.docs;
|
|
6044
|
+
sub.status = 'cached';
|
|
6045
|
+
sub.isStale = true;
|
|
6046
|
+
this.notifySubscription(sub);
|
|
6047
|
+
}
|
|
6048
|
+
}
|
|
6049
|
+
// Step 2: Connect and subscribe via WS
|
|
6050
|
+
sub.status = sub.docs.size > 0 ? 'cached' : 'loading';
|
|
6051
|
+
this.notifyState(sub);
|
|
6052
|
+
try {
|
|
6053
|
+
await this.ensureConnected();
|
|
6054
|
+
this.sendSubscribe(sub);
|
|
6055
|
+
}
|
|
6056
|
+
catch (_b) {
|
|
6057
|
+
sub.status = 'error';
|
|
6058
|
+
sub.error = new Error('Connection failed');
|
|
6059
|
+
this.notifyState(sub);
|
|
6060
|
+
}
|
|
6061
|
+
return this.createUnsubscribe(subKey, opts.onData, opts.onState);
|
|
6062
|
+
}
|
|
6063
|
+
getRef(path, opts = {}) {
|
|
6064
|
+
var _a;
|
|
6065
|
+
const subKey = this.getSubKey(path, opts);
|
|
6066
|
+
const sub = this.subscriptions.get(subKey);
|
|
6067
|
+
if (sub)
|
|
6068
|
+
return sub.ref;
|
|
6069
|
+
// Auto-subscribe in ref mode
|
|
6070
|
+
const ref = { current: new Map() };
|
|
6071
|
+
this.subscribe(path, Object.assign(Object.assign({}, opts), { mode: 'ref', tier: 'ephemeral' })).catch(() => { });
|
|
6072
|
+
const newSub = this.subscriptions.get(this.getSubKey(path, Object.assign(Object.assign({}, opts), { tier: 'ephemeral' })));
|
|
6073
|
+
return (_a = newSub === null || newSub === void 0 ? void 0 : newSub.ref) !== null && _a !== void 0 ? _a : ref;
|
|
6074
|
+
}
|
|
6075
|
+
// -----------------------------------------------------------------------
|
|
6076
|
+
// CRUD operations
|
|
6077
|
+
// -----------------------------------------------------------------------
|
|
6078
|
+
async set(path, doc) {
|
|
6079
|
+
var _a;
|
|
6080
|
+
await this.ensureConnected();
|
|
6081
|
+
// Resolve operations (Increment, Time.Now) client-side for optimistic update
|
|
6082
|
+
const resolvedDoc = this.resolveOperations(doc, path);
|
|
6083
|
+
// Optimistic update: apply to local state immediately
|
|
6084
|
+
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
6085
|
+
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
6086
|
+
const optimisticDoc = Object.assign(Object.assign({ _id: normalizedPath, pathId: normalizedPath }, resolvedDoc), { tarobase_updated_at: Date.now() });
|
|
6087
|
+
const sub = this.findSubscriptionByPath(collectionPath);
|
|
6088
|
+
let prevDoc = null;
|
|
6089
|
+
if (sub) {
|
|
6090
|
+
prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;
|
|
6091
|
+
sub.docs.set(normalizedPath, optimisticDoc);
|
|
6092
|
+
sub.ref.current = sub.docs;
|
|
6093
|
+
this.notifySubscription(sub);
|
|
6094
|
+
}
|
|
6095
|
+
// Send to server
|
|
6096
|
+
const requestId = `r_${nextRequestId++}`;
|
|
6097
|
+
try {
|
|
6098
|
+
const result = await this.sendRequest(requestId, {
|
|
6099
|
+
type: 'set',
|
|
6100
|
+
requestId,
|
|
6101
|
+
documents: [{ destinationPath: normalizedPath, document: doc }],
|
|
6102
|
+
});
|
|
6103
|
+
// Replace optimistic doc with server-confirmed version
|
|
6104
|
+
if (sub && result && typeof result === 'object') {
|
|
6105
|
+
const serverDoc = Array.isArray(result) ? result[0] : result;
|
|
6106
|
+
if (serverDoc && serverDoc._id) {
|
|
6107
|
+
sub.docs.set(serverDoc._id, serverDoc);
|
|
6108
|
+
sub.ref.current = sub.docs;
|
|
6109
|
+
this.notifySubscription(sub);
|
|
6110
|
+
this.markIdbDirty(collectionPath);
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
return Array.isArray(result) ? result[0] : result;
|
|
6114
|
+
}
|
|
6115
|
+
catch (err) {
|
|
6116
|
+
// Revert optimistic update
|
|
6117
|
+
if (sub) {
|
|
6118
|
+
if (prevDoc) {
|
|
6119
|
+
sub.docs.set(normalizedPath, prevDoc);
|
|
6120
|
+
}
|
|
6121
|
+
else {
|
|
6122
|
+
sub.docs.delete(normalizedPath);
|
|
6123
|
+
}
|
|
6124
|
+
sub.ref.current = sub.docs;
|
|
6125
|
+
this.notifySubscription(sub);
|
|
6126
|
+
}
|
|
6127
|
+
throw err;
|
|
6128
|
+
}
|
|
6129
|
+
}
|
|
6130
|
+
async get(path) {
|
|
6131
|
+
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
6132
|
+
// Check local subscriptions first
|
|
6133
|
+
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
6134
|
+
const sub = this.findSubscriptionByPath(collectionPath);
|
|
6135
|
+
if (sub && sub.status === 'live') {
|
|
6136
|
+
const doc = sub.docs.get(normalizedPath);
|
|
6137
|
+
return doc !== null && doc !== void 0 ? doc : null;
|
|
6138
|
+
}
|
|
6139
|
+
// One-shot WS fetch
|
|
6140
|
+
await this.ensureConnected();
|
|
6141
|
+
const requestId = `r_${nextRequestId++}`;
|
|
6142
|
+
return this.sendRequest(requestId, {
|
|
6143
|
+
type: 'get',
|
|
6144
|
+
requestId,
|
|
6145
|
+
path: normalizedPath,
|
|
6146
|
+
});
|
|
6147
|
+
}
|
|
6148
|
+
async getMany(paths) {
|
|
6149
|
+
await this.ensureConnected();
|
|
6150
|
+
const normalizedPaths = paths.map(p => p.startsWith('/') ? p.slice(1) : p);
|
|
6151
|
+
const requestId = `r_${nextRequestId++}`;
|
|
6152
|
+
return this.sendRequest(requestId, {
|
|
6153
|
+
type: 'getMany',
|
|
6154
|
+
requestId,
|
|
6155
|
+
paths: normalizedPaths,
|
|
6156
|
+
});
|
|
6157
|
+
}
|
|
6158
|
+
async delete(path) {
|
|
6159
|
+
var _a;
|
|
6160
|
+
await this.ensureConnected();
|
|
6161
|
+
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
6162
|
+
// Optimistic: remove from local state
|
|
6163
|
+
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
6164
|
+
const sub = this.findSubscriptionByPath(collectionPath);
|
|
6165
|
+
let prevDoc = null;
|
|
6166
|
+
if (sub) {
|
|
6167
|
+
prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;
|
|
6168
|
+
sub.docs.delete(normalizedPath);
|
|
6169
|
+
sub.ref.current = sub.docs;
|
|
6170
|
+
this.notifySubscription(sub);
|
|
6171
|
+
}
|
|
6172
|
+
const requestId = `r_${nextRequestId++}`;
|
|
6173
|
+
try {
|
|
6174
|
+
await this.sendRequest(requestId, {
|
|
6175
|
+
type: 'delete',
|
|
6176
|
+
requestId,
|
|
6177
|
+
path: normalizedPath,
|
|
6178
|
+
});
|
|
6179
|
+
if (sub)
|
|
6180
|
+
this.markIdbDirty(collectionPath);
|
|
6181
|
+
}
|
|
6182
|
+
catch (err) {
|
|
6183
|
+
// Revert
|
|
6184
|
+
if (sub && prevDoc) {
|
|
6185
|
+
sub.docs.set(normalizedPath, prevDoc);
|
|
6186
|
+
sub.ref.current = sub.docs;
|
|
6187
|
+
this.notifySubscription(sub);
|
|
6188
|
+
}
|
|
6189
|
+
throw err;
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6192
|
+
async query(path, opts) {
|
|
6193
|
+
await this.ensureConnected();
|
|
6194
|
+
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
6195
|
+
const requestId = `r_${nextRequestId++}`;
|
|
6196
|
+
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 } : {})));
|
|
6197
|
+
}
|
|
6198
|
+
async count(path) {
|
|
6199
|
+
var _a;
|
|
6200
|
+
await this.ensureConnected();
|
|
6201
|
+
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
6202
|
+
const requestId = `r_${nextRequestId++}`;
|
|
6203
|
+
const result = await this.sendRequest(requestId, {
|
|
6204
|
+
type: 'count',
|
|
6205
|
+
requestId,
|
|
6206
|
+
path: normalizedPath,
|
|
6207
|
+
});
|
|
6208
|
+
return typeof result === 'number' ? result : ((_a = result === null || result === void 0 ? void 0 : result.value) !== null && _a !== void 0 ? _a : 0);
|
|
6209
|
+
}
|
|
6210
|
+
async aggregate(path, operation, opts) {
|
|
6211
|
+
await this.ensureConnected();
|
|
6212
|
+
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
6213
|
+
const requestId = `r_${nextRequestId++}`;
|
|
6214
|
+
return this.sendRequest(requestId, Object.assign({ type: 'aggregate', requestId, path: normalizedPath, operation }, ((opts === null || opts === void 0 ? void 0 : opts.field) ? { field: opts.field } : {})));
|
|
6215
|
+
}
|
|
6216
|
+
// -----------------------------------------------------------------------
|
|
6217
|
+
// Helpers
|
|
6218
|
+
// -----------------------------------------------------------------------
|
|
6219
|
+
sendSubscribe(sub) {
|
|
6220
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
6221
|
+
return;
|
|
6222
|
+
const msg = {
|
|
6223
|
+
type: 'subscribe',
|
|
6224
|
+
subscriptionId: sub.id,
|
|
6225
|
+
path: sub.path,
|
|
6226
|
+
};
|
|
6227
|
+
if (sub.options.filter)
|
|
6228
|
+
msg.filter = sub.options.filter;
|
|
6229
|
+
if (sub.options.includeSubPaths)
|
|
6230
|
+
msg.includeSubPaths = true;
|
|
6231
|
+
if (sub.options.limit)
|
|
6232
|
+
msg.limit = sub.options.limit;
|
|
6233
|
+
if (sub.options.prompt)
|
|
6234
|
+
msg.prompt = sub.options.prompt;
|
|
6235
|
+
this.ws.send(JSON.stringify(msg));
|
|
6236
|
+
}
|
|
6237
|
+
sendRequest(requestId, msg) {
|
|
6238
|
+
return new Promise((resolve, reject) => {
|
|
6239
|
+
const timeout = setTimeout(() => {
|
|
6240
|
+
this.pendingRequests.delete(requestId);
|
|
6241
|
+
reject(new Error('Request timed out'));
|
|
6242
|
+
}, 30000);
|
|
6243
|
+
this.pendingRequests.set(requestId, { resolve, reject, timeout });
|
|
6244
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
6245
|
+
this.ws.send(JSON.stringify(msg));
|
|
6246
|
+
}
|
|
6247
|
+
else {
|
|
6248
|
+
this.pendingRequests.delete(requestId);
|
|
6249
|
+
clearTimeout(timeout);
|
|
6250
|
+
reject(new Error('WebSocket not connected'));
|
|
6251
|
+
}
|
|
6252
|
+
});
|
|
6253
|
+
}
|
|
6254
|
+
notifySubscription(sub) {
|
|
6255
|
+
const data = this.docsToArray(sub);
|
|
6256
|
+
const callbacks = Array.from(sub.callbacks);
|
|
6257
|
+
for (const cb of callbacks) {
|
|
6258
|
+
try {
|
|
6259
|
+
cb(data);
|
|
6260
|
+
}
|
|
6261
|
+
catch ( /* swallow callback errors */_a) { /* swallow callback errors */ }
|
|
6262
|
+
}
|
|
6263
|
+
this.notifyState(sub);
|
|
6264
|
+
}
|
|
6265
|
+
notifyState(sub) {
|
|
6266
|
+
const state = this.getState(sub);
|
|
6267
|
+
const callbacks = Array.from(sub.stateCallbacks);
|
|
6268
|
+
for (const cb of callbacks) {
|
|
6269
|
+
try {
|
|
6270
|
+
cb(state);
|
|
6271
|
+
}
|
|
6272
|
+
catch ( /* swallow */_a) { /* swallow */ }
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
getState(sub) {
|
|
6276
|
+
return {
|
|
6277
|
+
data: this.docsToArray(sub),
|
|
6278
|
+
status: sub.status,
|
|
6279
|
+
isStale: sub.isStale,
|
|
6280
|
+
error: sub.error,
|
|
6281
|
+
};
|
|
6282
|
+
}
|
|
6283
|
+
docsToArray(sub) {
|
|
6284
|
+
return Array.from(sub.docs.values());
|
|
6285
|
+
}
|
|
6286
|
+
findSubscriptionById(id) {
|
|
6287
|
+
for (const sub of this.subscriptions.values()) {
|
|
6288
|
+
if (sub.id === id)
|
|
6289
|
+
return sub;
|
|
6290
|
+
}
|
|
6291
|
+
return undefined;
|
|
6292
|
+
}
|
|
6293
|
+
findSubscriptionByPath(collectionPath) {
|
|
6294
|
+
for (const sub of this.subscriptions.values()) {
|
|
6295
|
+
const subPath = sub.path.startsWith('/') ? sub.path.slice(1) : sub.path;
|
|
6296
|
+
if (subPath === collectionPath)
|
|
6297
|
+
return sub;
|
|
6298
|
+
if (collectionPath.startsWith(subPath + '/'))
|
|
6299
|
+
return sub;
|
|
6300
|
+
}
|
|
6301
|
+
return undefined;
|
|
6302
|
+
}
|
|
6303
|
+
getCollectionPath(docPath) {
|
|
6304
|
+
const segments = docPath.split('/');
|
|
6305
|
+
if (segments.length % 2 === 0) {
|
|
6306
|
+
return segments.slice(0, -1).join('/');
|
|
6307
|
+
}
|
|
6308
|
+
return docPath;
|
|
6309
|
+
}
|
|
6310
|
+
getSubKey(path, opts) {
|
|
6311
|
+
const parts = [path];
|
|
6312
|
+
if (opts.filter)
|
|
6313
|
+
parts.push(JSON.stringify(opts.filter));
|
|
6314
|
+
if (opts.prompt)
|
|
6315
|
+
parts.push(opts.prompt);
|
|
6316
|
+
if (opts.tier)
|
|
6317
|
+
parts.push(opts.tier);
|
|
6318
|
+
return parts.join('::');
|
|
6319
|
+
}
|
|
6320
|
+
idbKey(path) {
|
|
6321
|
+
return `${this.appId}:${path}`;
|
|
6322
|
+
}
|
|
6323
|
+
markIdbDirty(path) {
|
|
6324
|
+
const sub = this.findSubscriptionByPath(path);
|
|
6325
|
+
if (sub && sub.tier === 'ephemeral')
|
|
6326
|
+
return;
|
|
6327
|
+
this.idbDirtyKeys.add(path);
|
|
6328
|
+
if (!this.idbFlushTimer) {
|
|
6329
|
+
this.idbFlushTimer = setTimeout(() => {
|
|
6330
|
+
this.flushIdb();
|
|
6331
|
+
this.idbFlushTimer = null;
|
|
6332
|
+
}, 500);
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
6335
|
+
async flushIdb() {
|
|
6336
|
+
const keys = Array.from(this.idbDirtyKeys);
|
|
6337
|
+
this.idbDirtyKeys.clear();
|
|
6338
|
+
for (const path of keys) {
|
|
6339
|
+
const sub = this.findSubscriptionByPath(path);
|
|
6340
|
+
if (sub && sub.tier !== 'ephemeral') {
|
|
6341
|
+
const docs = this.docsToArray(sub);
|
|
6342
|
+
await idbSet(this.idbKey(path), docs);
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6346
|
+
createUnsubscribe(subKey, onData, onState) {
|
|
6347
|
+
return async () => {
|
|
6348
|
+
const sub = this.subscriptions.get(subKey);
|
|
6349
|
+
if (!sub)
|
|
6350
|
+
return;
|
|
6351
|
+
if (onData)
|
|
6352
|
+
sub.callbacks.delete(onData);
|
|
6353
|
+
if (onState)
|
|
6354
|
+
sub.stateCallbacks.delete(onState);
|
|
6355
|
+
// If no more callbacks, unsubscribe entirely
|
|
6356
|
+
if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0) {
|
|
6357
|
+
this.subscriptions.delete(subKey);
|
|
6358
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
6359
|
+
this.ws.send(JSON.stringify({
|
|
6360
|
+
type: 'unsubscribe',
|
|
6361
|
+
subscriptionId: sub.id,
|
|
6362
|
+
}));
|
|
6363
|
+
}
|
|
6364
|
+
}
|
|
6365
|
+
};
|
|
6366
|
+
}
|
|
6367
|
+
resolveOperations(doc, path) {
|
|
6368
|
+
var _a;
|
|
6369
|
+
if (!doc || typeof doc !== 'object')
|
|
6370
|
+
return doc;
|
|
6371
|
+
const resolved = {};
|
|
6372
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
6373
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && value.operation) {
|
|
6374
|
+
const op = value;
|
|
6375
|
+
if (op.operation === 'time' && op.value === 'now') {
|
|
6376
|
+
resolved[key] = Math.floor(Date.now() / 1000);
|
|
6377
|
+
}
|
|
6378
|
+
else if (op.operation === 'increment') {
|
|
6379
|
+
// For optimistic: get current value and add
|
|
6380
|
+
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
6381
|
+
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
6382
|
+
const sub = this.findSubscriptionByPath(collectionPath);
|
|
6383
|
+
const existing = sub === null || sub === void 0 ? void 0 : sub.docs.get(normalizedPath);
|
|
6384
|
+
const current = (_a = existing === null || existing === void 0 ? void 0 : existing[key]) !== null && _a !== void 0 ? _a : 0;
|
|
6385
|
+
resolved[key] = (typeof current === 'number' ? current : 0) + op.value;
|
|
6386
|
+
}
|
|
6387
|
+
else {
|
|
6388
|
+
resolved[key] = value;
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
else {
|
|
6392
|
+
resolved[key] = value;
|
|
6393
|
+
}
|
|
6394
|
+
}
|
|
6395
|
+
return resolved;
|
|
6396
|
+
}
|
|
6397
|
+
rejectAllPending(reason) {
|
|
6398
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
6399
|
+
clearTimeout(pending.timeout);
|
|
6400
|
+
pending.reject(new Error(reason));
|
|
6401
|
+
}
|
|
6402
|
+
this.pendingRequests.clear();
|
|
6403
|
+
}
|
|
6404
|
+
setAllSubscriptionStatus(status) {
|
|
6405
|
+
for (const sub of this.subscriptions.values()) {
|
|
6406
|
+
sub.status = status;
|
|
6407
|
+
this.notifyState(sub);
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
// -----------------------------------------------------------------------
|
|
6411
|
+
// Lifecycle
|
|
6412
|
+
// -----------------------------------------------------------------------
|
|
6413
|
+
close() {
|
|
6414
|
+
this.closed = true;
|
|
6415
|
+
if (this.reconnectTimer)
|
|
6416
|
+
clearTimeout(this.reconnectTimer);
|
|
6417
|
+
if (this.idbFlushTimer)
|
|
6418
|
+
clearTimeout(this.idbFlushTimer);
|
|
6419
|
+
this.flushIdb();
|
|
6420
|
+
if (this.ws) {
|
|
6421
|
+
this.ws.close(1000, 'Store closed');
|
|
6422
|
+
this.ws = null;
|
|
6423
|
+
}
|
|
6424
|
+
this.rejectAllPending('Store closed');
|
|
6425
|
+
this.subscriptions.clear();
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
// ---------------------------------------------------------------------------
|
|
6429
|
+
// Singleton instance
|
|
6430
|
+
// ---------------------------------------------------------------------------
|
|
6431
|
+
let storeInstance = null;
|
|
6432
|
+
function getRealtimeStore() {
|
|
6433
|
+
if (!storeInstance) {
|
|
6434
|
+
storeInstance = new RealtimeStore();
|
|
6435
|
+
}
|
|
6436
|
+
return storeInstance;
|
|
6437
|
+
}
|
|
6438
|
+
function resetRealtimeStore() {
|
|
6439
|
+
if (storeInstance) {
|
|
6440
|
+
storeInstance.close();
|
|
6441
|
+
storeInstance = null;
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
|
|
5470
6445
|
exports.InsufficientBalanceError = InsufficientBalanceError;
|
|
5471
6446
|
exports.ReactNativeSessionManager = ReactNativeSessionManager;
|
|
6447
|
+
exports.RealtimeStore = RealtimeStore;
|
|
5472
6448
|
exports.ServerSessionManager = ServerSessionManager;
|
|
5473
6449
|
exports.WebSessionManager = WebSessionManager;
|
|
5474
6450
|
exports.aggregate = aggregate;
|
|
@@ -5487,9 +6463,12 @@ exports.getConfig = getConfig;
|
|
|
5487
6463
|
exports.getFiles = getFiles;
|
|
5488
6464
|
exports.getIdToken = getIdToken;
|
|
5489
6465
|
exports.getMany = getMany;
|
|
6466
|
+
exports.getRealtimeStore = getRealtimeStore;
|
|
6467
|
+
exports.hasActiveConnection = hasActiveConnection;
|
|
5490
6468
|
exports.init = init;
|
|
5491
6469
|
exports.reconnectWithNewAuth = reconnectWithNewAuth;
|
|
5492
6470
|
exports.refreshSession = refreshSession;
|
|
6471
|
+
exports.resetRealtimeStore = resetRealtimeStore;
|
|
5493
6472
|
exports.runExpression = runExpression;
|
|
5494
6473
|
exports.runExpressionMany = runExpressionMany;
|
|
5495
6474
|
exports.runQuery = runQuery;
|
|
@@ -5502,4 +6481,9 @@ exports.signMessage = signMessage;
|
|
|
5502
6481
|
exports.signSessionCreateMessage = signSessionCreateMessage;
|
|
5503
6482
|
exports.signTransaction = signTransaction;
|
|
5504
6483
|
exports.subscribe = subscribe;
|
|
6484
|
+
exports.wsDelete = wsDelete;
|
|
6485
|
+
exports.wsGet = wsGet;
|
|
6486
|
+
exports.wsGetMany = wsGetMany;
|
|
6487
|
+
exports.wsQuery = wsQuery;
|
|
6488
|
+
exports.wsSet = wsSet;
|
|
5505
6489
|
//# sourceMappingURL=index.js.map
|