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.js CHANGED
@@ -7476,6 +7476,8 @@ var Subscription = class {
7476
7476
  maxBufferSize;
7477
7477
  handler = null;
7478
7478
  _isClosed = false;
7479
+ // The unsubscribe request currently on the wire, shared by concurrent callers.
7480
+ pendingUnsubscribe = null;
7479
7481
  /**
7480
7482
  * @internal
7481
7483
  * @param options - Subscription configuration options
@@ -7494,6 +7496,20 @@ var Subscription = class {
7494
7496
  get isClosed() {
7495
7497
  return this._isClosed;
7496
7498
  }
7499
+ /**
7500
+ * Closes the subscription locally, without contacting the node.
7501
+ *
7502
+ * Used when the channel knows the subscription is gone and cannot be recovered — a
7503
+ * re-subscribe refused after a reconnection, for instance. Without it the object would keep
7504
+ * reporting itself as open while no event could ever reach its handler again.
7505
+ * @internal
7506
+ */
7507
+ _markClosed() {
7508
+ if (this._isClosed) return;
7509
+ this._isClosed = true;
7510
+ this.events.emit("unsubscribe", void 0);
7511
+ this.events.clear();
7512
+ }
7497
7513
  /**
7498
7514
  * Internal method to handle incoming events from the WebSocket channel.
7499
7515
  * If a handler is attached, it's invoked immediately. Otherwise, the event is buffered.
@@ -7540,14 +7556,24 @@ var Subscription = class {
7540
7556
  if (this._isClosed) {
7541
7557
  return true;
7542
7558
  }
7543
- const success = await this.channel.unsubscribe(this.id);
7544
- if (success) {
7545
- this._isClosed = true;
7546
- this.channel.removeSubscription(this.id);
7547
- this.events.emit("unsubscribe", void 0);
7548
- this.events.clear();
7559
+ if (this.pendingUnsubscribe) {
7560
+ return this.pendingUnsubscribe;
7549
7561
  }
7550
- return success;
7562
+ this.pendingUnsubscribe = (async () => {
7563
+ try {
7564
+ const success = await this.channel.unsubscribe(this.id);
7565
+ if (success) {
7566
+ this._isClosed = true;
7567
+ this.channel.removeSubscription(this.id);
7568
+ this.events.emit("unsubscribe", void 0);
7569
+ this.events.clear();
7570
+ }
7571
+ return success;
7572
+ } finally {
7573
+ this.pendingUnsubscribe = null;
7574
+ }
7575
+ })();
7576
+ return this.pendingUnsubscribe;
7551
7577
  }
7552
7578
  };
7553
7579
 
@@ -7578,6 +7604,17 @@ var WebSocketChannel = class {
7578
7604
  // resetting the reconnection attempt counter.
7579
7605
  reconnectStabilityTimeoutId = null;
7580
7606
  requestQueue = [];
7607
+ /** Abort handles for the requests currently on the wire, one per pending `sendReceive`. */
7608
+ inFlight = /* @__PURE__ */ new Set();
7609
+ /**
7610
+ * Callers blocked in `waitForUnsubscription`, keyed by subscription id.
7611
+ *
7612
+ * Held here rather than as `unsubscribe` event listeners because that event only ever
7613
+ * announces success: a waiter attached to it cannot learn that the node refused the
7614
+ * unsubscribe or that the connection went away, and would wait forever with no timeout of
7615
+ * its own to fall back on.
7616
+ */
7617
+ unsubscribeWaiters = /* @__PURE__ */ new Map();
7581
7618
  events = new EventEmitter();
7582
7619
  openListener = (ev) => {
7583
7620
  this.scheduleReconnectAttemptsReset();
@@ -7661,12 +7698,19 @@ var WebSocketChannel = class {
7661
7698
  });
7662
7699
  }
7663
7700
  const sendId = this.send(method, params);
7701
+ const socket = this.websocket;
7664
7702
  return new Promise((resolve, reject) => {
7665
- let timeoutId;
7666
- if (!this.websocket || this.websocket.readyState !== ws_default.OPEN) {
7703
+ if (socket.readyState !== ws_default.OPEN) {
7667
7704
  reject(new WebSocketNotConnectedError("WebSocket not available or not connected."));
7668
7705
  return;
7669
7706
  }
7707
+ let timeoutId;
7708
+ const settle = () => {
7709
+ clearTimeout(timeoutId);
7710
+ socket.removeEventListener("message", messageHandler);
7711
+ socket.removeEventListener("error", errorHandler);
7712
+ this.inFlight.delete(abort);
7713
+ };
7670
7714
  const messageHandler = (event) => {
7671
7715
  if (!isString(event.data)) {
7672
7716
  logger.warn("WebSocket received non-string message data:", event.data);
@@ -7681,34 +7725,35 @@ var WebSocketChannel = class {
7681
7725
  );
7682
7726
  return;
7683
7727
  }
7684
- if (message.id === sendId) {
7685
- clearTimeout(timeoutId);
7686
- this.websocket.removeEventListener("message", messageHandler);
7687
- this.websocket.removeEventListener("error", errorHandler);
7688
- if ("result" in message) {
7689
- resolve(message.result);
7690
- } else {
7691
- reject(
7692
- new Error(`Error on ${method} (id: ${sendId}): ${JSON.stringify(message.error)}`)
7693
- );
7694
- }
7728
+ if (message.id !== sendId) return;
7729
+ settle();
7730
+ if ("result" in message) {
7731
+ resolve(message.result);
7732
+ } else {
7733
+ reject(new Error(`Error on ${method} (id: ${sendId}): ${JSON.stringify(message.error)}`));
7695
7734
  }
7696
7735
  };
7697
7736
  const errorHandler = (event) => {
7698
- clearTimeout(timeoutId);
7699
- this.websocket.removeEventListener("message", messageHandler);
7700
- this.websocket.removeEventListener("error", errorHandler);
7737
+ settle();
7701
7738
  reject(
7702
7739
  new Error(
7703
7740
  `WebSocket error during ${method} (id: ${sendId}): ${event.type || "Unknown error"}`
7704
7741
  )
7705
7742
  );
7706
7743
  };
7707
- this.websocket.addEventListener("message", messageHandler);
7708
- this.websocket.addEventListener("error", errorHandler);
7744
+ const abort = (reason) => {
7745
+ settle();
7746
+ reject(
7747
+ new WebSocketNotConnectedError(
7748
+ `Request ${method} (id: ${sendId}) went unanswered: ${reason}`
7749
+ )
7750
+ );
7751
+ };
7752
+ socket.addEventListener("message", messageHandler);
7753
+ socket.addEventListener("error", errorHandler);
7754
+ this.inFlight.add(abort);
7709
7755
  timeoutId = setTimeout(() => {
7710
- this.websocket.removeEventListener("message", messageHandler);
7711
- this.websocket.removeEventListener("error", errorHandler);
7756
+ settle();
7712
7757
  reject(
7713
7758
  new TimeoutError(
7714
7759
  `Request ${method} (id: ${sendId}) timed out after ${this.requestTimeout}ms`
@@ -7765,6 +7810,8 @@ var WebSocketChannel = class {
7765
7810
  this.userInitiatedClose = true;
7766
7811
  this.isReconnecting = false;
7767
7812
  this._rejectRequestQueue("the connection was closed by the user");
7813
+ this._rejectInFlight("the connection was closed by the user");
7814
+ this._rejectUnsubscribeWaiters("the connection was closed by the user");
7768
7815
  this.websocket.close(code, reason);
7769
7816
  }
7770
7817
  /**
@@ -7789,14 +7836,48 @@ var WebSocketChannel = class {
7789
7836
  * @returns {Promise<boolean>} A Promise that resolves with `true` if the unsubscription was successful.
7790
7837
  */
7791
7838
  async unsubscribe(subscriptionId) {
7792
- const status = await this.sendReceive("starknet_unsubscribe", {
7793
- subscription_id: subscriptionId
7794
- });
7839
+ let status;
7840
+ try {
7841
+ status = await this.sendReceive("starknet_unsubscribe", {
7842
+ subscription_id: subscriptionId
7843
+ });
7844
+ } catch (error) {
7845
+ this._settleUnsubscribeWaiters(subscriptionId, error);
7846
+ throw error;
7847
+ }
7795
7848
  if (status) {
7796
7849
  this.events.emit("unsubscribe", subscriptionId);
7850
+ this._settleUnsubscribeWaiters(subscriptionId);
7851
+ } else {
7852
+ this._settleUnsubscribeWaiters(
7853
+ subscriptionId,
7854
+ new Error(`Node refused to unsubscribe subscription ${subscriptionId}`)
7855
+ );
7797
7856
  }
7798
7857
  return status;
7799
7858
  }
7859
+ /** Settles every caller waiting on one subscription id: resolved, or rejected with `error`. */
7860
+ _settleUnsubscribeWaiters(subscriptionId, error) {
7861
+ const waiters = this.unsubscribeWaiters.get(subscriptionId);
7862
+ if (!waiters) return;
7863
+ this.unsubscribeWaiters.delete(subscriptionId);
7864
+ waiters.forEach((waiter) => error ? waiter.reject(error) : waiter.resolve());
7865
+ }
7866
+ /**
7867
+ * Rejects every caller still waiting on any subscription.
7868
+ *
7869
+ * Once the connection is gone, no unsubscribe can be observed on it: a reconnection restores
7870
+ * each subscription under a fresh id, so the id being waited on will never be announced.
7871
+ */
7872
+ _rejectUnsubscribeWaiters(reason) {
7873
+ if (this.unsubscribeWaiters.size === 0) return;
7874
+ Array.from(this.unsubscribeWaiters.keys()).forEach(
7875
+ (id) => this._settleUnsubscribeWaiters(
7876
+ id,
7877
+ new WebSocketNotConnectedError(`Subscription ${id} was never unsubscribed: ${reason}`)
7878
+ )
7879
+ );
7880
+ }
7800
7881
  /**
7801
7882
  * Returns a Promise that resolves when a specific subscription is successfully unsubscribed.
7802
7883
  * @param {SUBSCRIPTION_ID} targetId - The ID of the subscription to wait for.
@@ -7808,14 +7889,10 @@ var WebSocketChannel = class {
7808
7889
  * ```
7809
7890
  */
7810
7891
  waitForUnsubscription(targetId) {
7811
- return new Promise((resolve) => {
7812
- const listener = (unsubId) => {
7813
- if (unsubId === targetId) {
7814
- this.events.off("unsubscribe", listener);
7815
- resolve();
7816
- }
7817
- };
7818
- this.events.on("unsubscribe", listener);
7892
+ return new Promise((resolve, reject) => {
7893
+ const waiters = this.unsubscribeWaiters.get(targetId) ?? /* @__PURE__ */ new Set();
7894
+ waiters.add({ resolve, reject });
7895
+ this.unsubscribeWaiters.set(targetId, waiters);
7819
7896
  });
7820
7897
  }
7821
7898
  /**
@@ -7856,6 +7933,21 @@ var WebSocketChannel = class {
7856
7933
  reject(new WebSocketNotConnectedError(`Request ${method} was never sent: ${reason}`));
7857
7934
  });
7858
7935
  }
7936
+ /**
7937
+ * Settle every request already on the wire.
7938
+ *
7939
+ * The counterpart of `_rejectRequestQueue`, for requests past the queue. Their only other
7940
+ * exit is the `requestTimeout` timer, so without this the caller waits the whole timeout —
7941
+ * 60s by default — for a reply that can no longer arrive, and that pending timer keeps the
7942
+ * Node event loop alive for just as long.
7943
+ */
7944
+ _rejectInFlight(reason) {
7945
+ if (this.inFlight.size === 0) return;
7946
+ const pending = Array.from(this.inFlight);
7947
+ this.inFlight.clear();
7948
+ logger.info(`WebSocket: Rejecting ${pending.length} in-flight request(s). Reason: ${reason}.`);
7949
+ pending.forEach((abort) => abort(reason));
7950
+ }
7859
7951
  async _restoreSubscriptions() {
7860
7952
  const oldSubscriptions = Array.from(this.activeSubscriptions.values());
7861
7953
  this.activeSubscriptions.clear();
@@ -7867,6 +7959,7 @@ var WebSocketChannel = class {
7867
7959
  logger.info(`Subscription ${sub.method} restored with new ID: ${newSubId}`);
7868
7960
  } catch (error) {
7869
7961
  logger.error(`Failed to restore subscription ${sub.method}:`, error);
7962
+ sub._markClosed();
7870
7963
  }
7871
7964
  });
7872
7965
  await Promise.all(restorePromises);
@@ -7909,18 +8002,24 @@ var WebSocketChannel = class {
7909
8002
  `WebSocket: Connection lost. Attempting to reconnect... (${this.reconnectAttempts}/${this.reconnectOptions.retries})`
7910
8003
  );
7911
8004
  this.reconnect();
8005
+ let attemptSettled = false;
8006
+ const scheduleRetry = () => {
8007
+ if (attemptSettled || !this.isReconnecting) return;
8008
+ attemptSettled = true;
8009
+ const delay = this.reconnectOptions.exponential ? this.reconnectOptions.delay * 2 ** (this.reconnectAttempts - 1) : this.reconnectOptions.delay;
8010
+ logger.info(`WebSocket: Reconnect attempt failed. Retrying in ${delay}ms.`);
8011
+ this.reconnectTimeoutId = setTimeout(tryReconnect, delay);
8012
+ };
7912
8013
  this.websocket.onopen = async () => {
7913
8014
  logger.info("WebSocket: Reconnection successful.");
8015
+ attemptSettled = true;
7914
8016
  this.isReconnecting = false;
7915
8017
  await this._restoreSubscriptions();
7916
8018
  this._processRequestQueue();
7917
8019
  this.events.emit("open", new Event("open"));
7918
8020
  };
7919
- this.websocket.onerror = () => {
7920
- const delay = this.reconnectOptions.exponential ? this.reconnectOptions.delay * 2 ** (this.reconnectAttempts - 1) : this.reconnectOptions.delay;
7921
- logger.info(`WebSocket: Reconnect attempt failed. Retrying in ${delay}ms.`);
7922
- this.reconnectTimeoutId = setTimeout(tryReconnect, delay);
7923
- };
8021
+ this.websocket.onerror = scheduleRetry;
8022
+ this.websocket.addEventListener("close", scheduleRetry);
7924
8023
  };
7925
8024
  tryReconnect();
7926
8025
  }
@@ -7929,6 +8028,8 @@ var WebSocketChannel = class {
7929
8028
  this.websocket.removeEventListener("close", this.closeListener);
7930
8029
  this.websocket.removeEventListener("message", this.messageListener);
7931
8030
  this.websocket.removeEventListener("error", this.errorListener);
8031
+ this._rejectInFlight("the connection was closed");
8032
+ this._rejectUnsubscribeWaiters("the connection was closed");
7932
8033
  this.events.emit("close", ev);
7933
8034
  if (!this.userInitiatedClose) {
7934
8035
  this._startReconnect();
@@ -7950,9 +8051,16 @@ var WebSocketChannel = class {
7950
8051
  if (subscription) {
7951
8052
  subscription._handleEvent(result);
7952
8053
  } else {
7953
- logger.warn(
7954
- `WebSocketChannel: Received event for untracked subscription ID: ${subscription_id}.`
7955
- );
8054
+ queueMicrotask(() => {
8055
+ const registered = this.activeSubscriptions.get(subscription_id);
8056
+ if (registered) {
8057
+ registered._handleEvent(result);
8058
+ } else {
8059
+ logger.warn(
8060
+ `WebSocketChannel: Received event for untracked subscription ID: ${subscription_id}.`
8061
+ );
8062
+ }
8063
+ });
7956
8064
  }
7957
8065
  }
7958
8066
  logger.debug("onMessageProxy:", event.data);