@bounded-sh/core 0.0.51 → 0.0.53-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/client/operations.d.ts +2 -0
  2. package/dist/client/subscribe-query.d.ts +101 -0
  3. package/dist/{index-DpfaeNTQ.mjs → index-DHigIn6m.mjs} +598 -11
  4. package/dist/index-DHigIn6m.mjs.map +1 -0
  5. package/dist/{index-BaSw33ec.js → index-Dmnzhzk1.js} +603 -10
  6. package/dist/index-Dmnzhzk1.js.map +1 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +7 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +1 -1
  11. package/dist/{realtime-store-PVPxsvPr.js → realtime-store-4Yr6LyTX.js} +2 -2
  12. package/dist/{realtime-store-PVPxsvPr.js.map → realtime-store-4Yr6LyTX.js.map} +1 -1
  13. package/dist/{realtime-store-CBdRkQfZ.mjs → realtime-store-DkvVNYdn.mjs} +2 -2
  14. package/dist/{realtime-store-CBdRkQfZ.mjs.map → realtime-store-DkvVNYdn.mjs.map} +1 -1
  15. package/dist/{sol-utils-pVrt7VOx.js → sol-utils-Dni65Z63.js} +3 -4
  16. package/dist/sol-utils-Dni65Z63.js.map +1 -0
  17. package/dist/{sol-utils-D0Tdlpsc.mjs → sol-utils-v8cCs2pm.mjs} +3 -4
  18. package/dist/sol-utils-v8cCs2pm.mjs.map +1 -0
  19. package/dist/{solana-message-BVJWVB6C.js → solana-message-BP0L24CQ.js} +2 -2
  20. package/dist/{solana-message-BVJWVB6C.js.map → solana-message-BP0L24CQ.js.map} +1 -1
  21. package/dist/{solana-message-kTeT43eQ.mjs → solana-message-DoiFXEx_.mjs} +2 -2
  22. package/dist/{solana-message-kTeT43eQ.mjs.map → solana-message-DoiFXEx_.mjs.map} +1 -1
  23. package/dist/{solana-operations-bXuuPlov.js → solana-operations-DZldDVeJ.js} +2 -2
  24. package/dist/{solana-operations-bXuuPlov.js.map → solana-operations-DZldDVeJ.js.map} +1 -1
  25. package/dist/{solana-operations-B8UyHL26.mjs → solana-operations-SSv6xHs-.mjs} +2 -2
  26. package/dist/{solana-operations-B8UyHL26.mjs.map → solana-operations-SSv6xHs-.mjs.map} +1 -1
  27. package/package.json +2 -2
  28. package/dist/index-BaSw33ec.js.map +0 -1
  29. package/dist/index-DpfaeNTQ.mjs.map +0 -1
  30. package/dist/sol-utils-D0Tdlpsc.mjs.map +0 -1
  31. package/dist/sol-utils-pVrt7VOx.js.map +0 -1
@@ -3021,8 +3021,13 @@ class WebSessionManager {
3021
3021
  const { exp } = JSON.parse(this.decodeBase64Url(idToken.split(".")[1]));
3022
3022
  if (Date.now() > exp * 1000) {
3023
3023
  const { refreshToken } = sessionObj;
3024
- if (!refreshToken)
3024
+ // Expired token with no way to refresh — clear the stale session
3025
+ // (mirrors the refresh-failed branch below) so isAuthenticated()
3026
+ // does not stay true and every request does not 401 + retry forever.
3027
+ if (!refreshToken) {
3028
+ this.clearSession();
3025
3029
  return null;
3030
+ }
3026
3031
  const refreshed = await refreshSession(refreshToken, sessionObj.issuer);
3027
3032
  if ((refreshed === null || refreshed === void 0 ? void 0 : refreshed.idToken) && (refreshed === null || refreshed === void 0 ? void 0 : refreshed.accessToken)) {
3028
3033
  await this.updateIdTokenAndAccessToken(refreshed.idToken, refreshed.accessToken,
@@ -3233,8 +3238,13 @@ class ReactNativeSessionManager {
3233
3238
  const { exp } = JSON.parse(this.decodeBase64Url(idToken.split(".")[1]));
3234
3239
  if (Date.now() > exp * 1000) {
3235
3240
  const { refreshToken } = sessionObj;
3236
- if (!refreshToken)
3241
+ // Expired token with no way to refresh — clear the stale session
3242
+ // (mirrors the refresh-failed branch below) so isAuthenticated()
3243
+ // does not stay true and every request does not 401 + retry forever.
3244
+ if (!refreshToken) {
3245
+ this.clearSession();
3237
3246
  return null;
3247
+ }
3238
3248
  const refreshed = await refreshSession(refreshToken, sessionObj.issuer);
3239
3249
  if ((refreshed === null || refreshed === void 0 ? void 0 : refreshed.idToken) && (refreshed === null || refreshed === void 0 ? void 0 : refreshed.accessToken)) {
3240
3250
  await this.updateIdTokenAndAccessToken(refreshed.idToken, refreshed.accessToken, refreshed.refreshToken);
@@ -3410,7 +3420,7 @@ class ServerSessionManager {
3410
3420
  const address = kp.publicKey.toBase58();
3411
3421
  /* fetch nonce from auth API */
3412
3422
  const nonce = await genAuthNonce();
3413
- const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-BVJWVB6C.js'); });
3423
+ const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-BP0L24CQ.js'); });
3414
3424
  const message = await genSolanaMessage(address, nonce);
3415
3425
  /* sign the message */
3416
3426
  const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
@@ -5557,11 +5567,11 @@ async function setMany(many, options) {
5557
5567
  transactionResult = await handleEvmTransaction(curTx.evmTransaction, authProvider, options);
5558
5568
  }
5559
5569
  else if (curTx.serializedTransaction) {
5560
- const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-bXuuPlov.js'); });
5570
+ const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-DZldDVeJ.js'); });
5561
5571
  transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
5562
5572
  }
5563
5573
  else {
5564
- const { handleSolanaTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-bXuuPlov.js'); });
5574
+ const { handleSolanaTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-DZldDVeJ.js'); });
5565
5575
  transactionResult = await handleSolanaTransaction(curTx, authProvider, options, expectedWritePaths);
5566
5576
  }
5567
5577
  // Sync items after all transactions are confirmed
@@ -5670,6 +5680,18 @@ function clearCacheByPrefix(prefix) {
5670
5680
  }
5671
5681
  });
5672
5682
  }
5683
+ /**
5684
+ * Listeners notified after an identity change wipes the read caches. Registered
5685
+ * via `onReadCacheAuthChange` so downstream modules (e.g. the subscribeQuery poll
5686
+ * manager) can re-key/drop stale in-flight work WITHOUT operations.ts importing
5687
+ * them (avoids a static cycle; keeps the coalesce test bundle lean).
5688
+ */
5689
+ const authChangeListeners = new Set();
5690
+ /** Register a callback invoked after `clearReadCacheForAuthChange`. Returns an unregister fn. */
5691
+ function onReadCacheAuthChange(listener) {
5692
+ authChangeListeners.add(listener);
5693
+ return () => { authChangeListeners.delete(listener); };
5694
+ }
5673
5695
  /**
5674
5696
  * SECURITY (H1): Wipe ALL HTTP read caches + in-flight reads. Call this whenever
5675
5697
  * the logged-in identity changes (login / logout / switch identity) so a freshly
@@ -5679,6 +5701,12 @@ function clearCacheByPrefix(prefix) {
5679
5701
  function clearReadCacheForAuthChange() {
5680
5702
  Object.keys(getCache).forEach(key => delete getCache[key]);
5681
5703
  Object.keys(pendingRequests).forEach(key => delete pendingRequests[key]);
5704
+ authChangeListeners.forEach(fn => {
5705
+ try {
5706
+ fn();
5707
+ }
5708
+ catch ( /* a listener throwing must not abort the cache wipe */_a) { /* a listener throwing must not abort the cache wipe */ }
5709
+ });
5682
5710
  }
5683
5711
  async function getFiles(path, options) {
5684
5712
  try {
@@ -5686,7 +5714,7 @@ async function getFiles(path, options) {
5686
5714
  if (!normalizedPath || normalizedPath.length === 0) {
5687
5715
  throw new Error("Invalid path provided.");
5688
5716
  }
5689
- const apiPath = `storage?path=${normalizedPath}`;
5717
+ const apiPath = `storage?path=${encodeURIComponent(normalizedPath)}`;
5690
5718
  const response = await makeApiRequest('GET', apiPath, null, options === null || options === void 0 ? void 0 : options._overrides);
5691
5719
  return response.data;
5692
5720
  }
@@ -5780,6 +5808,7 @@ var operations = /*#__PURE__*/Object.freeze({
5780
5808
  get: get,
5781
5809
  getFiles: getFiles,
5782
5810
  getMany: getMany,
5811
+ onReadCacheAuthChange: onReadCacheAuthChange,
5783
5812
  queryAggregate: queryAggregate,
5784
5813
  runExpression: runExpression,
5785
5814
  runExpressionMany: runExpressionMany,
@@ -7476,7 +7505,7 @@ async function doReconnectWithNewAuth() {
7476
7505
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
7477
7506
  }
7478
7507
  try {
7479
- const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-PVPxsvPr.js'); });
7508
+ const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-4Yr6LyTX.js'); });
7480
7509
  await reconnectRealtimeStoreWithNewAuth();
7481
7510
  }
7482
7511
  catch (error) {
@@ -7711,6 +7740,564 @@ async function reconnectWithNewAuth() {
7711
7740
  return reconnectWithNewAuthV2();
7712
7741
  }
7713
7742
 
7743
+ /**
7744
+ * Live named-query subscriptions (subscribeQuery) - HTTP client poller (v1).
7745
+ *
7746
+ * A standardized client-side poller for a policy's computed/named queries. Same
7747
+ * callback contract as `subscribe()` (onData/onError), but instead of a socket it
7748
+ * polls `runQuery` on an interval. It exists to replace ad-hoc app-side polling
7749
+ * with one controlled, deduped, backoff-aware poller.
7750
+ *
7751
+ * v1 is HTTP-only and requires ZERO server change: it rides the existing
7752
+ * `runQuery` microtask coalescer (operations.ts), which batches due keys into
7753
+ * <=10-per-POST chunks, surfaces per-row errors, and (via utils/api) does a
7754
+ * bounded in-request 429 retry. The websocket transport and true server-push are
7755
+ * deferred; see LIVE-QUERIES-SDK-PLAN.md section 9. The public contract here is
7756
+ * pinned so either upgrade stays transparent.
7757
+ *
7758
+ * Design notes that are load-bearing (do not "simplify" away):
7759
+ * - The coalescer does NOT dedupe; the manager's refcount dedupe below is the
7760
+ * ONLY dedupe, so it is load-bearing for billed-read correctness.
7761
+ * - There is no durable transport backoff: `utils/api` retries twice then
7762
+ * rejects. Steady-state backoff on a rejected poll is owned HERE.
7763
+ * - A transport rejection (429/network/chunk) must NOT reach onError - it would
7764
+ * blast the whole up-to-10-key chunk every interval and rebuild the exact 429
7765
+ * noise this feature removes. Only a genuine per-row query error / policy
7766
+ * decline reaches onError.
7767
+ */
7768
+ /** Default poll cadence. Sits on the hot named-query cadence oapps already runs. */
7769
+ const DEFAULT_QUERY_POLL_INTERVAL_MS = 5000;
7770
+ /** Hard lower clamp so an app cannot rebuild the 429 storm. */
7771
+ const MIN_QUERY_POLL_INTERVAL_MS = 1000;
7772
+ /** Ceiling for the manager-owned exponential backoff on a rejected poll. */
7773
+ const QUERY_POLL_BACKOFF_CAP_MS = 60000;
7774
+ /** Max extra delay sprinkled on a visibility-resume refresh so N keys don't burst-sync. */
7775
+ const RESUME_JITTER_MS = 250;
7776
+ /** Retry cadence for a re-key that could not (re-)attach - so a live query is never
7777
+ * silently orphaned by a transient auth failure during an identity change. */
7778
+ const REATTACH_RETRY_MS = 3000;
7779
+ /**
7780
+ * Server sentinel for a query that cannot be polled (e.g. onchain/EVM live-call).
7781
+ * Absorbed internally: keep polling, never surface. Pin it to the design-of-record
7782
+ * `unsupported` frame.
7783
+ */
7784
+ const UNSUPPORTED_QUERY_SENTINEL = 'unsupported';
7785
+ const registry = new Map();
7786
+ /**
7787
+ * Every live subscriber, INCLUDING ones momentarily detached from the registry
7788
+ * mid-re-key (see reattach). Teardown walks this - not just the registry - so a
7789
+ * pending re-key retry can never revive polling after closeAllQuerySubscriptions.
7790
+ */
7791
+ const liveSubscribers = new Set();
7792
+ /** Poll ticks pause while the document is hidden (env-guarded / test-injectable). */
7793
+ let hidden = false;
7794
+ // ---------------------------------------------------------------------------
7795
+ // Canonicalization (shared with a future server group key).
7796
+ // ---------------------------------------------------------------------------
7797
+ function sortDeep(value) {
7798
+ if (Array.isArray(value))
7799
+ return value.map(sortDeep);
7800
+ if (value && typeof value === 'object') {
7801
+ const out = {};
7802
+ for (const k of Object.keys(value).sort())
7803
+ out[k] = sortDeep(value[k]);
7804
+ return out;
7805
+ }
7806
+ return value;
7807
+ }
7808
+ /** Deterministic key-sorted serialization of query args. */
7809
+ function canonicalizeArgs(args) {
7810
+ return JSON.stringify(sortDeep(args !== null && args !== void 0 ? args : null));
7811
+ }
7812
+ /**
7813
+ * Deterministic key-sorted serialization of a query RESULT for change detection.
7814
+ * Robust to shuffled object key order and the JSON-wire shapes the endpoint
7815
+ * yields (string u64, ISO-date string). BigInt/Date OBJECTS never cross the HTTP
7816
+ * boundary, so they are intentionally not handled here.
7817
+ */
7818
+ function canonicalizeResult(result) {
7819
+ return JSON.stringify(sortDeep(result === undefined ? null : result));
7820
+ }
7821
+ // ---------------------------------------------------------------------------
7822
+ // Identity (two-part: compact bucket + exact bearer, mirroring subscribe()'s H1).
7823
+ // ---------------------------------------------------------------------------
7824
+ function hashToken(value) {
7825
+ let h = 5381;
7826
+ for (let i = 0; i < value.length; i++)
7827
+ h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
7828
+ return h.toString(36);
7829
+ }
7830
+ async function currentIdentity(appId) {
7831
+ const token = await getIdToken(clientConfig.isServer);
7832
+ return token ? { bucket: `${appId}:t${hashToken(token)}`, token } : { bucket: `${appId}:anon`, token: null };
7833
+ }
7834
+ function makeKey(appId, path, queryName, args, identity) {
7835
+ var _a;
7836
+ return [appId, path, queryName, canonicalizeArgs(args), identity.bucket, (_a = identity.token) !== null && _a !== void 0 ? _a : ''].join('\u0000');
7837
+ }
7838
+ // ---------------------------------------------------------------------------
7839
+ // Error classification.
7840
+ // ---------------------------------------------------------------------------
7841
+ /** A genuine query-level error (surfaced to onError), as opposed to transport. */
7842
+ function isQueryLevelError(error) {
7843
+ return (error === null || error === void 0 ? void 0 : error.isQueryError) === true || (error === null || error === void 0 ? void 0 : error.isBoundedDecline) === true || (error === null || error === void 0 ? void 0 : error.decline) != null;
7844
+ }
7845
+ /**
7846
+ * A RECOGNIZED transient transport failure that a retry might clear (HTTP status like
7847
+ * 429/5xx, a network/abort error). These back off SILENTLY - surfacing them per-poll
7848
+ * would rebuild the 429 noise. Anything else (a bad path, a malformed response - things
7849
+ * a retry won't fix) is NOT transient, so it gets surfaced once to onError instead of a
7850
+ * silently-dead subscription.
7851
+ */
7852
+ function isTransientTransportError(error) {
7853
+ if (error == null)
7854
+ return true; // unknown - be conservative and keep retrying
7855
+ if (typeof error.status === 'number' || typeof error.statusCode === 'number')
7856
+ return true;
7857
+ const code = error.code;
7858
+ if (code === 'ECONNABORTED' || code === 'ERR_NETWORK' || code === 'ETIMEDOUT' || code === 'ERR_CANCELED')
7859
+ return true;
7860
+ if (error.name === 'AbortError' || error.name === 'CanceledError')
7861
+ return true;
7862
+ if (error.request && !error.response)
7863
+ return true; // request sent, no response - network
7864
+ return false;
7865
+ }
7866
+ function enrichError(error, phase, willRetry) {
7867
+ var _a;
7868
+ const err = error instanceof Error ? error : new Error(String((_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error));
7869
+ try {
7870
+ err.phase = phase;
7871
+ err.willRetry = willRetry;
7872
+ }
7873
+ catch (_b) {
7874
+ /* frozen error object - deliver as-is */
7875
+ }
7876
+ return err;
7877
+ }
7878
+ // ---------------------------------------------------------------------------
7879
+ // Interval / scheduling.
7880
+ // ---------------------------------------------------------------------------
7881
+ function clampInterval(value) {
7882
+ const n = typeof value === 'number' && isFinite(value) ? value : DEFAULT_QUERY_POLL_INTERVAL_MS;
7883
+ return Math.max(MIN_QUERY_POLL_INTERVAL_MS, n);
7884
+ }
7885
+ function recomputeInterval(entry) {
7886
+ let min = Infinity;
7887
+ for (const s of entry.subscribers)
7888
+ if (s.intervalMs < min)
7889
+ min = s.intervalMs;
7890
+ const next = isFinite(min) ? min : DEFAULT_QUERY_POLL_INTERVAL_MS;
7891
+ const changed = next !== entry.effectiveInterval;
7892
+ entry.effectiveInterval = next;
7893
+ // Realign a pending (not in-flight) timer to the new cadence so a newly-joined
7894
+ // faster subscriber does not wait out a slower subscriber's interval (and a
7895
+ // departed fast subscriber relaxes the survivors back to their own cadence).
7896
+ // But NEVER while backing off - realigning to the (shorter) steady cadence would
7897
+ // discard the backoff and re-hit a still-failing transport (the 429 storm).
7898
+ if (changed && entry.timer && !entry.inFlight && entry.consecutiveFailures === 0) {
7899
+ scheduleNext(entry, entry.effectiveInterval);
7900
+ }
7901
+ }
7902
+ function clearTimer(entry) {
7903
+ if (entry.timer) {
7904
+ clearTimeout(entry.timer);
7905
+ entry.timer = null;
7906
+ }
7907
+ }
7908
+ function backoffDelay(entry) {
7909
+ const base = Math.min(QUERY_POLL_BACKOFF_CAP_MS, entry.effectiveInterval * 2 ** entry.consecutiveFailures);
7910
+ // Full jitter over the capped exponential, floored at the base interval so a
7911
+ // backed-off key never polls FASTER than its steady cadence.
7912
+ return entry.effectiveInterval + Math.floor(Math.random() * Math.max(0, base - entry.effectiveInterval));
7913
+ }
7914
+ function scheduleNext(entry, delayMs) {
7915
+ clearTimer(entry);
7916
+ if (entry.subscribers.size === 0)
7917
+ return;
7918
+ if (hidden)
7919
+ return; // paused; resume reschedules
7920
+ entry.timer = setTimeout(() => {
7921
+ entry.timer = null;
7922
+ void pollEntry(entry);
7923
+ }, delayMs);
7924
+ }
7925
+ // ---------------------------------------------------------------------------
7926
+ // Polling.
7927
+ // ---------------------------------------------------------------------------
7928
+ function pollEntry(entry) {
7929
+ if (entry.inFlight)
7930
+ return Promise.resolve(); // single-flight
7931
+ if (entry.subscribers.size === 0)
7932
+ return Promise.resolve();
7933
+ entry.inFlight = true;
7934
+ const gen = entry.generation;
7935
+ return runQuery(entry.path, entry.queryName, entry.args).then((value) => onPollResult(entry, gen, value, undefined), (error) => onPollResult(entry, gen, undefined, error));
7936
+ }
7937
+ // Staleness is caught by the per-entry generation (bumped on teardown/re-key) and
7938
+ // the registry identity check (the entry was removed/replaced) - so an old
7939
+ // principal's late result can never reach a re-keyed subscriber. No global epoch
7940
+ // is needed, which is why a plain reconnect (identity unchanged) is a no-op.
7941
+ function onPollResult(entry, gen, value, error) {
7942
+ var _a;
7943
+ entry.inFlight = false;
7944
+ const stale = gen !== entry.generation || registry.get(entry.key) !== entry;
7945
+ if (error !== undefined) {
7946
+ if (isQueryLevelError(error)) {
7947
+ // Query executed but this row errored (or was declined): transport is
7948
+ // healthy, so keep the steady cadence and reset backoff.
7949
+ entry.consecutiveFailures = 0;
7950
+ if (!stale) {
7951
+ if (!((error === null || error === void 0 ? void 0 : error.isQueryError) === true && (error === null || error === void 0 ? void 0 : error.message) === UNSUPPORTED_QUERY_SENTINEL)) {
7952
+ const phase = entry.hasValue ? 'refresh' : 'initial';
7953
+ // De-dupe: a permanently-broken query must not toast onError every
7954
+ // interval - fire only when the error (phase + message) changes.
7955
+ const sig = `${phase}${String((_a = (error && error.message)) !== null && _a !== void 0 ? _a : error)}`;
7956
+ if (entry.lastErrorSig !== sig) {
7957
+ entry.lastErrorSig = sig;
7958
+ const enriched = enrichError(error, phase, true);
7959
+ entry.lastError = enriched; // remembered so a late joiner learns it's broken (F6)
7960
+ for (const s of entry.subscribers)
7961
+ safeCall(s.onError, enriched);
7962
+ }
7963
+ }
7964
+ scheduleNext(entry, entry.effectiveInterval);
7965
+ }
7966
+ return;
7967
+ }
7968
+ // Transport-level rejection: back off. A RECOGNIZED transient (429/5xx/network/abort)
7969
+ // stays SILENT (surfacing it per-poll would rebuild the 429 noise); an UNRECOGNIZED
7970
+ // rejection (a bad path, a malformed response - things a retry won't fix) is surfaced
7971
+ // ONCE so the subscription is never silently dead. Both keep backing off in case it
7972
+ // was misclassified / recovers.
7973
+ entry.consecutiveFailures += 1;
7974
+ if (!stale) {
7975
+ if (!isTransientTransportError(error)) {
7976
+ fireEntryError(entry, error, entry.hasValue ? 'refresh' : 'initial', true);
7977
+ }
7978
+ scheduleNext(entry, backoffDelay(entry));
7979
+ }
7980
+ return;
7981
+ }
7982
+ entry.consecutiveFailures = 0;
7983
+ if (!stale) {
7984
+ entry.lastErrorSig = null; // a good result clears the error-dedup latch...
7985
+ entry.lastError = undefined; // ...and the remembered error (joiners no longer replay it)
7986
+ deliver(entry, value);
7987
+ scheduleNext(entry, entry.effectiveInterval);
7988
+ }
7989
+ }
7990
+ /**
7991
+ * Fire onError to all current subscribers, de-duped (a persistently-broken query must not
7992
+ * toast every interval) and REMEMBER the error so a late joiner with no value yet can learn
7993
+ * the query is broken instead of spinning forever (F6).
7994
+ */
7995
+ function fireEntryError(entry, error, phase, willRetry) {
7996
+ var _a;
7997
+ const sig = `${phase} ${String((_a = (error && error.message)) !== null && _a !== void 0 ? _a : error)}`;
7998
+ if (entry.lastErrorSig === sig)
7999
+ return;
8000
+ entry.lastErrorSig = sig;
8001
+ const enriched = enrichError(error, phase, willRetry);
8002
+ entry.lastError = enriched;
8003
+ for (const s of entry.subscribers)
8004
+ safeCall(s.onError, enriched);
8005
+ }
8006
+ function deliver(entry, value) {
8007
+ const canonical = canonicalizeResult(value);
8008
+ if (entry.hasValue && entry.lastCanonical === canonical)
8009
+ return; // no change
8010
+ entry.hasValue = true;
8011
+ entry.lastValue = value;
8012
+ entry.lastCanonical = canonical;
8013
+ for (const s of entry.subscribers)
8014
+ safeCall(s.onData, value);
8015
+ }
8016
+ function safeCall(fn, arg) {
8017
+ if (!fn)
8018
+ return;
8019
+ try {
8020
+ fn(arg);
8021
+ }
8022
+ catch (_a) {
8023
+ /* an app callback throwing must not break the fan-out loop */
8024
+ }
8025
+ }
8026
+ // ---------------------------------------------------------------------------
8027
+ // Attach / detach.
8028
+ // ---------------------------------------------------------------------------
8029
+ function createEntry(key, sub, identity) {
8030
+ return {
8031
+ key,
8032
+ appId: sub.appId,
8033
+ path: sub.path,
8034
+ queryName: sub.queryName,
8035
+ args: sub.args,
8036
+ identity,
8037
+ subscribers: new Set(),
8038
+ generation: 0,
8039
+ lastErrorSig: null,
8040
+ lastError: undefined,
8041
+ effectiveInterval: sub.intervalMs,
8042
+ timer: null,
8043
+ inFlight: false,
8044
+ consecutiveFailures: 0,
8045
+ hasValue: false,
8046
+ lastValue: undefined,
8047
+ lastCanonical: '',
8048
+ readyPromise: Promise.resolve(),
8049
+ };
8050
+ }
8051
+ /** Register (or re-register, on identity change) a subscriber under its current identity. */
8052
+ async function attach(sub) {
8053
+ const identity = await currentIdentity(sub.appId);
8054
+ // The sub may have been unsubscribed / torn down WHILE we awaited identity. The
8055
+ // registry+generation staleness guards can't catch this because the entry would be
8056
+ // created fresh inside the race, so re-check here before touching the registry.
8057
+ if (sub.detached)
8058
+ return;
8059
+ const key = makeKey(sub.appId, sub.path, sub.queryName, sub.args, identity);
8060
+ let entry = registry.get(key);
8061
+ let created = false;
8062
+ if (!entry) {
8063
+ entry = createEntry(key, sub, identity);
8064
+ registry.set(key, entry);
8065
+ created = true;
8066
+ }
8067
+ entry.subscribers.add(sub);
8068
+ sub.entry = entry;
8069
+ recomputeInterval(entry);
8070
+ if (created) {
8071
+ // Eager initial fetch: do NOT wait a tick. The returned promise resolves
8072
+ // after the first fetch settles; an initial failure surfaces via onError
8073
+ // (phase 'initial') and still resolves - never leaves the sub in limbo.
8074
+ // While hidden, do NOT poll - stay registered so visibility-resume schedules
8075
+ // it (covers fresh-subscribe, reconcile re-key, and reattach-retry uniformly).
8076
+ if (hidden) {
8077
+ entry.readyPromise = Promise.resolve();
8078
+ return;
8079
+ }
8080
+ entry.readyPromise = pollEntry(entry);
8081
+ await entry.readyPromise;
8082
+ return;
8083
+ }
8084
+ if (entry.hasValue) {
8085
+ // Joiner of an established key gets the last good value immediately.
8086
+ safeCall(sub.onData, entry.lastValue);
8087
+ return;
8088
+ }
8089
+ if (entry.lastError) {
8090
+ // Joiner of a key that has NO value yet but is in a latched error state: replay the
8091
+ // error so a late-mounting component learns the query is broken instead of spinning
8092
+ // forever with neither data nor error (F6).
8093
+ safeCall(sub.onError, entry.lastError);
8094
+ }
8095
+ // Joined during the first in-flight fetch: piggyback it (deliver fans out to us).
8096
+ await entry.readyPromise;
8097
+ }
8098
+ function detach(sub) {
8099
+ const entry = sub.entry;
8100
+ if (!entry || !entry.subscribers.has(sub))
8101
+ return; // idempotent
8102
+ entry.subscribers.delete(sub);
8103
+ sub.entry = null;
8104
+ if (entry.subscribers.size === 0) {
8105
+ entry.generation += 1; // drop any in-flight result
8106
+ clearTimer(entry);
8107
+ if (registry.get(entry.key) === entry)
8108
+ registry.delete(entry.key);
8109
+ }
8110
+ else {
8111
+ recomputeInterval(entry);
8112
+ }
8113
+ }
8114
+ // ---------------------------------------------------------------------------
8115
+ // Public API.
8116
+ // ---------------------------------------------------------------------------
8117
+ /**
8118
+ * Subscribe to the live value of a named (computed) query.
8119
+ *
8120
+ * @param path - The document path the query is declared on (e.g. 'market/mytoken').
8121
+ * @param queryName - The named query to run (e.g. 'tokensForOneSol').
8122
+ * @param args - Query arguments (canonicalized; drives the dedupe key).
8123
+ * @param options - onData/onError callbacks and optional intervalMs; a bare
8124
+ * function is coerced to `{ onData }` (mirrors subscribe()).
8125
+ * @returns An async unsubscribe function.
8126
+ *
8127
+ * @example
8128
+ * ```typescript
8129
+ * const unsubscribe = await subscribeQuery('market/mytoken', 'tokensForOneSol', {}, {
8130
+ * onData: (price) => setPrice(price),
8131
+ * onError: (err) => { if (err.phase === 'initial') showSpinnerError(); },
8132
+ * });
8133
+ * await unsubscribe();
8134
+ * ```
8135
+ */
8136
+ async function subscribeQuery(path, queryName, args, options) {
8137
+ const opts = typeof options === 'function' ? { onData: options } : (options !== null && options !== void 0 ? options : {});
8138
+ const sub = {
8139
+ onData: opts.onData,
8140
+ onError: opts.onError,
8141
+ intervalMs: clampInterval(opts.intervalMs),
8142
+ appId: clientConfig.appId, // v1 single-app: see QuerySubscriptionOptions
8143
+ path,
8144
+ queryName,
8145
+ args,
8146
+ entry: null,
8147
+ detached: false,
8148
+ reattachTimer: null,
8149
+ };
8150
+ liveSubscribers.add(sub);
8151
+ try {
8152
+ await attach(sub);
8153
+ }
8154
+ catch (error) {
8155
+ // Initial identity resolution threw (auth flux) - do not leak the sub into
8156
+ // liveSubscribers (it holds the app's callbacks). The caller gets the rejection
8157
+ // and no unsubscribe handle, so retire it here. (reattach tolerates this on the
8158
+ // re-key path; the initial path must too.)
8159
+ stopSubscriber(sub);
8160
+ throw error;
8161
+ }
8162
+ return async () => {
8163
+ stopSubscriber(sub); // mark detached + cancel any pending re-key retry
8164
+ detach(sub);
8165
+ };
8166
+ }
8167
+ /** Permanently retire a subscriber: no future re-key retry can revive it. */
8168
+ function stopSubscriber(sub) {
8169
+ sub.detached = true;
8170
+ if (sub.reattachTimer) {
8171
+ clearTimeout(sub.reattachTimer);
8172
+ sub.reattachTimer = null;
8173
+ }
8174
+ liveSubscribers.delete(sub);
8175
+ }
8176
+ /**
8177
+ * Wired to the SDK's auth-change chokepoint (operations.clearReadCacheForAuthChange,
8178
+ * driven by reconnectWithNewAuth). That chokepoint fires on ANY reconnect - including
8179
+ * a transient browser `online` nudge - not only on a real identity change, so this
8180
+ * MUST be idempotent when the principal is unchanged: it re-resolves identity per
8181
+ * key and re-keys ONLY the entries whose principal actually changed, preserving the
8182
+ * timer and backoff of every unchanged entry (otherwise a flaky connection would
8183
+ * rebuild the exact 429 re-poll storm this feature removes).
8184
+ */
8185
+ function notifyQueryAuthChanged() {
8186
+ void reconcileIdentity();
8187
+ }
8188
+ async function reconcileIdentity() {
8189
+ var _a;
8190
+ const resolvedByApp = new Map();
8191
+ for (const entry of [...registry.values()]) {
8192
+ // The entry may have been torn down while we awaited a prior identity.
8193
+ if (registry.get(entry.key) !== entry || entry.subscribers.size === 0)
8194
+ continue;
8195
+ let current = (_a = resolvedByApp.get(entry.appId)) !== null && _a !== void 0 ? _a : null;
8196
+ if (!current) {
8197
+ try {
8198
+ current = await currentIdentity(entry.appId);
8199
+ resolvedByApp.set(entry.appId, current);
8200
+ }
8201
+ catch (_b) {
8202
+ // Could not resolve the current identity (e.g. getIdToken threw during
8203
+ // an auth-state flux). Treat identity as UNKNOWN and re-key
8204
+ // conservatively below - reattach() retries until it resolves, so we
8205
+ // neither leak the previous principal's data nor freeze the sub.
8206
+ current = null;
8207
+ }
8208
+ }
8209
+ if (registry.get(entry.key) !== entry)
8210
+ continue; // re-check across the await
8211
+ if (current && current.bucket === entry.identity.bucket && current.token === entry.identity.token) {
8212
+ continue; // identity unchanged - keep the entry, its timer, and its backoff
8213
+ }
8214
+ // Identity changed (or is momentarily unresolvable): drop the entry (its in-flight
8215
+ // result is caught by the generation + registry staleness checks) and re-key its
8216
+ // subscribers under the new principal. reattach() owns the eager fetch AND a retry,
8217
+ // so a transient failure here can never silently orphan a live subscription.
8218
+ const subs = [...entry.subscribers].filter((s) => s.entry === entry);
8219
+ entry.generation += 1;
8220
+ clearTimer(entry);
8221
+ // Only delete if THIS entry still owns the key - a replacement entry may have taken
8222
+ // it over while we awaited identity (line 499 guards the same window; mirror the
8223
+ // check at the delete site so it stays correct if code is inserted between them).
8224
+ if (registry.get(entry.key) === entry)
8225
+ registry.delete(entry.key);
8226
+ for (const s of subs) {
8227
+ s.entry = null;
8228
+ reattach(s);
8229
+ }
8230
+ }
8231
+ }
8232
+ /**
8233
+ * Re-register a re-keyed subscriber under its current identity, retrying on failure.
8234
+ * A re-keyed subscriber has already been removed from the registry, so if attach()
8235
+ * fails there is no other path that would ever bring it back - reattach() IS that
8236
+ * path. It stops only when the subscriber re-attaches or the caller unsubscribes.
8237
+ */
8238
+ function reattach(sub) {
8239
+ sub.reattachTimer = null; // this run consumes any scheduled retry
8240
+ if (sub.detached)
8241
+ return;
8242
+ attach(sub).then(() => {
8243
+ // The caller may have unsubscribed (or the manager been torn down) while this
8244
+ // attach was in flight; if so, tear the freshly-revived entry back down instead
8245
+ // of leaving a zombie poll.
8246
+ if (sub.detached)
8247
+ detach(sub);
8248
+ }, () => {
8249
+ if (sub.detached)
8250
+ return;
8251
+ sub.reattachTimer = setTimeout(() => reattach(sub), REATTACH_RETRY_MS);
8252
+ });
8253
+ }
8254
+ /** Tear down every query subscription: cancel timers, clear the registry. */
8255
+ function closeAllQuerySubscriptions() {
8256
+ // Retire EVERY live subscriber first (including any detached mid-re-key and waiting
8257
+ // on a retry) so a pending reattach can never revive polling after teardown.
8258
+ for (const sub of [...liveSubscribers])
8259
+ stopSubscriber(sub);
8260
+ liveSubscribers.clear();
8261
+ for (const entry of registry.values()) {
8262
+ entry.generation += 1;
8263
+ clearTimer(entry);
8264
+ entry.subscribers.clear();
8265
+ }
8266
+ registry.clear();
8267
+ }
8268
+ // ---------------------------------------------------------------------------
8269
+ // Visibility (pause polling while hidden; single realigned refresh on resume).
8270
+ // ---------------------------------------------------------------------------
8271
+ function setHidden(next) {
8272
+ if (next === hidden)
8273
+ return;
8274
+ hidden = next;
8275
+ if (hidden) {
8276
+ for (const entry of registry.values())
8277
+ clearTimer(entry);
8278
+ return;
8279
+ }
8280
+ // Resume: one refresh per key at a small jitter so N keys don't burst-sync;
8281
+ // single-flight collapses it with any still-outstanding poll, and the recurring
8282
+ // tick is (re)scheduled by that poll's completion - so exactly one POST resumes.
8283
+ // A key that was BACKING OFF resumes on its remaining backoff, not the jitter, so a
8284
+ // hide/resume can't be used to bypass 429 throttling.
8285
+ for (const entry of registry.values()) {
8286
+ const delay = entry.consecutiveFailures > 0
8287
+ ? backoffDelay(entry)
8288
+ : Math.floor(Math.random() * RESUME_JITTER_MS);
8289
+ scheduleNext(entry, delay);
8290
+ }
8291
+ }
8292
+ if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
8293
+ hidden = !!document.hidden;
8294
+ document.addEventListener('visibilitychange', () => {
8295
+ setHidden(!!document.hidden);
8296
+ });
8297
+ }
8298
+ // Auto-wire the identity-change signal to the SDK's existing chokepoint.
8299
+ onReadCacheAuthChange(notifyQueryAuthChanged);
8300
+
7714
8301
  // ---------------------------------------------------------------------------
7715
8302
  // field-values.ts -- Server-resolved field-value operations for set()/setMany().
7716
8303
  //
@@ -7816,12 +8403,12 @@ function toMillis(seconds) {
7816
8403
 
7817
8404
  /** Load the Anchor/Web3 transaction builder only when an onchain write uses it. */
7818
8405
  async function buildSetDocumentsTransaction(connection, idl, programProvider, payerPublicKey, args, remainingAccounts, lutKey, preInstructions, simulate, additionalLutAddresses) {
7819
- const implementation = await Promise.resolve().then(function () { return require('./sol-utils-pVrt7VOx.js'); });
8406
+ const implementation = await Promise.resolve().then(function () { return require('./sol-utils-Dni65Z63.js'); });
7820
8407
  return implementation.buildSetDocumentsTransaction(connection, idl, programProvider, payerPublicKey, args, remainingAccounts, lutKey, preInstructions, simulate, additionalLutAddresses);
7821
8408
  }
7822
8409
  /** SIWS message creation is async already, so its onchain module can be lazy. */
7823
8410
  async function genSolanaMessage(address, nonce) {
7824
- const implementation = await Promise.resolve().then(function () { return require('./solana-message-BVJWVB6C.js'); });
8411
+ const implementation = await Promise.resolve().then(function () { return require('./solana-message-BP0L24CQ.js'); });
7825
8412
  return implementation.genSolanaMessage(address, nonce);
7826
8413
  }
7827
8414
  const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
@@ -8357,10 +8944,12 @@ exports.BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR = BOUNDED_RUNTIME_SET_DO
8357
8944
  exports.BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR;
8358
8945
  exports.BoundedDeclineError = BoundedDeclineError;
8359
8946
  exports.DECLINE_EVENT_NAME = DECLINE_EVENT_NAME;
8947
+ exports.DEFAULT_QUERY_POLL_INTERVAL_MS = DEFAULT_QUERY_POLL_INTERVAL_MS;
8360
8948
  exports.EFFECT_INTENT_ADDRESS = EFFECT_INTENT_ADDRESS;
8361
8949
  exports.FunctionInvokeError = FunctionInvokeError;
8362
8950
  exports.InsufficientBalanceError = InsufficientBalanceError;
8363
8951
  exports.LiveIntentError = LiveIntentError;
8952
+ exports.MIN_QUERY_POLL_INTERVAL_MS = MIN_QUERY_POLL_INTERVAL_MS;
8364
8953
  exports.RATE_LIMIT_EVENT_NAME = RATE_LIMIT_EVENT_NAME;
8365
8954
  exports.ReactNativeSessionManager = ReactNativeSessionManager;
8366
8955
  exports.ServerSessionManager = ServerSessionManager;
@@ -8370,7 +8959,10 @@ exports.assertCanonicalBoundedRuntimeWriteCalldata = assertCanonicalBoundedRunti
8370
8959
  exports.assertEvmTransactionMatchesWrite = assertEvmTransactionMatchesWrite;
8371
8960
  exports.bufferExports = bufferExports;
8372
8961
  exports.buildSetDocumentsTransaction = buildSetDocumentsTransaction;
8962
+ exports.canonicalizeArgs = canonicalizeArgs;
8963
+ exports.canonicalizeResult = canonicalizeResult;
8373
8964
  exports.clearCache = clearCache;
8965
+ exports.closeAllQuerySubscriptions = closeAllQuerySubscriptions;
8374
8966
  exports.closeAllSubscriptions = closeAllSubscriptions;
8375
8967
  exports.convertRemainingAccounts = convertRemainingAccounts;
8376
8968
  exports.count = count;
@@ -8421,9 +9013,10 @@ exports.signSessionCreateMessage = signSessionCreateMessage;
8421
9013
  exports.signTransaction = signTransaction;
8422
9014
  exports.status = status;
8423
9015
  exports.subscribe = subscribe;
9016
+ exports.subscribeQuery = subscribeQuery;
8424
9017
  exports.subscribeView = subscribeView;
8425
9018
  exports.toLowerEvmAddress = toLowerEvmAddress;
8426
9019
  exports.toMillis = toMillis;
8427
9020
  exports.toSeconds = toSeconds;
8428
9021
  exports.withEffects = withEffects;
8429
- //# sourceMappingURL=index-BaSw33ec.js.map
9022
+ //# sourceMappingURL=index-Dmnzhzk1.js.map