@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.js CHANGED
@@ -3966,6 +3966,10 @@ var WSClient = class {
3966
3966
  */
3967
3967
  async connect() {
3968
3968
  this.isExplicitlyDisconnected = false;
3969
+ if (this.reconnectTimer) {
3970
+ clearTimeout(this.reconnectTimer);
3971
+ this.reconnectTimer = null;
3972
+ }
3969
3973
  let WebSocketClass = this.options.WebSocketCtor || GlobalWebSocket;
3970
3974
  if (!WebSocketClass) {
3971
3975
  try {
@@ -3987,6 +3991,10 @@ var WSClient = class {
3987
3991
  const socket = this.ws;
3988
3992
  if (typeof socket.on === "function") {
3989
3993
  socket.on("open", () => {
3994
+ if (this.reconnectTimer) {
3995
+ clearTimeout(this.reconnectTimer);
3996
+ this.reconnectTimer = null;
3997
+ }
3990
3998
  this.emit("open", {});
3991
3999
  resolve();
3992
4000
  });
@@ -4004,12 +4012,20 @@ var WSClient = class {
4004
4012
  reject(error);
4005
4013
  }
4006
4014
  });
4007
- socket.on("close", () => {
4008
- this.emit("close", {});
4009
- this.handleDisconnect();
4015
+ socket.on("close", (code, reason) => {
4016
+ this.handleDisconnect({
4017
+ code,
4018
+ reason: this.normalizeReason(reason),
4019
+ // ws does not provide wasClean on Node-style close callback
4020
+ wasClean: code === 1e3
4021
+ });
4010
4022
  });
4011
4023
  } else {
4012
4024
  this.ws.onopen = () => {
4025
+ if (this.reconnectTimer) {
4026
+ clearTimeout(this.reconnectTimer);
4027
+ this.reconnectTimer = null;
4028
+ }
4013
4029
  this.emit("open", {});
4014
4030
  resolve();
4015
4031
  };
@@ -4030,9 +4046,12 @@ var WSClient = class {
4030
4046
  reject(error);
4031
4047
  }
4032
4048
  };
4033
- this.ws.onclose = () => {
4034
- this.emit("close", {});
4035
- this.handleDisconnect();
4049
+ this.ws.onclose = (event) => {
4050
+ this.handleDisconnect({
4051
+ code: event.code,
4052
+ reason: event.reason,
4053
+ wasClean: event.wasClean
4054
+ });
4036
4055
  };
4037
4056
  }
4038
4057
  } catch (error) {
@@ -4040,13 +4059,80 @@ var WSClient = class {
4040
4059
  }
4041
4060
  });
4042
4061
  }
4043
- handleDisconnect() {
4062
+ normalizeReason(reason) {
4063
+ if (reason === null || reason === void 0) return void 0;
4064
+ if (typeof reason === "string") return reason;
4065
+ if (typeof reason === "object" && reason && "toString" in reason) {
4066
+ try {
4067
+ const text = String(reason.toString());
4068
+ return text || void 0;
4069
+ } catch {
4070
+ return void 0;
4071
+ }
4072
+ }
4073
+ return void 0;
4074
+ }
4075
+ rejectPending(error) {
4076
+ this.messageQueue.forEach((pending) => pending.reject(error));
4077
+ this.messageQueue = [];
4078
+ }
4079
+ buildDisconnectError(info) {
4080
+ const details = [
4081
+ info.code !== void 0 ? `code=${info.code}` : void 0,
4082
+ info.reason ? `reason=${info.reason}` : void 0
4083
+ ].filter(Boolean).join(", ");
4084
+ const suffix = details ? ` (${details})` : "";
4085
+ return new Error(`WebSocket disconnected${suffix}`);
4086
+ }
4087
+ handleDisconnect(close = {}) {
4088
+ const reconnectDelayMs = 3e3;
4089
+ const unexpected = !this.isExplicitlyDisconnected;
4090
+ const info = {
4091
+ code: close.code,
4092
+ reason: close.reason,
4093
+ wasClean: close.wasClean,
4094
+ unexpected,
4095
+ timestamp: Date.now(),
4096
+ reconnectScheduled: false
4097
+ };
4044
4098
  this.ws = null;
4045
- if (!this.isExplicitlyDisconnected) {
4099
+ this.emit("close", info);
4100
+ if (this.reconnectTimer) {
4101
+ clearTimeout(this.reconnectTimer);
4102
+ this.reconnectTimer = null;
4103
+ }
4104
+ if (unexpected) {
4105
+ const disconnectError = this.buildDisconnectError(info);
4106
+ this.rejectPending(disconnectError);
4107
+ this.emit("disconnect", info);
4108
+ info.reconnectScheduled = true;
4109
+ info.reconnectDelayMs = reconnectDelayMs;
4110
+ if (this.options.onUnexpectedClose) {
4111
+ try {
4112
+ this.options.onUnexpectedClose(info);
4113
+ } catch (callbackError) {
4114
+ console.error("[Granular] onUnexpectedClose callback failed:", callbackError);
4115
+ }
4116
+ }
4046
4117
  this.reconnectTimer = setTimeout(() => {
4047
4118
  console.log("[Granular] Attempting reconnect...");
4048
- this.connect().catch((e) => console.error("[Granular] Reconnect failed:", e));
4049
- }, 3e3);
4119
+ this.connect().catch((error) => {
4120
+ console.error("[Granular] Reconnect failed:", error);
4121
+ const reconnectInfo = {
4122
+ error: error instanceof Error ? error.message : String(error),
4123
+ sessionId: this.sessionId,
4124
+ timestamp: Date.now()
4125
+ };
4126
+ this.emit("reconnect_error", reconnectInfo);
4127
+ if (this.options.onReconnectError) {
4128
+ try {
4129
+ this.options.onReconnectError(reconnectInfo);
4130
+ } catch (callbackError) {
4131
+ console.error("[Granular] onReconnectError callback failed:", callbackError);
4132
+ }
4133
+ }
4134
+ });
4135
+ }, reconnectDelayMs);
4050
4136
  }
4051
4137
  }
4052
4138
  handleMessage(message) {
@@ -4266,13 +4352,15 @@ var WSClient = class {
4266
4352
  */
4267
4353
  disconnect() {
4268
4354
  this.isExplicitlyDisconnected = true;
4269
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
4355
+ if (this.reconnectTimer) {
4356
+ clearTimeout(this.reconnectTimer);
4357
+ this.reconnectTimer = null;
4358
+ }
4270
4359
  if (this.ws) {
4271
- this.ws.close();
4360
+ this.ws.close(1e3, "Client disconnect");
4272
4361
  this.ws = null;
4273
4362
  }
4274
- this.messageQueue.forEach((q) => q.reject(new Error("Client explicitly disconnected")));
4275
- this.messageQueue = [];
4363
+ this.rejectPending(new Error("Client explicitly disconnected"));
4276
4364
  this.rpcHandlers.clear();
4277
4365
  this.emit("disconnect", {});
4278
4366
  }
@@ -4817,7 +4905,7 @@ import { ${allImports} } from "./sandbox-tools";
4817
4905
  this.checkForToolChanges();
4818
4906
  });
4819
4907
  this.client.on("prompt", (prompt) => this.emit("prompt", prompt));
4820
- this.client.on("disconnect", () => this.emit("disconnect", {}));
4908
+ this.client.on("disconnect", (payload) => this.emit("disconnect", payload || {}));
4821
4909
  this.client.on("job.status", (data) => {
4822
4910
  this.emit("job:status", data);
4823
4911
  });
@@ -5744,6 +5832,8 @@ var Granular = class {
5744
5832
  apiUrl;
5745
5833
  httpUrl;
5746
5834
  WebSocketCtor;
5835
+ onUnexpectedClose;
5836
+ onReconnectError;
5747
5837
  /** Sandbox-level effect registry: sandboxId → (toolName → ToolWithHandler) */
5748
5838
  sandboxEffects = /* @__PURE__ */ new Map();
5749
5839
  /** Active environments tracker: sandboxId → Environment[] */
@@ -5760,6 +5850,8 @@ var Granular = class {
5760
5850
  this.apiKey = auth;
5761
5851
  this.apiUrl = options.apiUrl || "wss://api.granular.dev/v2/ws";
5762
5852
  this.WebSocketCtor = options.WebSocketCtor;
5853
+ this.onUnexpectedClose = options.onUnexpectedClose;
5854
+ this.onReconnectError = options.onReconnectError;
5763
5855
  this.httpUrl = this.apiUrl.replace("wss://", "https://").replace("/ws", "");
5764
5856
  }
5765
5857
  /**
@@ -5844,7 +5936,9 @@ var Granular = class {
5844
5936
  url: this.apiUrl,
5845
5937
  sessionId: envData.environmentId,
5846
5938
  token: this.apiKey,
5847
- WebSocketCtor: this.WebSocketCtor
5939
+ WebSocketCtor: this.WebSocketCtor,
5940
+ onUnexpectedClose: this.onUnexpectedClose,
5941
+ onReconnectError: this.onReconnectError
5848
5942
  });
5849
5943
  await client.connect();
5850
5944
  const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;