starknet 10.6.6 → 10.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -7292,6 +7292,8 @@ var Subscription = class {
7292
7292
  maxBufferSize;
7293
7293
  handler = null;
7294
7294
  _isClosed = false;
7295
+ // The unsubscribe request currently on the wire, shared by concurrent callers.
7296
+ pendingUnsubscribe = null;
7295
7297
  /**
7296
7298
  * @internal
7297
7299
  * @param options - Subscription configuration options
@@ -7310,6 +7312,20 @@ var Subscription = class {
7310
7312
  get isClosed() {
7311
7313
  return this._isClosed;
7312
7314
  }
7315
+ /**
7316
+ * Closes the subscription locally, without contacting the node.
7317
+ *
7318
+ * Used when the channel knows the subscription is gone and cannot be recovered — a
7319
+ * re-subscribe refused after a reconnection, for instance. Without it the object would keep
7320
+ * reporting itself as open while no event could ever reach its handler again.
7321
+ * @internal
7322
+ */
7323
+ _markClosed() {
7324
+ if (this._isClosed) return;
7325
+ this._isClosed = true;
7326
+ this.events.emit("unsubscribe", void 0);
7327
+ this.events.clear();
7328
+ }
7313
7329
  /**
7314
7330
  * Internal method to handle incoming events from the WebSocket channel.
7315
7331
  * If a handler is attached, it's invoked immediately. Otherwise, the event is buffered.
@@ -7356,14 +7372,24 @@ var Subscription = class {
7356
7372
  if (this._isClosed) {
7357
7373
  return true;
7358
7374
  }
7359
- const success = await this.channel.unsubscribe(this.id);
7360
- if (success) {
7361
- this._isClosed = true;
7362
- this.channel.removeSubscription(this.id);
7363
- this.events.emit("unsubscribe", void 0);
7364
- this.events.clear();
7375
+ if (this.pendingUnsubscribe) {
7376
+ return this.pendingUnsubscribe;
7365
7377
  }
7366
- return success;
7378
+ this.pendingUnsubscribe = (async () => {
7379
+ try {
7380
+ const success = await this.channel.unsubscribe(this.id);
7381
+ if (success) {
7382
+ this._isClosed = true;
7383
+ this.channel.removeSubscription(this.id);
7384
+ this.events.emit("unsubscribe", void 0);
7385
+ this.events.clear();
7386
+ }
7387
+ return success;
7388
+ } finally {
7389
+ this.pendingUnsubscribe = null;
7390
+ }
7391
+ })();
7392
+ return this.pendingUnsubscribe;
7367
7393
  }
7368
7394
  };
7369
7395
 
@@ -7394,6 +7420,17 @@ var WebSocketChannel = class {
7394
7420
  // resetting the reconnection attempt counter.
7395
7421
  reconnectStabilityTimeoutId = null;
7396
7422
  requestQueue = [];
7423
+ /** Abort handles for the requests currently on the wire, one per pending `sendReceive`. */
7424
+ inFlight = /* @__PURE__ */ new Set();
7425
+ /**
7426
+ * Callers blocked in `waitForUnsubscription`, keyed by subscription id.
7427
+ *
7428
+ * Held here rather than as `unsubscribe` event listeners because that event only ever
7429
+ * announces success: a waiter attached to it cannot learn that the node refused the
7430
+ * unsubscribe or that the connection went away, and would wait forever with no timeout of
7431
+ * its own to fall back on.
7432
+ */
7433
+ unsubscribeWaiters = /* @__PURE__ */ new Map();
7397
7434
  events = new EventEmitter();
7398
7435
  openListener = (ev) => {
7399
7436
  this.scheduleReconnectAttemptsReset();
@@ -7477,12 +7514,19 @@ var WebSocketChannel = class {
7477
7514
  });
7478
7515
  }
7479
7516
  const sendId = this.send(method, params);
7517
+ const socket = this.websocket;
7480
7518
  return new Promise((resolve, reject) => {
7481
- let timeoutId;
7482
- if (!this.websocket || this.websocket.readyState !== ws_default.OPEN) {
7519
+ if (socket.readyState !== ws_default.OPEN) {
7483
7520
  reject(new WebSocketNotConnectedError("WebSocket not available or not connected."));
7484
7521
  return;
7485
7522
  }
7523
+ let timeoutId;
7524
+ const settle = () => {
7525
+ clearTimeout(timeoutId);
7526
+ socket.removeEventListener("message", messageHandler);
7527
+ socket.removeEventListener("error", errorHandler);
7528
+ this.inFlight.delete(abort);
7529
+ };
7486
7530
  const messageHandler = (event) => {
7487
7531
  if (!isString(event.data)) {
7488
7532
  logger.warn("WebSocket received non-string message data:", event.data);
@@ -7497,34 +7541,35 @@ var WebSocketChannel = class {
7497
7541
  );
7498
7542
  return;
7499
7543
  }
7500
- if (message.id === sendId) {
7501
- clearTimeout(timeoutId);
7502
- this.websocket.removeEventListener("message", messageHandler);
7503
- this.websocket.removeEventListener("error", errorHandler);
7504
- if ("result" in message) {
7505
- resolve(message.result);
7506
- } else {
7507
- reject(
7508
- new Error(`Error on ${method} (id: ${sendId}): ${JSON.stringify(message.error)}`)
7509
- );
7510
- }
7544
+ if (message.id !== sendId) return;
7545
+ settle();
7546
+ if ("result" in message) {
7547
+ resolve(message.result);
7548
+ } else {
7549
+ reject(new Error(`Error on ${method} (id: ${sendId}): ${JSON.stringify(message.error)}`));
7511
7550
  }
7512
7551
  };
7513
7552
  const errorHandler = (event) => {
7514
- clearTimeout(timeoutId);
7515
- this.websocket.removeEventListener("message", messageHandler);
7516
- this.websocket.removeEventListener("error", errorHandler);
7553
+ settle();
7517
7554
  reject(
7518
7555
  new Error(
7519
7556
  `WebSocket error during ${method} (id: ${sendId}): ${event.type || "Unknown error"}`
7520
7557
  )
7521
7558
  );
7522
7559
  };
7523
- this.websocket.addEventListener("message", messageHandler);
7524
- this.websocket.addEventListener("error", errorHandler);
7560
+ const abort = (reason) => {
7561
+ settle();
7562
+ reject(
7563
+ new WebSocketNotConnectedError(
7564
+ `Request ${method} (id: ${sendId}) went unanswered: ${reason}`
7565
+ )
7566
+ );
7567
+ };
7568
+ socket.addEventListener("message", messageHandler);
7569
+ socket.addEventListener("error", errorHandler);
7570
+ this.inFlight.add(abort);
7525
7571
  timeoutId = setTimeout(() => {
7526
- this.websocket.removeEventListener("message", messageHandler);
7527
- this.websocket.removeEventListener("error", errorHandler);
7572
+ settle();
7528
7573
  reject(
7529
7574
  new TimeoutError(
7530
7575
  `Request ${method} (id: ${sendId}) timed out after ${this.requestTimeout}ms`
@@ -7581,6 +7626,8 @@ var WebSocketChannel = class {
7581
7626
  this.userInitiatedClose = true;
7582
7627
  this.isReconnecting = false;
7583
7628
  this._rejectRequestQueue("the connection was closed by the user");
7629
+ this._rejectInFlight("the connection was closed by the user");
7630
+ this._rejectUnsubscribeWaiters("the connection was closed by the user");
7584
7631
  this.websocket.close(code, reason);
7585
7632
  }
7586
7633
  /**
@@ -7605,14 +7652,48 @@ var WebSocketChannel = class {
7605
7652
  * @returns {Promise<boolean>} A Promise that resolves with `true` if the unsubscription was successful.
7606
7653
  */
7607
7654
  async unsubscribe(subscriptionId) {
7608
- const status = await this.sendReceive("starknet_unsubscribe", {
7609
- subscription_id: subscriptionId
7610
- });
7655
+ let status;
7656
+ try {
7657
+ status = await this.sendReceive("starknet_unsubscribe", {
7658
+ subscription_id: subscriptionId
7659
+ });
7660
+ } catch (error) {
7661
+ this._settleUnsubscribeWaiters(subscriptionId, error);
7662
+ throw error;
7663
+ }
7611
7664
  if (status) {
7612
7665
  this.events.emit("unsubscribe", subscriptionId);
7666
+ this._settleUnsubscribeWaiters(subscriptionId);
7667
+ } else {
7668
+ this._settleUnsubscribeWaiters(
7669
+ subscriptionId,
7670
+ new Error(`Node refused to unsubscribe subscription ${subscriptionId}`)
7671
+ );
7613
7672
  }
7614
7673
  return status;
7615
7674
  }
7675
+ /** Settles every caller waiting on one subscription id: resolved, or rejected with `error`. */
7676
+ _settleUnsubscribeWaiters(subscriptionId, error) {
7677
+ const waiters = this.unsubscribeWaiters.get(subscriptionId);
7678
+ if (!waiters) return;
7679
+ this.unsubscribeWaiters.delete(subscriptionId);
7680
+ waiters.forEach((waiter) => error ? waiter.reject(error) : waiter.resolve());
7681
+ }
7682
+ /**
7683
+ * Rejects every caller still waiting on any subscription.
7684
+ *
7685
+ * Once the connection is gone, no unsubscribe can be observed on it: a reconnection restores
7686
+ * each subscription under a fresh id, so the id being waited on will never be announced.
7687
+ */
7688
+ _rejectUnsubscribeWaiters(reason) {
7689
+ if (this.unsubscribeWaiters.size === 0) return;
7690
+ Array.from(this.unsubscribeWaiters.keys()).forEach(
7691
+ (id) => this._settleUnsubscribeWaiters(
7692
+ id,
7693
+ new WebSocketNotConnectedError(`Subscription ${id} was never unsubscribed: ${reason}`)
7694
+ )
7695
+ );
7696
+ }
7616
7697
  /**
7617
7698
  * Returns a Promise that resolves when a specific subscription is successfully unsubscribed.
7618
7699
  * @param {SUBSCRIPTION_ID} targetId - The ID of the subscription to wait for.
@@ -7624,14 +7705,10 @@ var WebSocketChannel = class {
7624
7705
  * ```
7625
7706
  */
7626
7707
  waitForUnsubscription(targetId) {
7627
- return new Promise((resolve) => {
7628
- const listener = (unsubId) => {
7629
- if (unsubId === targetId) {
7630
- this.events.off("unsubscribe", listener);
7631
- resolve();
7632
- }
7633
- };
7634
- this.events.on("unsubscribe", listener);
7708
+ return new Promise((resolve, reject) => {
7709
+ const waiters = this.unsubscribeWaiters.get(targetId) ?? /* @__PURE__ */ new Set();
7710
+ waiters.add({ resolve, reject });
7711
+ this.unsubscribeWaiters.set(targetId, waiters);
7635
7712
  });
7636
7713
  }
7637
7714
  /**
@@ -7672,6 +7749,21 @@ var WebSocketChannel = class {
7672
7749
  reject(new WebSocketNotConnectedError(`Request ${method} was never sent: ${reason}`));
7673
7750
  });
7674
7751
  }
7752
+ /**
7753
+ * Settle every request already on the wire.
7754
+ *
7755
+ * The counterpart of `_rejectRequestQueue`, for requests past the queue. Their only other
7756
+ * exit is the `requestTimeout` timer, so without this the caller waits the whole timeout —
7757
+ * 60s by default — for a reply that can no longer arrive, and that pending timer keeps the
7758
+ * Node event loop alive for just as long.
7759
+ */
7760
+ _rejectInFlight(reason) {
7761
+ if (this.inFlight.size === 0) return;
7762
+ const pending = Array.from(this.inFlight);
7763
+ this.inFlight.clear();
7764
+ logger.info(`WebSocket: Rejecting ${pending.length} in-flight request(s). Reason: ${reason}.`);
7765
+ pending.forEach((abort) => abort(reason));
7766
+ }
7675
7767
  async _restoreSubscriptions() {
7676
7768
  const oldSubscriptions = Array.from(this.activeSubscriptions.values());
7677
7769
  this.activeSubscriptions.clear();
@@ -7683,6 +7775,7 @@ var WebSocketChannel = class {
7683
7775
  logger.info(`Subscription ${sub.method} restored with new ID: ${newSubId}`);
7684
7776
  } catch (error) {
7685
7777
  logger.error(`Failed to restore subscription ${sub.method}:`, error);
7778
+ sub._markClosed();
7686
7779
  }
7687
7780
  });
7688
7781
  await Promise.all(restorePromises);
@@ -7725,18 +7818,24 @@ var WebSocketChannel = class {
7725
7818
  `WebSocket: Connection lost. Attempting to reconnect... (${this.reconnectAttempts}/${this.reconnectOptions.retries})`
7726
7819
  );
7727
7820
  this.reconnect();
7821
+ let attemptSettled = false;
7822
+ const scheduleRetry = () => {
7823
+ if (attemptSettled || !this.isReconnecting) return;
7824
+ attemptSettled = true;
7825
+ const delay = this.reconnectOptions.exponential ? this.reconnectOptions.delay * 2 ** (this.reconnectAttempts - 1) : this.reconnectOptions.delay;
7826
+ logger.info(`WebSocket: Reconnect attempt failed. Retrying in ${delay}ms.`);
7827
+ this.reconnectTimeoutId = setTimeout(tryReconnect, delay);
7828
+ };
7728
7829
  this.websocket.onopen = async () => {
7729
7830
  logger.info("WebSocket: Reconnection successful.");
7831
+ attemptSettled = true;
7730
7832
  this.isReconnecting = false;
7731
7833
  await this._restoreSubscriptions();
7732
7834
  this._processRequestQueue();
7733
7835
  this.events.emit("open", new Event("open"));
7734
7836
  };
7735
- this.websocket.onerror = () => {
7736
- const delay = this.reconnectOptions.exponential ? this.reconnectOptions.delay * 2 ** (this.reconnectAttempts - 1) : this.reconnectOptions.delay;
7737
- logger.info(`WebSocket: Reconnect attempt failed. Retrying in ${delay}ms.`);
7738
- this.reconnectTimeoutId = setTimeout(tryReconnect, delay);
7739
- };
7837
+ this.websocket.onerror = scheduleRetry;
7838
+ this.websocket.addEventListener("close", scheduleRetry);
7740
7839
  };
7741
7840
  tryReconnect();
7742
7841
  }
@@ -7745,6 +7844,8 @@ var WebSocketChannel = class {
7745
7844
  this.websocket.removeEventListener("close", this.closeListener);
7746
7845
  this.websocket.removeEventListener("message", this.messageListener);
7747
7846
  this.websocket.removeEventListener("error", this.errorListener);
7847
+ this._rejectInFlight("the connection was closed");
7848
+ this._rejectUnsubscribeWaiters("the connection was closed");
7748
7849
  this.events.emit("close", ev);
7749
7850
  if (!this.userInitiatedClose) {
7750
7851
  this._startReconnect();
@@ -7766,9 +7867,16 @@ var WebSocketChannel = class {
7766
7867
  if (subscription) {
7767
7868
  subscription._handleEvent(result);
7768
7869
  } else {
7769
- logger.warn(
7770
- `WebSocketChannel: Received event for untracked subscription ID: ${subscription_id}.`
7771
- );
7870
+ queueMicrotask(() => {
7871
+ const registered = this.activeSubscriptions.get(subscription_id);
7872
+ if (registered) {
7873
+ registered._handleEvent(result);
7874
+ } else {
7875
+ logger.warn(
7876
+ `WebSocketChannel: Received event for untracked subscription ID: ${subscription_id}.`
7877
+ );
7878
+ }
7879
+ });
7772
7880
  }
7773
7881
  }
7774
7882
  logger.debug("onMessageProxy:", event.data);