@atolljs/core 0.1.2 → 0.1.3

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,4 +1,4 @@
1
- import { z } from './zod';
1
+ import { z, fluent, type Fluent } from './zod';
2
2
  import type {
3
3
  ScalarKind,
4
4
  ListFieldSpec,
@@ -8,26 +8,30 @@ import type {
8
8
 
9
9
  const utf8 = new TextEncoder();
10
10
 
11
- /** Storage-bounded zod schemas per scalar kind — validation catches
11
+ /** Storage-bounded schemas per scalar kind — validation catches
12
12
  * out-of-range values at the contract boundary instead of silently
13
13
  * truncating at write time. */
14
+ const boundedInt = (min: number, max: number) =>
15
+ fluent(z.number().check(z.int(), z.gte(min), z.lte(max)));
16
+
14
17
  const SCALAR_SCHEMAS: Record<ScalarKind, z.ZodTypeAny> = {
15
- i8: z.number().int().min(-128).max(127),
16
- u8: z.number().int().min(0).max(255),
17
- i16: z.number().int().min(-32768).max(32767),
18
- u16: z.number().int().min(0).max(65535),
19
- i32: z.number().int().min(-2147483648).max(2147483647),
20
- u32: z.number().int().min(0).max(4294967295),
21
- f32: z.number(),
22
- f64: z.number(),
23
- i64: z.bigint(),
24
- u64: z.bigint().nonnegative(),
18
+ i8: boundedInt(-128, 127),
19
+ u8: boundedInt(0, 255),
20
+ i16: boundedInt(-32768, 32767),
21
+ u16: boundedInt(0, 65535),
22
+ i32: fluent(z.int32()),
23
+ u32: fluent(z.uint32()),
24
+ f32: fluent(z.number()),
25
+ f64: fluent(z.number()),
26
+ i64: fluent(z.bigint()),
27
+ u64: fluent(z.bigint().check(z.nonnegative())),
25
28
  };
26
29
 
27
30
  const byteBounded = (bytes: number) =>
28
- z.string().refine(
29
- (s) => utf8.encode(s).byteLength <= bytes,
30
- `exceeds ${bytes} UTF-8 bytes`
31
+ fluent(
32
+ z.string().check(
33
+ z.refine((s: string) => utf8.encode(s).byteLength <= bytes, `exceeds ${bytes} UTF-8 bytes`)
34
+ )
31
35
  );
32
36
 
33
37
  /** Maps a list member to the zod shape whose inferred output type is
@@ -47,7 +51,7 @@ export type ListZodShape<F extends ListFields> = {
47
51
 
48
52
  /**
49
53
  * Builds a zod object schema from a member map — the shape you would pass
50
- * to `mz.object(...)` for `field.list({ schema, count })`. Members may be
54
+ * to `reef.object(...)` for `field.list({ schema, count })`. Members may be
51
55
  * spec tokens or zod schemas; the result's `z.infer` is the record type:
52
56
  *
53
57
  * ```ts
@@ -60,21 +64,35 @@ export type ListZodShape<F extends ListFields> = {
60
64
  * capacity the connector enforces; zod `z.string().meta({ bytes: n })`
61
65
  * members get the same bound attached to the declared schema.
62
66
  */
63
- export function listSchema<F extends ListFields>(fields: F): z.ZodObject<ListZodShape<F>> {
67
+ export function listSchema<F extends ListFields>(fields: F): Fluent<z.ZodMiniObject<ListZodShape<F>>> {
64
68
  const shape: Record<string, z.ZodTypeAny> = {};
65
69
  for (const key of Object.keys(fields)) {
66
70
  shape[key] = memberToSchema(fields[key]);
67
71
  }
68
- return z.object(shape) as unknown as z.ZodObject<ListZodShape<F>>;
72
+ return fluent(z.object(shape)) as unknown as Fluent<z.ZodMiniObject<ListZodShape<F>>>;
73
+ }
74
+
75
+ /** Duck-typed `$Zod*`-style detection — covers atoll-minted, classic zod,
76
+ * and zod/mini schemas: they all carry `_zod.def`. */
77
+ function isZodSchema(value: unknown): value is z.ZodTypeAny {
78
+ return (
79
+ typeof value === 'object' && value !== null &&
80
+ typeof (value as { _zod?: { def?: unknown } })._zod?.def === 'object'
81
+ );
69
82
  }
70
83
 
71
84
  function memberToSchema(member: ListMember): z.ZodTypeAny {
72
- if (member instanceof z.ZodType) {
85
+ if (isZodSchema(member)) {
73
86
  const bytes =
74
87
  member._zod.def?.type === 'string' ? zodMetaBytes(member) : undefined;
75
- return typeof bytes === 'number'
76
- ? member.refine((s) => utf8.encode(s as string).byteLength <= bytes, `exceeds ${bytes} UTF-8 bytes`)
77
- : member;
88
+ if (typeof bytes !== 'number') return member;
89
+ const fits = (s: unknown) => utf8.encode(s as string).byteLength <= bytes;
90
+ const message = `exceeds ${bytes} UTF-8 bytes`;
91
+ // classic + fluent-augmented members carry `.refine`; a raw mini schema
92
+ // takes the same check through `.check` — both keep def.type 'string'.
93
+ return typeof (member as { refine?: unknown }).refine === 'function'
94
+ ? (member as unknown as { refine: (fn: typeof fits, msg: string) => z.ZodTypeAny }).refine(fits, message)
95
+ : (member as unknown as { check: (c: unknown) => z.ZodTypeAny }).check(z.refine(fits, message));
78
96
  }
79
97
  return typeof member === 'string'
80
98
  ? SCALAR_SCHEMAS[member]
@@ -155,7 +173,7 @@ function numberMemberToSpec(def: ZodMemberDef): ListFieldSpec {
155
173
  */
156
174
  export function memberToSpec(member: ListMember, name: string): ListFieldSpec {
157
175
  if (typeof member === 'string') return member;
158
- if (!(member instanceof z.ZodType)) {
176
+ if (!isZodSchema(member)) {
159
177
  if (typeof (member as { string?: unknown }).string === 'number') return member as { string: number };
160
178
  if ((member as { bool?: unknown }).bool === true) return member as { bool: true };
161
179
  throw new TypeError(`member "${name}": expected a scalar token, { string: n }, { bool: true }, or a zod schema`);
@@ -182,7 +200,7 @@ export function memberToSpec(member: ListMember, name: string): ListFieldSpec {
182
200
 
183
201
  /** The declared member map of a zod object, or null when `schema` isn't one. */
184
202
  export function zodObjectShape(schema: unknown): ListFields | null {
185
- if (!(schema instanceof z.ZodObject)) return null;
203
+ if (!isZodSchema(schema) || schema._zod.def.type !== 'object') return null;
186
204
  return (schema._zod.def as unknown as { shape: ListFields }).shape;
187
205
  }
188
206
 
@@ -192,7 +210,7 @@ export function zodObjectShape(schema: unknown): ListFields | null {
192
210
  * schemas; `capacity` is null for unbounded arrays.
193
211
  */
194
212
  export function zodArrayInfo(schema: unknown): { element: z.ZodTypeAny; capacity: number | null } | null {
195
- if (!(schema instanceof z.ZodArray)) return null;
213
+ if (!isZodSchema(schema) || schema._zod.def.type !== 'array') return null;
196
214
  const def = schema._zod.def as unknown as ZodMemberDef & { element: z.ZodTypeAny };
197
215
  let capacity: number | null = null;
198
216
  for (const c of def.checks ?? []) {
@@ -203,14 +221,18 @@ export function zodArrayInfo(schema: unknown): { element: z.ZodTypeAny; capacity
203
221
  return { element: def.element, capacity };
204
222
  }
205
223
 
206
- /** `.meta()?.bytes` — optional-call: mini schemas ($ZodString too) have no `.meta` method. */
224
+ /** Byte budget carried in the schema's meta — atoll-minted schemas register
225
+ * into the local `globalRegistry`; classic zod's `.meta()` reads its own
226
+ * registry, so the `.meta?.()` call doubles as the consumer-zod channel. */
207
227
  function zodMetaBytes(schema: unknown): number | null {
208
- const meta = (schema as { meta?: () => { bytes?: unknown } }).meta?.();
228
+ const meta =
229
+ (z.globalRegistry.get(schema as z.ZodTypeAny) as { bytes?: unknown } | undefined) ??
230
+ (schema as { meta?: () => { bytes?: unknown } }).meta?.();
209
231
  return typeof meta?.bytes === 'number' ? meta.bytes : null;
210
232
  }
211
233
 
212
- /** The byte budget carried by `mz.string(n)` / `z.string().meta({ bytes: n })` — null otherwise. */
234
+ /** The byte budget carried by `reef.string(n)` / `z.string().meta({ bytes: n })` — null otherwise. */
213
235
  export function zodStringBytes(schema: unknown): number | null {
214
- if (!(schema instanceof z.ZodString)) return null;
236
+ if (!isZodSchema(schema) || schema._zod.def.type !== 'string') return null;
215
237
  return zodMetaBytes(schema);
216
238
  }
@@ -0,0 +1,62 @@
1
+ import { z, fluent } from './zod';
2
+
3
+ const utf8 = new TextEncoder();
4
+
5
+ /**
6
+ * Reef — the atoll's structural barrier: the fixed-width schema
7
+ * vocabulary standing between your data and the raw buffer. Every helper
8
+ * mints a schema the layout compiler can read a byte width from: number
9
+ * formats (`reef.u32()`), int bounds (`reef.int(0, 3)` → u8), byte budgets
10
+ * (`reef.string(10)`), or flags (`reef.boolean()` → u8). There are no unfixed
11
+ * members — `reef.number()`/`z.date()`/`z.enum()` aren't exposed because they
12
+ * can't promise a width.
13
+ *
14
+ * Schemas are minted by the vendored engine in `./zod.ts` — a zod-shaped
15
+ * `_zod.def` surface (`type`/`format`/`checks`/`shape`/`element`), so no
16
+ * zod dependency ships. Width coverage: int32/uint32/float32/float64/
17
+ * int64/uint64 are format-tagged; reef fills the gaps (i8/u8/i16/u16 via
18
+ * bounded ints, which the compiler narrows identically) so all ten scalar
19
+ * kinds have one spelling.
20
+ *
21
+ * Composition derives byte width upward — `reef.object({...})` members lay out
22
+ * as a fixed record (field.object derives byteLength from it, no maxBytes),
23
+ * `reef.array(el).max(n)` bounds element capacity for field.array, and
24
+ * `reef.string(n)` carries the budget for field.string. Schemas keep the
25
+ * classic chain spellings — `.meta()`, `.refine()`, `.max()`/`.length()` —
26
+ * and consumer-supplied zod schemas introspect identically through their
27
+ * `_zod.def`, so `reef.object({ id: z.uint32() })` mixes freely.
28
+ */
29
+ export const reef = {
30
+ i8: () => fluent(z.number().check(z.int(), z.gte(-128), z.lte(127))),
31
+ u8: () => fluent(z.number().check(z.int(), z.gte(0), z.lte(255))),
32
+ i16: () => fluent(z.number().check(z.int(), z.gte(-32768), z.lte(32767))),
33
+ u16: () => fluent(z.number().check(z.int(), z.gte(0), z.lte(65535))),
34
+ i32: () => fluent(z.int32()),
35
+ u32: () => fluent(z.uint32()),
36
+ f32: () => fluent(z.float32()),
37
+ f64: () => fluent(z.float64()),
38
+ i64: () => fluent(z.int64()),
39
+ u64: () => fluent(z.uint64()),
40
+ /** Bounded int — compiles to the narrowest covering kind (0..3 → u8). */
41
+ int: (min: number, max: number) => fluent(z.number().check(z.int(), z.gte(min), z.lte(max))),
42
+ /** A flag byte — reads and writes as boolean. */
43
+ boolean: () => fluent(z.boolean()),
44
+ /** Fixed-capacity inline UTF-8 string — `bytes` is the byte budget. The
45
+ * schema itself refuses over-budget values, so `.parse` and connector
46
+ * writes enforce the same limit. */
47
+ string: (bytes: number) =>
48
+ fluent(
49
+ z.string()
50
+ .check(
51
+ z.refine(
52
+ (s: string) => utf8.encode(s).byteLength <= bytes,
53
+ `exceeds ${bytes} UTF-8 bytes`
54
+ )
55
+ )
56
+ .register(z.globalRegistry, { bytes })
57
+ ),
58
+ /** A fixed-layout record shape — every member must be a fixed-width schema. */
59
+ object: <S extends z.ZodRawShape>(shape: S) => fluent(z.object(shape)),
60
+ /** A fixed-stride element — bound it with `.max(n)` or `.length(n)` before field.array. */
61
+ array: <E extends z.ZodTypeAny>(element: E) => fluent(z.array(element)),
62
+ };
@@ -2,7 +2,7 @@ import type { Prettify, Schema } from './types';
2
2
  import { msgpackrCodec } from './msgpackrCodec';
3
3
  import { memberToSpec, zodArrayInfo, zodObjectShape, zodStringBytes } from './listSchema';
4
4
  import { fmtBytes, scoped } from '../log';
5
- import { z } from './zod';
5
+ import { z, fluent } from './zod';
6
6
 
7
7
  const memLog = scoped('memory');
8
8
 
@@ -156,7 +156,7 @@ export type ListSpec = Record<string, ListFieldSpec>;
156
156
  /**
157
157
  * A fixed-width member inside a record or element position — a binary spec
158
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)`).
159
+ * expresses the same thing (`reef.u32()`, `reef.int(0, 3)`, `reef.string(n)`).
160
160
  * Schemas are compiled to tokens for layout and kept as the field's
161
161
  * validation schema — one declaration.
162
162
  */
@@ -170,7 +170,7 @@ export interface ListDescriptor<S extends z.ZodObject<z.ZodRawShape> = z.ZodObje
170
170
  extends Omit<FieldDescriptor<ListRecord<S>[]>, 'schema'> {
171
171
  readonly kind: 'list';
172
172
  /**
173
- * The declared record schema — an `mz.object` of fixed-width members. It
173
+ * The declared record schema — a `reef.object` of fixed-width members. It
174
174
  * IS the layout source AND the record validator (`schemas.lists.x` is this
175
175
  * same object). Note it parses one record, not the whole array.
176
176
  */
@@ -290,7 +290,7 @@ export interface FixedArrayDescriptor<T = unknown> extends FieldDescriptor<T[]>
290
290
  interface ObjectFieldFactory {
291
291
  /**
292
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()`);
293
+ * (`reef` helpers, `z.uint32()`, bounded ints, `reef.string(n)`, `z.boolean()`);
294
294
  * `byteLength` derives from the member widths. Compile fails loudly for
295
295
  * members with no fixed width (unbounded strings, nested objects).
296
296
  */
@@ -322,9 +322,9 @@ const objectFieldImpl: ObjectFieldFactory = (options: { schema?: Schema<unknown>
322
322
  interface ArrayFieldFactory {
323
323
  /**
324
324
  * Inline fixed layout — the schema is a bounded zod array
325
- * (`mz.array(mz.u32()).max(n)`, `.length(n)`); `byteLength` derives from
325
+ * (`reef.array(reef.u32()).max(n)`, `.length(n)`); `byteLength` derives from
326
326
  * the element width × the bound. Elements must be fixed-width members
327
- * (scalars, `mz.string(n)`, booleans) — use `field.list` for record arrays.
327
+ * (scalars, `reef.string(n)`, booleans) — use `field.list` for record arrays.
328
328
  */
329
329
  <S extends z.ZodArray>(options: { schema: S }): FixedArrayDescriptor<S extends z.ZodArray<infer E> ? z.output<E> : unknown>;
330
330
  /** Codec-encoded blob — explicit byte budget for arbitrary payloads. */
@@ -363,7 +363,7 @@ const arrayFieldImpl: ArrayFieldFactory = (options: { schema?: Schema<unknown>;
363
363
  interface StringFieldFactory {
364
364
  /** Explicit byte budget — 4-byte length header + up to `maxBytes` of UTF-8. */
365
365
  (options: { maxBytes: number; schema?: Schema<string> }): FieldDescriptor<string>;
366
- /** Budget derived from the schema — `mz.string(n)` / `z.string().meta({ bytes: n })`. */
366
+ /** Budget derived from the schema — `reef.string(n)` / `z.string().meta({ bytes: n })`. */
367
367
  (options: { schema: Schema<string> }): FieldDescriptor<string>;
368
368
  }
369
369
 
@@ -373,7 +373,7 @@ const stringFieldImpl: StringFieldFactory = (options: { schema?: Schema<string>;
373
373
  const bytes = schema ? zodStringBytes(schema) : null;
374
374
  if (bytes !== null) return { kind: 'string', byteLength: 4 + bytes, schema };
375
375
  throw new TypeError(
376
- `field.string: pass { maxBytes } or a schema carrying a byte budget — mz.string(n) / z.string().meta({ bytes: n })`
376
+ `field.string: pass { maxBytes } or a schema carrying a byte budget — reef.string(n) / z.string().meta({ bytes: n })`
377
377
  );
378
378
  };
379
379
 
@@ -391,16 +391,16 @@ export const field = {
391
391
  bigInt64Array: ({ length }: { length: number }): FieldDescriptor<BigInt64Array> => ({ kind: 'BigInt64', byteLength: length * 8 }),
392
392
  uint8Array: ({ length }: { length: number }): FieldDescriptor<Uint8Array> => ({ kind: 'Uint8', byteLength: length }),
393
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`
394
+ * A fixed-layout array of `count` records. `schema` is a `reef.object` of
395
+ * fixed-width members (`reef.u32()`, `reef.int(0, 3)`, `reef.string(n)`,
396
+ * `reef.boolean()`) — the same schema `field.object` takes, repeated `count`
397
397
  * times. Every record occupies `recordSize` bytes at a computable offset:
398
398
  * reads and writes are direct memory access, no serialization.
399
399
  */
400
400
  list: <S extends z.ZodObject<z.ZodRawShape>>({ schema, count }: { schema: S; count: number }): ListDescriptor<S> => {
401
401
  const shape = zodObjectShape(schema);
402
402
  if (!shape) {
403
- throw new TypeError(`field.list: schema must be an mz.object() of fixed-width members`);
403
+ throw new TypeError(`field.list: schema must be a reef.object() of fixed-width members`);
404
404
  }
405
405
  const { layout, offsets, recordSize } = compileListLayout(shape);
406
406
  return {
@@ -640,7 +640,7 @@ registerConnectorFactory('object', structuredFactory);
640
640
  registerConnectorFactory('array', structuredFactory);
641
641
 
642
642
  function listFactory(d: FieldDescriptor, ctx: ConnectorContext, byteOffset: number): ListConnector<any> {
643
- const info = d as ListDescriptor;
643
+ const info = d as unknown as ListDescriptor;
644
644
  const { offsets, recordSize, count } = info;
645
645
  // `layout` is the compiled token map; `schema` stays the validation schema.
646
646
  const record = makeIO(ctx).compile(info.layout, offsets);
@@ -694,7 +694,7 @@ registerConnectorFactory('list', listFactory);
694
694
  const definedSharedMemories: SharedMemory<any>[] = [];
695
695
 
696
696
  /**
697
- * The zod schema for a field's logical unit: the declared `schema` for
697
+ * The schema for a field's logical unit: the declared `schema` for
698
698
  * object/array/list fields (for lists, the record schema), and a derived
699
699
  * scalar/typed-array schema for everything else.
700
700
  */
@@ -704,7 +704,7 @@ export type FieldSchema<D> =
704
704
  : z.ZodType<InferField<D>>;
705
705
 
706
706
  /**
707
- * A zod schema per field — `memory.schemas.<field>` re-exports what the spec
707
+ * A schema per field — `memory.schemas.<field>` re-exports what the spec
708
708
  * declared inline. Intent groups mirror their spec shape:
709
709
  * `memory.schemas.lists.incidents` is the record schema.
710
710
  */
@@ -733,14 +733,14 @@ function buildSchemas<S extends SharedSpec>(spec: S): SpecSchemas<S> {
733
733
  function fieldSchemaOf(descriptor: FieldDescriptor): Schema<unknown> {
734
734
  if (descriptor.schema) return descriptor.schema;
735
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();
736
+ case 'number': return fluent(z.number());
737
+ case 'boolean': return fluent(z.boolean());
738
+ case 'string': return fluent(z.string());
739
+ case 'Int32': return fluent(z.instanceof(Int32Array));
740
+ case 'Float64': return fluent(z.instanceof(Float64Array));
741
+ case 'BigInt64': return fluent(z.instanceof(BigInt64Array));
742
+ case 'Uint8': return fluent(z.instanceof(Uint8Array));
743
+ default: return fluent(z.unknown());
744
744
  }
745
745
  }
746
746
 
@@ -755,9 +755,9 @@ export class SharedMemory<S extends SharedSpec = SharedSpec> {
755
755
  /** The field descriptors the contract was defined with — the inline spec. */
756
756
  public readonly spec: S;
757
757
  /**
758
- * One zod schema per field, derived from the spec: declared schemas on
758
+ * One schema per field, derived from the spec: declared schemas on
759
759
  * object/array fields, the record schema on list fields, and derived
760
- * scalar schemas (z.number(), z.string(), …) elsewhere.
760
+ * scalar schemas elsewhere.
761
761
  */
762
762
  public readonly schemas: SpecSchemas<S>;
763
763
  private readonly codec: Codec;