diff --git a/.changeset/quiet-streams-order.md b/.changeset/quiet-streams-order.md new file mode 100644 index 0000000000..cadb40cbae --- /dev/null +++ b/.changeset/quiet-streams-order.md @@ -0,0 +1,5 @@ +--- +"@workflow/world-local": patch +--- + +Fix race condition in streamer when multiple writes share a promise runId. diff --git a/packages/world-local/src/streamer.test.ts b/packages/world-local/src/streamer.test.ts index 62f1a98c0c..6fb9fb1915 100644 --- a/packages/world-local/src/streamer.test.ts +++ b/packages/world-local/src/streamer.test.ts @@ -102,8 +102,10 @@ describe('streamer', () => { const chunk = deserializeChunk( await fs.readFile(`${testDir}/streams/chunks/${file}`) ); - const stream_id = String(file.split('-').at(-1)).split('.')[0]; - const time = decodeTime(stream_id); + // Extract ULID from filename: "streamName-chnk_ULID.json" + const chunkIdPart = String(file.split('-').at(-1)).split('.')[0]; // "chnk_ULID" + const ulid = chunkIdPart.replace('chnk_', ''); // Just the ULID + const time = decodeTime(ulid); const timeDiff = time - lastTime; lastTime = time; diff --git a/packages/world-local/src/streamer.ts b/packages/world-local/src/streamer.ts index b8380e4029..bf22112d68 100644 --- a/packages/world-local/src/streamer.ts +++ b/packages/world-local/src/streamer.ts @@ -93,14 +93,17 @@ export function createStreamer(basedir: string): Streamer { _runId: string | Promise, chunk: string | Uint8Array ) { + // Generate ULID synchronously BEFORE any await to preserve call order. + // This ensures that chunks written in sequence maintain their order even + // when runId is a promise that multiple writes are waiting on. + const chunkId = `chnk_${monotonicUlid()}`; + // Await runId if it's a promise to ensure proper flushing const runId = await _runId; // Register this stream for the run await registerStreamForRun(runId, name); - const chunkId = `chnk_${monotonicUlid()}`; - // Convert chunk to buffer for serialization let chunkBuffer: Buffer; if (typeof chunk === 'string') { @@ -136,13 +139,14 @@ export function createStreamer(basedir: string): Streamer { }, async closeStream(name: string, _runId: string | Promise) { + // Generate ULID synchronously BEFORE any await to preserve call order. + const chunkId = `chnk_${monotonicUlid()}`; + // Await runId if it's a promise to ensure proper flushing const runId = await _runId; // Register this stream for the run (in case writeToStream wasn't called) await registerStreamForRun(runId, name); - - const chunkId = `chnk_${monotonicUlid()}`; const chunkPath = path.join( basedir, 'streams',