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.

feat: shared() tuple schema support

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

+231
+1
src/shared/index.ts
··· 21 21 export { RwLock, ReadGuard, WriteGuard } from './rwlock.ts'; 22 22 export { slab } from './slab.ts'; 23 23 export { SharedStruct } from './shared-struct.ts'; 24 + export { Tuple } from './tuple.ts'; 24 25 export { shared } from './shared.ts'; 25 26 export { 26 27 int8, uint8, int16, uint16, int32, uint32, int64, uint64, bool,
+133
src/shared/shared.ts
··· 1 1 import type { Descriptor } from './descriptors.ts'; 2 2 import { SharedStruct } from './shared-struct.ts'; 3 + import { Tuple } from './tuple.ts'; 3 4 4 5 function isDescriptor(value: unknown): value is Descriptor<unknown> { 5 6 return typeof value === 'function' && 'byteSize' in value && '_class' in value; ··· 20 21 offset: number; 21 22 } 22 23 24 + interface ArrayLeafEntry { 25 + type: 'leaf'; 26 + descriptor: Descriptor<unknown>; 27 + offset: number; 28 + } 29 + 30 + interface ArrayStructEntry { 31 + type: 'struct'; 32 + schema: Record<string, unknown>; 33 + leaves: LeafEntry[]; 34 + } 35 + 36 + interface ArrayTupleEntry { 37 + type: 'tuple'; 38 + entries: ArrayEntry[]; 39 + } 40 + 41 + type ArrayEntry = ArrayLeafEntry | ArrayStructEntry | ArrayTupleEntry; 42 + 23 43 function collectLeaves(schema: Record<string, unknown>, path: string[], leaves: LeafEntry[], cursor: { offset: number }): void { 24 44 for (const key in schema) { 25 45 const value = schema[key]; ··· 27 47 cursor.offset = align(cursor.offset, value.byteAlignment); 28 48 leaves.push({ path: [...path, key], descriptor: value, offset: cursor.offset }); 29 49 cursor.offset += value.byteSize; 50 + } else if (Array.isArray(value)) { 51 + collectArrayLeaves(value, leaves, cursor); 30 52 } else if (isStructSchema(value)) { 31 53 collectLeaves(value as Record<string, unknown>, [...path, key], leaves, cursor); 32 54 } 33 55 } 34 56 } 35 57 58 + function collectArrayLeaves(schema: unknown[], leaves: LeafEntry[], cursor: { offset: number }): void { 59 + for (const element of schema) { 60 + if (isDescriptor(element)) { 61 + cursor.offset = align(cursor.offset, element.byteAlignment); 62 + leaves.push({ path: [], descriptor: element, offset: cursor.offset }); 63 + cursor.offset += element.byteSize; 64 + } else if (Array.isArray(element)) { 65 + collectArrayLeaves(element, leaves, cursor); 66 + } else if (isStructSchema(element)) { 67 + collectLeaves(element as Record<string, unknown>, [], leaves, cursor); 68 + } 69 + } 70 + } 71 + 72 + function processArraySchema(schema: unknown[], cursor: { offset: number }): { entries: ArrayEntry[] } { 73 + const entries: ArrayEntry[] = []; 74 + for (const element of schema) { 75 + if (isDescriptor(element)) { 76 + cursor.offset = align(cursor.offset, element.byteAlignment); 77 + entries.push({ type: 'leaf', descriptor: element, offset: cursor.offset }); 78 + cursor.offset += element.byteSize; 79 + } else if (Array.isArray(element)) { 80 + const nested = processArraySchema(element, cursor); 81 + entries.push({ type: 'tuple', entries: nested.entries }); 82 + } else if (isStructSchema(element)) { 83 + const leaves: LeafEntry[] = []; 84 + collectLeaves(element as Record<string, unknown>, [], leaves, cursor); 85 + entries.push({ type: 'struct', schema: element as Record<string, unknown>, leaves }); 86 + } 87 + } 88 + return { entries }; 89 + } 90 + 91 + function buildTupleFromEntries(entries: ArrayEntry[], buffer: SharedArrayBuffer): Tuple<any> { 92 + const elements = entries.map((entry) => { 93 + if (entry.type === 'leaf') { 94 + return new entry.descriptor._class(buffer, entry.offset); 95 + } 96 + if (entry.type === 'struct') { 97 + const leafIndex = { i: 0 }; 98 + return buildStructTree(entry.schema, entry.leaves, buffer, leafIndex); 99 + } 100 + if (entry.type === 'tuple') { 101 + return buildTupleFromEntries(entry.entries, buffer); 102 + } 103 + }); 104 + return new Tuple(elements as any); 105 + } 106 + 36 107 function buildStructTree(schema: Record<string, unknown>, leaves: LeafEntry[], buffer: SharedArrayBuffer, leafIndex: { i: number }): SharedStruct<any> { 37 108 const fields: Record<string, any> = {}; 38 109 for (const key in schema) { ··· 40 111 if (isDescriptor(value)) { 41 112 const leaf = leaves[leafIndex.i++]; 42 113 fields[key] = new leaf.descriptor._class(buffer, leaf.offset); 114 + } else if (Array.isArray(value)) { 115 + const arrayEntries = processArraySchemaFromLeaves(value, leaves, leafIndex); 116 + fields[key] = buildTupleFromEntries(arrayEntries, buffer); 43 117 } else if (isStructSchema(value)) { 44 118 fields[key] = buildStructTree(value as Record<string, unknown>, leaves, buffer, leafIndex); 45 119 } ··· 47 121 return new SharedStruct(fields); 48 122 } 49 123 124 + function processArraySchemaFromLeaves(schema: unknown[], leaves: LeafEntry[], leafIndex: { i: number }): ArrayEntry[] { 125 + const entries: ArrayEntry[] = []; 126 + for (const element of schema) { 127 + if (isDescriptor(element)) { 128 + const leaf = leaves[leafIndex.i++]; 129 + entries.push({ type: 'leaf', descriptor: leaf.descriptor, offset: leaf.offset }); 130 + } else if (Array.isArray(element)) { 131 + const nested = processArraySchemaFromLeaves(element, leaves, leafIndex); 132 + entries.push({ type: 'tuple', entries: nested }); 133 + } else if (isStructSchema(element)) { 134 + const structLeaves: LeafEntry[] = []; 135 + const startIndex = leafIndex.i; 136 + // Count leaves for this struct 137 + countStructLeaves(element as Record<string, unknown>, leaves, leafIndex); 138 + const endIndex = leafIndex.i; 139 + const subLeaves = leaves.slice(startIndex, endIndex); 140 + const subLeafIndex = { i: 0 }; 141 + entries.push({ 142 + type: 'struct', 143 + schema: element as Record<string, unknown>, 144 + leaves: subLeaves, 145 + }); 146 + } 147 + } 148 + return entries; 149 + } 150 + 151 + function countStructLeaves(schema: Record<string, unknown>, leaves: LeafEntry[], leafIndex: { i: number }): void { 152 + for (const key in schema) { 153 + const value = schema[key]; 154 + if (isDescriptor(value)) { 155 + leafIndex.i++; 156 + } else if (Array.isArray(value)) { 157 + countArrayLeaves(value, leaves, leafIndex); 158 + } else if (isStructSchema(value)) { 159 + countStructLeaves(value as Record<string, unknown>, leaves, leafIndex); 160 + } 161 + } 162 + } 163 + 164 + function countArrayLeaves(schema: unknown[], leaves: LeafEntry[], leafIndex: { i: number }): void { 165 + for (const element of schema) { 166 + if (isDescriptor(element)) { 167 + leafIndex.i++; 168 + } else if (Array.isArray(element)) { 169 + countArrayLeaves(element, leaves, leafIndex); 170 + } else if (isStructSchema(element)) { 171 + countStructLeaves(element as Record<string, unknown>, leaves, leafIndex); 172 + } 173 + } 174 + } 175 + 50 176 export function shared(schema: unknown): any { 51 177 if (isDescriptor(schema)) { 52 178 return schema(); 179 + } 180 + 181 + if (Array.isArray(schema)) { 182 + const cursor = { offset: 0 }; 183 + const { entries } = processArraySchema(schema, cursor); 184 + const buffer = new SharedArrayBuffer(cursor.offset); 185 + return buildTupleFromEntries(entries, buffer); 53 186 } 54 187 55 188 if (isStructSchema(schema)) {
+40
src/shared/tuple.ts
··· 1 + import type { Loadable } from './loadable.ts'; 2 + 3 + const SHARED = Symbol.for('moroutine.shared'); 4 + 5 + export class Tuple<T extends Loadable<any>[]> implements Loadable<{ [K in keyof T]: T[K] extends Loadable<infer V> ? V : never }> { 6 + private readonly elements: T; 7 + readonly length: number; 8 + 9 + constructor(elements: T) { 10 + this.elements = elements; 11 + this.length = elements.length; 12 + } 13 + 14 + get(index: number): T[number] { 15 + if (index < 0 || index >= this.length) { 16 + throw new RangeError(`Index ${index} out of bounds for tuple of length ${this.length}`); 17 + } 18 + return this.elements[index]; 19 + } 20 + 21 + load(): { [K in keyof T]: T[K] extends Loadable<infer V> ? V : never } { 22 + return this.elements.map((el) => el.load()) as any; 23 + } 24 + 25 + store(values: { [K in keyof T]: T[K] extends Loadable<infer V> ? V : never }): void { 26 + for (let i = 0; i < this.elements.length; i++) { 27 + this.elements[i].store((values as any)[i]); 28 + } 29 + } 30 + 31 + [SHARED](): { tag: string; elements: unknown[] } { 32 + const serializedElements: unknown[] = []; 33 + for (const el of this.elements) { 34 + if (typeof el === 'object' && el !== null && SHARED in el) { 35 + serializedElements.push((el as any)[SHARED]()); 36 + } 37 + } 38 + return { tag: 'Tuple', elements: serializedElements }; 39 + } 40 + }
+57
test/shared/tuple.test.ts
··· 1 + import { describe, it } from 'node:test'; 2 + import assert from 'node:assert/strict'; 3 + import { shared, int32, int64, bool } from 'moroutine'; 4 + 5 + describe('shared() tuple', () => { 6 + it('creates a tuple from array schema', () => { 7 + const t = shared([int32, int64]); 8 + assert.deepEqual(t.load(), [0, 0n]); 9 + }); 10 + 11 + it('store writes all elements', () => { 12 + const t = shared([int32, int64, bool]); 13 + t.store([42, 99n, true]); 14 + assert.deepEqual(t.load(), [42, 99n, true]); 15 + }); 16 + 17 + it('get() returns element Loadable', () => { 18 + const t = shared([int32, int32]); 19 + t.get(0).store(10); 20 + t.get(1).store(20); 21 + assert.deepEqual(t.load(), [10, 20]); 22 + }); 23 + 24 + it('length returns fixed element count', () => { 25 + const t = shared([int32, int32, bool]); 26 + assert.equal(t.length, 3); 27 + }); 28 + 29 + it('get() out of bounds throws', () => { 30 + const t = shared([int32]); 31 + assert.throws(() => t.get(1), /out of bounds/i); 32 + assert.throws(() => t.get(-1), /out of bounds/i); 33 + }); 34 + 35 + it('tuple elements share one buffer', () => { 36 + const t = shared([int32, int32]); 37 + const SHARED = Symbol.for('moroutine.shared'); 38 + const s0 = (t.get(0) as any)[SHARED](); 39 + const s1 = (t.get(1) as any)[SHARED](); 40 + assert.equal(s0.buffer, s1.buffer); 41 + }); 42 + 43 + it('tuple with struct elements', () => { 44 + const t = shared([{ x: int32, y: int32 }, { x: int32, y: int32 }]); 45 + t.store([{ x: 1, y: 2 }, { x: 3, y: 4 }]); 46 + assert.deepEqual(t.load(), [{ x: 1, y: 2 }, { x: 3, y: 4 }]); 47 + }); 48 + 49 + it('struct containing tuple field', () => { 50 + const s = shared({ 51 + points: [int32, int32, int32], 52 + count: int32, 53 + }); 54 + s.store({ points: [1, 2, 3], count: 3 }); 55 + assert.deepEqual(s.load(), { points: [1, 2, 3], count: 3 }); 56 + }); 57 + });