Skip to content

Commit

Permalink
feat: add SSE for browser support
Browse files Browse the repository at this point in the history
  • Loading branch information
thedadams committed May 2, 2024
1 parent 415a4a9 commit 4148ddd
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 47 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"dependencies": {
"adm-zip": "^0.5.10",
"node-downloader-helper": "^2.1.9",
"sse.js": "^2.4.1",
"tar": "^6.2.0"
},
"devDependencies": {
Expand Down
145 changes: 99 additions & 46 deletions src/gptscript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as path from "path"
import child_process from "child_process"
import net from "node:net"
import http from "http"
//@ts-ignore
import SSE from "sse.js"

export interface RunOpts {
gptscriptURL?: string
Expand Down Expand Up @@ -52,6 +54,7 @@ export class Run {

private promise?: Promise<string>
private process?: child_process.ChildProcess
private sse?: SSE
private req?: http.ClientRequest
private stdout?: string
private stderr?: string
Expand Down Expand Up @@ -179,41 +182,34 @@ export class Run {
const options = this.requestOptions(this.opts.gptscriptURL, path, postData, tool)

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

if (c === "[DONE]") {
return
}
this.sse.addEventListener("open", () => {
this.state = RunState.Running
})

let e: any
try {
e = JSON.parse(c)
} catch {
frag = c
return
}
frag = ""

if (e.stderr) {
this.stderr = (this.stderr || "") + (typeof e.stderr === "string" ? e.stderr : JSON.stringify(e.stderr))
} else if (e.stdout) {
this.stdout = (this.stdout || "") + (typeof e.stdout === "string" ? e.stdout : JSON.stringify(e.stdout))
} else {
frag = this.emitEvent(c)
}
this.sse.addEventListener("message", (data: any) => {
if (data.data === "[DONE]") {
this.sse.close()
return
}

const e = JSON.parse(data.data)
if (e.stderr) {
this.stderr = (this.stderr || "") + (typeof e.stderr === "string" ? e.stderr : JSON.stringify(e.stderr))
} else if (e.stdout) {
this.stdout = (this.stdout || "") + (typeof e.stdout === "string" ? e.stdout : JSON.stringify(e.stdout))
} else {
this.emitEvent(data.data)
}
})

res.on("end", () => {
this.sse.addEventListener("close", () => {
if (this.state === RunState.Running || this.state === RunState.Finished) {
this.state = RunState.Finished
resolve(this.stdout || "")
Expand All @@ -222,29 +218,81 @@ export class Run {
}
})

res.on("aborted", () => {
if (this.state !== RunState.Finished) {
this.sse.addEventListener("error", (err: any) => {
this.state = RunState.Error
this.err = err
reject(err)
})
} else {
// If not in the browser, then we use HTTP.

// Use frag to keep track of partial object writes.
let frag = ""
this.req = http.request(options, (res: http.IncomingMessage) => {
this.state = RunState.Running
res.on("data", (chunk: any) => {
for (let line of (chunk.toString() + frag).split("\n")) {
const c = line.replace(/^(data: )/, "").trim()
if (!c) {
continue
}

if (c === "[DONE]") {
return
}

let e: any
try {
e = JSON.parse(c)
} catch {
frag = c
return
}
frag = ""

if (e.stderr) {
this.stderr = (this.stderr || "") + (typeof e.stderr === "string" ? e.stderr : JSON.stringify(e.stderr))
} else if (e.stdout) {
this.stdout = (this.stdout || "") + (typeof e.stdout === "string" ? e.stdout : JSON.stringify(e.stdout))
} else {
frag = this.emitEvent(c)
}
}
})

res.on("end", () => {
if (this.state === RunState.Running || this.state === RunState.Finished) {
this.state = RunState.Finished
resolve(this.stdout || "")
} else if (this.state === RunState.Error) {
reject(this.err)
}
})

res.on("aborted", () => {
if (this.state !== RunState.Finished) {
this.state = RunState.Error
this.err = "Run has been aborted"
reject(this.err)
}
})

res.on("error", (error: Error) => {
this.state = RunState.Error
this.err = "Run has been aborted"
this.err = error.message || ""
reject(this.err)
}
})
})

res.on("error", (error: Error) => {
this.req.on("error", (error: Error) => {
this.state = RunState.Error
this.err = error.message || ""
reject(this.err)
})
})

this.req.on("error", (error: Error) => {
this.state = RunState.Error
this.err = error.message || ""
reject(this.err)
})

this.req.write(postData)
this.req.end()
this.req.write(postData)
this.req.end()
}
})
}

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

if (!call) {
Expand Down Expand Up @@ -382,7 +430,7 @@ export class Run {
return JSON.parse(await this.text())
}

public abort(): void {
public close(): void {
if (this.process) {
if (this.process.exitCode === null) {
this.process.kill("SIGKILL")
Expand All @@ -395,6 +443,11 @@ export class Run {
return
}

if (this.sse) {
this.sse.close()
return
}

throw new Error("Run not started")
}

Expand Down
2 changes: 1 addition & 1 deletion tests/gptscript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ describe("gptscript module", () => {
try {
const run = gptscript.run(testGptPath, opts)
run.on(gptscript.RunEventType.CallProgress, data => {
run.abort()
run.close()
})
await run.text()
err = run.err
Expand Down

0 comments on commit 4148ddd

Please sign in to comment.