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