@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.
- package/README.md +110 -0
- package/dist/index.js +19 -0
- package/dist/index10.js +186 -0
- package/dist/index11.js +52 -0
- package/dist/index12.js +66 -0
- package/dist/index13.js +27 -0
- package/dist/index14.js +63 -0
- package/dist/index15.js +50 -0
- package/dist/index16.js +46 -0
- package/dist/index17.js +42 -0
- package/dist/index18.js +61 -0
- package/dist/index19.js +56 -0
- package/dist/index2.js +10 -0
- package/dist/index20.js +59 -0
- package/dist/index3.js +120 -0
- package/dist/index4.js +40 -0
- package/dist/index5.js +481 -0
- package/dist/index6.js +23 -0
- package/dist/index7.js +10 -0
- package/dist/index8.js +37 -0
- package/dist/index9.js +22 -0
- package/package.json +68 -0
- package/src/contract/listSchema.ts +211 -0
- package/src/contract/msgpackCodec.ts +9 -0
- package/src/contract/msgpackrCodec.ts +16 -0
- package/src/contract/mz.ts +48 -0
- package/src/contract/sharedMemory.ts +909 -0
- package/src/contract/types.ts +123 -0
- package/src/index.ts +114 -0
- package/src/log.ts +69 -0
- package/src/observable.ts +151 -0
- package/src/pool/errors.ts +31 -0
- package/src/pool/memory.ts +68 -0
- package/src/pool/workerClient.ts +181 -0
- package/src/pool/workerPool.ts +438 -0
- package/src/reactive.ts +161 -0
- package/src/service.ts +283 -0
- package/src/shared/sharedWorkerClient.ts +142 -0
- package/src/shared/sharedWorkerHost.ts +104 -0
- package/src/task.ts +99 -0
- package/src/testing/inProcessWorker.ts +127 -0
- package/src/worker/defineWorker.ts +135 -0
- package/src/worker/registry.ts +42 -0
- package/src/worker/workerBootstrap.ts +56 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type {
|
|
3
|
+
ScalarKind,
|
|
4
|
+
ListFieldSpec,
|
|
5
|
+
ListFields,
|
|
6
|
+
ListMember,
|
|
7
|
+
} from './sharedMemory';
|
|
8
|
+
|
|
9
|
+
const utf8 = new TextEncoder();
|
|
10
|
+
|
|
11
|
+
/** Storage-bounded zod schemas per scalar kind — validation catches
|
|
12
|
+
* out-of-range values at the contract boundary instead of silently
|
|
13
|
+
* truncating at write time. */
|
|
14
|
+
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(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const byteBounded = (bytes: number) =>
|
|
28
|
+
z.string().refine(
|
|
29
|
+
(s) => utf8.encode(s).byteLength <= bytes,
|
|
30
|
+
`exceeds ${bytes} UTF-8 bytes`
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/** Maps a list member to the zod shape whose inferred output type is
|
|
34
|
+
* identical to `ListRecord<F>[K]` — zod members pass through as
|
|
35
|
+
* themselves so declared bounds (e.g. `.max(3)`) stay in effect. */
|
|
36
|
+
export type ListZodShape<F extends ListFields> = {
|
|
37
|
+
[K in keyof F]: F[K] extends z.ZodTypeAny
|
|
38
|
+
? F[K]
|
|
39
|
+
: F[K] extends { string: number }
|
|
40
|
+
? z.ZodType<string>
|
|
41
|
+
: F[K] extends { bool: boolean }
|
|
42
|
+
? z.ZodType<boolean>
|
|
43
|
+
: F[K] extends 'i64' | 'u64'
|
|
44
|
+
? z.ZodType<bigint>
|
|
45
|
+
: z.ZodType<number>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 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
|
|
51
|
+
* spec tokens or zod schemas; the result's `z.infer` is the record type:
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* const shape = { id: 'u32', site: { string: 10 } } as const;
|
|
55
|
+
* const schema = listSchema(shape); // zod object with storage bounds
|
|
56
|
+
* type Row = z.infer<typeof schema>;
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* `{ string: n }` fields validate UTF-8 byte length, matching the inline
|
|
60
|
+
* capacity the connector enforces; zod `z.string().meta({ bytes: n })`
|
|
61
|
+
* members get the same bound attached to the declared schema.
|
|
62
|
+
*/
|
|
63
|
+
export function listSchema<F extends ListFields>(fields: F): z.ZodObject<ListZodShape<F>> {
|
|
64
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
65
|
+
for (const key of Object.keys(fields)) {
|
|
66
|
+
shape[key] = memberToSchema(fields[key]);
|
|
67
|
+
}
|
|
68
|
+
return z.object(shape) as unknown as z.ZodObject<ListZodShape<F>>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function memberToSchema(member: ListMember): z.ZodTypeAny {
|
|
72
|
+
if (member instanceof z.ZodType) {
|
|
73
|
+
const bytes =
|
|
74
|
+
member.def?.type === 'string' ? (member as z.ZodString).meta()?.bytes : undefined;
|
|
75
|
+
return typeof bytes === 'number'
|
|
76
|
+
? member.refine((s) => utf8.encode(s as string).byteLength <= bytes, `exceeds ${bytes} UTF-8 bytes`)
|
|
77
|
+
: member;
|
|
78
|
+
}
|
|
79
|
+
return typeof member === 'string'
|
|
80
|
+
? SCALAR_SCHEMAS[member]
|
|
81
|
+
: 'string' in member ? byteBounded(member.string) : z.boolean();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ------------------------------------------------------------------ */
|
|
85
|
+
/* zod members → binary layout */
|
|
86
|
+
/* ------------------------------------------------------------------ */
|
|
87
|
+
|
|
88
|
+
const NUMBER_FORMATS: Record<string, ScalarKind> = {
|
|
89
|
+
int32: 'i32',
|
|
90
|
+
uint32: 'u32',
|
|
91
|
+
float32: 'f32',
|
|
92
|
+
float64: 'f64',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const BIGINT_FORMATS: Record<string, ScalarKind> = {
|
|
96
|
+
int64: 'i64',
|
|
97
|
+
uint64: 'u64',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Narrowest-first, unsigned-preferred — `min(0).max(3)` → 'u8'. */
|
|
101
|
+
const INT_BOUNDS: { kind: ScalarKind; min: number; max: number }[] = [
|
|
102
|
+
{ kind: 'u8', min: 0, max: 255 },
|
|
103
|
+
{ kind: 'i8', min: -128, max: 127 },
|
|
104
|
+
{ kind: 'u16', min: 0, max: 65535 },
|
|
105
|
+
{ kind: 'i16', min: -32768, max: 32767 },
|
|
106
|
+
{ kind: 'u32', min: 0, max: 4294967295 },
|
|
107
|
+
{ kind: 'i32', min: -2147483648, max: 2147483647 },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
interface ZodCheckDef {
|
|
111
|
+
check?: string;
|
|
112
|
+
value?: number;
|
|
113
|
+
inclusive?: boolean;
|
|
114
|
+
format?: string;
|
|
115
|
+
/** `max_length` check payload. */
|
|
116
|
+
maximum?: number;
|
|
117
|
+
/** `length_equals` check payload. */
|
|
118
|
+
length?: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface ZodMemberDef {
|
|
122
|
+
type?: string;
|
|
123
|
+
format?: string;
|
|
124
|
+
// check defs sit on `c.def` (ZodNumberFormat) or `c._zod.def` ($ZodCheck*)
|
|
125
|
+
checks?: { def?: ZodCheckDef; _zod?: { def?: ZodCheckDef } }[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const checkDef = (c: { def?: ZodCheckDef; _zod?: { def?: ZodCheckDef } }) => c.def ?? c._zod?.def;
|
|
129
|
+
|
|
130
|
+
function numberMemberToSpec(def: ZodMemberDef): ListFieldSpec {
|
|
131
|
+
const formats = [def.format, ...(def.checks ?? []).map((c) => checkDef(c)?.format)];
|
|
132
|
+
const direct = formats.find((f): f is string => !!f && f in NUMBER_FORMATS);
|
|
133
|
+
if (direct) return NUMBER_FORMATS[direct];
|
|
134
|
+
// Bounded integers narrow to the smallest covering kind — `z.int().min(0).max(3)`
|
|
135
|
+
// declares a domain (0..3) AND a storage width (u8) in one expression.
|
|
136
|
+
if (formats.includes('safeint')) {
|
|
137
|
+
let min = -Infinity;
|
|
138
|
+
let max = Infinity;
|
|
139
|
+
for (const c of def.checks ?? []) {
|
|
140
|
+
const cd = checkDef(c);
|
|
141
|
+
if (cd?.check === 'greater_than' && cd.inclusive) min = cd.value ?? min;
|
|
142
|
+
if (cd?.check === 'less_than' && cd.inclusive) max = cd.value ?? max;
|
|
143
|
+
}
|
|
144
|
+
for (const b of INT_BOUNDS) {
|
|
145
|
+
if (min >= b.min && max <= b.max) return b.kind;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return 'f64';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Compiles one fixed-layout member — a spec token or a zod schema — to its
|
|
153
|
+
* binary layout form. Throws for members that can't express a fixed-width
|
|
154
|
+
* layout.
|
|
155
|
+
*/
|
|
156
|
+
export function memberToSpec(member: ListMember, name: string): ListFieldSpec {
|
|
157
|
+
if (typeof member === 'string') return member;
|
|
158
|
+
if (!(member instanceof z.ZodType)) {
|
|
159
|
+
if (typeof (member as { string?: unknown }).string === 'number') return member as { string: number };
|
|
160
|
+
if ((member as { bool?: unknown }).bool === true) return member as { bool: true };
|
|
161
|
+
throw new TypeError(`member "${name}": expected a scalar token, { string: n }, { bool: true }, or a zod schema`);
|
|
162
|
+
}
|
|
163
|
+
const def = member.def as ZodMemberDef;
|
|
164
|
+
if (def.type === 'number') return numberMemberToSpec(def);
|
|
165
|
+
if (def.type === 'bigint') return BIGINT_FORMATS[def.format ?? ''] ?? 'i64';
|
|
166
|
+
if (def.type === 'boolean') return { bool: true };
|
|
167
|
+
if (def.type === 'string') {
|
|
168
|
+
const bytes = (member as z.ZodString).meta()?.bytes;
|
|
169
|
+
if (typeof bytes === 'number') return { string: bytes };
|
|
170
|
+
throw new TypeError(
|
|
171
|
+
`member "${name}": z.string() needs .meta({ bytes: n }) — inline strings are fixed-width`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
throw new TypeError(
|
|
175
|
+
`member "${name}": zod "${def.type}" can't lay out as a fixed-width scalar — use number, bigint, boolean, or a bounded string`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* ------------------------------------------------------------------ */
|
|
180
|
+
/* zod schema → fixed layout (introspection for field factories) */
|
|
181
|
+
/* ------------------------------------------------------------------ */
|
|
182
|
+
|
|
183
|
+
/** The declared member map of a zod object, or null when `schema` isn't one. */
|
|
184
|
+
export function zodObjectShape(schema: unknown): ListFields | null {
|
|
185
|
+
if (!(schema instanceof z.ZodObject)) return null;
|
|
186
|
+
return (schema.def as { shape: ListFields }).shape;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Element schema + capacity of a bounded zod array — `.max(n)` /
|
|
191
|
+
* `.length(n)` carry the bound in their checks. Returns null for non-array
|
|
192
|
+
* schemas; `capacity` is null for unbounded arrays.
|
|
193
|
+
*/
|
|
194
|
+
export function zodArrayInfo(schema: unknown): { element: z.ZodTypeAny; capacity: number | null } | null {
|
|
195
|
+
if (!(schema instanceof z.ZodArray)) return null;
|
|
196
|
+
const def = schema.def as ZodMemberDef & { element: z.ZodTypeAny };
|
|
197
|
+
let capacity: number | null = null;
|
|
198
|
+
for (const c of def.checks ?? []) {
|
|
199
|
+
const cd = checkDef(c);
|
|
200
|
+
if (cd?.check === 'max_length' && typeof cd.maximum === 'number') capacity = cd.maximum;
|
|
201
|
+
if (cd?.check === 'length_equals' && typeof cd.length === 'number') capacity = cd.length;
|
|
202
|
+
}
|
|
203
|
+
return { element: def.element, capacity };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The byte budget carried by `mz.string(n)` / `z.string().meta({ bytes: n })` — null otherwise. */
|
|
207
|
+
export function zodStringBytes(schema: unknown): number | null {
|
|
208
|
+
if (!(schema instanceof z.ZodString)) return null;
|
|
209
|
+
const bytes = schema.meta()?.bytes;
|
|
210
|
+
return typeof bytes === 'number' ? bytes : null;
|
|
211
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { decode, encode } from '@msgpack/msgpack';
|
|
2
|
+
import type { Codec } from './sharedMemory';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MessagePack transport codec: smaller payloads than JSON and supports types
|
|
6
|
+
* JSON can't express (Uint8Array, BigInt, etc.). Decoded values are still
|
|
7
|
+
* validated by each field's zod schema.
|
|
8
|
+
*/
|
|
9
|
+
export const msgpackCodec: Codec = { encode, decode };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Packr } from 'msgpackr';
|
|
2
|
+
import type { Codec } from './sharedMemory';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MessagePack via msgpackr, with `useRecords` — structure-sharing for repeated
|
|
6
|
+
* same-shape objects (field names encoded once, then compact record bodies).
|
|
7
|
+
* The fast path for large arrays of uniform records. Both threads get this
|
|
8
|
+
* codec from the shared contract, so the extension format stays consistent.
|
|
9
|
+
* Drop-in Codec: `defineSharedMemory(spec, { codec: msgpackrCodec })`.
|
|
10
|
+
*/
|
|
11
|
+
const packr = new Packr({ useRecords: true });
|
|
12
|
+
|
|
13
|
+
export const msgpackrCodec: Codec = {
|
|
14
|
+
encode: (value) => packr.pack(value),
|
|
15
|
+
decode: (data) => packr.unpack(data),
|
|
16
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Atoll zod — the fixed-width schema vocabulary. Every helper produces a
|
|
5
|
+
* plain zod schema the layout compiler can read a byte width from: number
|
|
6
|
+
* formats (`mz.u32()`), int bounds (`mz.int(0, 3)` → u8), byte budgets
|
|
7
|
+
* (`mz.string(10)`), or flags (`mz.boolean()` → u8). There are no unfixed
|
|
8
|
+
* members — `mz.number()`/`z.date()`/`z.enum()` aren't exposed because they
|
|
9
|
+
* can't promise a width.
|
|
10
|
+
*
|
|
11
|
+
* Width coverage: zod 4 provides int32/uint32/float32/float64/int64/uint64
|
|
12
|
+
* formats; mz fills the gaps (i8/u8/i16/u16 via bounded ints, which the
|
|
13
|
+
* compiler narrows identically) so all ten scalar kinds have one spelling.
|
|
14
|
+
*
|
|
15
|
+
* Composition derives byte width upward — `mz.object({...})` members lay out
|
|
16
|
+
* as a fixed record (field.object derives byteLength from it, no maxBytes),
|
|
17
|
+
* `mz.array(el).max(n)` bounds element capacity for field.array, and
|
|
18
|
+
* `mz.string(n)` carries the budget for field.string. The returned values are
|
|
19
|
+
* ordinary zod schemas — `.meta()`, `.refine()`, `.max()` all still work.
|
|
20
|
+
*/
|
|
21
|
+
export const mz = {
|
|
22
|
+
i8: () => z.number().int().min(-128).max(127),
|
|
23
|
+
u8: () => z.number().int().min(0).max(255),
|
|
24
|
+
i16: () => z.number().int().min(-32768).max(32767),
|
|
25
|
+
u16: () => z.number().int().min(0).max(65535),
|
|
26
|
+
i32: () => z.int32(),
|
|
27
|
+
u32: () => z.uint32(),
|
|
28
|
+
f32: () => z.float32(),
|
|
29
|
+
f64: () => z.float64(),
|
|
30
|
+
i64: () => z.int64(),
|
|
31
|
+
u64: () => z.uint64(),
|
|
32
|
+
/** Bounded int — compiles to the narrowest covering kind (0..3 → u8). */
|
|
33
|
+
int: (min: number, max: number) => z.number().int().min(min).max(max),
|
|
34
|
+
/** A flag byte — reads and writes as boolean. */
|
|
35
|
+
boolean: () => z.boolean(),
|
|
36
|
+
/** Fixed-capacity inline UTF-8 string — `bytes` is the byte budget. The
|
|
37
|
+
* schema itself refuses over-budget values, so `.parse` and connector
|
|
38
|
+
* writes enforce the same limit. */
|
|
39
|
+
string: (bytes: number) =>
|
|
40
|
+
z.string().refine(
|
|
41
|
+
(s) => new TextEncoder().encode(s).byteLength <= bytes,
|
|
42
|
+
`exceeds ${bytes} UTF-8 bytes`
|
|
43
|
+
).meta({ bytes }),
|
|
44
|
+
/** A fixed-layout record shape — every member must be a fixed-width schema. */
|
|
45
|
+
object: <S extends z.ZodRawShape>(shape: S) => z.object(shape),
|
|
46
|
+
/** A fixed-stride element — bound it with `.max(n)` or `.length(n)` before field.array. */
|
|
47
|
+
array: <E extends z.ZodTypeAny>(element: E) => z.array(element),
|
|
48
|
+
};
|