Skip to content

Commit 4148ddd

Browse files
committed
feat: add SSE for browser support
1 parent 415a4a9 commit 4148ddd

File tree

4 files changed

+107
-47
lines changed

4 files changed

+107
-47
lines changed

package-lock.json

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"dependencies": {
2929
"adm-zip": "^0.5.10",
3030
"node-downloader-helper": "^2.1.9",
31+
"sse.js": "^2.4.1",
3132
"tar": "^6.2.0"
3233
},
3334
"devDependencies": {

src/gptscript.ts

+99-46
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import * as path from "path"
22
import child_process from "child_process"
33
import net from "node:net"
44
import http from "http"
5+
//@ts-ignore
6+
import SSE from "sse.js"
57

68
export interface RunOpts {
79
gptscriptURL?: string
@@ -52,6 +54,7 @@ export class Run {
5254

5355
private promise?: Promise<string>
5456
private process?: child_process.ChildProcess
57+
private sse?: SSE
5558
private req?: http.ClientRequest
5659
private stdout?: string
5760
private stderr?: string
@@ -179,41 +182,34 @@ export class Run {
179182
const options = this.requestOptions(this.opts.gptscriptURL, path, postData, tool)
180183

181184
this.promise = new Promise<string>((resolve, reject) => {
182-
// Use frag to keep track of partial object writes.
183-
let frag = ""
184-
this.req = http.request(options, (res: http.IncomingMessage) => {
185-
this.state = RunState.Running
186-
res.on("data", (chunk: any) => {
187-
for (let line of (chunk.toString() + frag).split("\n")) {
188-
const c = line.replace(/^(data: )/, "").trim()
189-
if (!c) {
190-
continue
191-
}
185+
// This checks that the code is running in a browser. If it is, then we use SSE.
186+
if (typeof window !== "undefined" && typeof window.document !== "undefined") {
187+
this.sse = new SSE(this.opts.gptscriptURL + "/" + path, {
188+
headers: {"Content-Type": "application/json"},
189+
payload: postData
190+
})
192191

193-
if (c === "[DONE]") {
194-
return
195-
}
192+
this.sse.addEventListener("open", () => {
193+
this.state = RunState.Running
194+
})
196195

197-
let e: any
198-
try {
199-
e = JSON.parse(c)
200-
} catch {
201-
frag = c
202-
return
203-
}
204-
frag = ""
205-
206-
if (e.stderr) {
207-
this.stderr = (this.stderr || "") + (typeof e.stderr === "string" ? e.stderr : JSON.stringify(e.stderr))
208-
} else if (e.stdout) {
209-
this.stdout = (this.stdout || "") + (typeof e.stdout === "string" ? e.stdout : JSON.stringify(e.stdout))
210-
} else {
211-
frag = this.emitEvent(c)
212-
}
196+
this.sse.addEventListener("message", (data: any) => {
197+
if (data.data === "[DONE]") {
198+
this.sse.close()
199+
return
200+
}
201+
202+
const e = JSON.parse(data.data)
203+
if (e.stderr) {
204+
this.stderr = (this.stderr || "") + (typeof e.stderr === "string" ? e.stderr : JSON.stringify(e.stderr))
205+
} else if (e.stdout) {
206+
this.stdout = (this.stdout || "") + (typeof e.stdout === "string" ? e.stdout : JSON.stringify(e.stdout))
207+
} else {
208+
this.emitEvent(data.data)
213209
}
214210
})
215211

216-
res.on("end", () => {
212+
this.sse.addEventListener("close", () => {
217213
if (this.state === RunState.Running || this.state === RunState.Finished) {
218214
this.state = RunState.Finished
219215
resolve(this.stdout || "")
@@ -222,29 +218,81 @@ export class Run {
222218
}
223219
})
224220

225-
res.on("aborted", () => {
226-
if (this.state !== RunState.Finished) {
221+
this.sse.addEventListener("error", (err: any) => {
222+
this.state = RunState.Error
223+
this.err = err
224+
reject(err)
225+
})
226+
} else {
227+
// If not in the browser, then we use HTTP.
228+
229+
// Use frag to keep track of partial object writes.
230+
let frag = ""
231+
this.req = http.request(options, (res: http.IncomingMessage) => {
232+
this.state = RunState.Running
233+
res.on("data", (chunk: any) => {
234+
for (let line of (chunk.toString() + frag).split("\n")) {
235+
const c = line.replace(/^(data: )/, "").trim()
236+
if (!c) {
237+
continue
238+
}
239+
240+
if (c === "[DONE]") {
241+
return
242+
}
243+
244+
let e: any
245+
try {
246+
e = JSON.parse(c)
247+
} catch {
248+
frag = c
249+
return
250+
}
251+
frag = ""
252+
253+
if (e.stderr) {
254+
this.stderr = (this.stderr || "") + (typeof e.stderr === "string" ? e.stderr : JSON.stringify(e.stderr))
255+
} else if (e.stdout) {
256+
this.stdout = (this.stdout || "") + (typeof e.stdout === "string" ? e.stdout : JSON.stringify(e.stdout))
257+
} else {
258+
frag = this.emitEvent(c)
259+
}
260+
}
261+
})
262+
263+
res.on("end", () => {
264+
if (this.state === RunState.Running || this.state === RunState.Finished) {
265+
this.state = RunState.Finished
266+
resolve(this.stdout || "")
267+
} else if (this.state === RunState.Error) {
268+
reject(this.err)
269+
}
270+
})
271+
272+
res.on("aborted", () => {
273+
if (this.state !== RunState.Finished) {
274+
this.state = RunState.Error
275+
this.err = "Run has been aborted"
276+
reject(this.err)
277+
}
278+
})
279+
280+
res.on("error", (error: Error) => {
227281
this.state = RunState.Error
228-
this.err = "Run has been aborted"
282+
this.err = error.message || ""
229283
reject(this.err)
230-
}
284+
})
231285
})
232286

233-
res.on("error", (error: Error) => {
287+
this.req.on("error", (error: Error) => {
234288
this.state = RunState.Error
235289
this.err = error.message || ""
236290
reject(this.err)
237291
})
238-
})
239-
240-
this.req.on("error", (error: Error) => {
241-
this.state = RunState.Error
242-
this.err = error.message || ""
243-
reject(this.err)
244-
})
245292

246-
this.req.write(postData)
247-
this.req.end()
293+
this.req.write(postData)
294+
this.req.end()
295+
}
248296
})
249297
}
250298

@@ -297,7 +345,7 @@ export class Run {
297345
this.state = RunState.Finished
298346
this.stdout = f.output || ""
299347
}
300-
} else if (f.type.startsWith("call")) {
348+
} else if ((f.type as string).startsWith("call")) {
301349
let call = this.calls?.find((x) => x.id === f.callContext.id)
302350

303351
if (!call) {
@@ -382,7 +430,7 @@ export class Run {
382430
return JSON.parse(await this.text())
383431
}
384432

385-
public abort(): void {
433+
public close(): void {
386434
if (this.process) {
387435
if (this.process.exitCode === null) {
388436
this.process.kill("SIGKILL")
@@ -395,6 +443,11 @@ export class Run {
395443
return
396444
}
397445

446+
if (this.sse) {
447+
this.sse.close()
448+
return
449+
}
450+
398451
throw new Error("Run not started")
399452
}
400453

tests/gptscript.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ describe("gptscript module", () => {
131131
try {
132132
const run = gptscript.run(testGptPath, opts)
133133
run.on(gptscript.RunEventType.CallProgress, data => {
134-
run.abort()
134+
run.close()
135135
})
136136
await run.text()
137137
err = run.err

0 commit comments

Comments
 (0)