@bounded-sh/core 0.0.101 → 0.0.103

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/realtime-store.d.ts +2 -0
  2. package/dist/client/reconnect-backoff.d.ts +16 -0
  3. package/dist/client/socket-heartbeat.d.ts +47 -0
  4. package/dist/client/subscription-v2.d.ts +6 -0
  5. package/dist/{index-CMT0saxy.mjs → index-CC2WVN43.mjs} +170 -5
  6. package/dist/index-CC2WVN43.mjs.map +1 -0
  7. package/dist/{index-BEGeDFQI.js → index-D7dRe0PL.js} +170 -5
  8. package/dist/index-D7dRe0PL.js.map +1 -0
  9. package/dist/index.js +1 -1
  10. package/dist/index.mjs +1 -1
  11. package/dist/{realtime-store-DqAAqfo9.mjs → realtime-store-2iWofj5n.mjs} +2 -2
  12. package/dist/realtime-store-2iWofj5n.mjs.map +1 -0
  13. package/dist/{realtime-store-DrtEj-bE.js → realtime-store-CuFYPLgk.js} +2 -2
  14. package/dist/realtime-store-CuFYPLgk.js.map +1 -0
  15. package/dist/{solana-message-C_717vAx.mjs → solana-message-Bx04VMG1.mjs} +2 -2
  16. package/dist/{solana-message-C_717vAx.mjs.map → solana-message-Bx04VMG1.mjs.map} +1 -1
  17. package/dist/{solana-message-CJTnYiJu.js → solana-message-DLBiBfnG.js} +2 -2
  18. package/dist/{solana-message-CJTnYiJu.js.map → solana-message-DLBiBfnG.js.map} +1 -1
  19. package/dist/{solana-operations-CU0YbiMu.js → solana-operations-Be0dWtVZ.js} +3 -3
  20. package/dist/{solana-operations-CU0YbiMu.js.map → solana-operations-Be0dWtVZ.js.map} +1 -1
  21. package/dist/{solana-operations-DGNA2nGZ.mjs → solana-operations-CCFNQIgd.mjs} +4 -4
  22. package/dist/{solana-operations-DGNA2nGZ.mjs.map → solana-operations-CCFNQIgd.mjs.map} +1 -1
  23. package/dist/{solana-v1-operations-yQII8r1s.js → solana-v1-operations-Bbj0F4cp.js} +3 -3
  24. package/dist/{solana-v1-operations-yQII8r1s.js.map → solana-v1-operations-Bbj0F4cp.js.map} +1 -1
  25. package/dist/{solana-v1-operations-Bpd4TASG.mjs → solana-v1-operations-C6Qev1fZ.mjs} +3 -3
  26. package/dist/{solana-v1-operations-Bpd4TASG.mjs.map → solana-v1-operations-C6Qev1fZ.mjs.map} +1 -1
  27. package/package.json +2 -2
  28. package/dist/index-BEGeDFQI.js.map +0 -1
  29. package/dist/index-CMT0saxy.mjs.map +0 -1
  30. package/dist/realtime-store-DqAAqfo9.mjs.map +0 -1
  31. package/dist/realtime-store-DrtEj-bE.js.map +0 -1
@@ -33,6 +33,8 @@ export declare class RealtimeStore {
33
33
  private reconnectTimer;
34
34
  private reconnectDelay;
35
35
  private maxReconnectDelay;
36
+ /** When the current socket finished connecting; decides whether a close resets the backoff. */
37
+ private connectedAtMs;
36
38
  private idbFlushTimer;
37
39
  private idbDirtyKeys;
38
40
  private closed;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Reconnect backoff for RealtimeStore's WebSocket.
3
+ *
4
+ * A server that sheds load closes many sockets at once (1013). Without jitter
5
+ * every client comes back at the same instant; without growth on quick closes a
6
+ * socket that is accepted and then immediately dropped retries at the floor
7
+ * forever. So: a connection that stayed up for STABLE_CONNECTION_MS resets the
8
+ * delay, a shorter one doubles it, and every scheduled wait is jittered.
9
+ */
10
+ export declare const RECONNECT_BASE_DELAY_MS = 1000;
11
+ export declare const RECONNECT_MAX_DELAY_MS = 30000;
12
+ export declare const STABLE_CONNECTION_MS = 10000;
13
+ /** Delay to use after a close, given how long the closed connection had been up (null: never connected). */
14
+ export declare function reconnectDelayAfterClose(currentDelayMs: number, connectedForMs: number | null, maxDelayMs?: number): number;
15
+ /** The wait actually scheduled: the delay scaled by a random factor in [0.5, 1.5). */
16
+ export declare function jitteredReconnectDelayMs(delayMs: number, random?: () => number): number;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Idle heartbeat for the realtime WebSocket connection (subscription-v2.ts).
3
+ *
4
+ * A socket whose server has stopped delivering frames looks exactly like a
5
+ * quiet one: the browser reports it open and nothing arrives. The client only
6
+ * learns the difference by asking. Once nothing has been received for
7
+ * HEARTBEAT_IDLE_MS the heartbeat asks the connection to send a ping; if
8
+ * nothing at all arrives within HEARTBEAT_TIMEOUT_MS after that, the socket is
9
+ * declared dead and the connection is reconnected. Any inbound frame counts as
10
+ * proof of life, so a busy socket is never pinged and a pong is just another
11
+ * frame. The connection may decline to ping (it already has a request in
12
+ * flight with its own timeout); the idle wait then simply starts over.
13
+ *
14
+ * The timeout sits above both budgets a pong can wait behind: the connection's
15
+ * own 30 s request budget (the server handles one frame per socket at a time)
16
+ * and the object's 30 s CPU slice, which another socket's handler can hold
17
+ * while every pong waits. A healthy socket must never be declared dead
18
+ * within either.
19
+ *
20
+ * Pure: timers come from the clock passed in, so tests can drive it.
21
+ */
22
+ export declare const HEARTBEAT_IDLE_MS = 25000;
23
+ export declare const HEARTBEAT_TIMEOUT_MS = 45000;
24
+ export interface HeartbeatClock {
25
+ setTimeout(callback: () => void, ms: number): unknown;
26
+ clearTimeout(handle: unknown): void;
27
+ }
28
+ export declare class SocketHeartbeat {
29
+ /** Send a ping now; return false to skip this round (the idle wait restarts). */
30
+ private readonly sendPing;
31
+ private readonly onDead;
32
+ private readonly clock;
33
+ private readonly idleMs;
34
+ private readonly timeoutMs;
35
+ private idleTimer;
36
+ private timeoutTimer;
37
+ private stopped;
38
+ constructor(
39
+ /** Send a ping now; return false to skip this round (the idle wait restarts). */
40
+ sendPing: () => boolean, onDead: () => void, clock?: HeartbeatClock, idleMs?: number, timeoutMs?: number);
41
+ start(): void;
42
+ /** An inbound frame arrived: the socket is alive, so the idle wait starts over. */
43
+ noteActivity(): void;
44
+ stop(): void;
45
+ private armIdle;
46
+ private clearTimers;
47
+ }
@@ -28,6 +28,12 @@ export declare const SHARD_CAP = 45;
28
28
  * target, far beyond any legitimate UI.
29
29
  */
30
30
  export declare const MAX_SHARDS_PER_KEY = 8;
31
+ /**
32
+ * Every idle client on a stalled object hits its dead timer at about the same
33
+ * moment, so the replacement waits out the wrapper's own first-attempt delay
34
+ * with the same jitter, instead of every client reopening in one instant.
35
+ */
36
+ export declare const HEARTBEAT_RECONNECT_DELAY_MAX_MS: number;
31
37
  /**
32
38
  * Subscribe to data at a path using WebSocket v2.
33
39
  * Returns an unsubscribe function.
@@ -4188,7 +4188,7 @@ class ServerSessionManager {
4188
4188
  const sessionConfig = (_a = this.sessionConfig) !== null && _a !== void 0 ? _a : undefined;
4189
4189
  /* fetch nonce from auth API (bound to the snapshotted app+issuer) */
4190
4190
  const nonce = await genAuthNonce(sessionConfig);
4191
- const { genSolanaMessage } = await import('./solana-message-C_717vAx.mjs');
4191
+ const { genSolanaMessage } = await import('./solana-message-Bx04VMG1.mjs');
4192
4192
  const message = await genSolanaMessage(address, nonce, sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId);
4193
4193
  /* sign the message */
4194
4194
  const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
@@ -5229,6 +5229,83 @@ async function resolveClientTransactionVersions(authProvider) {
5229
5229
  return typeof authProvider.signTransactionV1 === 'function' ? ['legacy', 0, 1] : ['legacy', 0];
5230
5230
  }
5231
5231
 
5232
+ /**
5233
+ * Idle heartbeat for the realtime WebSocket connection (subscription-v2.ts).
5234
+ *
5235
+ * A socket whose server has stopped delivering frames looks exactly like a
5236
+ * quiet one: the browser reports it open and nothing arrives. The client only
5237
+ * learns the difference by asking. Once nothing has been received for
5238
+ * HEARTBEAT_IDLE_MS the heartbeat asks the connection to send a ping; if
5239
+ * nothing at all arrives within HEARTBEAT_TIMEOUT_MS after that, the socket is
5240
+ * declared dead and the connection is reconnected. Any inbound frame counts as
5241
+ * proof of life, so a busy socket is never pinged and a pong is just another
5242
+ * frame. The connection may decline to ping (it already has a request in
5243
+ * flight with its own timeout); the idle wait then simply starts over.
5244
+ *
5245
+ * The timeout sits above both budgets a pong can wait behind: the connection's
5246
+ * own 30 s request budget (the server handles one frame per socket at a time)
5247
+ * and the object's 30 s CPU slice, which another socket's handler can hold
5248
+ * while every pong waits. A healthy socket must never be declared dead
5249
+ * within either.
5250
+ *
5251
+ * Pure: timers come from the clock passed in, so tests can drive it.
5252
+ */
5253
+ const HEARTBEAT_IDLE_MS = 25000;
5254
+ const HEARTBEAT_TIMEOUT_MS = 45000;
5255
+ class SocketHeartbeat {
5256
+ constructor(
5257
+ /** Send a ping now; return false to skip this round (the idle wait restarts). */
5258
+ sendPing, onDead, clock = globalThis, idleMs = HEARTBEAT_IDLE_MS, timeoutMs = HEARTBEAT_TIMEOUT_MS) {
5259
+ this.sendPing = sendPing;
5260
+ this.onDead = onDead;
5261
+ this.clock = clock;
5262
+ this.idleMs = idleMs;
5263
+ this.timeoutMs = timeoutMs;
5264
+ this.idleTimer = null;
5265
+ this.timeoutTimer = null;
5266
+ this.stopped = false;
5267
+ }
5268
+ start() {
5269
+ if (this.stopped)
5270
+ return;
5271
+ this.armIdle();
5272
+ }
5273
+ /** An inbound frame arrived: the socket is alive, so the idle wait starts over. */
5274
+ noteActivity() {
5275
+ if (this.stopped)
5276
+ return;
5277
+ this.clearTimers();
5278
+ this.armIdle();
5279
+ }
5280
+ stop() {
5281
+ this.stopped = true;
5282
+ this.clearTimers();
5283
+ }
5284
+ armIdle() {
5285
+ this.idleTimer = this.clock.setTimeout(() => {
5286
+ this.idleTimer = null;
5287
+ if (!this.sendPing()) {
5288
+ this.armIdle();
5289
+ return;
5290
+ }
5291
+ this.timeoutTimer = this.clock.setTimeout(() => {
5292
+ this.timeoutTimer = null;
5293
+ this.onDead();
5294
+ }, this.timeoutMs);
5295
+ }, this.idleMs);
5296
+ }
5297
+ clearTimers() {
5298
+ if (this.idleTimer !== null) {
5299
+ this.clock.clearTimeout(this.idleTimer);
5300
+ this.idleTimer = null;
5301
+ }
5302
+ if (this.timeoutTimer !== null) {
5303
+ this.clock.clearTimeout(this.timeoutTimer);
5304
+ this.timeoutTimer = null;
5305
+ }
5306
+ }
5307
+ }
5308
+
5232
5309
  /**
5233
5310
  * WebSocket v2 Subscription Manager
5234
5311
  *
@@ -5323,6 +5400,81 @@ const WS_CONFIG = {
5323
5400
  // Give handshakes more time before considering the attempt failed.
5324
5401
  connectionTimeout: 10000,
5325
5402
  };
5403
+ /** Client-chosen close code for a socket the heartbeat declared dead (4000-4999 are application codes). */
5404
+ const HEARTBEAT_CLOSE_CODE = 4000;
5405
+ function heartbeatReconnectDelayMs() {
5406
+ return BASE_MIN_RECONNECT_DELAY_MS + Math.floor(Math.random() * MIN_RECONNECT_DELAY_JITTER_MS);
5407
+ }
5408
+ /**
5409
+ * Start the idle heartbeat for the native socket that just opened on `ws`.
5410
+ * A ping goes out after HEARTBEAT_IDLE_MS of silence, and a socket that
5411
+ * answers nothing for HEARTBEAT_TIMEOUT_MS more is reconnected through the
5412
+ * wrapper after a jittered delay, the same path an auth change takes. A
5413
+ * server that has stopped dispatching frames leaves the socket open and
5414
+ * silent, so nothing else on the client would ever notice.
5415
+ */
5416
+ function startHeartbeat(connection, ws) {
5417
+ stopHeartbeat(connection);
5418
+ const heartbeat = new SocketHeartbeat(() => {
5419
+ // An outbound wait already has its own timeout, and the server
5420
+ // answers one frame per socket at a time, so a ping behind it would
5421
+ // only measure that wait. Let the idle wait start over instead.
5422
+ if (hasOutboundWait(connection))
5423
+ return false;
5424
+ // The wrapper drops a send on a socket that is not open, and a send
5425
+ // that throws is a socket that cannot carry a ping either way; the
5426
+ // dead timer then reports it.
5427
+ try {
5428
+ ws.send(JSON.stringify({ type: 'ping' }));
5429
+ }
5430
+ catch (error) {
5431
+ console.warn('[WS v2] Heartbeat ping could not be sent:', error);
5432
+ }
5433
+ return true;
5434
+ }, () => {
5435
+ // Only the socket this heartbeat was started for. A replaced or
5436
+ // torn-down connection must never be resurrected by a late timer.
5437
+ if (connection.heartbeat !== heartbeat || connection.ws !== ws)
5438
+ return;
5439
+ console.warn('[WS v2] No frames within the heartbeat window; reconnecting');
5440
+ // The heartbeat stays installed through the delay: a frame that
5441
+ // arrives meanwhile cancels the replacement (see the message
5442
+ // listener) and starts a fresh idle window on the same socket.
5443
+ connection.heartbeatReconnectTimer = setTimeout(() => {
5444
+ connection.heartbeatReconnectTimer = null;
5445
+ if (connection.heartbeat !== heartbeat || connection.ws !== ws)
5446
+ return;
5447
+ connection.heartbeat = null;
5448
+ try {
5449
+ ws.reconnect(HEARTBEAT_CLOSE_CODE, 'Heartbeat timeout');
5450
+ }
5451
+ catch (error) {
5452
+ console.error('[WS v2] Error reconnecting after heartbeat timeout:', error);
5453
+ }
5454
+ }, heartbeatReconnectDelayMs());
5455
+ });
5456
+ connection.heartbeat = heartbeat;
5457
+ heartbeat.start();
5458
+ }
5459
+ function stopHeartbeat(connection) {
5460
+ var _a;
5461
+ (_a = connection.heartbeat) === null || _a === void 0 ? void 0 : _a.stop();
5462
+ connection.heartbeat = null;
5463
+ if (connection.heartbeatReconnectTimer) {
5464
+ clearTimeout(connection.heartbeatReconnectTimer);
5465
+ connection.heartbeatReconnectTimer = null;
5466
+ }
5467
+ }
5468
+ /** A frame of ours the server has not answered yet: a request, a subscription confirmation, or a snapshot refresh. */
5469
+ function hasOutboundWait(connection) {
5470
+ if (connection.pendingRequests.size > 0 || connection.pendingSubscriptions.size > 0)
5471
+ return true;
5472
+ for (const subscription of connection.subscriptions.values()) {
5473
+ if (subscription.refresh)
5474
+ return true;
5475
+ }
5476
+ return false;
5477
+ }
5326
5478
  const WS_V2_PATH = '/ws/v2';
5327
5479
  let browserReconnectHooksAttached = false;
5328
5480
  let lastBrowserTriggeredReconnectAt = 0;
@@ -5791,6 +5943,7 @@ function closeConnectionNow(connection) {
5791
5943
  clearInterval(connection.tokenRefreshTimer);
5792
5944
  connection.tokenRefreshTimer = null;
5793
5945
  }
5946
+ stopHeartbeat(connection);
5794
5947
  const ws = connection.ws;
5795
5948
  connection.ws = null;
5796
5949
  connection.isConnecting = false;
@@ -6075,6 +6228,8 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
6075
6228
  authTokenValue: null,
6076
6229
  authPrincipal: null,
6077
6230
  socketGeneration: 0,
6231
+ heartbeat: null,
6232
+ heartbeatReconnectTimer: null,
6078
6233
  tokenRefreshTimer: null,
6079
6234
  idleCloseTimer: null,
6080
6235
  consecutiveAuthFailures: 0,
@@ -6149,6 +6304,7 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
6149
6304
  connection.socketGeneration += 1;
6150
6305
  connection.isConnecting = false;
6151
6306
  connection.isConnected = true;
6307
+ startHeartbeat(connection, ws);
6152
6308
  // NOTE: Do NOT reset consecutiveAuthFailures here. It is reset when a
6153
6309
  // fresh auth token is actually obtained or on an explicit auth change.
6154
6310
  // Schedule periodic token freshness checks. An anonymous (read-only
@@ -6183,6 +6339,13 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
6183
6339
  });
6184
6340
  // Handle incoming messages
6185
6341
  ws.addEventListener('message', (event) => {
6342
+ var _a;
6343
+ if (connection.heartbeatReconnectTimer) {
6344
+ // A frame inside the replacement delay: the socket is alive after all.
6345
+ clearTimeout(connection.heartbeatReconnectTimer);
6346
+ connection.heartbeatReconnectTimer = null;
6347
+ }
6348
+ (_a = connection.heartbeat) === null || _a === void 0 ? void 0 : _a.noteActivity();
6186
6349
  try {
6187
6350
  const message = JSON.parse(event.data);
6188
6351
  handleServerMessage(connection, message);
@@ -6204,6 +6367,7 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
6204
6367
  });
6205
6368
  // Handle close
6206
6369
  ws.addEventListener('close', () => {
6370
+ stopHeartbeat(connection);
6207
6371
  connection.isConnected = false;
6208
6372
  connection.isAuthenticating = false;
6209
6373
  const disconnectError = new Error('WebSocket disconnected');
@@ -7183,6 +7347,7 @@ async function closeAllSubscriptionsV2() {
7183
7347
  connection.tokenRefreshTimer = null;
7184
7348
  }
7185
7349
  if (connection.ws) {
7350
+ stopHeartbeat(connection);
7186
7351
  const ws = connection.ws;
7187
7352
  connection.ws = null;
7188
7353
  connection.isConnected = false;
@@ -7302,7 +7467,7 @@ async function doReconnectWithNewAuth() {
7302
7467
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
7303
7468
  }
7304
7469
  try {
7305
- const { reconnectRealtimeStoreWithNewAuth } = await import('./realtime-store-DqAAqfo9.mjs');
7470
+ const { reconnectRealtimeStoreWithNewAuth } = await import('./realtime-store-2iWofj5n.mjs');
7306
7471
  await reconnectRealtimeStoreWithNewAuth();
7307
7472
  }
7308
7473
  catch (error) {
@@ -8756,7 +8921,7 @@ async function setMany(many, options) {
8756
8921
  'Nothing was signed or submitted.');
8757
8922
  }
8758
8923
  const solanaChain = workerChain;
8759
- const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await import('./solana-operations-DGNA2nGZ.mjs');
8924
+ const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await import('./solana-operations-CCFNQIgd.mjs');
8760
8925
  // OA-0001: derive the expected intent from OUR OWN request (the one
8761
8926
  // ground truth a tampered builder response cannot change) so the
8762
8927
  // validator can reject any transaction whose operation semantics
@@ -10531,7 +10696,7 @@ function toMillis(seconds) {
10531
10696
 
10532
10697
  /** SIWS message creation is async already, so its onchain module can be lazy. */
10533
10698
  async function genSolanaMessage(address, nonce) {
10534
- const implementation = await import('./solana-message-C_717vAx.mjs');
10699
+ const implementation = await import('./solana-message-Bx04VMG1.mjs');
10535
10700
  return implementation.genSolanaMessage(address, nonce);
10536
10701
  }
10537
10702
 
@@ -11081,4 +11246,4 @@ function defineLiveModule(mod) {
11081
11246
  }
11082
11247
 
11083
11248
  export { serverTimestamp as $, subscribe as A, closeAllSubscriptions as B, clearCache as C, getCachedData as D, reconnectWithNewAuth as E, createClient as F, ScopedClientReadOnlyError as G, exchangeAppToken as H, InsufficientBalanceError as I, clearAppTokenCache as J, createAppTokenAuthHeaders as K, AppTokenExchangeError as L, subscribeQuery as M, closeAllQuerySubscriptions as N, canonicalizeArgs as O, canonicalizeResult as P, DEFAULT_QUERY_POLL_INTERVAL_MS as Q, MIN_QUERY_POLL_INTERVAL_MS as R, SOLANA_MAX_SUPPORTED_TRANSACTION_VERSION as S, BoundedDeclineError as T, deriveDecline as U, dispatchDeclineEvent as V, DECLINE_EVENT_NAME as W, dispatchRateLimitEvent as X, RATE_LIMIT_EVENT_NAME as Y, hasActiveConnection as Z, increment as _, getTransactionJson as a, now as a0, toSeconds as a1, toMillis as a2, getIdToken as a3, deriveUserIdentityFromIdToken as a4, getSessionGeneration as a5, WebSessionManager as a6, ReactNativeSessionManager as a7, getActiveSessionManager as a8, onSessionCleared as a9, intent as aA, status as aB, subscribeView as aC, LiveIntentError as aD, withEffects as aE, isEffectResult as aF, defineLiveModule as aG, EFFECT_INTENT_ADDRESS as aH, SessionEnvironmentError as aa, UNSUPPORTED_ENVIRONMENT_CODE as ab, AUTH_CHANGED_CODE as ac, AUTH_EXPIRED_CODE as ad, isAuthExpiredError as ae, getFreshAuthToken as af, ServerSessionManager as ag, createSessionWithSignature as ah, refreshSession as ai, revokeSession as aj, signSessionCreateMessage as ak, genAuthNonce as al, genSolanaMessage as am, createSolanaSignInRequest as an, createSolanaSignInMessageText as ao, genSolanaSignInRequest as ap, genSolanaSignInMessage as aq, browserSignInDomain as ar, SOLANA_SIGN_IN_STATEMENT as as, genSiweMessage as at, normalizeEvmAddress as au, toLowerEvmAddress as av, functions as aw, invoke as ax, FunctionInvokeError as ay, live as az, bufferExports as b, getWebhookKeysUrl as c, getConfiguredAppId as d, get as e, getMany as f, getConfig as g, docId as h, init as i, setMany as j, setFile as k, getFiles as l, search as m, runQueryMany as n, runExpression as o, runExpressionMany as p, queryAggregate as q, runQuery as r, set as s, signMessage as t, signTransaction as u, signAndSubmitTransaction as v, count as w, aggregate as x, reconcileSetResult as y, waitForSetResult as z };
11084
- //# sourceMappingURL=index-CMT0saxy.mjs.map
11249
+ //# sourceMappingURL=index-CC2WVN43.mjs.map