@atolljs/core 0.1.5 → 0.1.7

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.
@@ -1,6 +1,7 @@
1
1
  import type { Prettify, Schema } from './types';
2
2
  import { msgpackrCodec } from './msgpackrCodec';
3
3
  import { memberToSpec, zodArrayInfo, zodObjectShape, zodStringBytes } from './listSchema';
4
+ import { devtoolsEnabled, emitDevtools } from '../devtools';
4
5
  import { fmtBytes, scoped } from '../log';
5
6
  import { z, fluent } from './zod';
6
7
 
@@ -101,6 +102,8 @@ export interface ConnectorContext {
101
102
  codec: Codec;
102
103
  /** Slot in the shared version counter — bump + notify on granular writes. */
103
104
  version?: { view: Int32Array; index: number };
105
+ /** The field's contract path — for devtools write events on granular commits. */
106
+ path?: string;
104
107
  }
105
108
 
106
109
  /**
@@ -670,10 +673,13 @@ function listFactory(d: FieldDescriptor, ctx: ConnectorContext, byteOffset: numb
670
673
  writeAt: writeAt as ListConnector<any>['writeAt'],
671
674
  commit: () => {
672
675
  if (ctx.version) {
673
- Atomics.add(ctx.version.view, ctx.version.index, 1);
676
+ const prev = Atomics.add(ctx.version.view, ctx.version.index, 1);
674
677
  // Wake EVERY waitAsync observer — several watchers may share this
675
678
  // field's counter; a bounded notify starves all but one.
676
679
  Atomics.notify(ctx.version.view, ctx.version.index);
680
+ if (ctx.path && devtoolsEnabled()) {
681
+ emitDevtools({ type: 'memory:write', path: ctx.path, version: prev + 1 });
682
+ }
677
683
  }
678
684
  },
679
685
  read: () => Array.from({ length: count }, (_, i) => readAt(i)),
@@ -803,7 +809,7 @@ export class SharedMemory<S extends SharedSpec = SharedSpec> {
803
809
  throw new Error(`Unknown shared memory field kind: ${descriptor.kind}`);
804
810
  }
805
811
  memLog.debug(`bind "${path}" (${descriptor.kind}, ${fmtBytes(descriptor.byteLength)}) via ${this.plugins?.[descriptor.kind] ? 'plugin' : 'built-in'} factory`);
806
- const ctx: ConnectorContext = { buffer, codec: this.codec, version: { view: versionView, index } };
812
+ const ctx: ConnectorContext = { buffer, codec: this.codec, version: { view: versionView, index }, path };
807
813
  const raw = factory(descriptor, ctx, this.offsets.get(path)!);
808
814
  this.connectors.set(path, {
809
815
  ...raw,
@@ -812,12 +818,22 @@ export class SharedMemory<S extends SharedSpec = SharedSpec> {
812
818
  // Bump the version counter and explicitly wake waitAsync observers —
813
819
  // V8 only resolves waitAsync waiters on notify, not on value changes.
814
820
  // No count bound: every watcher on this field must wake.
815
- Atomics.add(versionView, index, 1);
821
+ const prev = Atomics.add(versionView, index, 1);
816
822
  Atomics.notify(versionView, index);
823
+ if (devtoolsEnabled()) {
824
+ emitDevtools({ type: 'memory:write', path, version: prev + 1 });
825
+ }
817
826
  },
818
827
  _version: { view: versionView, index },
819
828
  });
820
829
  });
830
+ if (devtoolsEnabled()) {
831
+ emitDevtools({
832
+ type: 'memory:bind',
833
+ fields: this.entries.map((e, i) => [i, e.path]),
834
+ totalBytes: this.totalBytes,
835
+ });
836
+ }
821
837
  this.isBound = true;
822
838
  this.boundBuffer = buffer;
823
839
  for (const cb of [...this.boundListeners]) cb();
@@ -65,6 +65,8 @@ export interface WorkerPoolConfig<S extends SharedSpec = SharedSpec, T extends T
65
65
  persistence?: (memory: SharedMemory<S>) => MemoryPersistence | undefined;
66
66
  /** Worker count — a number, or 'auto' (the default) for navigator.hardwareConcurrency ?? 4. */
67
67
  poolSize?: number | 'auto';
68
+ /** Display label for devtools/dashboards — falls back to a generated id. */
69
+ name?: string;
68
70
  memory?: MemoryConfig;
69
71
  /** Max in-flight tasks per worker (default 1); excess tasks queue FIFO. */
70
72
  concurrency?: number;
@@ -98,6 +98,10 @@ export interface SchemaDef {
98
98
  format?: string;
99
99
  shape?: Record<string, ZodLike>;
100
100
  element?: ZodLike;
101
+ /** union members. */
102
+ options?: readonly ZodLike[];
103
+ /** literal value. */
104
+ literal?: unknown;
101
105
  cls?: abstract new (...args: any[]) => unknown;
102
106
  }
103
107
 
@@ -205,9 +209,24 @@ class ZodInstanceof<T> extends ZodType<T> {
205
209
  }
206
210
  }
207
211
 
208
- type ShapeOutput<S extends Record<string, ZodLike>> = {
209
- [K in keyof S]: S[K] extends { _zod: { output: infer O } } ? O : never;
210
- };
212
+ /** Members whose output admits `undefined` become optional keys — mirroring
213
+ * zod's `z.optional()` object-member semantics (message-domain schemas like
214
+ * island prop contracts need this; reef's fixed-width members never produce
215
+ * it, so the split preserves their all-required output). */
216
+ type IsOptional<S, K extends keyof S> =
217
+ S[K] extends { _zod: { output: infer O } }
218
+ ? undefined extends O
219
+ ? true
220
+ : false
221
+ : false;
222
+
223
+ type Flatten<T> = { [K in keyof T]: T[K] };
224
+
225
+ type ShapeOutput<S extends Record<string, ZodLike>> = Flatten<
226
+ { [K in keyof S as IsOptional<S, K> extends true ? never : K]: OutputOf<S[K]> } & {
227
+ [K in keyof S as IsOptional<S, K> extends true ? K : never]?: OutputOf<S[K]>;
228
+ }
229
+ >;
211
230
 
212
231
  /** Strips unknown keys like zod's object parse — output is the shape only. */
213
232
  class ZodObjectSchema<S extends Record<string, ZodLike>> extends ZodType<ShapeOutput<S>> {
@@ -242,21 +261,111 @@ class ZodArraySchema<E extends ZodLike> extends ZodType<OutputOf<E>[]> {
242
261
 
243
262
  type OutputOf<S> = S extends { _zod: { output: infer O } } ? O : never;
244
263
 
245
- /* ------------------------------------------------------------------ */
246
- /* Factories — the vendored `zod/mini` spelling */
247
- /* ------------------------------------------------------------------ */
264
+ /* ── Message-domain kinds ────────────────────────────────────────────────
265
+ * Layout-free members for contracts that ride postMessage instead of the
266
+ * shared buffer — island prop/event schemas (defineIslandContract). The
267
+ * layout compiler doesn't understand these def.types and rejects them with
268
+ * "can't lay out" if they ever appear inside a fixed-width field — that
269
+ * loud failure is intended: they have no byte width.
270
+ */
271
+
272
+ class ZodOptional<E extends ZodLike> extends ZodType<OutputOf<E> | undefined> {
273
+ declare readonly _zod: {
274
+ output: OutputOf<E> | undefined; input: OutputOf<E> | undefined;
275
+ def: SchemaDef & { type: 'optional'; element: E };
276
+ };
277
+ inner(v: unknown): OutputOf<E> | undefined {
278
+ // The intersected def narrows element to E; read it as the element's
279
+ // output type so the wrapper's output lines up.
280
+ return v === undefined
281
+ ? undefined
282
+ : (this._zod.def.element as ZodLike<OutputOf<E>>).parse(v);
283
+ }
284
+ }
285
+ class ZodNullable<E extends ZodLike> extends ZodType<OutputOf<E> | null> {
286
+ declare readonly _zod: {
287
+ output: OutputOf<E> | null; input: OutputOf<E> | null;
288
+ def: SchemaDef & { type: 'nullable'; element: E };
289
+ };
290
+ inner(v: unknown): OutputOf<E> | null {
291
+ return v === null
292
+ ? null
293
+ : (this._zod.def.element as ZodLike<OutputOf<E>>).parse(v);
294
+ }
295
+ }
296
+ class ZodLiteral<V extends string | number | boolean> extends ZodType<V> {
297
+ declare readonly _zod: { output: V; input: V; def: SchemaDef & { type: 'literal' } };
298
+ inner(v: unknown): V {
299
+ return v === this._zod.def.literal ? (v as V) : fail(JSON.stringify(this._zod.def.literal));
300
+ }
301
+ }
302
+ class ZodUnion<O extends readonly ZodLike[]> extends ZodType<OutputOf<O[number]>> {
303
+ declare readonly _zod: {
304
+ output: OutputOf<O[number]>; input: OutputOf<O[number]>;
305
+ def: SchemaDef & { type: 'union'; options: O };
306
+ };
307
+ inner(v: unknown): OutputOf<O[number]> {
308
+ for (const option of this._zod.def.options as readonly ZodLike<OutputOf<O[number]>>[]) {
309
+ const r = option.safeParse(v);
310
+ if (r.success) return r.data;
311
+ }
312
+ return fail('a union member');
313
+ }
314
+ }
315
+ class ZodRecord<E extends ZodLike> extends ZodType<Record<string, OutputOf<E>>> {
316
+ declare readonly _zod: {
317
+ output: Record<string, OutputOf<E>>; input: Record<string, OutputOf<E>>;
318
+ def: SchemaDef & { type: 'record'; element: E };
319
+ };
320
+ inner(v: unknown): Record<string, OutputOf<E>> {
321
+ if (typeof v !== 'object' || v === null || Array.isArray(v)) fail('object');
322
+ const el = this._zod.def.element;
323
+ const out: Record<string, unknown> = {};
324
+ for (const [k, value] of Object.entries(v as Record<string, unknown>)) {
325
+ out[k] = el.parse(value);
326
+ }
327
+ return out as Record<string, OutputOf<E>>;
328
+ }
329
+ }
330
+ /**
331
+ * Function-typed members — how a props schema declares a `callbackProp`
332
+ * slot: the wire carries a `{__cb:id}` handle that `unmarshalCallbackProps`
333
+ * restores to a callable BEFORE contract validation runs, so at parse time
334
+ * the member is a real function.
335
+ */
336
+ class ZodCallback<F extends (...args: any[]) => any = (...args: unknown[]) => unknown>
337
+ extends ZodType<F> {
338
+ declare readonly _zod: { output: F; input: F; def: SchemaDef & { type: 'function' } };
339
+ inner(v: unknown): F {
340
+ return typeof v === 'function' ? (v as F) : fail('function');
341
+ }
342
+ }
248
343
 
249
- const number = () => new ZodNumber({ type: 'number' });
250
- const string = () => new ZodString({ type: 'string' });
251
- const boolean_ = () => new ZodBoolean({ type: 'boolean' });
252
- const bigint_ = () => new ZodBigint({ type: 'bigint' });
253
- const unknown_ = () => new ZodUnknown({ type: 'unknown' });
344
+ // z factories return FLUENT schemas — `z.string().optional()` chains like
345
+ // classic zod; the mini spellings (`z.optional(z.string())`) stay valid.
346
+ const number = () => fluent(new ZodNumber({ type: 'number' }));
347
+ const string = () => fluent(new ZodString({ type: 'string' }));
348
+ const boolean_ = () => fluent(new ZodBoolean({ type: 'boolean' }));
349
+ const bigint_ = () => fluent(new ZodBigint({ type: 'bigint' }));
350
+ const unknown_ = () => fluent(new ZodUnknown({ type: 'unknown' }));
254
351
  const instanceof_ = <T extends abstract new (...args: any[]) => any>(cls: T) =>
255
- new ZodInstanceof<InstanceType<T>>({ type: 'instanceof', cls });
352
+ fluent(new ZodInstanceof<InstanceType<T>>({ type: 'instanceof', cls }));
256
353
  const object_ = <S extends Record<string, ZodLike>>(shape: S) =>
257
- new ZodObjectSchema<S>({ type: 'object', shape });
354
+ fluent(new ZodObjectSchema<S>({ type: 'object', shape }));
258
355
  const array = <E extends ZodLike>(element: E) =>
259
- new ZodArraySchema<E>({ type: 'array', element });
356
+ fluent(new ZodArraySchema<E>({ type: 'array', element }));
357
+ const optional = <E extends ZodLike>(element: E) =>
358
+ fluent(new ZodOptional<E>({ type: 'optional', element }));
359
+ const nullable = <E extends ZodLike>(element: E) =>
360
+ fluent(new ZodNullable<E>({ type: 'nullable', element }));
361
+ const literal = <V extends string | number | boolean>(value: V) =>
362
+ fluent(new ZodLiteral<V>({ type: 'literal', literal: value }));
363
+ const union = <O extends readonly [ZodLike, ...ZodLike[]]>(options: O) =>
364
+ fluent(new ZodUnion<O>({ type: 'union', options }));
365
+ const record = <E extends ZodLike>(element: E) =>
366
+ fluent(new ZodRecord<E>({ type: 'record', element }));
367
+ const callback = <F extends (...args: any[]) => any = (...args: unknown[]) => unknown>() =>
368
+ fluent(new ZodCallback<F>({ type: 'function' }));
260
369
 
261
370
  const int32 = () => new ZodNumber({ type: 'number', format: 'int32' }).check(intRange('int32', int32min, int32max));
262
371
  const uint32 = () => new ZodNumber({ type: 'number', format: 'uint32' }).check(intRange('uint32', 0, 4294967295));
@@ -288,6 +397,10 @@ export type Fluent<S extends ZodLike> = S & {
288
397
  max(value: number | bigint): Fluent<S>;
289
398
  length(n: number): Fluent<S>;
290
399
  int(): Fluent<S>;
400
+ /** Wraps in an optional member — the `label?: string` spelling. */
401
+ optional(): Fluent<ZodOptional<S>>;
402
+ /** Wraps in a nullable member — `T | null`. */
403
+ nullable(): Fluent<ZodNullable<S>>;
291
404
  };
292
405
 
293
406
  /** Attaches the `Fluent` methods as non-enumerable own properties. */
@@ -302,6 +415,8 @@ export function fluent<S extends ZodType>(schema: S): Fluent<S> {
302
415
  max: (v: number | bigint) => fluent(boundCheck_(boundCheck(schema, 'max', v))),
303
416
  length: (n: number) => fluent(boundCheck_(length(n))),
304
417
  int: () => fluent(boundCheck_(int())),
418
+ optional: () => fluent(new ZodOptional({ type: 'optional', element: schema })),
419
+ nullable: () => fluent(new ZodNullable({ type: 'nullable', element: schema })),
305
420
  };
306
421
  for (const [key, fn] of Object.entries(own)) {
307
422
  Object.defineProperty(schema, key, { value: fn, enumerable: false, configurable: true });
@@ -316,6 +431,7 @@ export function fluent<S extends ZodType>(schema: S): Fluent<S> {
316
431
  export const z = {
317
432
  number, string, boolean: boolean_, bigint: bigint_, object: object_, array,
318
433
  instanceof: instanceof_, unknown: unknown_,
434
+ optional, nullable, literal, union, record, callback,
319
435
  int32, uint32, float32, float64, int64, uint64,
320
436
  int, gte, lte, nonnegative, refine, length, maxLength, minLength,
321
437
  globalRegistry,