/** * `buildDispatchMessage` — zero-copy transferList contract. * * `worker-pool.ts`'s `buildDispatchMessage` is the boundary between the * pool's generic `dispatch(items)` and the parse-worker-specific * postMessage payload shape. For items shaped as `{path, content: string}[]` * (the parse-worker contract), each file's content is encoded to a * `Uint8Array` via `TextEncoder` so its underlying `ArrayBuffer` can be * transferred zero-copy via `transferList`. For any other shape, the * items array is passed through as the `files` field with no transfer. * * Tests pin: * - parse-worker shape produces `{type:'sub-batch', files:[{path, content: Uint8Array}]}` * + transferList of every content.buffer in input order * - non-parse shape stays as `{type:'sub-batch', files: items}` (no transfer) * - content bytes round-trip byte-for-byte through `TextDecoder` * - each content buffer owns a dedicated `ArrayBuffer` (no shared * `Buffer.poolSize` slab) so transferring one cannot detach another * - empty / mixed-shape inputs fall back to the no-transfer path */ import { describe, it, expect } from 'vitest'; import { buildDispatchMessage } from '../../src/core/ingestion/workers/worker-pool.js'; describe('worker pool — buildDispatchMessage', () => { it('parse-worker shape produces a POJO sub-batch + transferList of one buffer per file', () => { const items = [ { path: 'a.ts', content: 'export const A = 1;' }, { path: 'b.ts', content: 'export const B = 2;' }, ]; const { message, transferList } = buildDispatchMessage(items); const msg = message as { type: 'sub-batch'; files: Array<{ path: string; content: Uint8Array }>; }; expect(msg.type).toBe('sub-batch'); expect(msg.files).toHaveLength(2); expect(msg.files[0].path).toBe('a.ts'); expect(msg.files[0].content).toBeInstanceOf(Uint8Array); expect(msg.files[1].path).toBe('b.ts'); expect(msg.files[1].content).toBeInstanceOf(Uint8Array); // transferList carries one ArrayBuffer per file, in input order. // Identity check is the strict contract — transferring a different // ArrayBuffer reference would no-op the ownership swap. expect(transferList).toHaveLength(2); expect(transferList?.[0]).toBe(msg.files[0].content.buffer); expect(transferList?.[1]).toBe(msg.files[1].content.buffer); }); it('content bytes round-trip byte-for-byte through TextDecoder', () => { // Mix ASCII, multi-byte UTF-8 (café = c-a-f-é where é is 2 bytes), // and an emoji (4 UTF-8 bytes) to cover the encoder boundaries. const items = [ { path: 'a.ts', content: 'plain ASCII' }, { path: 'b.ts', content: 'café au lait' }, { path: 'c.ts', content: 'rocket: 🚀 emoji' }, ]; const { message } = buildDispatchMessage(items); const files = (message as { files: Array<{ content: Uint8Array }> }).files; const decoder = new TextDecoder('utf-8'); expect(decoder.decode(files[0].content)).toBe('plain ASCII'); expect(decoder.decode(files[1].content)).toBe('café au lait'); expect(decoder.decode(files[2].content)).toBe('rocket: 🚀 emoji'); }); it('each content buffer owns a dedicated ArrayBuffer (no shared Buffer pool slab)', () => { // Pin the transfer-safety contract: TextEncoder allocates each // Uint8Array on its own ArrayBuffer, so transferring one cannot // detach the backing of another. If a future refactor swaps to // `Buffer.from(str, 'utf8')` (which carves from `Buffer.poolSize` // slabs for small strings), small files would share an // ArrayBuffer and transferList would detach unrelated content. const items = Array.from({ length: 8 }, (_, i) => ({ path: `f${i}.ts`, content: `tiny ${i}`, })); const { message } = buildDispatchMessage(items); const files = (message as { files: Array<{ content: Uint8Array }> }).files; const buffers = new Set(files.map((f) => f.content.buffer)); expect(buffers.size).toBe(8); // Each content's view covers the entire ArrayBuffer (no offset). for (const f of files) { expect(f.content.byteOffset).toBe(0); expect(f.content.byteLength).toBe(f.content.buffer.byteLength); } }); it('non-parse-worker shape falls back to a no-transfer pass-through', () => { // Items lacking a string `content` field don't match the // parse-worker shape detector and ride through as the `files` // array of the sub-batch POJO without any encoding or // transferList. const items = [{ id: 1, payload: 'arbitrary' }]; const { message, transferList } = buildDispatchMessage(items); expect(message).toEqual({ type: 'sub-batch', files: items }); expect(transferList).toBeUndefined(); }); it('empty items array falls back to the no-transfer path', () => { const { message, transferList } = buildDispatchMessage([]); expect(message).toEqual({ type: 'sub-batch', files: [] }); expect(transferList).toBeUndefined(); }); it('mixed-shape items (some missing content) fall back to the no-transfer path', () => { // Strict shape detection: every element must have a string content. // A single non-conforming element disqualifies the transfer path — // safer than partially transferring some and embedding others. const items = [{ path: 'a.ts', content: 'ok' }, { path: 'b.ts' /* no content */ }]; const { message, transferList } = buildDispatchMessage(items); expect(message).toEqual({ type: 'sub-batch', files: items }); expect(transferList).toBeUndefined(); }); });