@cloudflare/sandbox 0.0.0-c0d9d33 → 0.0.0-c87db11

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.
@@ -0,0 +1,122 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "sandbox-server",
6
+ "dependencies": {
7
+ "@jupyterlab/services": "^7.0.0",
8
+ "uuid": "^9.0.1",
9
+ "ws": "^8.16.0",
10
+ },
11
+ "devDependencies": {
12
+ "@types/uuid": "^9.0.7",
13
+ "@types/ws": "^8.5.10",
14
+ },
15
+ },
16
+ },
17
+ "packages": {
18
+ "@jupyter/ydoc": ["@jupyter/ydoc@3.1.0", "", { "dependencies": { "@jupyterlab/nbformat": "^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0", "@lumino/coreutils": "^1.11.0 || ^2.0.0", "@lumino/disposable": "^1.10.0 || ^2.0.0", "@lumino/signaling": "^1.10.0 || ^2.0.0", "y-protocols": "^1.0.5", "yjs": "^13.5.40" } }, "sha512-+hLNUBZr8Zz8NiuaoYKINDURDJHbjFGxg8EcSU52y+rBe412TsoFxfPSng4eP7w3cZFrVlm7D+8K0nAMHxj0ZQ=="],
19
+
20
+ "@jupyterlab/coreutils": ["@jupyterlab/coreutils@6.4.5", "", { "dependencies": { "@lumino/coreutils": "^2.2.1", "@lumino/disposable": "^2.1.4", "@lumino/signaling": "^2.1.4", "minimist": "~1.2.0", "path-browserify": "^1.0.0", "url-parse": "~1.5.4" } }, "sha512-fNEefnqTNP/3alrmGYDb0Mu1heS5Cu39FIDiDDoZuuWv4bsdE2xf7VaStuwjljsQm7VZwi5aeholi1+ciBkZKg=="],
21
+
22
+ "@jupyterlab/nbformat": ["@jupyterlab/nbformat@4.4.5", "", { "dependencies": { "@lumino/coreutils": "^2.2.1" } }, "sha512-VOlm1klsb1LUNp47AL8iDZPP71pXu9a+WqukJFuuWVxg//+iR6wdqTYcunE9Lx/7SZDqMsXGD+doBZC36Lzenw=="],
23
+
24
+ "@jupyterlab/services": ["@jupyterlab/services@7.4.5", "", { "dependencies": { "@jupyter/ydoc": "^3.0.4", "@jupyterlab/coreutils": "^6.4.5", "@jupyterlab/nbformat": "^4.4.5", "@jupyterlab/settingregistry": "^4.4.5", "@jupyterlab/statedb": "^4.4.5", "@lumino/coreutils": "^2.2.1", "@lumino/disposable": "^2.1.4", "@lumino/polling": "^2.1.4", "@lumino/properties": "^2.0.3", "@lumino/signaling": "^2.1.4", "ws": "^8.11.0" } }, "sha512-JEr8+4VS2MqFyYHh4AnpK/g5V0qRBpmnfXmU0p9YFjcIgGR365D8JF0880IJ0e8+jaW88gSAvZFB8x3o9xRoAQ=="],
25
+
26
+ "@jupyterlab/settingregistry": ["@jupyterlab/settingregistry@4.4.5", "", { "dependencies": { "@jupyterlab/nbformat": "^4.4.5", "@jupyterlab/statedb": "^4.4.5", "@lumino/commands": "^2.3.2", "@lumino/coreutils": "^2.2.1", "@lumino/disposable": "^2.1.4", "@lumino/signaling": "^2.1.4", "@rjsf/utils": "^5.13.4", "ajv": "^8.12.0", "json5": "^2.2.3" }, "peerDependencies": { "react": ">=16" } }, "sha512-6hEBq3qI99VZwO97W0AM0mP1is57bWC8Vk2xudic1a90wA9G6ovUBzayqSMm/6QhTuIbCY+vruznwhhsLrVMeg=="],
27
+
28
+ "@jupyterlab/statedb": ["@jupyterlab/statedb@4.4.5", "", { "dependencies": { "@lumino/commands": "^2.3.2", "@lumino/coreutils": "^2.2.1", "@lumino/disposable": "^2.1.4", "@lumino/properties": "^2.0.3", "@lumino/signaling": "^2.1.4" } }, "sha512-UpqhOujKwoWoxtNBL2Qk0nrw+ORLJ3ckwiJg2eA0CI+n5kO3IBYAnPzak0tSS0mQNspayXr3KAm4GQ80op5yuA=="],
29
+
30
+ "@lumino/algorithm": ["@lumino/algorithm@2.0.3", "", {}, "sha512-DIcF7cIrGEC1Wh8DNjdwaL7IdcNs4Jj1VjO/90oHefeQPszKgc6DSfCxvbQiRanKR6tl/JL7tq4ZRPZES2oVAA=="],
31
+
32
+ "@lumino/commands": ["@lumino/commands@2.3.2", "", { "dependencies": { "@lumino/algorithm": "^2.0.3", "@lumino/coreutils": "^2.2.1", "@lumino/disposable": "^2.1.4", "@lumino/domutils": "^2.0.3", "@lumino/keyboard": "^2.0.3", "@lumino/signaling": "^2.1.4", "@lumino/virtualdom": "^2.0.3" } }, "sha512-aAFEiUpp2hrkQU82Z85w1L80g0iDzsQRncLBa+pqVR/k0k1lz6H9F6xZ1ff+lBumZKKtsxBxNomvd0hfxLLqGw=="],
33
+
34
+ "@lumino/coreutils": ["@lumino/coreutils@2.2.1", "", { "dependencies": { "@lumino/algorithm": "^2.0.3" } }, "sha512-yij4TnxDIum7xfFUsVvZB0oLv4shs2mNbn3juwtEIsruvVBPmurNzKX0Y8z2QetbP2AZ6MSFtBzEKsihf0H0VA=="],
35
+
36
+ "@lumino/disposable": ["@lumino/disposable@2.1.4", "", { "dependencies": { "@lumino/signaling": "^2.1.4" } }, "sha512-qTJiDbglPE2QnG4x4gtBcRbcfKQibxyyinNGKcNDrcK2TGTbbhK5PpMQ8d70l2V2Xw2pb/LfksBAg5pxkJ/G4A=="],
37
+
38
+ "@lumino/domutils": ["@lumino/domutils@2.0.3", "", {}, "sha512-bXAbZg3mf2ZDNdBBpCGDike3U+osRGHePTh8H2Ud2KwaN4g/5IryFJm/TiO4K5IYs91bWF2Zqhf3FsdbZKHlGw=="],
39
+
40
+ "@lumino/keyboard": ["@lumino/keyboard@2.0.3", "", {}, "sha512-bU2OxAR8a9eNBdV0YFjU6/lVVpbOw1gM7yHOuDGDdNu4J0UpKapFoR9gopNGSaHTmTwDtx9RHdFfIAgHwjZ+VQ=="],
41
+
42
+ "@lumino/polling": ["@lumino/polling@2.1.4", "", { "dependencies": { "@lumino/coreutils": "^2.2.1", "@lumino/disposable": "^2.1.4", "@lumino/signaling": "^2.1.4" } }, "sha512-gSkxlIJ/4/esY2G7bsRrY9A4KimDMHTo0shaD+MCbhd09fZMCWJoDMcA447/dykB1rM5NXgugNLjpdGGL/e8cw=="],
43
+
44
+ "@lumino/properties": ["@lumino/properties@2.0.3", "", {}, "sha512-zkXIU5uYz/ScHCHGl5Bt4gMYsfPxZEduZd80zqDslBWvTIMro3NnzLe66NMnecbdr5N3hDJagYyA8//Qy3XjiA=="],
45
+
46
+ "@lumino/signaling": ["@lumino/signaling@2.1.4", "", { "dependencies": { "@lumino/algorithm": "^2.0.3", "@lumino/coreutils": "^2.2.1" } }, "sha512-nC5Z6d9om369Jkh1Vp3b7C89hV4cjr1fQDVcxhemyKXwc9r6VW7FpKixC+jElcAknP5KLj1FAa8Np+K06mMkEA=="],
47
+
48
+ "@lumino/virtualdom": ["@lumino/virtualdom@2.0.3", "", { "dependencies": { "@lumino/algorithm": "^2.0.3" } }, "sha512-q2C8eBxPvvOOQjN3KuxZ+vJi082JH/GF7KwMdaWsy5g+7wjKdnXPuLQFTBLOrVqIzmbxBDlLeFr93CEhdQXcyQ=="],
49
+
50
+ "@rjsf/utils": ["@rjsf/utils@5.24.12", "", { "dependencies": { "json-schema-merge-allof": "^0.8.1", "jsonpointer": "^5.0.1", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "react-is": "^18.2.0" }, "peerDependencies": { "react": "^16.14.0 || >=17" } }, "sha512-fDwQB0XkjZjpdFUz5UAnuZj8nnbxDbX5tp+jTOjjJKw2TMQ9gFFYCQ12lSpdhezA2YgEGZfxyYTGW0DKDL5Drg=="],
51
+
52
+ "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
53
+
54
+ "@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="],
55
+
56
+ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
57
+
58
+ "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
59
+
60
+ "compute-gcd": ["compute-gcd@1.2.1", "", { "dependencies": { "validate.io-array": "^1.0.3", "validate.io-function": "^1.0.2", "validate.io-integer-array": "^1.0.0" } }, "sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg=="],
61
+
62
+ "compute-lcm": ["compute-lcm@1.1.2", "", { "dependencies": { "compute-gcd": "^1.2.1", "validate.io-array": "^1.0.3", "validate.io-function": "^1.0.2", "validate.io-integer-array": "^1.0.0" } }, "sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ=="],
63
+
64
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
65
+
66
+ "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
67
+
68
+ "isomorphic.js": ["isomorphic.js@0.2.5", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="],
69
+
70
+ "json-schema-compare": ["json-schema-compare@0.2.2", "", { "dependencies": { "lodash": "^4.17.4" } }, "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ=="],
71
+
72
+ "json-schema-merge-allof": ["json-schema-merge-allof@0.8.1", "", { "dependencies": { "compute-lcm": "^1.1.2", "json-schema-compare": "^0.2.2", "lodash": "^4.17.20" } }, "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w=="],
73
+
74
+ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
75
+
76
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
77
+
78
+ "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
79
+
80
+ "lib0": ["lib0@0.2.114", "", { "dependencies": { "isomorphic.js": "^0.2.4" }, "bin": { "0gentesthtml": "bin/gentesthtml.js", "0serve": "bin/0serve.js", "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js" } }, "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ=="],
81
+
82
+ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
83
+
84
+ "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
85
+
86
+ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
87
+
88
+ "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
89
+
90
+ "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="],
91
+
92
+ "react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
93
+
94
+ "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
95
+
96
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
97
+
98
+ "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
99
+
100
+ "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
101
+
102
+ "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="],
103
+
104
+ "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
105
+
106
+ "validate.io-array": ["validate.io-array@1.0.6", "", {}, "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg=="],
107
+
108
+ "validate.io-function": ["validate.io-function@1.0.2", "", {}, "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ=="],
109
+
110
+ "validate.io-integer": ["validate.io-integer@1.0.5", "", { "dependencies": { "validate.io-number": "^1.0.3" } }, "sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ=="],
111
+
112
+ "validate.io-integer-array": ["validate.io-integer-array@1.0.0", "", { "dependencies": { "validate.io-array": "^1.0.3", "validate.io-integer": "^1.0.4" } }, "sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA=="],
113
+
114
+ "validate.io-number": ["validate.io-number@1.0.3", "", {}, "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg=="],
115
+
116
+ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
117
+
118
+ "y-protocols": ["y-protocols@1.0.6", "", { "dependencies": { "lib0": "^0.2.85" }, "peerDependencies": { "yjs": "^13.0.0" } }, "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q=="],
119
+
120
+ "yjs": ["yjs@13.6.27", "", { "dependencies": { "lib0": "^0.2.99" } }, "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw=="],
121
+ }
122
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Circuit Breaker implementation to prevent cascading failures
3
+ */
4
+ export class CircuitBreaker {
5
+ private failures = 0;
6
+ private lastFailure: number = 0;
7
+ private successCount = 0;
8
+ private state: "closed" | "open" | "half-open" = "closed";
9
+
10
+ // Configuration
11
+ private readonly threshold: number;
12
+ private readonly timeout: number;
13
+ private readonly halfOpenSuccessThreshold: number;
14
+ private readonly name: string;
15
+
16
+ constructor(options: {
17
+ name: string;
18
+ threshold?: number;
19
+ timeout?: number;
20
+ halfOpenSuccessThreshold?: number;
21
+ }) {
22
+ this.name = options.name;
23
+ this.threshold = options.threshold || 5;
24
+ this.timeout = options.timeout || 30000; // 30 seconds
25
+ this.halfOpenSuccessThreshold = options.halfOpenSuccessThreshold || 3;
26
+ }
27
+
28
+ /**
29
+ * Execute an operation with circuit breaker protection
30
+ */
31
+ async execute<T>(operation: () => Promise<T>): Promise<T> {
32
+ // Check circuit state
33
+ if (this.state === "open") {
34
+ if (Date.now() - this.lastFailure > this.timeout) {
35
+ console.log(
36
+ `[CircuitBreaker ${this.name}] Transitioning from open to half-open`
37
+ );
38
+ this.state = "half-open";
39
+ this.successCount = 0;
40
+ } else {
41
+ throw new Error(
42
+ `Circuit breaker is open for ${this.name}. Retry after ${
43
+ this.timeout - (Date.now() - this.lastFailure)
44
+ }ms`
45
+ );
46
+ }
47
+ }
48
+
49
+ try {
50
+ const result = await operation();
51
+
52
+ // Record success
53
+ if (this.state === "half-open") {
54
+ this.successCount++;
55
+ if (this.successCount >= this.halfOpenSuccessThreshold) {
56
+ console.log(
57
+ `[CircuitBreaker ${this.name}] Transitioning from half-open to closed`
58
+ );
59
+ this.state = "closed";
60
+ this.failures = 0;
61
+ }
62
+ } else if (this.state === "closed") {
63
+ // Reset failure count on success
64
+ this.failures = 0;
65
+ }
66
+
67
+ return result;
68
+ } catch (error) {
69
+ this.recordFailure();
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Record a failure and update circuit state
76
+ */
77
+ private recordFailure() {
78
+ this.failures++;
79
+ this.lastFailure = Date.now();
80
+
81
+ if (this.state === "half-open") {
82
+ console.log(
83
+ `[CircuitBreaker ${this.name}] Failure in half-open state, transitioning to open`
84
+ );
85
+ this.state = "open";
86
+ } else if (this.failures >= this.threshold) {
87
+ console.log(
88
+ `[CircuitBreaker ${this.name}] Threshold reached (${this.failures}/${this.threshold}), transitioning to open`
89
+ );
90
+ this.state = "open";
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get current circuit breaker state
96
+ */
97
+ getState(): {
98
+ state: string;
99
+ failures: number;
100
+ lastFailure: number;
101
+ isOpen: boolean;
102
+ } {
103
+ return {
104
+ state: this.state,
105
+ failures: this.failures,
106
+ lastFailure: this.lastFailure,
107
+ isOpen: this.state === "open",
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Reset the circuit breaker
113
+ */
114
+ reset() {
115
+ this.state = "closed";
116
+ this.failures = 0;
117
+ this.successCount = 0;
118
+ this.lastFailure = 0;
119
+ console.log(`[CircuitBreaker ${this.name}] Reset to closed state`);
120
+ }
121
+ }
@@ -0,0 +1,340 @@
1
+ import { type SpawnOptions, spawn } from "node:child_process";
2
+ import type { ExecuteOptions, ExecuteRequest, SessionData } from "../types";
3
+
4
+ function executeCommand(
5
+ sessions: Map<string, SessionData>,
6
+ command: string,
7
+ options: ExecuteOptions,
8
+ ): Promise<{
9
+ success: boolean;
10
+ stdout: string;
11
+ stderr: string;
12
+ exitCode: number;
13
+ }> {
14
+ return new Promise((resolve, reject) => {
15
+ const spawnOptions: SpawnOptions = {
16
+ shell: true,
17
+ stdio: ["pipe", "pipe", "pipe"] as const,
18
+ detached: options.background || false,
19
+ cwd: options.cwd || "/workspace", // Default to clean /workspace directory
20
+ env: options.env ? { ...process.env, ...options.env } : process.env
21
+ };
22
+
23
+ const child = spawn(command, spawnOptions);
24
+
25
+ // Store the process reference for cleanup if sessionId is provided
26
+ if (options.sessionId && sessions.has(options.sessionId)) {
27
+ const session = sessions.get(options.sessionId)!;
28
+ session.activeProcess = child;
29
+ }
30
+
31
+ let stdout = "";
32
+ let stderr = "";
33
+
34
+ child.stdout?.on("data", (data) => {
35
+ stdout += data.toString();
36
+ });
37
+
38
+ child.stderr?.on("data", (data) => {
39
+ stderr += data.toString();
40
+ });
41
+
42
+ if (options.background) {
43
+ // For background processes, unref and return quickly
44
+ child.unref();
45
+
46
+ // Collect initial output for 100ms then return
47
+ setTimeout(() => {
48
+ resolve({
49
+ exitCode: 0, // Process is still running
50
+ stderr,
51
+ stdout,
52
+ success: true,
53
+ });
54
+ }, 100);
55
+
56
+ // Still handle errors
57
+ child.on("error", (error) => {
58
+ console.error(`[Server] Background process error: ${command}`, error);
59
+ // Don't reject since we might have already resolved
60
+ });
61
+ } else {
62
+ // Normal synchronous execution
63
+ child.on("close", (code) => {
64
+ // Clear the active process reference
65
+ if (options.sessionId && sessions.has(options.sessionId)) {
66
+ const session = sessions.get(options.sessionId)!;
67
+ session.activeProcess = null;
68
+ }
69
+
70
+ console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
71
+
72
+ resolve({
73
+ exitCode: code || 0,
74
+ stderr,
75
+ stdout,
76
+ success: code === 0,
77
+ });
78
+ });
79
+
80
+ child.on("error", (error) => {
81
+ // Clear the active process reference
82
+ if (options.sessionId && sessions.has(options.sessionId)) {
83
+ const session = sessions.get(options.sessionId)!;
84
+ session.activeProcess = null;
85
+ }
86
+
87
+ reject(error);
88
+ });
89
+ }
90
+ });
91
+ }
92
+
93
+ export async function handleExecuteRequest(
94
+ sessions: Map<string, SessionData>,
95
+ req: Request,
96
+ corsHeaders: Record<string, string>
97
+ ): Promise<Response> {
98
+ try {
99
+ const body = (await req.json()) as ExecuteRequest;
100
+ const { command, sessionId, background, cwd, env } = body;
101
+
102
+ if (!command || typeof command !== "string") {
103
+ return new Response(
104
+ JSON.stringify({
105
+ error: "Command is required and must be a string",
106
+ }),
107
+ {
108
+ headers: {
109
+ "Content-Type": "application/json",
110
+ ...corsHeaders,
111
+ },
112
+ status: 400,
113
+ }
114
+ );
115
+ }
116
+
117
+ console.log(`[Server] Executing command: ${command}`);
118
+
119
+ const result = await executeCommand(sessions, command, { sessionId, background, cwd, env });
120
+
121
+ return new Response(
122
+ JSON.stringify({
123
+ command,
124
+ exitCode: result.exitCode,
125
+ stderr: result.stderr,
126
+ stdout: result.stdout,
127
+ success: result.success,
128
+ timestamp: new Date().toISOString(),
129
+ }),
130
+ {
131
+ headers: {
132
+ "Content-Type": "application/json",
133
+ ...corsHeaders,
134
+ },
135
+ }
136
+ );
137
+ } catch (error) {
138
+ console.error("[Server] Error in handleExecuteRequest:", error);
139
+ return new Response(
140
+ JSON.stringify({
141
+ error: "Failed to execute command",
142
+ message: error instanceof Error ? error.message : "Unknown error",
143
+ }),
144
+ {
145
+ headers: {
146
+ "Content-Type": "application/json",
147
+ ...corsHeaders,
148
+ },
149
+ status: 500,
150
+ }
151
+ );
152
+ }
153
+ }
154
+
155
+ export async function handleStreamingExecuteRequest(
156
+ sessions: Map<string, SessionData>,
157
+ req: Request,
158
+ corsHeaders: Record<string, string>
159
+ ): Promise<Response> {
160
+ try {
161
+ const body = (await req.json()) as ExecuteRequest;
162
+ const { command, sessionId, background, cwd, env } = body;
163
+
164
+ if (!command || typeof command !== "string") {
165
+ return new Response(
166
+ JSON.stringify({
167
+ error: "Command is required and must be a string",
168
+ }),
169
+ {
170
+ headers: {
171
+ "Content-Type": "application/json",
172
+ ...corsHeaders,
173
+ },
174
+ status: 400,
175
+ }
176
+ );
177
+ }
178
+
179
+ console.log(
180
+ `[Server] Executing streaming command: ${command}`
181
+ );
182
+
183
+ const stream = new ReadableStream({
184
+ start(controller) {
185
+ const spawnOptions: SpawnOptions = {
186
+ shell: true,
187
+ stdio: ["pipe", "pipe", "pipe"] as const,
188
+ detached: background || false,
189
+ cwd: cwd || "/workspace", // Default to clean /workspace directory
190
+ env: env ? { ...process.env, ...env } : process.env
191
+ };
192
+
193
+ const child = spawn(command, spawnOptions);
194
+
195
+ // Store the process reference for cleanup if sessionId is provided
196
+ if (sessionId && sessions.has(sessionId)) {
197
+ const session = sessions.get(sessionId)!;
198
+ session.activeProcess = child;
199
+ }
200
+
201
+ // For background processes, unref to prevent blocking
202
+ if (background) {
203
+ child.unref();
204
+ }
205
+
206
+ let stdout = "";
207
+ let stderr = "";
208
+
209
+ // Send command start event
210
+ controller.enqueue(
211
+ new TextEncoder().encode(
212
+ `data: ${JSON.stringify({
213
+ type: "start",
214
+ timestamp: new Date().toISOString(),
215
+ command,
216
+ background: background || false,
217
+ })}\n\n`
218
+ )
219
+ );
220
+
221
+ child.stdout?.on("data", (data) => {
222
+ const output = data.toString();
223
+ stdout += output;
224
+
225
+ // Send real-time output
226
+ controller.enqueue(
227
+ new TextEncoder().encode(
228
+ `data: ${JSON.stringify({
229
+ type: "stdout",
230
+ timestamp: new Date().toISOString(),
231
+ data: output,
232
+ command,
233
+ })}\n\n`
234
+ )
235
+ );
236
+ });
237
+
238
+ child.stderr?.on("data", (data) => {
239
+ const output = data.toString();
240
+ stderr += output;
241
+
242
+ // Send real-time error output
243
+ controller.enqueue(
244
+ new TextEncoder().encode(
245
+ `data: ${JSON.stringify({
246
+ type: "stderr",
247
+ timestamp: new Date().toISOString(),
248
+ data: output,
249
+ command,
250
+ })}\n\n`
251
+ )
252
+ );
253
+ });
254
+
255
+ child.on("close", (code) => {
256
+ // Clear the active process reference
257
+ if (sessionId && sessions.has(sessionId)) {
258
+ const session = sessions.get(sessionId)!;
259
+ session.activeProcess = null;
260
+ }
261
+
262
+ console.log(
263
+ `[Server] Command completed: ${command}, Exit code: ${code}`
264
+ );
265
+
266
+ // Send command completion event
267
+ controller.enqueue(
268
+ new TextEncoder().encode(
269
+ `data: ${JSON.stringify({
270
+ type: "complete",
271
+ timestamp: new Date().toISOString(),
272
+ command,
273
+ exitCode: code,
274
+ result: {
275
+ success: code === 0,
276
+ exitCode: code,
277
+ stdout,
278
+ stderr,
279
+ command,
280
+ timestamp: new Date().toISOString(),
281
+ },
282
+ })}\n\n`
283
+ )
284
+ );
285
+
286
+ // For non-background processes, close the stream
287
+ // For background processes with streaming, the stream stays open
288
+ if (!background) {
289
+ controller.close();
290
+ }
291
+ });
292
+
293
+ child.on("error", (error) => {
294
+ // Clear the active process reference
295
+ if (sessionId && sessions.has(sessionId)) {
296
+ const session = sessions.get(sessionId)!;
297
+ session.activeProcess = null;
298
+ }
299
+
300
+ controller.enqueue(
301
+ new TextEncoder().encode(
302
+ `data: ${JSON.stringify({
303
+ type: "error",
304
+ timestamp: new Date().toISOString(),
305
+ error: error.message,
306
+ command,
307
+ })}\n\n`
308
+ )
309
+ );
310
+
311
+ controller.close();
312
+ });
313
+ },
314
+ });
315
+
316
+ return new Response(stream, {
317
+ headers: {
318
+ "Cache-Control": "no-cache",
319
+ Connection: "keep-alive",
320
+ "Content-Type": "text/event-stream",
321
+ ...corsHeaders,
322
+ },
323
+ });
324
+ } catch (error) {
325
+ console.error("[Server] Error in handleStreamingExecuteRequest:", error);
326
+ return new Response(
327
+ JSON.stringify({
328
+ error: "Failed to execute streaming command",
329
+ message: error instanceof Error ? error.message : "Unknown error",
330
+ }),
331
+ {
332
+ headers: {
333
+ "Content-Type": "application/json",
334
+ ...corsHeaders,
335
+ },
336
+ status: 500,
337
+ }
338
+ );
339
+ }
340
+ }