( source: ReadableStream<ExecEvent>, encoding: E | undefined, )
| 289 | } |
| 290 | |
| 291 | function pipeEvents<E extends ExecEncoding>( |
| 292 | source: ReadableStream<ExecEvent>, |
| 293 | encoding: E | undefined, |
| 294 | ): ReadableStream<WorkspaceExecEvent<E>> { |
| 295 | if (encoding !== "utf8") { |
| 296 | // Identity pipe — the wire shape already matches. |
| 297 | return source as unknown as ReadableStream<WorkspaceExecEvent<E>>; |
| 298 | } |
| 299 | // Per-stream TextDecoders preserve multi-byte boundaries |
| 300 | // across chunk splits. |
| 301 | const stdoutDec = new TextDecoder("utf-8", { fatal: false }); |
| 302 | const stderrDec = new TextDecoder("utf-8", { fatal: false }); |
| 303 | let stdoutMeta: { id: string; seq: number } | undefined; |
| 304 | let stderrMeta: { id: string; seq: number } | undefined; |
| 305 | let lastSeq = 0; |
| 306 | const enqueue = ( |
| 307 | controller: TransformStreamDefaultController<WorkspaceExecEvent<E>>, |
| 308 | event: WorkspaceExecEvent<E>, |
| 309 | ) => { |
| 310 | lastSeq = event.seq; |
| 311 | controller.enqueue(event); |
| 312 | }; |
| 313 | const flushPending = ( |
| 314 | controller: TransformStreamDefaultController<WorkspaceExecEvent<E>>, |
| 315 | beforeSeq?: number, |
| 316 | ) => { |
| 317 | const pending: Array<{ |
| 318 | id: string; |
| 319 | seq: number; |
| 320 | name: "stdout" | "stderr"; |
| 321 | value: Chunk<E>; |
| 322 | }> = []; |
| 323 | const stdout = stdoutDec.decode(); |
| 324 | const stderr = stderrDec.decode(); |
| 325 | if (stdout && stdoutMeta) { |
| 326 | pending.push({ ...stdoutMeta, name: "stdout", value: stdout as Chunk<E> }); |
| 327 | } |
| 328 | if (stderr && stderrMeta) { |
| 329 | pending.push({ ...stderrMeta, name: "stderr", value: stderr as Chunk<E> }); |
| 330 | } |
| 331 | pending.sort((a, b) => a.seq - b.seq); |
| 332 | const span = beforeSeq !== undefined && beforeSeq > lastSeq ? beforeSeq - lastSeq : 1; |
| 333 | for (let index = 0; index < pending.length; index++) { |
| 334 | const event = pending[index]; |
| 335 | enqueue(controller, { |
| 336 | ...event, |
| 337 | seq: lastSeq + (span * (index + 1)) / (pending.length + 1), |
| 338 | }); |
| 339 | } |
| 340 | stdoutMeta = undefined; |
| 341 | stderrMeta = undefined; |
| 342 | }; |
| 343 | return source.pipeThrough( |
| 344 | new TransformStream<ExecEvent, WorkspaceExecEvent<E>>({ |
| 345 | transform(event, controller) { |
| 346 | if (event.name === "stdout") { |
| 347 | stdoutMeta = { id: event.id, seq: event.seq }; |
| 348 | enqueue(controller, { |
no test coverage detected