Offload functions to worker threads with shared memory primitives for Node.js.
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(shared): serialize Tuple nested inside SharedStruct/Tuple

serializeStructField had a SharedStruct branch but no Tuple branch, so a
Tuple used as a SharedStruct field (or as a Tuple element) fell through
to the primitive-shape branch and lost its `elements`. Worker-side
deserialization then crashed on `data.elements.map(...)` with
`Cannot read properties of undefined (reading 'map')`. Mirror the
top-level serializeArg Tuple case in the recursive helper so nested
tuples carry their elements across.

Adds cross-worker regression tests for struct-with-tuple-of-primitives,
struct-with-tuple-of-structs, and tuple-of-tuples.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+57
+5
.changeset/tuple-in-struct-serialize.md
··· 1 + --- 2 + 'moroutine': patch 3 + --- 4 + 5 + Fix task-arg roundtrip of `SharedStruct` with a `Tuple`-typed field (and `Tuple` with a `Tuple` element). The recursive `serializeStructField` helper was missing a `Tuple` branch, so a tuple nested inside another shared container would serialize as `{ __shared__: 'Tuple' }` with no `elements`, and worker-side deserialization crashed with `Cannot read properties of undefined (reading 'map')`. `serializeStructField` now mirrors the top-level `serializeArg` for tuples, recursing into elements. Struct-in-struct, tuple-alone, and tuple-of-structs were already handled; struct-with-tuple-field, tuple-of-tuples, and deeper combinations now work too.
+4
src/shared/reconstruct.ts
··· 44 44 } 45 45 return { __shared__: 'SharedStruct', fields: serializedFields }; 46 46 } 47 + if (data.tag === 'Tuple') { 48 + const serializedElements = (data.elements as any[]).map((el: any) => serializeStructField(el)); 49 + return { __shared__: 'Tuple', elements: serializedElements }; 50 + } 47 51 return { 48 52 __shared__: data.tag, 49 53 buffer: data.buffer,
+48
test/shared/cross-worker.test.ts
··· 80 80 } 81 81 }); 82 82 83 + it('struct with tuple-of-primitives field works across workers', async () => { 84 + const s = shared({ items: [int32, int32] }); 85 + s.store({ items: [42, 99] }); 86 + 87 + const run = workers(1); 88 + try { 89 + const result = await run(readValue(s)); 90 + assert.deepEqual(result, { items: [42, 99] }); 91 + } finally { 92 + run[Symbol.dispose](); 93 + } 94 + }); 95 + 96 + it('struct with tuple-of-structs field works across workers', async () => { 97 + const s = shared({ rows: [{ x: int32 }, { x: int32 }] }); 98 + s.store({ rows: [{ x: 7 }, { x: 8 }] }); 99 + 100 + const run = workers(1); 101 + try { 102 + const result = await run(readValue(s)); 103 + assert.deepEqual(result, { rows: [{ x: 7 }, { x: 8 }] }); 104 + } finally { 105 + run[Symbol.dispose](); 106 + } 107 + }); 108 + 109 + it('tuple of tuples works across workers', async () => { 110 + const t = shared([ 111 + [int32, int32], 112 + [int32, int32], 113 + ]); 114 + t.store([ 115 + [1, 2], 116 + [3, 4], 117 + ]); 118 + 119 + const run = workers(1); 120 + try { 121 + const result = await run(readValue(t)); 122 + assert.deepEqual(result, [ 123 + [1, 2], 124 + [3, 4], 125 + ]); 126 + } finally { 127 + run[Symbol.dispose](); 128 + } 129 + }); 130 + 83 131 it('string via shared() works across workers', async () => { 84 132 const s = shared(string(32)); 85 133 s.store('hello');