threadwire 0.1.11 → 0.1.12

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.
@@ -1,10 +1,9 @@
1
1
  // @ts-nocheck
2
2
  /* eslint-disable jsdoc/require-jsdoc */
3
3
 
4
- import {deadlineAfter, abortable, readResponseCapped} from "./absolute-deadline.js"
4
+ import {AbsoluteDeadline, deadlineAfter, abortable, readResponseCapped} from "./absolute-deadline.js"
5
5
 
6
6
  const MAX_RESPONSE_BYTES = 2_097_152
7
- const DEFAULT_TIMEOUT_MS = 3_660_000
8
7
  const MAX_RAW_OUTPUT_BYTES = 1_048_576
9
8
 
10
9
  export class IsolatedRuntimeClient {
@@ -13,18 +12,27 @@ export class IsolatedRuntimeClient {
13
12
  this.url = normalizedUrl(options.url)
14
13
  this.controlToken = nonempty(options.controlToken, "isolated runtime control token")
15
14
  this.fetch = options.fetchImplementation ?? fetch
16
- this.timeoutMs = positiveDuration(options.timeoutMs, DEFAULT_TIMEOUT_MS)
15
+ this.timeoutMs = optionalDuration(options.timeoutMs)
17
16
  }
18
17
 
19
18
  /**
20
- * @param {{provider: string, profile: string, repositoryRoot: string, cwd: string, providerArguments: string[], resumeSession?: string, signal?: AbortSignal, deadline?: import("./absolute-deadline.js").AbsoluteDeadline}} request
19
+ * @param {{provider: string, profile: string, repositoryRoot?: string, cwd?: string, binding?: unknown, providerArguments: string[], resumeSession?: string, signal?: AbortSignal, deadline?: import("./absolute-deadline.js").AbsoluteDeadline}} request
21
20
  * @returns {Promise<{preflightId: string, deadline?: import("./absolute-deadline.js").AbsoluteDeadline}>}
22
21
  */
23
22
  async preflight(request) {
24
- const deadline = request.deadline ?? deadlineAfter(this.timeoutMs, {signal: request.signal, timeoutMessage: "Isolated runtime launch timeout"})
25
- const response = await this.request("/preflight", {...request, launchDeadlineAt: deadline.expiresAt}, deadline)
26
- if (response.ok !== true || typeof response.preflightId !== "string") throw new Error("Isolated runtime preflight failed")
27
- return {preflightId: response.preflightId, deadline}
23
+ const deadline = request.deadline ?? (this.timeoutMs === undefined ? undefined : deadlineAfter(this.timeoutMs, {signal: request.signal, timeoutMessage: "Isolated runtime launch timeout"}))
24
+ const operation = deadline ?? new AbsoluteDeadline(undefined, {signal: request.signal, timeoutMessage: "Isolated runtime launch timeout"})
25
+ const body = {...request}
26
+ delete body.signal
27
+ delete body.deadline
28
+ if (deadline !== undefined) body.launchDeadlineAt = deadline.expiresAt
29
+ try {
30
+ const response = await this.request("/preflight", body, operation)
31
+ if (response.ok !== true || typeof response.preflightId !== "string") throw new Error("Isolated runtime preflight failed")
32
+ return deadline === undefined ? {preflightId: response.preflightId} : {preflightId: response.preflightId, deadline}
33
+ } finally {
34
+ if (deadline === undefined) operation.close()
35
+ }
28
36
  }
29
37
 
30
38
  /**
@@ -33,44 +41,42 @@ export class IsolatedRuntimeClient {
33
41
  * resumeSession?: string,
34
42
  * signal?: AbortSignal,
35
43
  * deadline?: import("./absolute-deadline.js").AbsoluteDeadline,
36
- * onEvent: (event: import("./types.js").WorkerEvent) => void | Promise<void>,
37
- * onRecord?: (record: unknown) => void | Promise<void>,
38
- * onStdoutChunk?: (chunk: Buffer) => void | Promise<void>,
39
- * onStderrChunk?: (chunk: Buffer) => void | Promise<void>
44
+ * onEvent: (event: import("./types.js").WorkerEvent) => unknown | Promise<unknown>,
45
+ * onRecord?: (record: unknown) => unknown | Promise<unknown>,
46
+ * onStdoutChunk?: (chunk: Buffer) => unknown | Promise<unknown>,
47
+ * onStderrChunk?: (chunk: Buffer) => unknown | Promise<unknown>
40
48
  * }} options
49
+ * @returns {Promise<number>}
41
50
  */
42
51
  async run(options) {
43
- const deadline = options.deadline ?? deadlineAfter(this.timeoutMs, {signal: options.signal, timeoutMessage: "Isolated runtime launch timeout"})
44
- const response = await this.request("/run", {
45
- preflightId: options.preflightId,
46
- prompt: options.prompt,
47
- providerArguments: options.providerArguments,
48
- ...(options.resumeSession === undefined ? {} : {resumeSession: options.resumeSession})
49
- }, deadline)
50
- if (!Array.isArray(response.records) || !Array.isArray(response.rawChunks) || !Number.isSafeInteger(response.exitCode)) throw new Error("Isolated runtime emitted an invalid response")
51
- let rawBytes = 0
52
- const decoded = response.rawChunks.map((item) => {
53
- if (!isRecord(item) || Object.keys(item).sort().join(",") !== "channel,data"
54
- || (item.channel !== "stdout" && item.channel !== "stderr") || typeof item.data !== "string"
55
- || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(item.data)) {
56
- throw new Error("Isolated runtime emitted an invalid response")
52
+ const deadline = options.deadline ?? (this.timeoutMs === undefined ? undefined : deadlineAfter(this.timeoutMs, {signal: options.signal, timeoutMessage: "Isolated runtime launch timeout"}))
53
+ const operation = deadline ?? new AbsoluteDeadline(undefined, {signal: options.signal, timeoutMessage: "Isolated runtime launch timeout"})
54
+ try {
55
+ const response = await abortable(this.fetch(`${this.url}/run`, {
56
+ method: "POST",
57
+ redirect: "error",
58
+ headers: {
59
+ authorization: `Bearer ${this.controlToken}`,
60
+ "content-type": "application/json",
61
+ connection: "close",
62
+ ...(operation.expiresAt === undefined ? {} : {"x-threadwire-launch-deadline": String(operation.expiresAt)})
63
+ },
64
+ body: JSON.stringify({
65
+ preflightId: options.preflightId,
66
+ prompt: options.prompt,
67
+ providerArguments: options.providerArguments,
68
+ ...(options.resumeSession === undefined ? {} : {resumeSession: options.resumeSession})
69
+ }),
70
+ signal: operation.signal
71
+ }), operation.signal)
72
+ if (!response.ok) {
73
+ const bytes = await readResponseCapped(response, MAX_RESPONSE_BYTES, operation.signal)
74
+ throw new Error(safeRemoteError(bytes))
57
75
  }
58
- const data = Buffer.from(item.data, "base64")
59
- rawBytes += data.length
60
- if (rawBytes > MAX_RAW_OUTPUT_BYTES) throw new Error("Isolated runtime emitted an invalid response")
61
- return {channel: item.channel, data}
62
- })
63
- for (const chunk of decoded) {
64
- deadline.throwIfAborted()
65
- const callback = chunk.channel === "stdout" ? options.onStdoutChunk : options.onStderrChunk
66
- await abortable(callback?.(chunk.data), deadline.signal)
76
+ return await consumeRunStream(response, options, operation)
77
+ } finally {
78
+ if (options.deadline === undefined) operation.close()
67
79
  }
68
- for (const record of response.records) {
69
- deadline.throwIfAborted()
70
- await abortable(options.onRecord?.(record), deadline.signal)
71
- if (isWorkerEventEnvelope(record)) await abortable(options.onEvent(record.event), deadline.signal)
72
- }
73
- return /** @type {number} */ (response.exitCode)
74
80
  }
75
81
 
76
82
  /** @param {string} path @param {unknown} body */
@@ -83,7 +89,7 @@ export class IsolatedRuntimeClient {
83
89
  authorization: `Bearer ${this.controlToken}`,
84
90
  "content-type": "application/json",
85
91
  connection: "close",
86
- "x-threadwire-launch-deadline": String(deadline.expiresAt)
92
+ ...(deadline.expiresAt === undefined ? {} : {"x-threadwire-launch-deadline": String(deadline.expiresAt)})
87
93
  },
88
94
  body: JSON.stringify(body),
89
95
  signal
@@ -106,8 +112,74 @@ export class IsolatedRuntimeClient {
106
112
  }
107
113
  }
108
114
 
109
- function positiveDuration(value, fallback) {
110
- if (value === undefined) return fallback
115
+ /** @returns {Promise<number>} */
116
+ async function consumeRunStream(response, options, operation) {
117
+ const reader = response.body?.getReader()
118
+ if (reader === undefined) throw new Error("Isolated runtime emitted an invalid response")
119
+ let pending = Buffer.alloc(0)
120
+ let totalBytes = 0
121
+ let rawBytes = 0
122
+ /** @type {number | undefined} */
123
+ let terminal
124
+ const consume = async (line) => {
125
+ if (line.length === 0 || terminal !== undefined) throw new Error("Isolated runtime emitted an invalid response")
126
+ let frame
127
+ try { frame = JSON.parse(line.toString("utf8")) } catch { throw new Error("Isolated runtime emitted an invalid response") }
128
+ if (!isRecord(frame) || typeof frame.type !== "string") throw new Error("Isolated runtime emitted an invalid response")
129
+ if (frame.type === "chunk") {
130
+ if (Object.keys(frame).sort().join(",") !== "channel,data,type"
131
+ || (frame.channel !== "stdout" && frame.channel !== "stderr") || typeof frame.data !== "string"
132
+ || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(frame.data)) {
133
+ throw new Error("Isolated runtime emitted an invalid response")
134
+ }
135
+ const data = Buffer.from(frame.data, "base64")
136
+ rawBytes += data.length
137
+ if (rawBytes > MAX_RAW_OUTPUT_BYTES) throw new Error("Isolated runtime emitted an invalid response")
138
+ const callback = frame.channel === "stdout" ? options.onStdoutChunk : options.onStderrChunk
139
+ await abortable(callback?.(data), operation.signal)
140
+ return
141
+ }
142
+ if (frame.type === "record") {
143
+ if (Object.keys(frame).sort().join(",") !== "record,type") throw new Error("Isolated runtime emitted an invalid response")
144
+ await abortable(options.onRecord?.(frame.record), operation.signal)
145
+ if (isWorkerEventEnvelope(frame.record)) await abortable(options.onEvent(frame.record.event), operation.signal)
146
+ return
147
+ }
148
+ if (frame.type === "terminal" && Object.keys(frame).sort().join(",") === "exitCode,type" && Number.isSafeInteger(frame.exitCode)) {
149
+ terminal = frame.exitCode
150
+ return
151
+ }
152
+ throw new Error("Isolated runtime emitted an invalid response")
153
+ }
154
+ try {
155
+ while (true) {
156
+ const {done, value} = await abortable(reader.read(), operation.signal)
157
+ if (done) break
158
+ const chunk = Buffer.from(value)
159
+ totalBytes += chunk.length
160
+ if (totalBytes > MAX_RESPONSE_BYTES) {
161
+ await reader.cancel(new Error("Response exceeded capacity")).catch(() => {})
162
+ throw new Error("Isolated runtime response exceeded the configured capacity")
163
+ }
164
+ pending = Buffer.concat([pending, chunk])
165
+ while (true) {
166
+ const newline = pending.indexOf(0x0a)
167
+ if (newline < 0) break
168
+ const line = pending.subarray(0, newline)
169
+ pending = pending.subarray(newline + 1)
170
+ await consume(line)
171
+ }
172
+ }
173
+ if (pending.length !== 0 || terminal === undefined) throw new Error("Isolated runtime emitted an invalid response")
174
+ return terminal
175
+ } finally {
176
+ if (operation.signal.aborted) await reader.cancel(operation.signal.reason).catch(() => {})
177
+ reader.releaseLock()
178
+ }
179
+ }
180
+
181
+ function optionalDuration(value) {
182
+ if (value === undefined || (typeof value === "string" && value.trim().length === 0)) return undefined
111
183
  const number = Number(value)
112
184
  if (!Number.isSafeInteger(number) || number < 1 || number > 604_800_000) throw new Error("Invalid isolated runtime client timeout")
113
185
  return number