Shared memory
defineSharedMemory declares a fixed byte layout once; both threads get identical connectors over the same SharedArrayBuffer.
import { defineSharedMemory, field, reef } from '@atolljs/core';
// Fields may group by intent — lists / state / signals nest one level and
// every surface mirrors it: memory.signals.count, spec.signals.count, …
export const memory = defineSharedMemory({
signals: {
count: field.number(), // f64 scalar
running: field.boolean(), // flag byte
},
state: {
label: field.string({ schema: reef.string(128) }), // budget derives from the schema
metrics: field.object({ schema: metricsSchema }), // reef.object → inline record, no maxBytes
payload: field.object({ maxBytes: 2048 }), // codec blob — the escape hatch for dynamic data
samples: field.float64Array({ length: 1024 }), // typed view, zero-copy
},
lists: {
// reef members are schemas that ARE the layout: reef.u32() → 'u32',
// reef.int(0, 3) → 'u8', reef.string(10) → 10 inline bytes
records: field.list({ schema: reef.object({ id: reef.u32(), score: reef.f64(), tag: reef.string(8) }), count: 1_000_000 }),
},
});Field kinds
| Factory | Layout | Connector API |
|---|---|---|
field.number() | 8 bytes (f64) | read() / write(v) |
field.boolean() | 8 bytes | read() / write(v) |
field.string({ maxBytes }) | 4-byte length + payload | read() / write(v) |
field.object({ maxBytes, schema? }) | codec-encoded blob | read() / write(v) |
field.array({ maxBytes, schema? }) | codec-encoded blob | read() / write(v) |
field.int32Array({ length }) · float64Array({ length }) · bigInt64Array({ length }) · uint8Array({ length }) | n × element size | typed-array views — direct indexed access, zero copy |
field.list({ schema, count }) | fixed-size records | readAt(i), writeAt(i, rec), commit() |
List scalar kinds: i8 u8 i16 u16 i32 u32 f32 f64 i64 u64 — or declare members as reef/zod schemas (reef.u32(), reef.int(0, 3) → narrowest covering kind, reef.string(10)) and the same declaration becomes both layout and validation schema — full vocabulary on Reef schemas. A field is accessed on the contract object — memory.state.metrics.read() — identical API on both threads, and observers address fields by path: observe(memory, 'signals.count').
Codecs
Structured fields (object, array, string) encode through the contract's codec. The default is msgpackrCodec — MessagePack with structure sharing, the fastest option for uniform records. Alternatives: jsonCodec or any { encode, decode } object:
import { jsonCodec } from '@atolljs/core';
defineSharedMemory(spec, { codec: jsonCodec }); // opt out of the defaultBinding lifecycle
WorkerPool binds the contract on the main thread and ships the buffer to workers in INIT_MEMORY. Reads before bind throw — memory.bound / memory.onBound(cb) tell you when it's safe. The observe()-based bindings handle this automatically: values stay undefined until bound, so SSR and early renders are safe.
Capacity
The pool sizes the shared buffer from the contract — memory.totalBytes. Configure growth via WorkerPoolConfig.memory (maximumPages, growthFactor).
Custom connectors & manual allocation
A field kind is just a registered ConnectorFactory: (descriptor, ctx, byteOffset) => Connector. The context hands the factory the SharedArrayBuffer, the contract's codec, and the field's slot in the shared version counter; the returned connector's read()/write(v) own that region. registerConnectorFactory(kind, factory) registers a custom storage backend SDK-wide; defineSharedMemory(spec, { plugins: { kind: factory } }) overrides per contract.
Underneath the pool sits MemoryManager — the allocator it wraps when sharedMemory is configured. It owns a shared WebAssembly.Memory (initialPages 16 = 1 MB, maximumPages 16384 = 1 GB by default), grows it via ensureCapacity(bytes), and exposes the storage with getView(type, byteOffset?, length?) and getBuffer(). Reach for it directly only when you manage the buffer yourself — e.g. a SharedWorker host allocating once for all clients.