@blaxel/core 0.2.49-dev.211 → 0.2.49-dev.212

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 (53) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/common/settings.js +2 -2
  3. package/dist/cjs/sandbox/codegen/codegen-ws.js +30 -0
  4. package/dist/cjs/sandbox/filesystem/filesystem-ws.js +106 -0
  5. package/dist/cjs/sandbox/network/network-ws.js +12 -0
  6. package/dist/cjs/sandbox/process/process-ws.js +139 -0
  7. package/dist/cjs/sandbox/sandbox.js +67 -10
  8. package/dist/cjs/sandbox/websocket/client.js +275 -0
  9. package/dist/cjs/sandbox/websocket/index.js +17 -0
  10. package/dist/cjs/types/sandbox/codegen/codegen-ws.d.ts +10 -0
  11. package/dist/cjs/types/sandbox/filesystem/filesystem-ws.d.ts +35 -0
  12. package/dist/cjs/types/sandbox/network/network-ws.d.ts +7 -0
  13. package/dist/cjs/types/sandbox/process/process-ws.d.ts +27 -0
  14. package/dist/cjs/types/sandbox/sandbox.d.ts +12 -6
  15. package/dist/cjs/types/sandbox/types.d.ts +3 -0
  16. package/dist/cjs/types/sandbox/websocket/client.d.ts +49 -0
  17. package/dist/cjs/types/sandbox/websocket/index.d.ts +1 -0
  18. package/dist/cjs-browser/.tsbuildinfo +1 -1
  19. package/dist/cjs-browser/common/settings.js +2 -2
  20. package/dist/cjs-browser/sandbox/codegen/codegen-ws.js +30 -0
  21. package/dist/cjs-browser/sandbox/filesystem/filesystem-ws.js +106 -0
  22. package/dist/cjs-browser/sandbox/network/network-ws.js +12 -0
  23. package/dist/cjs-browser/sandbox/process/process-ws.js +139 -0
  24. package/dist/cjs-browser/sandbox/sandbox.js +67 -10
  25. package/dist/cjs-browser/sandbox/websocket/client.js +275 -0
  26. package/dist/cjs-browser/sandbox/websocket/index.js +17 -0
  27. package/dist/cjs-browser/types/sandbox/codegen/codegen-ws.d.ts +10 -0
  28. package/dist/cjs-browser/types/sandbox/filesystem/filesystem-ws.d.ts +35 -0
  29. package/dist/cjs-browser/types/sandbox/network/network-ws.d.ts +7 -0
  30. package/dist/cjs-browser/types/sandbox/process/process-ws.d.ts +27 -0
  31. package/dist/cjs-browser/types/sandbox/sandbox.d.ts +12 -6
  32. package/dist/cjs-browser/types/sandbox/types.d.ts +3 -0
  33. package/dist/cjs-browser/types/sandbox/websocket/client.d.ts +49 -0
  34. package/dist/cjs-browser/types/sandbox/websocket/index.d.ts +1 -0
  35. package/dist/esm/.tsbuildinfo +1 -1
  36. package/dist/esm/common/settings.js +2 -2
  37. package/dist/esm/sandbox/codegen/codegen-ws.js +26 -0
  38. package/dist/esm/sandbox/filesystem/filesystem-ws.js +102 -0
  39. package/dist/esm/sandbox/network/network-ws.js +8 -0
  40. package/dist/esm/sandbox/process/process-ws.js +135 -0
  41. package/dist/esm/sandbox/sandbox.js +67 -10
  42. package/dist/esm/sandbox/websocket/client.js +271 -0
  43. package/dist/esm/sandbox/websocket/index.js +1 -0
  44. package/dist/esm-browser/.tsbuildinfo +1 -1
  45. package/dist/esm-browser/common/settings.js +2 -2
  46. package/dist/esm-browser/sandbox/codegen/codegen-ws.js +26 -0
  47. package/dist/esm-browser/sandbox/filesystem/filesystem-ws.js +102 -0
  48. package/dist/esm-browser/sandbox/network/network-ws.js +8 -0
  49. package/dist/esm-browser/sandbox/process/process-ws.js +135 -0
  50. package/dist/esm-browser/sandbox/sandbox.js +67 -10
  51. package/dist/esm-browser/sandbox/websocket/client.js +271 -0
  52. package/dist/esm-browser/sandbox/websocket/index.js +1 -0
  53. package/package.json +2 -2
@@ -0,0 +1,102 @@
1
+ import { SandboxAction } from "../action.js";
2
+ import { SandboxFileSystem } from "./filesystem.js";
3
+ export class SandboxFileSystemWebSocket extends SandboxAction {
4
+ process;
5
+ wsClient;
6
+ httpClient;
7
+ constructor(sandbox, process, wsClient) {
8
+ super(sandbox);
9
+ this.process = process;
10
+ this.wsClient = wsClient;
11
+ // Create HTTP client for fallback operations
12
+ this.httpClient = new SandboxFileSystem(sandbox, process);
13
+ }
14
+ async mkdir(path, permissions = "0755") {
15
+ path = this.formatPath(path);
16
+ const data = await this.wsClient.send("filesystem:create", {
17
+ path,
18
+ isDirectory: true,
19
+ permissions,
20
+ });
21
+ return data;
22
+ }
23
+ async write(path, content) {
24
+ path = this.formatPath(path);
25
+ const data = await this.wsClient.send("filesystem:create", {
26
+ path,
27
+ content,
28
+ isDirectory: false,
29
+ });
30
+ return data;
31
+ }
32
+ async writeBinary(path, content) {
33
+ return this.httpClient.writeBinary(path, content);
34
+ }
35
+ async writeTree(files, destinationPath = null) {
36
+ const path = this.formatPath(destinationPath ?? "");
37
+ const filesMap = files.reduce((acc, file) => {
38
+ acc[file.path] = file.content;
39
+ return acc;
40
+ }, {});
41
+ const data = await this.wsClient.send("filesystem:tree:create", {
42
+ path,
43
+ files: filesMap,
44
+ });
45
+ return data;
46
+ }
47
+ async read(path) {
48
+ path = this.formatPath(path);
49
+ const data = await this.wsClient.send("filesystem:get", { path });
50
+ return data.content;
51
+ }
52
+ async readBinary(path) {
53
+ // Binary downloads are better suited for HTTP
54
+ // Fall back to HTTP client for binary operations
55
+ return this.httpClient.readBinary(path);
56
+ }
57
+ async download(src, destinationPath, options = {}) {
58
+ // File downloads are better suited for HTTP
59
+ // Fall back to HTTP client
60
+ return this.httpClient.download(src, destinationPath, options);
61
+ }
62
+ async rm(path, recursive = false) {
63
+ path = this.formatPath(path);
64
+ const data = await this.wsClient.send("filesystem:delete", {
65
+ path,
66
+ recursive,
67
+ });
68
+ return data;
69
+ }
70
+ async ls(path) {
71
+ path = this.formatPath(path);
72
+ const data = await this.wsClient.send("filesystem:get", { path });
73
+ return data;
74
+ }
75
+ async cp(source, destination, { maxWait = 180000 } = {}) {
76
+ // Copy operation is typically done via process execution
77
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
78
+ let process = await this.process.exec({
79
+ command: `cp -r ${source} ${destination}`,
80
+ });
81
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
82
+ process = await this.process.wait(process.pid, { maxWait, interval: 100 });
83
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
84
+ if (process.status === "failed") {
85
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
86
+ throw new Error(`Could not copy ${source} to ${destination} cause: ${process.logs}`);
87
+ }
88
+ return {
89
+ message: "Files copied",
90
+ source,
91
+ destination,
92
+ };
93
+ }
94
+ watch(path, callback, options) {
95
+ // File watching uses HTTP streaming which is already optimized
96
+ // Fall back to HTTP client
97
+ return this.httpClient.watch(path, callback, options);
98
+ }
99
+ formatPath(path) {
100
+ return path;
101
+ }
102
+ }
@@ -0,0 +1,8 @@
1
+ import { SandboxAction } from "../action.js";
2
+ export class SandboxNetworkWebSocket extends SandboxAction {
3
+ wsClient;
4
+ constructor(sandbox, wsClient) {
5
+ super(sandbox);
6
+ this.wsClient = wsClient;
7
+ }
8
+ }
@@ -0,0 +1,135 @@
1
+ import { SandboxAction } from "../action.js";
2
+ import { SandboxProcess } from "./process.js";
3
+ export class SandboxProcessWebSocket extends SandboxAction {
4
+ wsClient;
5
+ httpClient;
6
+ constructor(sandbox, wsClient) {
7
+ super(sandbox);
8
+ this.wsClient = wsClient;
9
+ // Create HTTP client for fallback operations
10
+ this.httpClient = new SandboxProcess(sandbox);
11
+ }
12
+ streamLogs(identifier, options) {
13
+ const streamId = this.wsClient.sendStream("process:logs:stream:start", { identifier }, (data) => {
14
+ // Handle streaming log data
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
16
+ if (data && data.log) {
17
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
18
+ const log = String(data.log);
19
+ // Parse log format: "stdout:" or "stderr:" prefix
20
+ if (log.startsWith('stdout:')) {
21
+ const stdout = log.slice(7);
22
+ options.onStdout?.(stdout);
23
+ options.onLog?.(stdout);
24
+ }
25
+ else if (log.startsWith('stderr:')) {
26
+ const stderr = log.slice(7);
27
+ options.onStderr?.(stderr);
28
+ options.onLog?.(stderr);
29
+ }
30
+ else {
31
+ options.onLog?.(log);
32
+ }
33
+ }
34
+ }, () => {
35
+ // Stream ended
36
+ });
37
+ return {
38
+ close: () => this.wsClient.cancelStream(streamId),
39
+ };
40
+ }
41
+ async exec(process) {
42
+ let onLog;
43
+ if ('onLog' in process && process.onLog) {
44
+ onLog = process.onLog;
45
+ delete process.onLog;
46
+ }
47
+ // Store original wait_for_completion setting
48
+ const shouldWaitForCompletion = process.waitForCompletion;
49
+ // Always start process without wait_for_completion to avoid server-side blocking
50
+ if (shouldWaitForCompletion && onLog) {
51
+ process.waitForCompletion = false;
52
+ }
53
+ const data = await this.wsClient.send("process:execute", process);
54
+ let result = data;
55
+ // Handle wait_for_completion with parallel log streaming
56
+ if (shouldWaitForCompletion && onLog) {
57
+ const streamControl = this.streamLogs(result.pid, { onLog });
58
+ try {
59
+ // Wait for process completion
60
+ result = await this.wait(result.pid, { interval: 500, maxWait: 1000 * 60 * 60 });
61
+ }
62
+ finally {
63
+ // Clean up log streaming
64
+ if (streamControl) {
65
+ streamControl.close();
66
+ }
67
+ }
68
+ }
69
+ else {
70
+ // For non-blocking execution, set up log streaming immediately if requested
71
+ if (onLog) {
72
+ const streamControl = this.streamLogs(result.pid, { onLog });
73
+ return {
74
+ ...result,
75
+ close() {
76
+ if (streamControl) {
77
+ streamControl.close();
78
+ }
79
+ },
80
+ };
81
+ }
82
+ }
83
+ return { ...result, close: () => { } };
84
+ }
85
+ async wait(identifier, { maxWait = 60000, interval = 1000 } = {}) {
86
+ const startTime = Date.now();
87
+ let status = "running";
88
+ let data = await this.get(identifier);
89
+ while (status === "running") {
90
+ await new Promise((resolve) => setTimeout(resolve, interval));
91
+ try {
92
+ data = await this.get(identifier);
93
+ status = data.status ?? "running";
94
+ }
95
+ catch {
96
+ break;
97
+ }
98
+ if (Date.now() - startTime > maxWait) {
99
+ throw new Error("Process did not finish in time");
100
+ }
101
+ }
102
+ return data;
103
+ }
104
+ async get(identifier) {
105
+ const data = await this.wsClient.send("process:get", { identifier });
106
+ return data;
107
+ }
108
+ async list() {
109
+ const data = await this.wsClient.send("process:list", {});
110
+ return data;
111
+ }
112
+ async stop(identifier) {
113
+ const data = await this.wsClient.send("process:stop", { identifier });
114
+ return data;
115
+ }
116
+ async kill(identifier) {
117
+ const data = await this.wsClient.send("process:kill", { identifier });
118
+ return data;
119
+ }
120
+ async logs(identifier, type = "all") {
121
+ const data = await this.wsClient.send("process:logs", {
122
+ identifier,
123
+ });
124
+ if (type === "all") {
125
+ return data.logs || "";
126
+ }
127
+ else if (type === "stdout") {
128
+ return data.stdout || "";
129
+ }
130
+ else if (type === "stderr") {
131
+ return data.stderr || "";
132
+ }
133
+ throw new Error("Unsupported log type");
134
+ }
135
+ }
@@ -2,11 +2,16 @@ import { v4 as uuidv4 } from "uuid";
2
2
  import { createSandbox, deleteSandbox, getSandbox, listSandboxes, updateSandbox } from "../client/index.js";
3
3
  import { logger } from "../common/logger.js";
4
4
  import { SandboxFileSystem } from "./filesystem/index.js";
5
+ import { SandboxFileSystemWebSocket } from "./filesystem/filesystem-ws.js";
5
6
  import { SandboxNetwork } from "./network/index.js";
7
+ import { SandboxNetworkWebSocket } from "./network/network-ws.js";
6
8
  import { SandboxPreviews } from "./preview.js";
7
9
  import { SandboxProcess } from "./process/index.js";
10
+ import { SandboxProcessWebSocket } from "./process/process-ws.js";
8
11
  import { SandboxCodegen } from "./codegen/index.js";
12
+ import { SandboxCodegenWebSocket } from "./codegen/codegen-ws.js";
9
13
  import { SandboxSessions } from "./session.js";
14
+ import { WebSocketClient } from "./websocket/index.js";
10
15
  import { normalizeEnvs, normalizePorts, normalizeVolumes } from "./types.js";
11
16
  export class SandboxInstance {
12
17
  sandbox;
@@ -16,14 +21,32 @@ export class SandboxInstance {
16
21
  previews;
17
22
  sessions;
18
23
  codegen;
24
+ wsClient;
19
25
  constructor(sandbox) {
20
26
  this.sandbox = sandbox;
21
- this.process = new SandboxProcess(sandbox);
22
- this.fs = new SandboxFileSystem(sandbox, this.process);
23
- this.network = new SandboxNetwork(sandbox);
27
+ // If connection type is websocket, initialize WebSocket client and use WebSocket transport layers
28
+ if (sandbox.connectionType === "websocket") {
29
+ const url = sandbox.forceUrl || sandbox.metadata?.url || "";
30
+ this.wsClient = new WebSocketClient({
31
+ url,
32
+ headers: sandbox.headers,
33
+ });
34
+ // Initialize WebSocket-based action handlers
35
+ this.process = new SandboxProcessWebSocket(sandbox, this.wsClient);
36
+ this.fs = new SandboxFileSystemWebSocket(sandbox, this.process, this.wsClient);
37
+ this.network = new SandboxNetworkWebSocket(sandbox, this.wsClient);
38
+ this.codegen = new SandboxCodegenWebSocket(sandbox, this.wsClient);
39
+ }
40
+ else {
41
+ // Default to HTTP-based action handlers
42
+ this.process = new SandboxProcess(sandbox);
43
+ this.fs = new SandboxFileSystem(sandbox, this.process);
44
+ this.network = new SandboxNetwork(sandbox);
45
+ this.codegen = new SandboxCodegen(sandbox);
46
+ }
47
+ // These are always HTTP-based
24
48
  this.previews = new SandboxPreviews(sandbox);
25
49
  this.sessions = new SandboxSessions(sandbox);
26
- this.codegen = new SandboxCodegen(sandbox);
27
50
  }
28
51
  get metadata() {
29
52
  return this.sandbox.metadata;
@@ -42,10 +65,17 @@ export class SandboxInstance {
42
65
  logger.warn("⚠️ Warning: sandbox.wait() is deprecated. You don't need to wait for the sandbox to be deployed anymore.");
43
66
  return this;
44
67
  }
68
+ closeConnection() {
69
+ if (this.wsClient) {
70
+ this.wsClient.close();
71
+ }
72
+ }
45
73
  static async create(sandbox, { safe = true } = {}) {
46
74
  const defaultName = `sandbox-${uuidv4().replace(/-/g, '').substring(0, 8)}`;
47
75
  const defaultImage = `blaxel/base-image:latest`;
48
76
  const defaultMemory = 4096;
77
+ // Store connection type if provided
78
+ let connectionType;
49
79
  // Handle SandboxCreateConfiguration or simple dict with name/image/memory/ports/envs/volumes keys
50
80
  if (!sandbox ||
51
81
  'name' in sandbox ||
@@ -55,7 +85,8 @@ export class SandboxInstance {
55
85
  'envs' in sandbox ||
56
86
  'volumes' in sandbox ||
57
87
  'lifecycle' in sandbox ||
58
- 'snapshotEnabled' in sandbox) {
88
+ 'snapshotEnabled' in sandbox ||
89
+ 'connectionType' in sandbox) {
59
90
  if (!sandbox)
60
91
  sandbox = {};
61
92
  if (!sandbox.name)
@@ -64,6 +95,7 @@ export class SandboxInstance {
64
95
  sandbox.image = defaultImage;
65
96
  if (!sandbox.memory)
66
97
  sandbox.memory = defaultMemory;
98
+ connectionType = sandbox.connectionType;
67
99
  const ports = normalizePorts(sandbox.ports);
68
100
  const envs = normalizeEnvs(sandbox.envs);
69
101
  const volumes = normalizeVolumes(sandbox.volumes);
@@ -112,7 +144,16 @@ export class SandboxInstance {
112
144
  body: sandbox,
113
145
  throwOnError: true,
114
146
  });
115
- const instance = new SandboxInstance(data);
147
+ // Add connection type to configuration
148
+ const config = {
149
+ ...data,
150
+ connectionType: connectionType || "http",
151
+ };
152
+ const instance = new SandboxInstance(config);
153
+ // Connect WebSocket if needed
154
+ if (connectionType === "websocket" && instance.wsClient) {
155
+ await instance.wsClient.connect();
156
+ }
116
157
  // TODO remove this part once we have a better way to handle this
117
158
  if (safe) {
118
159
  try {
@@ -122,20 +163,34 @@ export class SandboxInstance {
122
163
  }
123
164
  return instance;
124
165
  }
125
- static async get(sandboxName) {
166
+ static async get(sandboxName, connectionType) {
126
167
  const { data } = await getSandbox({
127
168
  path: {
128
169
  sandboxName,
129
170
  },
130
171
  throwOnError: true,
131
172
  });
132
- return new SandboxInstance(data);
173
+ // Add connection type to configuration
174
+ const config = {
175
+ ...data,
176
+ connectionType: connectionType || "http",
177
+ };
178
+ const instance = new SandboxInstance(config);
179
+ // Connect WebSocket if needed
180
+ if (connectionType === "websocket" && instance.wsClient) {
181
+ await instance.wsClient.connect();
182
+ }
183
+ return instance;
133
184
  }
134
185
  static async list() {
135
186
  const { data } = await listSandboxes({ throwOnError: true });
136
187
  return data.map((sandbox) => new SandboxInstance(sandbox));
137
188
  }
138
- static async delete(sandboxName) {
189
+ static async delete(sandboxName, instance) {
190
+ // Close WebSocket connection if instance is provided
191
+ if (instance && instance.wsClient) {
192
+ instance.closeConnection();
193
+ }
139
194
  const { data } = await deleteSandbox({
140
195
  path: {
141
196
  sandboxName,
@@ -165,8 +220,10 @@ export class SandboxInstance {
165
220
  if (!name) {
166
221
  throw new Error("Sandbox name is required");
167
222
  }
223
+ // Get connection type if specified
224
+ const connectionType = 'connectionType' in sandbox ? sandbox.connectionType : undefined;
168
225
  // Get the existing sandbox to check its status
169
- const sandboxInstance = await SandboxInstance.get(name);
226
+ const sandboxInstance = await SandboxInstance.get(name, connectionType);
170
227
  // If the sandbox is TERMINATED, treat it as not existing
171
228
  if (sandboxInstance.status === "TERMINATED") {
172
229
  // Create a new sandbox - backend will handle cleanup of the terminated one
@@ -0,0 +1,271 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getWebSocket } from "../../common/node.js";
3
+ export class WebSocketClient {
4
+ ws = null;
5
+ WebSocketClass = null;
6
+ url;
7
+ headers;
8
+ reconnect;
9
+ reconnectInterval;
10
+ maxReconnectAttempts;
11
+ reconnectAttempts = 0;
12
+ pendingRequests = new Map();
13
+ streamHandlers = new Map();
14
+ isClosing = false;
15
+ connectionPromise = null;
16
+ heartbeatInterval = null;
17
+ reconnectTimeout = null;
18
+ lastPongReceived = Date.now();
19
+ constructor(options) {
20
+ this.url = options.url;
21
+ this.headers = options.headers || {};
22
+ this.reconnect = options.reconnect ?? true;
23
+ this.reconnectInterval = options.reconnectInterval ?? 5000;
24
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
25
+ }
26
+ async connect() {
27
+ if (this.connectionPromise) {
28
+ return this.connectionPromise;
29
+ }
30
+ this.connectionPromise = this.initializeConnection();
31
+ return this.connectionPromise;
32
+ }
33
+ async initializeConnection() {
34
+ return new Promise((resolve, reject) => {
35
+ // Get WebSocket class and connect
36
+ void (async () => {
37
+ try {
38
+ // Get WebSocket class if not already loaded
39
+ if (!this.WebSocketClass) {
40
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
41
+ this.WebSocketClass = await getWebSocket();
42
+ }
43
+ // Convert http/https URL to ws/wss
44
+ let wsUrl = this.url;
45
+ if (wsUrl.startsWith("http://")) {
46
+ wsUrl = wsUrl.replace("http://", "ws://");
47
+ }
48
+ else if (wsUrl.startsWith("https://")) {
49
+ wsUrl = wsUrl.replace("https://", "wss://");
50
+ }
51
+ // Add /ws endpoint if not present
52
+ if (!wsUrl.endsWith("/ws")) {
53
+ wsUrl = `${wsUrl}/ws`;
54
+ }
55
+ // Create WebSocket with headers (if supported by the environment)
56
+ const wsOptions = {};
57
+ if (Object.keys(this.headers).length > 0) {
58
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
59
+ wsOptions.headers = this.headers;
60
+ }
61
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
62
+ this.ws = new this.WebSocketClass(wsUrl, wsOptions);
63
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
64
+ this.ws.onopen = () => {
65
+ this.reconnectAttempts = 0;
66
+ this.startHeartbeat();
67
+ resolve();
68
+ };
69
+ this.ws.onmessage = (event) => {
70
+ this.handleMessage(event);
71
+ };
72
+ this.ws.onerror = (error) => {
73
+ console.error("WebSocket error:", error);
74
+ reject(new Error("WebSocket connection error"));
75
+ };
76
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
77
+ this.ws.onclose = () => {
78
+ this.stopHeartbeat();
79
+ this.connectionPromise = null;
80
+ if (!this.isClosing && this.reconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
81
+ this.reconnectAttempts++;
82
+ this.reconnectTimeout = setTimeout(() => {
83
+ this.connect().catch(console.error);
84
+ }, this.reconnectInterval);
85
+ // Allow process to exit even if reconnect timeout is pending
86
+ if (this.reconnectTimeout.unref) {
87
+ this.reconnectTimeout.unref();
88
+ }
89
+ }
90
+ else {
91
+ // Reject all pending requests
92
+ this.pendingRequests.forEach(({ reject }) => {
93
+ reject(new Error("WebSocket connection closed"));
94
+ });
95
+ this.pendingRequests.clear();
96
+ }
97
+ };
98
+ // Handle pong messages for heartbeat
99
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
100
+ if (typeof this.ws.on === 'function') {
101
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
102
+ this.ws.on('pong', () => {
103
+ this.lastPongReceived = Date.now();
104
+ });
105
+ }
106
+ }
107
+ catch (error) {
108
+ reject(error instanceof Error ? error : new Error(String(error)));
109
+ }
110
+ })();
111
+ });
112
+ }
113
+ startHeartbeat() {
114
+ // Send ping every 30 seconds
115
+ this.heartbeatInterval = setInterval(() => {
116
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
117
+ if (this.ws && this.WebSocketClass && this.ws.readyState === this.WebSocketClass.OPEN) {
118
+ // Check if we received a pong recently (within 60 seconds)
119
+ if (Date.now() - this.lastPongReceived > 60000) {
120
+ console.warn("WebSocket heartbeat timeout, closing connection");
121
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
122
+ this.ws.close();
123
+ return;
124
+ }
125
+ // Send ping
126
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
127
+ if (typeof this.ws.ping === 'function') {
128
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
129
+ this.ws.ping();
130
+ }
131
+ }
132
+ }, 30000);
133
+ // Allow process to exit even if heartbeat interval is active
134
+ if (this.heartbeatInterval.unref) {
135
+ this.heartbeatInterval.unref();
136
+ }
137
+ }
138
+ stopHeartbeat() {
139
+ if (this.heartbeatInterval) {
140
+ clearInterval(this.heartbeatInterval);
141
+ this.heartbeatInterval = null;
142
+ }
143
+ }
144
+ handleMessage(event) {
145
+ try {
146
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
147
+ const response = JSON.parse(String(event.data));
148
+ // Check if this is a streaming response
149
+ if (response.stream) {
150
+ const streamHandler = this.streamHandlers.get(response.id);
151
+ if (streamHandler) {
152
+ // Call the data handler with the response data
153
+ streamHandler.onData(response.data);
154
+ // If stream is done, call end handler and clean up
155
+ if (response.done) {
156
+ streamHandler.onEnd();
157
+ this.streamHandlers.delete(response.id);
158
+ }
159
+ }
160
+ return;
161
+ }
162
+ // Regular request-response handling
163
+ const pending = this.pendingRequests.get(response.id);
164
+ if (pending) {
165
+ this.pendingRequests.delete(response.id);
166
+ if (response.success) {
167
+ pending.resolve(response.data);
168
+ }
169
+ else {
170
+ pending.reject(new Error(response.error || "Unknown error"));
171
+ }
172
+ }
173
+ }
174
+ catch (error) {
175
+ console.error("Failed to parse WebSocket message:", error);
176
+ }
177
+ }
178
+ async send(operation, data = {}) {
179
+ // Ensure we're connected
180
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
181
+ if (!this.ws || !this.WebSocketClass || this.ws.readyState !== this.WebSocketClass.OPEN) {
182
+ await this.connect();
183
+ }
184
+ return new Promise((resolve, reject) => {
185
+ const id = uuidv4();
186
+ const message = {
187
+ id,
188
+ operation,
189
+ data,
190
+ };
191
+ // Store the promise handlers
192
+ this.pendingRequests.set(id, { resolve, reject });
193
+ // Set a timeout for the request (60 seconds)
194
+ setTimeout(() => {
195
+ if (this.pendingRequests.has(id)) {
196
+ this.pendingRequests.delete(id);
197
+ reject(new Error("Request timeout"));
198
+ }
199
+ }, 60000);
200
+ // Send the message
201
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
202
+ if (this.ws && this.WebSocketClass && this.ws.readyState === this.WebSocketClass.OPEN) {
203
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
204
+ this.ws.send(JSON.stringify(message));
205
+ }
206
+ else {
207
+ this.pendingRequests.delete(id);
208
+ reject(new Error("WebSocket not connected"));
209
+ }
210
+ });
211
+ }
212
+ sendStream(operation, data, onData, onEnd) {
213
+ // Ensure we're connected
214
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
215
+ if (!this.ws || !this.WebSocketClass || this.ws.readyState !== this.WebSocketClass.OPEN) {
216
+ throw new Error("WebSocket not connected");
217
+ }
218
+ const id = uuidv4();
219
+ const message = {
220
+ id,
221
+ operation,
222
+ data,
223
+ };
224
+ // Store the stream handlers
225
+ this.streamHandlers.set(id, { onData, onEnd });
226
+ // Send the message
227
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
228
+ this.ws.send(JSON.stringify(message));
229
+ return id;
230
+ }
231
+ cancelStream(id) {
232
+ this.streamHandlers.delete(id);
233
+ }
234
+ close() {
235
+ this.isClosing = true;
236
+ this.reconnect = false;
237
+ this.stopHeartbeat();
238
+ // Clear reconnect timeout if any
239
+ if (this.reconnectTimeout) {
240
+ clearTimeout(this.reconnectTimeout);
241
+ this.reconnectTimeout = null;
242
+ }
243
+ if (this.ws) {
244
+ // In Node.js (ws package), use terminate() to forcefully close the connection
245
+ // This immediately closes the socket without waiting for the close handshake
246
+ // In browser, terminate() doesn't exist, so we fall back to close()
247
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
248
+ if (typeof this.ws.terminate === 'function') {
249
+ // Node.js ws package - force immediate close
250
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
251
+ this.ws.terminate();
252
+ }
253
+ else {
254
+ // Browser WebSocket - graceful close
255
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
256
+ this.ws.close();
257
+ }
258
+ this.ws = null;
259
+ }
260
+ // Reject all pending requests
261
+ this.pendingRequests.forEach(({ reject }) => {
262
+ reject(new Error("WebSocket client closed"));
263
+ });
264
+ this.pendingRequests.clear();
265
+ this.connectionPromise = null;
266
+ }
267
+ get isConnected() {
268
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
269
+ return this.ws !== null && this.WebSocketClass !== null && this.ws.readyState === this.WebSocketClass.OPEN;
270
+ }
271
+ }
@@ -0,0 +1 @@
1
+ export * from "./client.js";