@atolljs/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +110 -0
  2. package/dist/index.js +19 -0
  3. package/dist/index10.js +186 -0
  4. package/dist/index11.js +52 -0
  5. package/dist/index12.js +66 -0
  6. package/dist/index13.js +27 -0
  7. package/dist/index14.js +63 -0
  8. package/dist/index15.js +50 -0
  9. package/dist/index16.js +46 -0
  10. package/dist/index17.js +42 -0
  11. package/dist/index18.js +61 -0
  12. package/dist/index19.js +56 -0
  13. package/dist/index2.js +10 -0
  14. package/dist/index20.js +59 -0
  15. package/dist/index3.js +120 -0
  16. package/dist/index4.js +40 -0
  17. package/dist/index5.js +481 -0
  18. package/dist/index6.js +23 -0
  19. package/dist/index7.js +10 -0
  20. package/dist/index8.js +37 -0
  21. package/dist/index9.js +22 -0
  22. package/package.json +68 -0
  23. package/src/contract/listSchema.ts +211 -0
  24. package/src/contract/msgpackCodec.ts +9 -0
  25. package/src/contract/msgpackrCodec.ts +16 -0
  26. package/src/contract/mz.ts +48 -0
  27. package/src/contract/sharedMemory.ts +909 -0
  28. package/src/contract/types.ts +123 -0
  29. package/src/index.ts +114 -0
  30. package/src/log.ts +69 -0
  31. package/src/observable.ts +151 -0
  32. package/src/pool/errors.ts +31 -0
  33. package/src/pool/memory.ts +68 -0
  34. package/src/pool/workerClient.ts +181 -0
  35. package/src/pool/workerPool.ts +438 -0
  36. package/src/reactive.ts +161 -0
  37. package/src/service.ts +283 -0
  38. package/src/shared/sharedWorkerClient.ts +142 -0
  39. package/src/shared/sharedWorkerHost.ts +104 -0
  40. package/src/task.ts +99 -0
  41. package/src/testing/inProcessWorker.ts +127 -0
  42. package/src/worker/defineWorker.ts +135 -0
  43. package/src/worker/registry.ts +42 -0
  44. package/src/worker/workerBootstrap.ts +56 -0
@@ -0,0 +1,909 @@
1
+ import type { Prettify, Schema } from './types';
2
+ import { msgpackrCodec } from './msgpackrCodec';
3
+ import { memberToSpec, zodArrayInfo, zodObjectShape, zodStringBytes } from './listSchema';
4
+ import { fmtBytes, scoped } from '../log';
5
+ import { z } from 'zod';
6
+
7
+ const memLog = scoped('memory');
8
+
9
+ const ALIGNMENT = 8;
10
+ const alignTo = (value: number, alignment: number) => (value + alignment - 1) & ~(alignment - 1);
11
+ const align = (bytes: number) => alignTo(bytes, ALIGNMENT);
12
+
13
+ /**
14
+ * Declares the datatype of a single field in the shared memory contract.
15
+ * `byteLength` is the fixed region reserved for the field; structured types
16
+ * (string, object, array) carry a 4-byte length header inside that region.
17
+ */
18
+ export interface FieldDescriptor<T = unknown> {
19
+ readonly kind: string;
20
+ readonly byteLength: number;
21
+ readonly schema?: Schema<T>;
22
+ readonly __fieldType?: T;
23
+ }
24
+
25
+ /**
26
+ * A group of fields under one intent — any plain-object member of the spec
27
+ * that isn't a descriptor. Group names are the user's vocabulary:
28
+ * `lists`, `state`, `signals`, `telemetry` — one level deep.
29
+ */
30
+ export type FieldGroup = Record<string, FieldDescriptor>;
31
+
32
+ export type SharedSpec = Record<string, FieldDescriptor | FieldGroup>;
33
+
34
+ /** Spec keys that hold a descriptor directly (not an intent group). */
35
+ export type FlatKey<S extends SharedSpec> = {
36
+ [K in keyof S]: S[K] extends FieldDescriptor ? K : never;
37
+ }[keyof S];
38
+
39
+ /**
40
+ * Every legal field path of a spec — flat fields give their key (`metrics`),
41
+ * grouped fields give `group.field` (`signals.seedProgress`).
42
+ */
43
+ export type SpecPath<S extends SharedSpec> = {
44
+ [K in keyof S & string]: S[K] extends FieldGroup
45
+ ? `${K}.${keyof S[K] & string}`
46
+ : K;
47
+ }[keyof S & string];
48
+
49
+ const isDescriptor = (v: unknown): v is FieldDescriptor =>
50
+ typeof v === 'object' && v !== null &&
51
+ typeof (v as FieldDescriptor).kind === 'string' &&
52
+ typeof (v as FieldDescriptor).byteLength === 'number';
53
+
54
+ interface FieldEntry {
55
+ /** Dotted path — `incidents` for flat fields, `lists.incidents` grouped. */
56
+ path: string;
57
+ descriptor: FieldDescriptor;
58
+ }
59
+
60
+ function flattenSpec(spec: SharedSpec): FieldEntry[] {
61
+ const entries: FieldEntry[] = [];
62
+ for (const [key, value] of Object.entries(spec)) {
63
+ if (isDescriptor(value)) {
64
+ entries.push({ path: key, descriptor: value });
65
+ } else {
66
+ for (const [member, descriptor] of Object.entries(value)) {
67
+ if (!isDescriptor(descriptor)) {
68
+ throw new TypeError(
69
+ `shared memory "${key}.${member}" is not a field descriptor — intent groups nest one level only`
70
+ );
71
+ }
72
+ entries.push({ path: `${key}.${member}`, descriptor });
73
+ }
74
+ }
75
+ }
76
+ return entries;
77
+ }
78
+
79
+ /**
80
+ * Serializer for structured (object/array) fields. Both threads must use the
81
+ * same codec — it comes from the shared contract definition, so it does.
82
+ */
83
+ export interface Codec {
84
+ encode(value: unknown): Uint8Array;
85
+ decode(data: Uint8Array): unknown;
86
+ }
87
+
88
+ const codecTextEncoder = new TextEncoder();
89
+ const codecTextDecoder = new TextDecoder();
90
+
91
+ export const jsonCodec: Codec = {
92
+ encode: (value) => codecTextEncoder.encode(JSON.stringify(value)),
93
+ decode: (data) => JSON.parse(codecTextDecoder.decode(data)),
94
+ };
95
+
96
+ /**
97
+ * Context handed to connector factories at bind time.
98
+ */
99
+ export interface ConnectorContext {
100
+ buffer: SharedArrayBuffer;
101
+ codec: Codec;
102
+ /** Slot in the shared version counter — bump + notify on granular writes. */
103
+ version?: { view: Int32Array; index: number };
104
+ }
105
+
106
+ /**
107
+ * A connector factory builds a connector for one field kind over a region of
108
+ * the shared buffer. Factories are the plugin unit of the SDK: register new
109
+ * kinds globally via `registerConnectorFactory` or override per contract via
110
+ * `SharedMemoryOptions.plugins`.
111
+ */
112
+ export type ConnectorFactory = (
113
+ descriptor: FieldDescriptor,
114
+ ctx: ConnectorContext,
115
+ byteOffset: number
116
+ ) => Connector<any>;
117
+
118
+ const CONNECTOR_FACTORIES = new Map<string, ConnectorFactory>();
119
+
120
+ /** Registers (or overrides) the connector factory for a field kind, SDK-wide. */
121
+ export function registerConnectorFactory(kind: string, factory: ConnectorFactory): void {
122
+ CONNECTOR_FACTORIES.set(kind, factory);
123
+ }
124
+
125
+ export interface SharedMemoryOptions {
126
+ codec?: Codec; // defaults to msgpackrCodec
127
+ plugins?: Record<string, ConnectorFactory>; // per-contract factory overrides, keyed by field kind
128
+ }
129
+
130
+ /** The element type a field descriptor holds — `InferField<typeof memory.spec.x>`. */
131
+ export type InferField<D> = D extends FieldDescriptor<infer T> ? T : unknown;
132
+
133
+ /**
134
+ * A bound connector reads and writes a single field of the shared memory
135
+ * contract. Both threads hold an identical connector for each field.
136
+ */
137
+ export interface Connector<T> {
138
+ readonly byteOffset: number;
139
+ readonly byteLength: number;
140
+ read(): T;
141
+ write(value: T): void;
142
+ /** Internal: slot in the shared version counter block, bumped on every write. */
143
+ readonly _version?: { view: Int32Array; index: number };
144
+ }
145
+
146
+ /* ------------------------------------------------------------------ */
147
+ /* Fixed-layout list fields — zero serialization */
148
+ /* ------------------------------------------------------------------ */
149
+
150
+ export type ScalarKind = 'i8' | 'u8' | 'i16' | 'u16' | 'i32' | 'u32' | 'f32' | 'f64' | 'i64' | 'u64';
151
+
152
+ /** A fixed-layout member spec: a scalar kind, `{ string: n }` for an inline UTF-8 field of n bytes, or `{ bool: true }` for a flag byte. */
153
+ export type ListFieldSpec = ScalarKind | { string: number } | { bool: true };
154
+ export type ListSpec = Record<string, ListFieldSpec>;
155
+
156
+ /**
157
+ * A fixed-width member inside a record or element position — a binary spec
158
+ * token (`'u32'`, `{ string: n }`, `{ bool: true }`) or a zod schema that
159
+ * expresses the same thing (`mz.u32()`, `mz.int(0, 3)`, `mz.string(n)`).
160
+ * Schemas are compiled to tokens for layout and kept as the field's
161
+ * validation schema — one declaration.
162
+ */
163
+ export type ListMember = ListFieldSpec | z.ZodTypeAny;
164
+ export type ListFields = Record<string, ListMember>;
165
+
166
+ /** The plain-object shape of one record in a list field — `z.output` of the declared record schema. */
167
+ export type ListRecord<S extends z.ZodObject> = z.output<S>;
168
+
169
+ export interface ListDescriptor<S extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.ZodRawShape>>
170
+ extends Omit<FieldDescriptor<ListRecord<S>[]>, 'schema'> {
171
+ readonly kind: 'list';
172
+ /**
173
+ * The declared record schema — an `mz.object` of fixed-width members. It
174
+ * IS the layout source AND the record validator (`schemas.lists.x` is this
175
+ * same object). Note it parses one record, not the whole array.
176
+ */
177
+ readonly schema: S;
178
+ /** The compiled binary spec per member — always scalar tokens/`{string:n}`/`{bool}`. */
179
+ readonly layout: Record<keyof z.output<S> & string, ListFieldSpec>;
180
+ readonly offsets: Record<keyof z.output<S> & string, number>;
181
+ readonly recordSize: number;
182
+ readonly count: number;
183
+ }
184
+
185
+ /**
186
+ * A connector over a fixed-layout record array. `readAt`/`writeAt` access a
187
+ * single record directly via DataView — no encode/decode pass, ever.
188
+ * `readAt(index, out)` reuses `out` for allocation-free scans.
189
+ */
190
+ export interface ListConnector<R> extends Connector<R[]> {
191
+ readonly recordCount: number;
192
+ readonly recordSize: number;
193
+ readAt(index: number): R;
194
+ /** Read into `out`; pass `fields` to read a subset (skips e.g. inline strings). */
195
+ readAt(index: number, out: Partial<R>, fields?: (keyof R)[]): R;
196
+ writeAt(index: number, record: R): void;
197
+ /** Write only the listed fields — partial updates leave other fields untouched. */
198
+ writeAt(index: number, record: Partial<R>, fields: (keyof R)[]): void;
199
+ /**
200
+ * Bump the field version counter and wake remote observers. `writeAt` is
201
+ * intentionally pure memory access — call commit() after a batch of writes
202
+ * to propagate them (bursts coalesce to latest anyway).
203
+ */
204
+ commit(): void;
205
+ }
206
+
207
+ type AccessMember<V> =
208
+ V extends FieldDescriptor
209
+ ? V extends ListDescriptor<infer S>
210
+ ? ListConnector<ListRecord<S>>
211
+ : Connector<InferField<V>>
212
+ : V extends FieldGroup
213
+ ? Prettify<SharedAccess<V>>
214
+ : never;
215
+
216
+ export type SharedAccess<S extends SharedSpec> = {
217
+ [K in keyof S]: AccessMember<S[K]>;
218
+ };
219
+
220
+ /**
221
+ * The connector a `SpecPath` resolves to — `PathConnector<S, 'state.metrics'>`
222
+ * digs through the intent group to the member's connector.
223
+ */
224
+ export type PathConnector<S extends SharedSpec, P> =
225
+ P extends `${infer G}.${infer F}`
226
+ ? S[G] extends FieldGroup
227
+ ? F extends keyof S[G] ? AccessMember<S[G][F]> : never
228
+ : never
229
+ : P extends keyof S ? AccessMember<S[P]> : never;
230
+
231
+ const SCALAR_SIZES: Record<ScalarKind, number> = {
232
+ i8: 1, u8: 1, i16: 2, u16: 2, i32: 4, u32: 4, f32: 4, f64: 8, i64: 8, u64: 8,
233
+ };
234
+
235
+ const listFieldSize = (spec: ListFieldSpec) => (typeof spec === 'string' ? SCALAR_SIZES[spec] : 'string' in spec ? spec.string : 1);
236
+ const listFieldAlign = (spec: ListFieldSpec) => (typeof spec === 'string' ? SCALAR_SIZES[spec] : 1);
237
+
238
+ /**
239
+ * Compiles a declared member map to its binary layout — spec tokens per
240
+ * member, byte offsets, and the aligned record size. Shared by `field.list`
241
+ * and the schema-driven `field.object`/`field.array` derivations.
242
+ */
243
+ function compileListLayout<F extends ListFields>(fields: F): {
244
+ layout: Record<keyof F, ListFieldSpec>;
245
+ offsets: Record<keyof F, number>;
246
+ recordSize: number;
247
+ } {
248
+ const offsets = {} as Record<keyof F, number>;
249
+ const layout = {} as Record<keyof F, ListFieldSpec>;
250
+ let offset = 0;
251
+ for (const key of Object.keys(fields) as (keyof F)[]) {
252
+ const spec = memberToSpec(fields[key], String(key));
253
+ layout[key] = spec;
254
+ offset = alignTo(offset, listFieldAlign(spec));
255
+ offsets[key] = offset;
256
+ offset += listFieldSize(spec);
257
+ }
258
+ return { layout, offsets, recordSize: align(offset) };
259
+ }
260
+
261
+ /**
262
+ * A `field.object({ schema })` whose zod members compiled to an inline fixed
263
+ * layout — an 8-byte header (`written` flag + reserved) plus one record.
264
+ * `maxBytes` never appears: the byte width IS the schema.
265
+ */
266
+ export interface FixedObjectDescriptor<T = unknown> extends FieldDescriptor<T> {
267
+ readonly kind: 'object';
268
+ readonly schema: Schema<T>;
269
+ /** The compiled binary spec per member — always scalar tokens/`{string:n}`/`{bool}`. */
270
+ readonly layout: Record<string, ListFieldSpec>;
271
+ readonly offsets: Record<string, number>;
272
+ readonly recordSize: number;
273
+ }
274
+
275
+ /**
276
+ * A `field.array({ schema })` whose bounded zod array compiled to an inline
277
+ * layout — an 8-byte header (`written` flag + element count) plus `capacity`
278
+ * fixed-stride elements.
279
+ */
280
+ export interface FixedArrayDescriptor<T = unknown> extends FieldDescriptor<T[]> {
281
+ readonly kind: 'array';
282
+ readonly schema: Schema<T[]>;
283
+ /** The compiled element spec — scalar token, `{string:n}`, or `{bool}`. */
284
+ readonly element: ListFieldSpec;
285
+ /** Bytes per element, aligned. */
286
+ readonly elementStride: number;
287
+ readonly capacity: number;
288
+ }
289
+
290
+ interface ObjectFieldFactory {
291
+ /**
292
+ * Inline fixed layout — the schema is a zod object of fixed-width members
293
+ * (`mz` helpers, `z.uint32()`, bounded ints, `mz.string(n)`, `z.boolean()`);
294
+ * `byteLength` derives from the member widths. Compile fails loudly for
295
+ * members with no fixed width (unbounded strings, nested objects).
296
+ */
297
+ <S extends z.ZodObject<z.ZodRawShape>>(options: { schema: S }): FixedObjectDescriptor<z.output<S>>;
298
+ /** Codec-encoded blob — explicit byte budget for arbitrary payloads. */
299
+ <T = unknown>(options: { maxBytes: number; schema?: Schema<T> }): FieldDescriptor<T>;
300
+ /** Schema with no derivable width — rejected at construction. */
301
+ <T = unknown>(options: { schema: Schema<T> }): FieldDescriptor<T>;
302
+ }
303
+
304
+ const objectFieldImpl: ObjectFieldFactory = (options: { schema?: Schema<unknown>; maxBytes?: number }): any => {
305
+ const { schema, maxBytes } = options;
306
+ if (maxBytes === undefined && schema) {
307
+ const shape = zodObjectShape(schema);
308
+ if (shape) {
309
+ const { layout, offsets, recordSize } = compileListLayout(shape);
310
+ return { kind: 'object', byteLength: 8 + recordSize, schema, layout, offsets, recordSize };
311
+ }
312
+ throw new TypeError(
313
+ `field.object: schema can't derive a fixed byte width (not a zod object of fixed-width members) — pass { maxBytes } for codec-encoded storage`
314
+ );
315
+ }
316
+ if (maxBytes === undefined) {
317
+ throw new TypeError(`field.object: pass { schema } for a fixed-layout field or { maxBytes } for a codec-encoded field`);
318
+ }
319
+ return { kind: 'object', byteLength: 4 + maxBytes, schema };
320
+ };
321
+
322
+ interface ArrayFieldFactory {
323
+ /**
324
+ * Inline fixed layout — the schema is a bounded zod array
325
+ * (`mz.array(mz.u32()).max(n)`, `.length(n)`); `byteLength` derives from
326
+ * the element width × the bound. Elements must be fixed-width members
327
+ * (scalars, `mz.string(n)`, booleans) — use `field.list` for record arrays.
328
+ */
329
+ <S extends z.ZodArray>(options: { schema: S }): FixedArrayDescriptor<S extends z.ZodArray<infer E> ? z.output<E> : unknown>;
330
+ /** Codec-encoded blob — explicit byte budget for arbitrary payloads. */
331
+ <T = unknown>(options: { maxBytes: number; schema?: Schema<T[]> }): FieldDescriptor<T[]>;
332
+ /** Schema with no derivable width — rejected at construction. */
333
+ <T = unknown>(options: { schema: Schema<T[]> }): FieldDescriptor<T[]>;
334
+ }
335
+
336
+ const arrayFieldImpl: ArrayFieldFactory = (options: { schema?: Schema<unknown>; maxBytes?: number }): any => {
337
+ const { schema, maxBytes } = options;
338
+ if (maxBytes === undefined && schema) {
339
+ const info = zodArrayInfo(schema);
340
+ if (info) {
341
+ if (info.capacity === null) {
342
+ throw new TypeError(`field.array: z.array() needs .max(n) or .length(n) — bounded arrays are fixed-width`);
343
+ }
344
+ let element: ListFieldSpec;
345
+ try {
346
+ element = memberToSpec(info.element as ListMember, 'array element');
347
+ } catch (e) {
348
+ throw new TypeError(`field.array: element isn't a fixed-width member — use field.list for arrays of records (${(e as Error).message})`);
349
+ }
350
+ const elementStride = alignTo(listFieldSize(element), listFieldAlign(element));
351
+ return { kind: 'array', byteLength: 8 + info.capacity * elementStride, schema, element, elementStride, capacity: info.capacity };
352
+ }
353
+ throw new TypeError(
354
+ `field.array: schema can't derive a fixed byte width (not a bounded zod array) — pass { maxBytes } for codec-encoded storage`
355
+ );
356
+ }
357
+ if (maxBytes === undefined) {
358
+ throw new TypeError(`field.array: pass { schema } for a fixed-layout field or { maxBytes } for a codec-encoded field`);
359
+ }
360
+ return { kind: 'array', byteLength: 4 + maxBytes, schema };
361
+ };
362
+
363
+ interface StringFieldFactory {
364
+ /** Explicit byte budget — 4-byte length header + up to `maxBytes` of UTF-8. */
365
+ (options: { maxBytes: number; schema?: Schema<string> }): FieldDescriptor<string>;
366
+ /** Budget derived from the schema — `mz.string(n)` / `z.string().meta({ bytes: n })`. */
367
+ (options: { schema: Schema<string> }): FieldDescriptor<string>;
368
+ }
369
+
370
+ const stringFieldImpl: StringFieldFactory = (options: { schema?: Schema<string>; maxBytes?: number }): any => {
371
+ const { schema, maxBytes } = options;
372
+ if (maxBytes !== undefined) return { kind: 'string', byteLength: 4 + maxBytes, schema };
373
+ const bytes = schema ? zodStringBytes(schema) : null;
374
+ if (bytes !== null) return { kind: 'string', byteLength: 4 + bytes, schema };
375
+ throw new TypeError(
376
+ `field.string: pass { maxBytes } or a schema carrying a byte budget — mz.string(n) / z.string().meta({ bytes: n })`
377
+ );
378
+ };
379
+
380
+ /**
381
+ * Field datatype helpers for `defineSharedMemory`.
382
+ */
383
+ export const field = {
384
+ number: (): FieldDescriptor<number> => ({ kind: 'number', byteLength: 8 }),
385
+ boolean: (): FieldDescriptor<boolean> => ({ kind: 'boolean', byteLength: 8 }),
386
+ string: stringFieldImpl,
387
+ object: objectFieldImpl,
388
+ array: arrayFieldImpl,
389
+ int32Array: ({ length }: { length: number }): FieldDescriptor<Int32Array> => ({ kind: 'Int32', byteLength: length * 4 }),
390
+ float64Array: ({ length }: { length: number }): FieldDescriptor<Float64Array> => ({ kind: 'Float64', byteLength: length * 8 }),
391
+ bigInt64Array: ({ length }: { length: number }): FieldDescriptor<BigInt64Array> => ({ kind: 'BigInt64', byteLength: length * 8 }),
392
+ uint8Array: ({ length }: { length: number }): FieldDescriptor<Uint8Array> => ({ kind: 'Uint8', byteLength: length }),
393
+ /**
394
+ * A fixed-layout array of `count` records. `schema` is an `mz.object` of
395
+ * fixed-width members (`mz.u32()`, `mz.int(0, 3)`, `mz.string(n)`,
396
+ * `mz.boolean()`) — the same schema `field.object` takes, repeated `count`
397
+ * times. Every record occupies `recordSize` bytes at a computable offset:
398
+ * reads and writes are direct memory access, no serialization.
399
+ */
400
+ list: <S extends z.ZodObject<z.ZodRawShape>>({ schema, count }: { schema: S; count: number }): ListDescriptor<S> => {
401
+ const shape = zodObjectShape(schema);
402
+ if (!shape) {
403
+ throw new TypeError(`field.list: schema must be an mz.object() of fixed-width members`);
404
+ }
405
+ const { layout, offsets, recordSize } = compileListLayout(shape);
406
+ return {
407
+ kind: 'list',
408
+ byteLength: recordSize * count,
409
+ schema,
410
+ layout,
411
+ offsets,
412
+ recordSize,
413
+ count,
414
+ };
415
+ },
416
+ };
417
+
418
+ type TypedArrayConstructor = {
419
+ new (buffer: SharedArrayBuffer, byteOffset: number, length: number): Int32Array | Float64Array | BigInt64Array | Uint8Array;
420
+ readonly BYTES_PER_ELEMENT: number;
421
+ };
422
+
423
+ const TYPED_ARRAYS: Record<string, TypedArrayConstructor> = {
424
+ Int32: Int32Array,
425
+ Float64: Float64Array,
426
+ BigInt64: BigInt64Array,
427
+ Uint8: Uint8Array,
428
+ };
429
+
430
+ /* ------------------------------------------------------------------ */
431
+ /* Built-in connector factories (the default plugins) */
432
+ /* ------------------------------------------------------------------ */
433
+
434
+ registerConnectorFactory('number', (d, ctx, byteOffset) => {
435
+ const view = new Float64Array(ctx.buffer, byteOffset, 1);
436
+ return { byteOffset, byteLength: d.byteLength, read: () => view[0], write: (v) => { view[0] = v; } };
437
+ });
438
+
439
+ registerConnectorFactory('boolean', (d, ctx, byteOffset) => {
440
+ const view = new Uint8Array(ctx.buffer, byteOffset, 1);
441
+ return { byteOffset, byteLength: d.byteLength, read: () => view[0] === 1, write: (v) => { view[0] = v ? 1 : 0; } };
442
+ });
443
+
444
+ for (const kind of Object.keys(TYPED_ARRAYS)) {
445
+ registerConnectorFactory(kind, (d, ctx, byteOffset) => {
446
+ const Ctor = TYPED_ARRAYS[kind];
447
+ const view = new Ctor(ctx.buffer, byteOffset, d.byteLength / Ctor.BYTES_PER_ELEMENT);
448
+ // read() returns the live view — zero-copy on both threads
449
+ return { byteOffset, byteLength: d.byteLength, read: () => view, write: (v) => view.set(v) };
450
+ });
451
+ }
452
+
453
+ registerConnectorFactory('string', (d, ctx, byteOffset) => {
454
+ const encoder = new TextEncoder();
455
+ const decoder = new TextDecoder();
456
+ const capacity = d.byteLength - 4;
457
+ const lengthView = new Uint32Array(ctx.buffer, byteOffset, 1);
458
+ const writeBytes = (bytes: Uint8Array) => {
459
+ if (bytes.byteLength > capacity) {
460
+ throw new Error(`Encoded value of ${bytes.byteLength} bytes exceeds field capacity of ${capacity} bytes at offset ${byteOffset}`);
461
+ }
462
+ lengthView[0] = bytes.byteLength;
463
+ new Uint8Array(ctx.buffer, byteOffset + 4, bytes.byteLength).set(bytes);
464
+ };
465
+ // slice() copies out of the SharedArrayBuffer — TextDecoder.decode
466
+ // rejects shared views in browsers.
467
+ const readBytes = () => new Uint8Array(ctx.buffer, byteOffset + 4, lengthView[0]).slice();
468
+ return {
469
+ byteOffset,
470
+ byteLength: d.byteLength,
471
+ read: () => decoder.decode(readBytes()),
472
+ write: (v: string) => writeBytes(encoder.encode(d.schema ? (d.schema.parse(v) as string) : v)),
473
+ };
474
+ });
475
+
476
+ type ScalarIO = { get(offset: number): number | bigint; set(offset: number, value: number | bigint): void };
477
+
478
+ /**
479
+ * Shared primitive IO over the bound buffer — scalar DataView accessors,
480
+ * inline string read/write, and `{ bool }` flags. `ops` resolves a member
481
+ * spec to direct closures once, so hot paths never re-dispatch per record.
482
+ */
483
+ function makeIO(ctx: ConnectorContext) {
484
+ const view = new DataView(ctx.buffer);
485
+ const byteView = new Uint8Array(ctx.buffer); // one view — avoids per-call view allocs
486
+ const encoder = new TextEncoder();
487
+ const decoder = new TextDecoder();
488
+
489
+ const scalarIO: Record<ScalarKind, ScalarIO> = {
490
+ i8: { get: (o) => view.getInt8(o), set: (o, v) => view.setInt8(o, v as number) },
491
+ u8: { get: (o) => view.getUint8(o), set: (o, v) => view.setUint8(o, v as number) },
492
+ i16: { get: (o) => view.getInt16(o), set: (o, v) => view.setInt16(o, v as number) },
493
+ u16: { get: (o) => view.getUint16(o), set: (o, v) => view.setUint16(o, v as number) },
494
+ i32: { get: (o) => view.getInt32(o), set: (o, v) => view.setInt32(o, v as number) },
495
+ u32: { get: (o) => view.getUint32(o), set: (o, v) => view.setUint32(o, v as number) },
496
+ f32: { get: (o) => view.getFloat32(o), set: (o, v) => view.setFloat32(o, v as number) },
497
+ f64: { get: (o) => view.getFloat64(o), set: (o, v) => view.setFloat64(o, v as number) },
498
+ i64: { get: (o) => view.getBigInt64(o), set: (o, v) => view.setBigInt64(o, v as bigint) },
499
+ u64: { get: (o) => view.getBigUint64(o), set: (o, v) => view.setBigUint64(o, v as bigint) },
500
+ };
501
+
502
+ const readString = (offset: number, max: number): string => {
503
+ const zero = byteView.indexOf(0, offset);
504
+ const len = zero === -1 || zero - offset >= max ? max : zero - offset;
505
+ return decoder.decode(byteView.slice(offset, offset + len)); // slice() — non-shared copy for decode
506
+ };
507
+
508
+ // encodeInto can't target shared views in browsers — encode into a scratch
509
+ // buffer (grown to the widest inline string encountered), then copy.
510
+ let scratch = new Uint8Array(64);
511
+ const writeString = (offset: number, max: number, value: string) => {
512
+ if (scratch.length < max) scratch = new Uint8Array(max);
513
+ const { read, written } = encoder.encodeInto(value, scratch);
514
+ if (read < value.length || written > max) {
515
+ throw new Error(`String exceeds inline field capacity of ${max} bytes`);
516
+ }
517
+ byteView.set(scratch.subarray(0, written), offset);
518
+ if (written < max) byteView.fill(0, offset + written, offset + max);
519
+ };
520
+
521
+ /** Resolves a member spec to direct get/set closures — dispatch happens once. */
522
+ const ops = (spec: ListFieldSpec): { get(offset: number): unknown; set(offset: number, value: unknown): void } => {
523
+ if (typeof spec === 'string') {
524
+ const io = scalarIO[spec];
525
+ return { get: (o) => io.get(o), set: (o, v) => io.set(o, v as number | bigint) };
526
+ }
527
+ if ('bool' in spec) {
528
+ return { get: (o) => !!view.getUint8(o), set: (o, v) => view.setUint8(o, v ? 1 : 0) };
529
+ }
530
+ return { get: (o) => readString(o, spec.string), set: (o, v) => writeString(o, spec.string, v as string) };
531
+ };
532
+
533
+ /** Precompiled record accessors over a member layout — shared by list and fixed-object connectors. */
534
+ const compile = (layout: Record<string, ListFieldSpec>, offsets: Record<string, number>) => {
535
+ const readers = new Map<string, (base: number, out: Record<string, unknown>) => void>();
536
+ const writers = new Map<string, (base: number, record: Record<string, unknown>) => void>();
537
+ for (const key of Object.keys(layout)) {
538
+ const { get, set } = ops(layout[key]);
539
+ const rel = offsets[key];
540
+ readers.set(key, (base, out) => { out[key] = get(base + rel); });
541
+ writers.set(key, (base, record) => set(base + rel, record[key]));
542
+ }
543
+ const allReaders = [...readers.values()];
544
+ const allWriters = [...writers.values()];
545
+ return {
546
+ read(base: number, out: Record<string, unknown> = {}, fieldNames?: string[]) {
547
+ if (fieldNames) {
548
+ for (const key of fieldNames) readers.get(key)!(base, out);
549
+ } else {
550
+ for (const read of allReaders) read(base, out);
551
+ }
552
+ return out;
553
+ },
554
+ write(base: number, record: Record<string, unknown>, fieldNames?: string[]) {
555
+ if (fieldNames) {
556
+ for (const key of fieldNames) writers.get(key)!(base, record);
557
+ } else {
558
+ for (const write of allWriters) write(base, record);
559
+ }
560
+ },
561
+ };
562
+ };
563
+
564
+ return { ops, compile };
565
+ }
566
+
567
+ const structuredFactory: ConnectorFactory = (d, ctx, byteOffset) => {
568
+ const { schema, byteLength, kind } = d;
569
+ const fixed = d as Partial<FixedObjectDescriptor> & Partial<FixedArrayDescriptor>;
570
+
571
+ // Schema-derived fixed layouts — no codec, width comes from the schema.
572
+ // 8-byte header: [u32 written flag][u32 aux] then the inline payload.
573
+ if (fixed.layout && fixed.offsets && fixed.recordSize !== undefined) {
574
+ const flag = new Uint32Array(ctx.buffer, byteOffset, 2);
575
+ const base = byteOffset + 8;
576
+ const record = makeIO(ctx).compile(fixed.layout, fixed.offsets);
577
+ return {
578
+ byteOffset,
579
+ byteLength,
580
+ read: () => (flag[0] ? record.read(base, {}) : undefined), // unwritten on either thread
581
+ write: (v) => {
582
+ record.write(base, (schema ? schema.parse(v) : v) as Record<string, unknown>);
583
+ flag[0] = 1; // flag last — a racing reader must never see it mid-record
584
+ },
585
+ };
586
+ }
587
+ if (fixed.element !== undefined && fixed.elementStride !== undefined && fixed.capacity !== undefined) {
588
+ const flag = new Uint32Array(ctx.buffer, byteOffset, 1);
589
+ const count = new Uint32Array(ctx.buffer, byteOffset + 4, 1);
590
+ const el = makeIO(ctx).ops(fixed.element);
591
+ const stride = fixed.elementStride;
592
+ const capacity = fixed.capacity;
593
+ const base = byteOffset + 8;
594
+ return {
595
+ byteOffset,
596
+ byteLength,
597
+ read: () => {
598
+ if (!flag[0]) return undefined;
599
+ const n = count[0];
600
+ return Array.from({ length: n }, (_, i) => el.get(base + i * stride));
601
+ },
602
+ write: (v: unknown[]) => {
603
+ const parsed = (schema ? schema.parse(v) : v) as unknown[];
604
+ if (parsed.length > capacity) {
605
+ throw new Error(`${parsed.length} elements exceeds array capacity of ${capacity}`);
606
+ }
607
+ parsed.forEach((v, i) => el.set(base + i * stride, v));
608
+ count[0] = parsed.length;
609
+ flag[0] = 1; // flag last — same mid-write guarantee as the object path
610
+ },
611
+ };
612
+ }
613
+
614
+ const capacity = byteLength - 4;
615
+ const lengthView = new Uint32Array(ctx.buffer, byteOffset, 1);
616
+ const writeBytes = (bytes: Uint8Array) => {
617
+ if (bytes.byteLength > capacity) {
618
+ throw new Error(`Encoded value of ${bytes.byteLength} bytes exceeds field capacity of ${capacity} bytes at offset ${byteOffset}`);
619
+ }
620
+ lengthView[0] = bytes.byteLength;
621
+ new Uint8Array(ctx.buffer, byteOffset + 4, bytes.byteLength).set(bytes);
622
+ };
623
+ // slice() copies out of the SharedArrayBuffer — TextDecoder.decode
624
+ // rejects shared views in browsers.
625
+ const readBytes = () => new Uint8Array(ctx.buffer, byteOffset + 4, lengthView[0]).slice();
626
+ return {
627
+ byteOffset,
628
+ byteLength,
629
+ read: () => {
630
+ const bytes = readBytes();
631
+ if (bytes.byteLength === 0) return undefined; // not yet written on either thread
632
+ const value = ctx.codec.decode(bytes);
633
+ return schema ? schema.parse(value) : value;
634
+ },
635
+ write: (v) => writeBytes(ctx.codec.encode(schema ? schema.parse(v) : v)),
636
+ };
637
+ };
638
+
639
+ registerConnectorFactory('object', structuredFactory);
640
+ registerConnectorFactory('array', structuredFactory);
641
+
642
+ function listFactory(d: FieldDescriptor, ctx: ConnectorContext, byteOffset: number): ListConnector<any> {
643
+ const info = d as ListDescriptor;
644
+ const { offsets, recordSize, count } = info;
645
+ // `layout` is the compiled token map; `schema` stays the validation schema.
646
+ const record = makeIO(ctx).compile(info.layout, offsets);
647
+
648
+ const checkIndex = (index: number) => {
649
+ if (index < 0 || index >= count) {
650
+ throw new RangeError(`Record index ${index} out of bounds (0..${count - 1})`);
651
+ }
652
+ };
653
+
654
+ const readAt = (index: number, out: Record<string, unknown> = {}, fieldNames?: string[]) => {
655
+ checkIndex(index);
656
+ return record.read(byteOffset + index * recordSize, out, fieldNames);
657
+ };
658
+
659
+ const writeAt = (index: number, rec: Record<string, unknown>, fieldNames?: string[]) => {
660
+ checkIndex(index);
661
+ record.write(byteOffset + index * recordSize, rec, fieldNames);
662
+ };
663
+
664
+ const connector: ListConnector<any> = {
665
+ byteOffset,
666
+ byteLength: d.byteLength,
667
+ recordCount: count,
668
+ recordSize,
669
+ readAt: readAt as ListConnector<any>['readAt'],
670
+ writeAt: writeAt as ListConnector<any>['writeAt'],
671
+ commit: () => {
672
+ if (ctx.version) {
673
+ Atomics.add(ctx.version.view, ctx.version.index, 1);
674
+ // Wake EVERY waitAsync observer — several watchers may share this
675
+ // field's counter; a bounded notify starves all but one.
676
+ Atomics.notify(ctx.version.view, ctx.version.index);
677
+ }
678
+ },
679
+ read: () => Array.from({ length: count }, (_, i) => readAt(i)),
680
+ write: (records: any[]) => {
681
+ if (records.length > count) {
682
+ throw new Error(`${records.length} records exceeds list array capacity of ${count}`);
683
+ }
684
+ records.forEach((r, i) => writeAt(i, r));
685
+ },
686
+ };
687
+ return connector;
688
+ }
689
+
690
+ registerConnectorFactory('list', listFactory);
691
+
692
+ /* ------------------------------------------------------------------ */
693
+
694
+ const definedSharedMemories: SharedMemory<any>[] = [];
695
+
696
+ /**
697
+ * The zod schema for a field's logical unit: the declared `schema` for
698
+ * object/array/list fields (for lists, the record schema), and a derived
699
+ * scalar/typed-array schema for everything else.
700
+ */
701
+ export type FieldSchema<D> =
702
+ 'schema' extends keyof D
703
+ ? NonNullable<D['schema']>
704
+ : z.ZodType<InferField<D>>;
705
+
706
+ /**
707
+ * A zod schema per field — `memory.schemas.<field>` re-exports what the spec
708
+ * declared inline. Intent groups mirror their spec shape:
709
+ * `memory.schemas.lists.incidents` is the record schema.
710
+ */
711
+ type SchemaMember<V> =
712
+ V extends FieldDescriptor
713
+ ? FieldSchema<V>
714
+ : V extends FieldGroup
715
+ ? Prettify<SpecSchemas<V>>
716
+ : never;
717
+
718
+ export type SpecSchemas<S extends SharedSpec> = {
719
+ [K in keyof S]: SchemaMember<S[K]>;
720
+ };
721
+
722
+ /** Mirrors the spec's intent groups — schemas nest exactly where the fields do. */
723
+ function buildSchemas<S extends SharedSpec>(spec: S): SpecSchemas<S> {
724
+ const schemas: Record<string, unknown> = {};
725
+ for (const [key, value] of Object.entries(spec)) {
726
+ schemas[key] = isDescriptor(value)
727
+ ? fieldSchemaOf(value)
728
+ : Object.fromEntries(Object.entries(value).map(([m, d]) => [m, fieldSchemaOf(d)]));
729
+ }
730
+ return schemas as SpecSchemas<S>;
731
+ }
732
+
733
+ function fieldSchemaOf(descriptor: FieldDescriptor): Schema<unknown> {
734
+ if (descriptor.schema) return descriptor.schema;
735
+ switch (descriptor.kind) {
736
+ case 'number': return z.number();
737
+ case 'boolean': return z.boolean();
738
+ case 'string': return z.string();
739
+ case 'Int32': return z.instanceof(Int32Array);
740
+ case 'Float64': return z.instanceof(Float64Array);
741
+ case 'BigInt64': return z.instanceof(BigInt64Array);
742
+ case 'Uint8': return z.instanceof(Uint8Array);
743
+ default: return z.unknown();
744
+ }
745
+ }
746
+
747
+ /**
748
+ * A shared memory contract: a fixed, deterministic layout of typed fields over
749
+ * a SharedArrayBuffer. Both threads import the same contract object; the pool
750
+ * binds it on the main thread and the worker bootstrap binds it inside workers,
751
+ * guaranteeing identical memory usage on both sides.
752
+ */
753
+ export class SharedMemory<S extends SharedSpec = SharedSpec> {
754
+ public readonly totalBytes: number;
755
+ /** The field descriptors the contract was defined with — the inline spec. */
756
+ public readonly spec: S;
757
+ /**
758
+ * One zod schema per field, derived from the spec: declared schemas on
759
+ * object/array fields, the record schema on list fields, and derived
760
+ * scalar schemas (z.number(), z.string(), …) elsewhere.
761
+ */
762
+ public readonly schemas: SpecSchemas<S>;
763
+ private readonly codec: Codec;
764
+ private readonly plugins?: Record<string, ConnectorFactory>;
765
+ private readonly entries: FieldEntry[];
766
+ private readonly offsets = new Map<string, number>();
767
+ private readonly versionOffset: number;
768
+ private readonly connectors = new Map<string, Connector<any>>();
769
+ private readonly boundListeners = new Set<() => void>();
770
+ private isBound = false;
771
+
772
+ constructor(spec: S, options: SharedMemoryOptions = {}) {
773
+ this.spec = spec;
774
+ this.codec = options.codec ?? msgpackrCodec;
775
+ this.plugins = options.plugins;
776
+ this.entries = flattenSpec(spec);
777
+ this.schemas = buildSchemas(spec);
778
+ let offset = 0;
779
+ for (const { path, descriptor } of this.entries) {
780
+ offset = align(offset);
781
+ this.offsets.set(path, offset);
782
+ offset += descriptor.byteLength;
783
+ }
784
+ // Version counter block: one Int32 per field, bumped via Atomics on every
785
+ // write so the other thread can observe changes.
786
+ this.versionOffset = align(offset);
787
+ this.totalBytes = this.versionOffset + align(this.entries.length * 4);
788
+ definedSharedMemories.push(this);
789
+ memLog.info(`contract defined: ${this.entries.length} field(s), ${fmtBytes(this.totalBytes)}`, {
790
+ fields: Object.fromEntries(this.entries.map((e) => [e.path, `${e.descriptor.kind}@${this.offsets.get(e.path)?.toLocaleString()}+${fmtBytes(e.descriptor.byteLength)}`])),
791
+ pluginOverrides: Object.keys(this.plugins ?? {}),
792
+ });
793
+ }
794
+
795
+ /** Binds this contract's connectors to the shared buffer. Called by the SDK on each thread. */
796
+ public bind(buffer: SharedArrayBuffer): void {
797
+ this.connectors.clear();
798
+ const versionView = new Int32Array(buffer, this.versionOffset, this.entries.length);
799
+ this.entries.forEach(({ path, descriptor }, index) => {
800
+ const factory = this.plugins?.[descriptor.kind] ?? CONNECTOR_FACTORIES.get(descriptor.kind);
801
+ if (!factory) {
802
+ throw new Error(`Unknown shared memory field kind: ${descriptor.kind}`);
803
+ }
804
+ memLog.debug(`bind "${path}" (${descriptor.kind}, ${fmtBytes(descriptor.byteLength)}) via ${this.plugins?.[descriptor.kind] ? 'plugin' : 'built-in'} factory`);
805
+ const ctx: ConnectorContext = { buffer, codec: this.codec, version: { view: versionView, index } };
806
+ const raw = factory(descriptor, ctx, this.offsets.get(path)!);
807
+ this.connectors.set(path, {
808
+ ...raw,
809
+ write: (v) => {
810
+ raw.write(v);
811
+ // Bump the version counter and explicitly wake waitAsync observers —
812
+ // V8 only resolves waitAsync waiters on notify, not on value changes.
813
+ // No count bound: every watcher on this field must wake.
814
+ Atomics.add(versionView, index, 1);
815
+ Atomics.notify(versionView, index);
816
+ },
817
+ _version: { view: versionView, index },
818
+ });
819
+ });
820
+ this.isBound = true;
821
+ for (const cb of [...this.boundListeners]) cb();
822
+ }
823
+
824
+ /** True once the contract has been bound to a buffer on this thread. */
825
+ public get bound(): boolean {
826
+ return this.isBound;
827
+ }
828
+
829
+ /**
830
+ * Fires when the contract is bound on this thread — immediately if already
831
+ * bound. Returns an unsubscribe function.
832
+ */
833
+ public onBound(cb: () => void): () => void {
834
+ if (this.isBound) {
835
+ cb();
836
+ return () => {};
837
+ }
838
+ this.boundListeners.add(cb);
839
+ return () => this.boundListeners.delete(cb);
840
+ }
841
+
842
+ /** Typed lookup for flat (ungrouped) field keys. */
843
+ public connector<K extends FlatKey<S>>(key: K): SharedAccess<S>[K];
844
+ /**
845
+ * Looks up a bound connector by its field path — `incidents` for flat
846
+ * fields, `lists.incidents` for grouped ones.
847
+ */
848
+ public connector(path: string): Connector<any>;
849
+ public connector(path: any): any {
850
+ const p = String(path);
851
+ const connector = this.connectors.get(p);
852
+ if (!connector) {
853
+ throw new Error(`Shared memory field "${p}" was accessed before the buffer was bound on this thread.`);
854
+ }
855
+ return connector;
856
+ }
857
+ }
858
+
859
+ /**
860
+ * Defines a shared memory contract. Spec values are field descriptors or
861
+ * intent groups (one level of nesting — the group's name is the intent):
862
+ *
863
+ * defineSharedMemory({
864
+ * lists: { incidents: field.list({ schema, count }) },
865
+ * state: { metrics: field.object({ maxBytes, schema }) },
866
+ * signals: { progress: field.number() },
867
+ * });
868
+ *
869
+ * The returned object exposes a typed connector per field, nested the same
870
+ * way: `memory.lists.incidents.readAt(0)`, `memory.signals.progress.write(…)`.
871
+ * Flat members (`memory.counter`) keep working for ungrouped fields.
872
+ */
873
+ export function defineSharedMemory<S extends SharedSpec>(
874
+ spec: S,
875
+ options: SharedMemoryOptions = {}
876
+ ): SharedMemory<S> & Prettify<SharedAccess<S>> {
877
+ const memory = new SharedMemory(spec, options);
878
+ for (const [key, value] of Object.entries(spec)) {
879
+ if (isDescriptor(value)) {
880
+ Object.defineProperty(memory, key, {
881
+ enumerable: true,
882
+ get: () => memory.connector(key),
883
+ });
884
+ } else {
885
+ const group: Record<string, Connector<any>> = {};
886
+ for (const member of Object.keys(value)) {
887
+ Object.defineProperty(group, member, {
888
+ enumerable: true,
889
+ get: () => memory.connector(`${key}.${member}`),
890
+ });
891
+ }
892
+ Object.defineProperty(memory, key, { enumerable: true, get: () => group });
893
+ }
894
+ }
895
+ return memory as SharedMemory<S> & Prettify<SharedAccess<S>>;
896
+ }
897
+
898
+ /** Binds every defined shared memory contract to the buffer. Used by the worker bootstrap. */
899
+ export function bindSharedMemories(buffer: SharedArrayBuffer): void {
900
+ for (const memory of definedSharedMemories) {
901
+ memory.bind(buffer);
902
+ }
903
+ memLog.info(`bound ${definedSharedMemories.length} contract(s) — ${fmtBytes(buffer.byteLength)} buffer`);
904
+ }
905
+
906
+ /** How many shared-memory contracts this module graph has defined. */
907
+ export function getDefinedSharedMemoryCount(): number {
908
+ return definedSharedMemories.length;
909
+ }