Shared memory

defineSharedMemory declares a fixed byte layout once; both threads get identical connectors over the same SharedArrayBuffer.

memory.contract.ts
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

FactoryLayoutConnector API
field.number()8 bytes (f64)read() / write(v)
field.boolean()8 bytesread() / write(v)
field.string({ maxBytes })4-byte length + payloadread() / write(v)
field.object({ maxBytes, schema? })codec-encoded blobread() / write(v)
field.array({ maxBytes, schema? })codec-encoded blobread() / write(v)
field.int32Array({ length }) · float64Array({ length }) · bigInt64Array({ length }) · uint8Array({ length })n × element sizetyped-array views — direct indexed access, zero copy
field.list({ schema, count })fixed-size recordsreadAt(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 default

Binding 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.