@granular-software/sdk 0.3.2 → 0.3.4

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
@@ -3944,6 +3944,10 @@ var WSClient = class {
3944
3944
  */
3945
3945
  async connect() {
3946
3946
  this.isExplicitlyDisconnected = false;
3947
+ if (this.reconnectTimer) {
3948
+ clearTimeout(this.reconnectTimer);
3949
+ this.reconnectTimer = null;
3950
+ }
3947
3951
  let WebSocketClass = this.options.WebSocketCtor || GlobalWebSocket;
3948
3952
  if (!WebSocketClass) {
3949
3953
  try {
@@ -3965,6 +3969,10 @@ var WSClient = class {
3965
3969
  const socket = this.ws;
3966
3970
  if (typeof socket.on === "function") {
3967
3971
  socket.on("open", () => {
3972
+ if (this.reconnectTimer) {
3973
+ clearTimeout(this.reconnectTimer);
3974
+ this.reconnectTimer = null;
3975
+ }
3968
3976
  this.emit("open", {});
3969
3977
  resolve();
3970
3978
  });
@@ -3982,12 +3990,20 @@ var WSClient = class {
3982
3990
  reject(error);
3983
3991
  }
3984
3992
  });
3985
- socket.on("close", () => {
3986
- this.emit("close", {});
3987
- this.handleDisconnect();
3993
+ socket.on("close", (code, reason) => {
3994
+ this.handleDisconnect({
3995
+ code,
3996
+ reason: this.normalizeReason(reason),
3997
+ // ws does not provide wasClean on Node-style close callback
3998
+ wasClean: code === 1e3
3999
+ });
3988
4000
  });
3989
4001
  } else {
3990
4002
  this.ws.onopen = () => {
4003
+ if (this.reconnectTimer) {
4004
+ clearTimeout(this.reconnectTimer);
4005
+ this.reconnectTimer = null;
4006
+ }
3991
4007
  this.emit("open", {});
3992
4008
  resolve();
3993
4009
  };
@@ -4008,9 +4024,12 @@ var WSClient = class {
4008
4024
  reject(error);
4009
4025
  }
4010
4026
  };
4011
- this.ws.onclose = () => {
4012
- this.emit("close", {});
4013
- this.handleDisconnect();
4027
+ this.ws.onclose = (event) => {
4028
+ this.handleDisconnect({
4029
+ code: event.code,
4030
+ reason: event.reason,
4031
+ wasClean: event.wasClean
4032
+ });
4014
4033
  };
4015
4034
  }
4016
4035
  } catch (error) {
@@ -4018,13 +4037,80 @@ var WSClient = class {
4018
4037
  }
4019
4038
  });
4020
4039
  }
4021
- handleDisconnect() {
4040
+ normalizeReason(reason) {
4041
+ if (reason === null || reason === void 0) return void 0;
4042
+ if (typeof reason === "string") return reason;
4043
+ if (typeof reason === "object" && reason && "toString" in reason) {
4044
+ try {
4045
+ const text = String(reason.toString());
4046
+ return text || void 0;
4047
+ } catch {
4048
+ return void 0;
4049
+ }
4050
+ }
4051
+ return void 0;
4052
+ }
4053
+ rejectPending(error) {
4054
+ this.messageQueue.forEach((pending) => pending.reject(error));
4055
+ this.messageQueue = [];
4056
+ }
4057
+ buildDisconnectError(info) {
4058
+ const details = [
4059
+ info.code !== void 0 ? `code=${info.code}` : void 0,
4060
+ info.reason ? `reason=${info.reason}` : void 0
4061
+ ].filter(Boolean).join(", ");
4062
+ const suffix = details ? ` (${details})` : "";
4063
+ return new Error(`WebSocket disconnected${suffix}`);
4064
+ }
4065
+ handleDisconnect(close = {}) {
4066
+ const reconnectDelayMs = 3e3;
4067
+ const unexpected = !this.isExplicitlyDisconnected;
4068
+ const info = {
4069
+ code: close.code,
4070
+ reason: close.reason,
4071
+ wasClean: close.wasClean,
4072
+ unexpected,
4073
+ timestamp: Date.now(),
4074
+ reconnectScheduled: false
4075
+ };
4022
4076
  this.ws = null;
4023
- if (!this.isExplicitlyDisconnected) {
4077
+ this.emit("close", info);
4078
+ if (this.reconnectTimer) {
4079
+ clearTimeout(this.reconnectTimer);
4080
+ this.reconnectTimer = null;
4081
+ }
4082
+ if (unexpected) {
4083
+ const disconnectError = this.buildDisconnectError(info);
4084
+ this.rejectPending(disconnectError);
4085
+ this.emit("disconnect", info);
4086
+ info.reconnectScheduled = true;
4087
+ info.reconnectDelayMs = reconnectDelayMs;
4088
+ if (this.options.onUnexpectedClose) {
4089
+ try {
4090
+ this.options.onUnexpectedClose(info);
4091
+ } catch (callbackError) {
4092
+ console.error("[Granular] onUnexpectedClose callback failed:", callbackError);
4093
+ }
4094
+ }
4024
4095
  this.reconnectTimer = setTimeout(() => {
4025
4096
  console.log("[Granular] Attempting reconnect...");
4026
- this.connect().catch((e) => console.error("[Granular] Reconnect failed:", e));
4027
- }, 3e3);
4097
+ this.connect().catch((error) => {
4098
+ console.error("[Granular] Reconnect failed:", error);
4099
+ const reconnectInfo = {
4100
+ error: error instanceof Error ? error.message : String(error),
4101
+ sessionId: this.sessionId,
4102
+ timestamp: Date.now()
4103
+ };
4104
+ this.emit("reconnect_error", reconnectInfo);
4105
+ if (this.options.onReconnectError) {
4106
+ try {
4107
+ this.options.onReconnectError(reconnectInfo);
4108
+ } catch (callbackError) {
4109
+ console.error("[Granular] onReconnectError callback failed:", callbackError);
4110
+ }
4111
+ }
4112
+ });
4113
+ }, reconnectDelayMs);
4028
4114
  }
4029
4115
  }
4030
4116
  handleMessage(message) {
@@ -4244,13 +4330,15 @@ var WSClient = class {
4244
4330
  */
4245
4331
  disconnect() {
4246
4332
  this.isExplicitlyDisconnected = true;
4247
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
4333
+ if (this.reconnectTimer) {
4334
+ clearTimeout(this.reconnectTimer);
4335
+ this.reconnectTimer = null;
4336
+ }
4248
4337
  if (this.ws) {
4249
- this.ws.close();
4338
+ this.ws.close(1e3, "Client disconnect");
4250
4339
  this.ws = null;
4251
4340
  }
4252
- this.messageQueue.forEach((q) => q.reject(new Error("Client explicitly disconnected")));
4253
- this.messageQueue = [];
4341
+ this.rejectPending(new Error("Client explicitly disconnected"));
4254
4342
  this.rpcHandlers.clear();
4255
4343
  this.emit("disconnect", {});
4256
4344
  }
@@ -4795,7 +4883,7 @@ import { ${allImports} } from "./sandbox-tools";
4795
4883
  this.checkForToolChanges();
4796
4884
  });
4797
4885
  this.client.on("prompt", (prompt) => this.emit("prompt", prompt));
4798
- this.client.on("disconnect", () => this.emit("disconnect", {}));
4886
+ this.client.on("disconnect", (payload) => this.emit("disconnect", payload || {}));
4799
4887
  this.client.on("job.status", (data) => {
4800
4888
  this.emit("job:status", data);
4801
4889
  });
@@ -5722,6 +5810,8 @@ var Granular = class {
5722
5810
  apiUrl;
5723
5811
  httpUrl;
5724
5812
  WebSocketCtor;
5813
+ onUnexpectedClose;
5814
+ onReconnectError;
5725
5815
  /** Sandbox-level effect registry: sandboxId → (toolName → ToolWithHandler) */
5726
5816
  sandboxEffects = /* @__PURE__ */ new Map();
5727
5817
  /** Active environments tracker: sandboxId → Environment[] */
@@ -5738,6 +5828,8 @@ var Granular = class {
5738
5828
  this.apiKey = auth;
5739
5829
  this.apiUrl = options.apiUrl || "wss://api.granular.dev/v2/ws";
5740
5830
  this.WebSocketCtor = options.WebSocketCtor;
5831
+ this.onUnexpectedClose = options.onUnexpectedClose;
5832
+ this.onReconnectError = options.onReconnectError;
5741
5833
  this.httpUrl = this.apiUrl.replace("wss://", "https://").replace("/ws", "");
5742
5834
  }
5743
5835
  /**
@@ -5822,7 +5914,9 @@ var Granular = class {
5822
5914
  url: this.apiUrl,
5823
5915
  sessionId: envData.environmentId,
5824
5916
  token: this.apiKey,
5825
- WebSocketCtor: this.WebSocketCtor
5917
+ WebSocketCtor: this.WebSocketCtor,
5918
+ onUnexpectedClose: this.onUnexpectedClose,
5919
+ onReconnectError: this.onReconnectError
5826
5920
  });
5827
5921
  await client.connect();
5828
5922
  const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;