@pooflabs/core 0.0.49 → 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/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
- // Cache miss or bypass - proceed with API request
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
- setResponse = await makeApiRequest('PUT', `items`, { documents }, options === null || options === void 0 ? void 0 : options._overrides);
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((_c = error.deficitLamports) !== null && _c !== void 0 ? _c : 0);
4260
- const deficitSol = Number((_d = error.deficitSol) !== null && _d !== void 0 ? _d : deficitLamports / 1000000000);
4261
- throw new InsufficientBalanceError(String((_e = error.address) !== null && _e !== void 0 ? _e : ''), Number((_f = error.balanceLamports) !== null && _f !== void 0 ? _f : 0), Number((_g = error.estimatedCostLamports) !== null && _g !== void 0 ? _g : 0), deficitLamports, deficitSol);
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 _h = setResponse.data, { success: _success } = _h, rest = __rest(_h, ["success"]);
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 {
@@ -5349,6 +5394,21 @@ async function wsGetMany(paths) {
5349
5394
  }));
5350
5395
  }
5351
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
+ });
5411
+
5352
5412
  /**
5353
5413
  * WebSocket Subscription Module
5354
5414
  *
@@ -5602,8 +5662,789 @@ class ReactNativeSessionManager {
5602
5662
  }
5603
5663
  ReactNativeSessionManager.TAROBASE_SESSION_STORAGE_KEY = "tarobase_session_storage";
5604
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
+
5605
6445
  exports.InsufficientBalanceError = InsufficientBalanceError;
5606
6446
  exports.ReactNativeSessionManager = ReactNativeSessionManager;
6447
+ exports.RealtimeStore = RealtimeStore;
5607
6448
  exports.ServerSessionManager = ServerSessionManager;
5608
6449
  exports.WebSessionManager = WebSessionManager;
5609
6450
  exports.aggregate = aggregate;
@@ -5622,10 +6463,12 @@ exports.getConfig = getConfig;
5622
6463
  exports.getFiles = getFiles;
5623
6464
  exports.getIdToken = getIdToken;
5624
6465
  exports.getMany = getMany;
6466
+ exports.getRealtimeStore = getRealtimeStore;
5625
6467
  exports.hasActiveConnection = hasActiveConnection;
5626
6468
  exports.init = init;
5627
6469
  exports.reconnectWithNewAuth = reconnectWithNewAuth;
5628
6470
  exports.refreshSession = refreshSession;
6471
+ exports.resetRealtimeStore = resetRealtimeStore;
5629
6472
  exports.runExpression = runExpression;
5630
6473
  exports.runExpressionMany = runExpressionMany;
5631
6474
  exports.runQuery = runQuery;