@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
@@ -4190,7 +4190,7 @@ class ServerSessionManager {
4190
4190
  const sessionConfig = (_a = this.sessionConfig) !== null && _a !== void 0 ? _a : undefined;
4191
4191
  /* fetch nonce from auth API (bound to the snapshotted app+issuer) */
4192
4192
  const nonce = await genAuthNonce(sessionConfig);
4193
- const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-CJTnYiJu.js'); });
4193
+ const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-DLBiBfnG.js'); });
4194
4194
  const message = await genSolanaMessage(address, nonce, sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId);
4195
4195
  /* sign the message */
4196
4196
  const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
@@ -5231,6 +5231,83 @@ async function resolveClientTransactionVersions(authProvider) {
5231
5231
  return typeof authProvider.signTransactionV1 === 'function' ? ['legacy', 0, 1] : ['legacy', 0];
5232
5232
  }
5233
5233
 
5234
+ /**
5235
+ * Idle heartbeat for the realtime WebSocket connection (subscription-v2.ts).
5236
+ *
5237
+ * A socket whose server has stopped delivering frames looks exactly like a
5238
+ * quiet one: the browser reports it open and nothing arrives. The client only
5239
+ * learns the difference by asking. Once nothing has been received for
5240
+ * HEARTBEAT_IDLE_MS the heartbeat asks the connection to send a ping; if
5241
+ * nothing at all arrives within HEARTBEAT_TIMEOUT_MS after that, the socket is
5242
+ * declared dead and the connection is reconnected. Any inbound frame counts as
5243
+ * proof of life, so a busy socket is never pinged and a pong is just another
5244
+ * frame. The connection may decline to ping (it already has a request in
5245
+ * flight with its own timeout); the idle wait then simply starts over.
5246
+ *
5247
+ * The timeout sits above both budgets a pong can wait behind: the connection's
5248
+ * own 30 s request budget (the server handles one frame per socket at a time)
5249
+ * and the object's 30 s CPU slice, which another socket's handler can hold
5250
+ * while every pong waits. A healthy socket must never be declared dead
5251
+ * within either.
5252
+ *
5253
+ * Pure: timers come from the clock passed in, so tests can drive it.
5254
+ */
5255
+ const HEARTBEAT_IDLE_MS = 25000;
5256
+ const HEARTBEAT_TIMEOUT_MS = 45000;
5257
+ class SocketHeartbeat {
5258
+ constructor(
5259
+ /** Send a ping now; return false to skip this round (the idle wait restarts). */
5260
+ sendPing, onDead, clock = globalThis, idleMs = HEARTBEAT_IDLE_MS, timeoutMs = HEARTBEAT_TIMEOUT_MS) {
5261
+ this.sendPing = sendPing;
5262
+ this.onDead = onDead;
5263
+ this.clock = clock;
5264
+ this.idleMs = idleMs;
5265
+ this.timeoutMs = timeoutMs;
5266
+ this.idleTimer = null;
5267
+ this.timeoutTimer = null;
5268
+ this.stopped = false;
5269
+ }
5270
+ start() {
5271
+ if (this.stopped)
5272
+ return;
5273
+ this.armIdle();
5274
+ }
5275
+ /** An inbound frame arrived: the socket is alive, so the idle wait starts over. */
5276
+ noteActivity() {
5277
+ if (this.stopped)
5278
+ return;
5279
+ this.clearTimers();
5280
+ this.armIdle();
5281
+ }
5282
+ stop() {
5283
+ this.stopped = true;
5284
+ this.clearTimers();
5285
+ }
5286
+ armIdle() {
5287
+ this.idleTimer = this.clock.setTimeout(() => {
5288
+ this.idleTimer = null;
5289
+ if (!this.sendPing()) {
5290
+ this.armIdle();
5291
+ return;
5292
+ }
5293
+ this.timeoutTimer = this.clock.setTimeout(() => {
5294
+ this.timeoutTimer = null;
5295
+ this.onDead();
5296
+ }, this.timeoutMs);
5297
+ }, this.idleMs);
5298
+ }
5299
+ clearTimers() {
5300
+ if (this.idleTimer !== null) {
5301
+ this.clock.clearTimeout(this.idleTimer);
5302
+ this.idleTimer = null;
5303
+ }
5304
+ if (this.timeoutTimer !== null) {
5305
+ this.clock.clearTimeout(this.timeoutTimer);
5306
+ this.timeoutTimer = null;
5307
+ }
5308
+ }
5309
+ }
5310
+
5234
5311
  /**
5235
5312
  * WebSocket v2 Subscription Manager
5236
5313
  *
@@ -5325,6 +5402,81 @@ const WS_CONFIG = {
5325
5402
  // Give handshakes more time before considering the attempt failed.
5326
5403
  connectionTimeout: 10000,
5327
5404
  };
5405
+ /** Client-chosen close code for a socket the heartbeat declared dead (4000-4999 are application codes). */
5406
+ const HEARTBEAT_CLOSE_CODE = 4000;
5407
+ function heartbeatReconnectDelayMs() {
5408
+ return BASE_MIN_RECONNECT_DELAY_MS + Math.floor(Math.random() * MIN_RECONNECT_DELAY_JITTER_MS);
5409
+ }
5410
+ /**
5411
+ * Start the idle heartbeat for the native socket that just opened on `ws`.
5412
+ * A ping goes out after HEARTBEAT_IDLE_MS of silence, and a socket that
5413
+ * answers nothing for HEARTBEAT_TIMEOUT_MS more is reconnected through the
5414
+ * wrapper after a jittered delay, the same path an auth change takes. A
5415
+ * server that has stopped dispatching frames leaves the socket open and
5416
+ * silent, so nothing else on the client would ever notice.
5417
+ */
5418
+ function startHeartbeat(connection, ws) {
5419
+ stopHeartbeat(connection);
5420
+ const heartbeat = new SocketHeartbeat(() => {
5421
+ // An outbound wait already has its own timeout, and the server
5422
+ // answers one frame per socket at a time, so a ping behind it would
5423
+ // only measure that wait. Let the idle wait start over instead.
5424
+ if (hasOutboundWait(connection))
5425
+ return false;
5426
+ // The wrapper drops a send on a socket that is not open, and a send
5427
+ // that throws is a socket that cannot carry a ping either way; the
5428
+ // dead timer then reports it.
5429
+ try {
5430
+ ws.send(JSON.stringify({ type: 'ping' }));
5431
+ }
5432
+ catch (error) {
5433
+ console.warn('[WS v2] Heartbeat ping could not be sent:', error);
5434
+ }
5435
+ return true;
5436
+ }, () => {
5437
+ // Only the socket this heartbeat was started for. A replaced or
5438
+ // torn-down connection must never be resurrected by a late timer.
5439
+ if (connection.heartbeat !== heartbeat || connection.ws !== ws)
5440
+ return;
5441
+ console.warn('[WS v2] No frames within the heartbeat window; reconnecting');
5442
+ // The heartbeat stays installed through the delay: a frame that
5443
+ // arrives meanwhile cancels the replacement (see the message
5444
+ // listener) and starts a fresh idle window on the same socket.
5445
+ connection.heartbeatReconnectTimer = setTimeout(() => {
5446
+ connection.heartbeatReconnectTimer = null;
5447
+ if (connection.heartbeat !== heartbeat || connection.ws !== ws)
5448
+ return;
5449
+ connection.heartbeat = null;
5450
+ try {
5451
+ ws.reconnect(HEARTBEAT_CLOSE_CODE, 'Heartbeat timeout');
5452
+ }
5453
+ catch (error) {
5454
+ console.error('[WS v2] Error reconnecting after heartbeat timeout:', error);
5455
+ }
5456
+ }, heartbeatReconnectDelayMs());
5457
+ });
5458
+ connection.heartbeat = heartbeat;
5459
+ heartbeat.start();
5460
+ }
5461
+ function stopHeartbeat(connection) {
5462
+ var _a;
5463
+ (_a = connection.heartbeat) === null || _a === void 0 ? void 0 : _a.stop();
5464
+ connection.heartbeat = null;
5465
+ if (connection.heartbeatReconnectTimer) {
5466
+ clearTimeout(connection.heartbeatReconnectTimer);
5467
+ connection.heartbeatReconnectTimer = null;
5468
+ }
5469
+ }
5470
+ /** A frame of ours the server has not answered yet: a request, a subscription confirmation, or a snapshot refresh. */
5471
+ function hasOutboundWait(connection) {
5472
+ if (connection.pendingRequests.size > 0 || connection.pendingSubscriptions.size > 0)
5473
+ return true;
5474
+ for (const subscription of connection.subscriptions.values()) {
5475
+ if (subscription.refresh)
5476
+ return true;
5477
+ }
5478
+ return false;
5479
+ }
5328
5480
  const WS_V2_PATH = '/ws/v2';
5329
5481
  let browserReconnectHooksAttached = false;
5330
5482
  let lastBrowserTriggeredReconnectAt = 0;
@@ -5793,6 +5945,7 @@ function closeConnectionNow(connection) {
5793
5945
  clearInterval(connection.tokenRefreshTimer);
5794
5946
  connection.tokenRefreshTimer = null;
5795
5947
  }
5948
+ stopHeartbeat(connection);
5796
5949
  const ws = connection.ws;
5797
5950
  connection.ws = null;
5798
5951
  connection.isConnecting = false;
@@ -6077,6 +6230,8 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
6077
6230
  authTokenValue: null,
6078
6231
  authPrincipal: null,
6079
6232
  socketGeneration: 0,
6233
+ heartbeat: null,
6234
+ heartbeatReconnectTimer: null,
6080
6235
  tokenRefreshTimer: null,
6081
6236
  idleCloseTimer: null,
6082
6237
  consecutiveAuthFailures: 0,
@@ -6151,6 +6306,7 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
6151
6306
  connection.socketGeneration += 1;
6152
6307
  connection.isConnecting = false;
6153
6308
  connection.isConnected = true;
6309
+ startHeartbeat(connection, ws);
6154
6310
  // NOTE: Do NOT reset consecutiveAuthFailures here. It is reset when a
6155
6311
  // fresh auth token is actually obtained or on an explicit auth change.
6156
6312
  // Schedule periodic token freshness checks. An anonymous (read-only
@@ -6185,6 +6341,13 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
6185
6341
  });
6186
6342
  // Handle incoming messages
6187
6343
  ws.addEventListener('message', (event) => {
6344
+ var _a;
6345
+ if (connection.heartbeatReconnectTimer) {
6346
+ // A frame inside the replacement delay: the socket is alive after all.
6347
+ clearTimeout(connection.heartbeatReconnectTimer);
6348
+ connection.heartbeatReconnectTimer = null;
6349
+ }
6350
+ (_a = connection.heartbeat) === null || _a === void 0 ? void 0 : _a.noteActivity();
6188
6351
  try {
6189
6352
  const message = JSON.parse(event.data);
6190
6353
  handleServerMessage(connection, message);
@@ -6206,6 +6369,7 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
6206
6369
  });
6207
6370
  // Handle close
6208
6371
  ws.addEventListener('close', () => {
6372
+ stopHeartbeat(connection);
6209
6373
  connection.isConnected = false;
6210
6374
  connection.isAuthenticating = false;
6211
6375
  const disconnectError = new Error('WebSocket disconnected');
@@ -7185,6 +7349,7 @@ async function closeAllSubscriptionsV2() {
7185
7349
  connection.tokenRefreshTimer = null;
7186
7350
  }
7187
7351
  if (connection.ws) {
7352
+ stopHeartbeat(connection);
7188
7353
  const ws = connection.ws;
7189
7354
  connection.ws = null;
7190
7355
  connection.isConnected = false;
@@ -7304,7 +7469,7 @@ async function doReconnectWithNewAuth() {
7304
7469
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
7305
7470
  }
7306
7471
  try {
7307
- const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-DrtEj-bE.js'); });
7472
+ const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-CuFYPLgk.js'); });
7308
7473
  await reconnectRealtimeStoreWithNewAuth();
7309
7474
  }
7310
7475
  catch (error) {
@@ -8758,7 +8923,7 @@ async function setMany(many, options) {
8758
8923
  'Nothing was signed or submitted.');
8759
8924
  }
8760
8925
  const solanaChain = workerChain;
8761
- const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-CU0YbiMu.js'); });
8926
+ const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-Be0dWtVZ.js'); });
8762
8927
  // OA-0001: derive the expected intent from OUR OWN request (the one
8763
8928
  // ground truth a tampered builder response cannot change) so the
8764
8929
  // validator can reject any transaction whose operation semantics
@@ -10533,7 +10698,7 @@ function toMillis(seconds) {
10533
10698
 
10534
10699
  /** SIWS message creation is async already, so its onchain module can be lazy. */
10535
10700
  async function genSolanaMessage(address, nonce) {
10536
- const implementation = await Promise.resolve().then(function () { return require('./solana-message-CJTnYiJu.js'); });
10701
+ const implementation = await Promise.resolve().then(function () { return require('./solana-message-DLBiBfnG.js'); });
10537
10702
  return implementation.genSolanaMessage(address, nonce);
10538
10703
  }
10539
10704
 
@@ -11180,4 +11345,4 @@ exports.toMillis = toMillis;
11180
11345
  exports.toSeconds = toSeconds;
11181
11346
  exports.waitForSetResult = waitForSetResult;
11182
11347
  exports.withEffects = withEffects;
11183
- //# sourceMappingURL=index-BEGeDFQI.js.map
11348
+ //# sourceMappingURL=index-D7dRe0PL.js.map