fix(e2b): address review round on cadence config, disposal, and SDK edge cases
- subprocess-e2b: the 20 ms remote poll cadence becomes a validated pollMs Config field (each tick is one control-plane request); the README documents the latency-versus-request-count trade. - subprocess-e2b: extract src/remote.ts owning asError, signalOpts, commandOpts, delay, waitTick, and one tolerant signalRemoteGroups shared by the pgid-keyed process ladder and sid-keyed terminal ladder, so the two teardown paths keep identical error tolerance. - subprocess-e2b: service disposal aggregates sibling cleanup failures into one AggregateError instead of discarding all but the first. - subprocess-e2b: waitForProcessGroupId refuses published group ids <= 1, so a same-UID rewrite of the pid file cannot aim termination at kill -- -1; README documents the same-UID control-state limitation. - subprocess-e2b: drain-grace expiry now releases an inherited-output E2B callback blocked on host backpressure before disconnecting, so the SDK settlement cannot stay pinned behind an unread host stream. - subprocess-e2b: spawn/spawnTerminal stop validating typed spec fields (trust-TypeScript rule; pty-local validates its config before specs exist); resolveExecutable rejects separator-containing relative paths per the seam contract; terminal setups tracked as a Set of records. - subprocess-e2b: PTY output push-without-backpressure is a documented contract (flowing consumer folds bytes; paused consumer buffers). - fs-e2b: streamText normalizes the pinned SDK's empty-file '' return into an empty stream instead of throwing on getReader(). - e2b overlays: comment the one-world cwd invariant across e2b.cwd, workspaceRoot, and bash-local's implicit default workdir.
This commit is contained in:
@@ -232,7 +232,13 @@ export class E2BFileSystem extends FileSystem {
|
||||
await this.requireRegular(target, signal)
|
||||
let stream: ReadableStream<Uint8Array>
|
||||
try {
|
||||
stream = await sandbox.files.read(String(target.targetKey), { format: 'stream', ...signalOpts(signal) })
|
||||
// The pinned SDK's stream overload lies for empty files: content-length 0
|
||||
// returns '' instead of a ReadableStream.
|
||||
const read = await sandbox.files.read(String(target.targetKey), { format: 'stream', ...signalOpts(signal) }) as
|
||||
ReadableStream<Uint8Array> | string
|
||||
stream = typeof read === 'string'
|
||||
? new ReadableStream<Uint8Array>({ start(controller) { controller.close() } })
|
||||
: read
|
||||
} catch (error: unknown) {
|
||||
throw mapError(error, 'read', target.displayPath, signal)
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ class FakeRemote {
|
||||
}
|
||||
return this.info(path)
|
||||
},
|
||||
read: async (path: string, options: { format: 'bytes' | 'stream'; signal?: AbortSignal }): Promise<Uint8Array | ReadableStream<Uint8Array>> => {
|
||||
read: async (path: string, options: { format: 'bytes' | 'stream'; signal?: AbortSignal }): Promise<Uint8Array | ReadableStream<Uint8Array> | string> => {
|
||||
this.checkAbort(options)
|
||||
if (this.nextReadError !== undefined) {
|
||||
const error = this.nextReadError
|
||||
@@ -158,6 +158,8 @@ class FakeRemote {
|
||||
}
|
||||
const data = this.followed(path).node.data
|
||||
if (options.format === 'bytes') return data.slice()
|
||||
// Pinned-SDK fidelity: a content-length-0 response returns '' even in stream format.
|
||||
if (data.length === 0 && this.streamChunks === undefined) return ''
|
||||
const chunks = this.streamChunks ?? [data.slice()]
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start: (controller) => {
|
||||
@@ -375,6 +377,15 @@ describe('E2BFileSystem identity, metadata, and reads', () => {
|
||||
expect(initiallyBuffered).toBe('€')
|
||||
})
|
||||
|
||||
it('streams an empty file even though the pinned SDK returns a non-stream value', async () => {
|
||||
const remote = new FakeRemote()
|
||||
remote.file('/workspace/empty.txt', '')
|
||||
const { fs } = await setup(remote)
|
||||
let streamed = ''
|
||||
for await (const chunk of await fs.streamText(await fs.resolve('empty.txt'))) streamed += chunk
|
||||
expect(streamed).toBe('')
|
||||
})
|
||||
|
||||
it('cancels a remote stream when its consumer stops early', async () => {
|
||||
const remote = new FakeRemote()
|
||||
remote.file('/workspace/text.txt', 'ab')
|
||||
|
||||
Reference in New Issue
Block a user