(name: string, options: SpritesExecOptions)
| 532 | } |
| 533 | |
| 534 | exec(name: string, options: SpritesExecOptions): SpritesExecStream { |
| 535 | const query = new URLSearchParams() |
| 536 | for (const arg of options.argv) query.append('cmd', arg) |
| 537 | if (options.cwd !== undefined) query.set('dir', options.cwd) |
| 538 | if (options.env) { |
| 539 | for (const [key, value] of Object.entries(options.env)) { |
| 540 | query.append('env', `${key}=${value}`) |
| 541 | } |
| 542 | } |
| 543 | |
| 544 | const wsBase = this.baseUrl.replace(/^http(s?):\/\//, 'ws$1://') |
| 545 | const url = `${wsBase}/v1/sprites/${encodeURIComponent(name)}/exec?${query.toString()}` |
| 546 | // The query carries cmd/env (possibly secrets); never surface it in errors. |
| 547 | const safeUrl = `${wsBase}/v1/sprites/${encodeURIComponent(name)}/exec` |
| 548 | |
| 549 | const stdoutQ = new AsyncChunkQueue() |
| 550 | const stderrQ = new AsyncChunkQueue() |
| 551 | const outDecoder = new TextDecoder() |
| 552 | const errDecoder = new TextDecoder() |
| 553 | |
| 554 | let sessionId: string | undefined |
| 555 | let exitCode: number | undefined |
| 556 | let exitObserved = false |
| 557 | let killedByCaller = false |
| 558 | let opened = false |
| 559 | let settled = false |
| 560 | let socketError: Error | undefined |
| 561 | let onAbort: (() => void) | undefined |
| 562 | let resolveClosed!: () => void |
| 563 | const closed = new Promise<void>((resolve) => { |
| 564 | resolveClosed = resolve |
| 565 | }) |
| 566 | // Resolves when the session id is known (or the socket closes without one), |
| 567 | // so kill() can reach the server-side kill endpoint even if it is called |
| 568 | // before the `session_info` frame arrives. |
| 569 | let resolveSession!: (id: string | undefined) => void |
| 570 | const sessionReady = new Promise<string | undefined>((resolve) => { |
| 571 | resolveSession = resolve |
| 572 | }) |
| 573 | |
| 574 | // The global (undici) WebSocket accepts a `headers` constructor option at |
| 575 | // runtime, but the WHATWG type only declares `(url, protocols?)`, so the two |
| 576 | // constructor signatures don't structurally overlap — bridge via `unknown`. |
| 577 | // eslint-disable-next-line no-restricted-syntax -- undici headers option not in the DOM WebSocket type |
| 578 | const WebSocketCtor = WebSocket as unknown as WsCtor |
| 579 | const ws = new WebSocketCtor(url, { headers: this.headers() }) |
| 580 | ws.binaryType = 'arraybuffer' |
| 581 | |
| 582 | // Bound the connect phase: if the socket never opens (e.g. the Sprite is |
| 583 | // restarting and stalls in CONNECTING), fail instead of hanging wait(). |
| 584 | const connectTimer: ReturnType<typeof setTimeout> = setTimeout(() => { |
| 585 | if (!opened) { |
| 586 | socketError ??= new Error( |
| 587 | `Sprites exec WebSocket did not connect within ${options.connectTimeoutMs ?? 30_000}ms (${safeUrl}).`, |
| 588 | ) |
| 589 | try { |
| 590 | ws.close() |
| 591 | } catch { |
nothing calls this directly
no test coverage detected