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() value shorthand (number, bigint, boolean)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+153 -3
+86 -3
src/shared/shared.ts
··· 1 1 import type { Descriptor } from './descriptors.ts'; 2 + import { int32, int64, bool } from './descriptors.ts'; 3 + import { Int32 } from './int32.ts'; 4 + import { Int64 } from './int64.ts'; 5 + import { Bool } from './bool.ts'; 2 6 import { SharedStruct } from './shared-struct.ts'; 3 7 import { Tuple } from './tuple.ts'; 4 8 ··· 15 19 return remainder === 0 ? offset : offset + (alignment - remainder); 16 20 } 17 21 22 + function resolveValue(value: unknown): { descriptor: Descriptor<unknown>; initialValue: unknown } | null { 23 + if (typeof value === 'number') { 24 + if (!Number.isInteger(value) || value > 2_147_483_647 || value < -2_147_483_648) { 25 + throw new RangeError(`Value ${value} out of range for int32. Use int64 with bigint instead.`); 26 + } 27 + return { descriptor: int32 as unknown as Descriptor<unknown>, initialValue: value }; 28 + } 29 + if (typeof value === 'bigint') { 30 + return { descriptor: int64 as unknown as Descriptor<unknown>, initialValue: value }; 31 + } 32 + if (typeof value === 'boolean') { 33 + return { descriptor: bool as unknown as Descriptor<unknown>, initialValue: value }; 34 + } 35 + return null; 36 + } 37 + 18 38 interface LeafEntry { 19 39 path: string[]; 20 40 descriptor: Descriptor<unknown>; 21 41 offset: number; 42 + initialValue?: unknown; 22 43 } 23 44 24 45 interface ArrayLeafEntry { 25 46 type: 'leaf'; 26 47 descriptor: Descriptor<unknown>; 27 48 offset: number; 49 + initialValue?: unknown; 28 50 } 29 51 30 52 interface ArrayStructEntry { ··· 51 73 collectArrayLeaves(value, leaves, cursor); 52 74 } else if (isStructSchema(value)) { 53 75 collectLeaves(value as Record<string, unknown>, [...path, key], leaves, cursor); 76 + } else { 77 + const resolved = resolveValue(value); 78 + if (resolved !== null) { 79 + const { descriptor, initialValue } = resolved; 80 + cursor.offset = align(cursor.offset, descriptor.byteAlignment); 81 + leaves.push({ path: [...path, key], descriptor, offset: cursor.offset, initialValue }); 82 + cursor.offset += descriptor.byteSize; 83 + } 54 84 } 55 85 } 56 86 } ··· 65 95 collectArrayLeaves(element, leaves, cursor); 66 96 } else if (isStructSchema(element)) { 67 97 collectLeaves(element as Record<string, unknown>, [], leaves, cursor); 98 + } else { 99 + const resolved = resolveValue(element); 100 + if (resolved !== null) { 101 + const { descriptor, initialValue } = resolved; 102 + cursor.offset = align(cursor.offset, descriptor.byteAlignment); 103 + leaves.push({ path: [], descriptor, offset: cursor.offset, initialValue }); 104 + cursor.offset += descriptor.byteSize; 105 + } 68 106 } 69 107 } 70 108 } ··· 83 121 const leaves: LeafEntry[] = []; 84 122 collectLeaves(element as Record<string, unknown>, [], leaves, cursor); 85 123 entries.push({ type: 'struct', schema: element as Record<string, unknown>, leaves }); 124 + } else { 125 + const resolved = resolveValue(element); 126 + if (resolved !== null) { 127 + const { descriptor, initialValue } = resolved; 128 + cursor.offset = align(cursor.offset, descriptor.byteAlignment); 129 + entries.push({ type: 'leaf', descriptor, offset: cursor.offset, initialValue }); 130 + cursor.offset += descriptor.byteSize; 131 + } 86 132 } 87 133 } 88 134 return { entries }; ··· 91 137 function buildTupleFromEntries(entries: ArrayEntry[], buffer: SharedArrayBuffer): Tuple<any> { 92 138 const elements = entries.map((entry) => { 93 139 if (entry.type === 'leaf') { 94 - return new entry.descriptor._class(buffer, entry.offset); 140 + const instance = new entry.descriptor._class(buffer, entry.offset); 141 + if ('initialValue' in entry) { 142 + (instance as any).store(entry.initialValue); 143 + } 144 + return instance; 95 145 } 96 146 if (entry.type === 'struct') { 97 147 const leafIndex = { i: 0 }; ··· 116 166 fields[key] = buildTupleFromEntries(arrayEntries, buffer); 117 167 } else if (isStructSchema(value)) { 118 168 fields[key] = buildStructTree(value as Record<string, unknown>, leaves, buffer, leafIndex); 169 + } else if (resolveValue(value) !== null) { 170 + const leaf = leaves[leafIndex.i++]; 171 + const instance = new leaf.descriptor._class(buffer, leaf.offset) as any; 172 + if ('initialValue' in leaf) { 173 + instance.store(leaf.initialValue); 174 + } 175 + fields[key] = instance; 119 176 } 120 177 } 121 178 return new SharedStruct(fields); ··· 131 188 const nested = processArraySchemaFromLeaves(element, leaves, leafIndex); 132 189 entries.push({ type: 'tuple', entries: nested }); 133 190 } else if (isStructSchema(element)) { 134 - const structLeaves: LeafEntry[] = []; 135 191 const startIndex = leafIndex.i; 136 192 // Count leaves for this struct 137 193 countStructLeaves(element as Record<string, unknown>, leaves, leafIndex); 138 194 const endIndex = leafIndex.i; 139 195 const subLeaves = leaves.slice(startIndex, endIndex); 140 - const subLeafIndex = { i: 0 }; 141 196 entries.push({ 142 197 type: 'struct', 143 198 schema: element as Record<string, unknown>, 144 199 leaves: subLeaves, 145 200 }); 201 + } else if (resolveValue(element) !== null) { 202 + const leaf = leaves[leafIndex.i++]; 203 + entries.push({ type: 'leaf', descriptor: leaf.descriptor, offset: leaf.offset, initialValue: leaf.initialValue }); 146 204 } 147 205 } 148 206 return entries; ··· 157 215 countArrayLeaves(value, leaves, leafIndex); 158 216 } else if (isStructSchema(value)) { 159 217 countStructLeaves(value as Record<string, unknown>, leaves, leafIndex); 218 + } else if (resolveValue(value) !== null) { 219 + leafIndex.i++; 160 220 } 161 221 } 162 222 } ··· 169 229 countArrayLeaves(element, leaves, leafIndex); 170 230 } else if (isStructSchema(element)) { 171 231 countStructLeaves(element as Record<string, unknown>, leaves, leafIndex); 232 + } else if (resolveValue(element) !== null) { 233 + leafIndex.i++; 172 234 } 173 235 } 174 236 } 175 237 176 238 export function shared(schema: unknown): any { 239 + if (typeof schema === 'number') { 240 + if (!Number.isInteger(schema) || schema > 2_147_483_647 || schema < -2_147_483_648) { 241 + throw new RangeError(`Value ${schema} out of range for int32. Use int64 with bigint instead.`); 242 + } 243 + const instance = new Int32(new SharedArrayBuffer(Int32.byteSize), 0); 244 + instance.store(schema); 245 + return instance; 246 + } 247 + 248 + if (typeof schema === 'bigint') { 249 + const instance = new Int64(new SharedArrayBuffer(Int64.byteSize), 0); 250 + instance.store(schema); 251 + return instance; 252 + } 253 + 254 + if (typeof schema === 'boolean') { 255 + const instance = new Bool(new SharedArrayBuffer(Bool.byteSize), 0); 256 + instance.store(schema); 257 + return instance; 258 + } 259 + 177 260 if (isDescriptor(schema)) { 178 261 return schema(); 179 262 }
+67
test/shared/value-shorthand.test.ts
··· 1 + import { describe, it } from 'node:test'; 2 + import assert from 'node:assert/strict'; 3 + import { shared } from 'moroutine'; 4 + 5 + describe('shared() value shorthand', () => { 6 + it('shared(0) creates int32 initialized to 0', () => { 7 + const x = shared(0); 8 + assert.equal(x.load(), 0); 9 + }); 10 + 11 + it('shared(42) creates int32 initialized to 42', () => { 12 + const x = shared(42); 13 + assert.equal(x.load(), 42); 14 + }); 15 + 16 + it('shared(-1) creates int32 initialized to -1', () => { 17 + const x = shared(-1); 18 + assert.equal(x.load(), -1); 19 + }); 20 + 21 + it('shared(true) creates bool initialized to true', () => { 22 + const x = shared(true); 23 + assert.equal(x.load(), true); 24 + }); 25 + 26 + it('shared(false) creates bool initialized to false', () => { 27 + const x = shared(false); 28 + assert.equal(x.load(), false); 29 + }); 30 + 31 + it('shared(0n) creates int64 initialized to 0n', () => { 32 + const x = shared(0n); 33 + assert.equal(x.load(), 0n); 34 + }); 35 + 36 + it('shared(99n) creates int64 initialized to 99n', () => { 37 + const x = shared(99n); 38 + assert.equal(x.load(), 99n); 39 + }); 40 + 41 + it('shared(value) throws for out-of-range int32', () => { 42 + assert.throws(() => shared(Number.MAX_SAFE_INTEGER), /out of range|exceeds/i); 43 + assert.throws(() => shared(2_147_483_648), /out of range|exceeds/i); 44 + assert.throws(() => shared(-2_147_483_649), /out of range|exceeds/i); 45 + }); 46 + 47 + it('shared(value) throws for non-integer number', () => { 48 + assert.throws(() => shared(1.5), /out of range|integer/i); 49 + }); 50 + 51 + it('value shorthand in struct schema', () => { 52 + const point = shared({ x: 10, y: 20 }); 53 + assert.deepEqual(point.load(), { x: 10, y: 20 }); 54 + }); 55 + 56 + it('value shorthand in tuple schema', () => { 57 + const t = shared([1, 2n, true]); 58 + assert.deepEqual(t.load(), [1, 2n, true]); 59 + }); 60 + 61 + it('mixed descriptors and values in struct', () => { 62 + const s = shared({ x: 0, y: 0, alive: true }); 63 + assert.deepEqual(s.load(), { x: 0, y: 0, alive: true }); 64 + s.store({ x: 10, y: 20, alive: false }); 65 + assert.deepEqual(s.load(), { x: 10, y: 20, alive: false }); 66 + }); 67 + });