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: bytes(n) fixed-size byte buffer

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

+161 -11
+2
src/index.ts
··· 4 4 export { transfer } from './transfer.ts'; 5 5 export type { Runner } from './runner.ts'; 6 6 export type { Loadable } from './shared/index.ts'; 7 + export type { Bytes } from './shared/index.ts'; 7 8 export { 8 9 Int8, 9 10 Uint8, ··· 36 37 int8, uint8, int16, uint16, int32, uint32, int64, uint64, bool, 37 38 int8atomic, uint8atomic, int16atomic, uint16atomic, int32atomic, uint32atomic, int64atomic, uint64atomic, boolatomic, 38 39 mutex, rwlock, 40 + bytes, 39 41 } from './shared/index.ts';
+32
src/shared/bytes.ts
··· 1 + import type { Loadable } from './loadable.ts'; 2 + 3 + const SHARED = Symbol.for('moroutine.shared'); 4 + 5 + export class Bytes implements Loadable<Readonly<Uint8Array>> { 6 + readonly size: number; 7 + readonly view: Uint8Array; 8 + 9 + static byteAlignment = 1; 10 + 11 + constructor(size: number, buffer?: SharedArrayBuffer, byteOffset?: number) { 12 + this.size = size; 13 + const buf = buffer ?? new SharedArrayBuffer(size); 14 + const offset = byteOffset ?? 0; 15 + this.view = new Uint8Array(buf, offset, size); 16 + } 17 + 18 + load(): Readonly<Uint8Array> { 19 + return this.view as Readonly<Uint8Array>; 20 + } 21 + 22 + store(value: Uint8Array): void { 23 + if (value.length !== this.size) { 24 + throw new RangeError(`Expected Uint8Array of length ${this.size}, got ${value.length}`); 25 + } 26 + this.view.set(value); 27 + } 28 + 29 + [SHARED](): { tag: string; buffer: SharedArrayBuffer; byteOffset: number; size: number } { 30 + return { tag: 'Bytes', buffer: this.view.buffer as SharedArrayBuffer, byteOffset: this.view.byteOffset, size: this.size }; 31 + } 32 + }
+18
src/shared/descriptors.ts
··· 1 + import { Bytes } from './bytes.ts'; 1 2 import { Int8 } from './int8.ts'; 2 3 import { Uint8 } from './uint8.ts'; 3 4 import { Int16 } from './int16.ts'; ··· 57 58 58 59 export const mutex: Descriptor<Mutex> = makeDescriptor(() => new Mutex(), Mutex.byteSize, Mutex.byteAlignment, Mutex); 59 60 export const rwlock: Descriptor<RwLock> = makeDescriptor(() => new RwLock(), RwLock.byteSize, RwLock.byteAlignment, RwLock); 61 + 62 + export interface BytesDescriptor extends Bytes { 63 + byteSize: number; 64 + byteAlignment: number; 65 + _class: typeof Bytes; 66 + _size: number; 67 + } 68 + 69 + export function bytes(size: number): BytesDescriptor { 70 + const instance = new Bytes(size); 71 + return Object.assign(instance, { 72 + byteSize: size, 73 + byteAlignment: Bytes.byteAlignment, 74 + _class: Bytes, 75 + _size: size, 76 + }) as BytesDescriptor; 77 + }
+3 -1
src/shared/index.ts
··· 1 1 export type { Loadable } from './loadable.ts'; 2 + export type { Bytes } from './bytes.ts'; 2 3 export { Int8 } from './int8.ts'; 3 4 export { Uint8 } from './uint8.ts'; 4 5 export { Int16 } from './int16.ts'; ··· 27 28 int8, uint8, int16, uint16, int32, uint32, int64, uint64, bool, 28 29 int8atomic, uint8atomic, int16atomic, uint16atomic, int32atomic, uint32atomic, int64atomic, uint64atomic, boolatomic, 29 30 mutex, rwlock, 31 + bytes, 30 32 } from './descriptors.ts'; 31 - export type { Descriptor } from './descriptors.ts'; 33 + export type { Descriptor, BytesDescriptor } from './descriptors.ts';
+7 -2
src/shared/reconstruct.ts
··· 1 1 import { SharedStruct } from './shared-struct.ts'; 2 + import { Bytes } from './bytes.ts'; 2 3 3 4 const SHARED = Symbol.for('moroutine.shared'); 4 5 ··· 18 19 } 19 20 return { __shared__: 'SharedStruct', fields: serializedFields }; 20 21 } 21 - return { __shared__: data.tag, buffer: data.buffer, byteOffset: data.byteOffset }; 22 + return { __shared__: data.tag, buffer: data.buffer, byteOffset: data.byteOffset, ...(data.size !== undefined && { size: data.size }) }; 22 23 } 23 24 return arg; 24 25 } ··· 31 32 } 32 33 return { __shared__: 'SharedStruct', fields: serializedFields }; 33 34 } 34 - return { __shared__: data.tag, buffer: data.buffer, byteOffset: data.byteOffset }; 35 + return { __shared__: data.tag, buffer: data.buffer, byteOffset: data.byteOffset, ...(data.size !== undefined && { size: data.size }) }; 35 36 } 36 37 37 38 export function deserializeArg(arg: unknown): unknown { 38 39 if (typeof arg === 'object' && arg !== null && '__shared__' in arg) { 39 40 const data = arg as { __shared__: string; [key: string]: unknown }; 41 + if (data.__shared__ === 'Bytes') { 42 + const typedData = data as { __shared__: string; buffer: SharedArrayBuffer; byteOffset: number; size: number }; 43 + return new Bytes(typedData.size, typedData.buffer, typedData.byteOffset); 44 + } 40 45 if (data.__shared__ === 'SharedStruct') { 41 46 const fields = data.fields as Record<string, unknown>; 42 47 const reconstructed: Record<string, unknown> = {};
+40 -8
src/shared/shared.ts
··· 1 - import type { Descriptor } from './descriptors.ts'; 1 + import type { Descriptor, BytesDescriptor } from './descriptors.ts'; 2 2 import { int32, int64, bool } from './descriptors.ts'; 3 3 import { Int32 } from './int32.ts'; 4 4 import { Int64 } from './int64.ts'; 5 5 import { Bool } from './bool.ts'; 6 + import { Bytes } from './bytes.ts'; 6 7 import { SharedStruct } from './shared-struct.ts'; 7 8 import { Tuple } from './tuple.ts'; 8 9 ··· 10 11 return typeof value === 'function' && 'byteSize' in value && '_class' in value; 11 12 } 12 13 14 + function isBytesDescriptor(value: unknown): value is BytesDescriptor { 15 + return typeof value === 'object' && value !== null && '_size' in value && '_class' in value; 16 + } 17 + 13 18 function isStructSchema(value: unknown): value is Record<string, unknown> { 14 19 return typeof value === 'object' && value !== null && !Array.isArray(value); 15 20 } ··· 40 45 descriptor: Descriptor<unknown>; 41 46 offset: number; 42 47 initialValue?: unknown; 48 + bytesSize?: number; 43 49 } 44 50 45 51 interface ArrayLeafEntry { ··· 47 53 descriptor: Descriptor<unknown>; 48 54 offset: number; 49 55 initialValue?: unknown; 56 + bytesSize?: number; 50 57 } 51 58 52 59 interface ArrayStructEntry { ··· 65 72 function collectLeaves(schema: Record<string, unknown>, path: string[], leaves: LeafEntry[], cursor: { offset: number }): void { 66 73 for (const key in schema) { 67 74 const value = schema[key]; 68 - if (isDescriptor(value)) { 75 + if (isBytesDescriptor(value)) { 76 + cursor.offset = align(cursor.offset, value.byteAlignment); 77 + leaves.push({ path: [...path, key], descriptor: value as unknown as Descriptor<unknown>, offset: cursor.offset, bytesSize: value._size }); 78 + cursor.offset += value.byteSize; 79 + } else if (isDescriptor(value)) { 69 80 cursor.offset = align(cursor.offset, value.byteAlignment); 70 81 leaves.push({ path: [...path, key], descriptor: value, offset: cursor.offset }); 71 82 cursor.offset += value.byteSize; ··· 87 98 88 99 function collectArrayLeaves(schema: unknown[], leaves: LeafEntry[], cursor: { offset: number }): void { 89 100 for (const element of schema) { 90 - if (isDescriptor(element)) { 101 + if (isBytesDescriptor(element)) { 102 + cursor.offset = align(cursor.offset, element.byteAlignment); 103 + leaves.push({ path: [], descriptor: element as unknown as Descriptor<unknown>, offset: cursor.offset, bytesSize: element._size }); 104 + cursor.offset += element.byteSize; 105 + } else if (isDescriptor(element)) { 91 106 cursor.offset = align(cursor.offset, element.byteAlignment); 92 107 leaves.push({ path: [], descriptor: element, offset: cursor.offset }); 93 108 cursor.offset += element.byteSize; ··· 110 125 function processArraySchema(schema: unknown[], cursor: { offset: number }): { entries: ArrayEntry[] } { 111 126 const entries: ArrayEntry[] = []; 112 127 for (const element of schema) { 113 - if (isDescriptor(element)) { 128 + if (isBytesDescriptor(element)) { 129 + cursor.offset = align(cursor.offset, element.byteAlignment); 130 + entries.push({ type: 'leaf', descriptor: element as unknown as Descriptor<unknown>, offset: cursor.offset, bytesSize: element._size }); 131 + cursor.offset += element.byteSize; 132 + } else if (isDescriptor(element)) { 114 133 cursor.offset = align(cursor.offset, element.byteAlignment); 115 134 entries.push({ type: 'leaf', descriptor: element, offset: cursor.offset }); 116 135 cursor.offset += element.byteSize; ··· 137 156 function buildTupleFromEntries(entries: ArrayEntry[], buffer: SharedArrayBuffer): Tuple<any> { 138 157 const elements = entries.map((entry) => { 139 158 if (entry.type === 'leaf') { 159 + if (entry.bytesSize !== undefined) { 160 + return new Bytes(entry.bytesSize, buffer, entry.offset); 161 + } 140 162 const instance = new entry.descriptor._class(buffer, entry.offset); 141 163 if ('initialValue' in entry) { 142 164 (instance as any).store(entry.initialValue); ··· 158 180 const fields: Record<string, any> = {}; 159 181 for (const key in schema) { 160 182 const value = schema[key]; 161 - if (isDescriptor(value)) { 183 + if (isBytesDescriptor(value)) { 184 + const leaf = leaves[leafIndex.i++]; 185 + fields[key] = new Bytes(leaf.bytesSize!, buffer, leaf.offset); 186 + } else if (isDescriptor(value)) { 162 187 const leaf = leaves[leafIndex.i++]; 163 188 fields[key] = new leaf.descriptor._class(buffer, leaf.offset); 164 189 } else if (Array.isArray(value)) { ··· 181 206 function processArraySchemaFromLeaves(schema: unknown[], leaves: LeafEntry[], leafIndex: { i: number }): ArrayEntry[] { 182 207 const entries: ArrayEntry[] = []; 183 208 for (const element of schema) { 184 - if (isDescriptor(element)) { 209 + if (isBytesDescriptor(element)) { 210 + const leaf = leaves[leafIndex.i++]; 211 + entries.push({ type: 'leaf', descriptor: leaf.descriptor, offset: leaf.offset, bytesSize: leaf.bytesSize }); 212 + } else if (isDescriptor(element)) { 185 213 const leaf = leaves[leafIndex.i++]; 186 214 entries.push({ type: 'leaf', descriptor: leaf.descriptor, offset: leaf.offset }); 187 215 } else if (Array.isArray(element)) { ··· 209 237 function countStructLeaves(schema: Record<string, unknown>, leaves: LeafEntry[], leafIndex: { i: number }): void { 210 238 for (const key in schema) { 211 239 const value = schema[key]; 212 - if (isDescriptor(value)) { 240 + if (isBytesDescriptor(value) || isDescriptor(value)) { 213 241 leafIndex.i++; 214 242 } else if (Array.isArray(value)) { 215 243 countArrayLeaves(value, leaves, leafIndex); ··· 223 251 224 252 function countArrayLeaves(schema: unknown[], leaves: LeafEntry[], leafIndex: { i: number }): void { 225 253 for (const element of schema) { 226 - if (isDescriptor(element)) { 254 + if (isBytesDescriptor(element) || isDescriptor(element)) { 227 255 leafIndex.i++; 228 256 } else if (Array.isArray(element)) { 229 257 countArrayLeaves(element, leaves, leafIndex); ··· 255 283 const instance = new Bool(new SharedArrayBuffer(Bool.byteSize), 0); 256 284 instance.store(schema); 257 285 return instance; 286 + } 287 + 288 + if (isBytesDescriptor(schema)) { 289 + return schema; // already a standalone instance 258 290 } 259 291 260 292 if (isDescriptor(schema)) {
+59
test/shared/bytes.test.ts
··· 1 + import { describe, it } from 'node:test'; 2 + import assert from 'node:assert/strict'; 3 + import { shared, bytes, bool } from 'moroutine'; 4 + 5 + describe('bytes', () => { 6 + it('bytes(n) creates standalone byte buffer', () => { 7 + const buf = bytes(16); 8 + const data = buf.load(); 9 + assert.equal(data.length, 16); 10 + assert.equal(data[0], 0); 11 + }); 12 + 13 + it('shared(bytes(n)) creates byte buffer', () => { 14 + const buf = shared(bytes(16)); 15 + assert.equal(buf.load().length, 16); 16 + }); 17 + 18 + it('load returns view (not copy)', () => { 19 + const buf = bytes(4); 20 + buf.view[0] = 42; 21 + assert.equal(buf.load()[0], 42); 22 + }); 23 + 24 + it('store writes exact length data', () => { 25 + const buf = bytes(4); 26 + buf.store(new Uint8Array([1, 2, 3, 4])); 27 + assert.deepEqual([...buf.load()], [1, 2, 3, 4]); 28 + }); 29 + 30 + it('store throws if length does not match', () => { 31 + const buf = bytes(4); 32 + assert.throws(() => buf.store(new Uint8Array([1, 2, 3])), /length/i); 33 + assert.throws(() => buf.store(new Uint8Array([1, 2, 3, 4, 5])), /length/i); 34 + }); 35 + 36 + it('view provides mutable direct access', () => { 37 + const buf = bytes(4); 38 + buf.view[0] = 0xff; 39 + buf.view[3] = 0xaa; 40 + assert.equal(buf.load()[0], 0xff); 41 + assert.equal(buf.load()[3], 0xaa); 42 + }); 43 + 44 + it('bytes in a struct schema', () => { 45 + const s = shared({ data: bytes(8), flag: bool }); 46 + s.fields.data.store(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])); 47 + s.fields.flag.store(true); 48 + assert.deepEqual([...s.fields.data.load()], [1, 2, 3, 4, 5, 6, 7, 8]); 49 + assert.equal(s.fields.flag.load(), true); 50 + }); 51 + 52 + it('bytes in struct shares one buffer', () => { 53 + const s = shared({ data: bytes(8), flag: bool }); 54 + const SHARED = Symbol.for('moroutine.shared'); 55 + const dataSer = (s.fields.data as any)[SHARED](); 56 + const flagSer = (s.fields.flag as any)[SHARED](); 57 + assert.equal(dataSer.buffer, flagSer.buffer); 58 + }); 59 + });