@atolljs/core 0.1.0 → 0.1.1

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 (51) hide show
  1. package/README.md +159 -59
  2. package/dist/contract/listSchema.d.ts +47 -0
  3. package/dist/contract/msgpackCodec.d.ts +7 -0
  4. package/dist/contract/msgpackrCodec.d.ts +2 -0
  5. package/dist/contract/mz.d.ts +43 -0
  6. package/dist/contract/sharedMemory.d.ts +350 -0
  7. package/dist/contract/types.d.ts +131 -0
  8. package/dist/contract/zod.d.ts +53 -0
  9. package/dist/index.d.ts +30 -0
  10. package/dist/index.js +18 -18
  11. package/dist/index10.js +14 -178
  12. package/dist/index11.js +188 -47
  13. package/dist/index12.js +45 -59
  14. package/dist/index13.js +59 -20
  15. package/dist/index14.js +24 -60
  16. package/dist/index15.js +56 -43
  17. package/dist/index16.js +45 -41
  18. package/dist/index17.js +42 -38
  19. package/dist/index18.js +36 -55
  20. package/dist/index19.js +52 -47
  21. package/dist/index20.js +48 -51
  22. package/dist/index21.js +59 -0
  23. package/dist/index3.js +24 -117
  24. package/dist/index4.js +116 -34
  25. package/dist/index5.js +32 -473
  26. package/dist/index6.js +489 -19
  27. package/dist/index7.js +19 -6
  28. package/dist/index8.js +6 -33
  29. package/dist/index9.js +29 -14
  30. package/dist/log.d.ts +35 -0
  31. package/dist/observable.d.ts +30 -0
  32. package/dist/pool/errors.d.ts +16 -0
  33. package/dist/pool/memory.d.ts +19 -0
  34. package/dist/pool/workerClient.d.ts +80 -0
  35. package/dist/pool/workerPool.d.ts +115 -0
  36. package/dist/reactive.d.ts +46 -0
  37. package/dist/service.d.ts +181 -0
  38. package/dist/shared/sharedWorkerClient.d.ts +35 -0
  39. package/dist/shared/sharedWorkerHost.d.ts +21 -0
  40. package/dist/task.d.ts +40 -0
  41. package/dist/worker/defineWorker.d.ts +63 -0
  42. package/dist/worker/registry.d.ts +8 -0
  43. package/dist/worker/workerBootstrap.d.ts +1 -0
  44. package/package.json +14 -2
  45. package/src/contract/listSchema.ts +13 -8
  46. package/src/contract/mz.ts +1 -1
  47. package/src/contract/sharedMemory.ts +28 -1
  48. package/src/contract/types.ts +21 -0
  49. package/src/contract/zod.ts +44 -0
  50. package/src/index.ts +2 -1
  51. package/src/pool/workerPool.ts +27 -1
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "@atolljs/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
+ "description": "Typed shared-memory worker pools — defineWorker/connectWorker, SharedArrayBuffer contracts, cross-thread reactivity",
5
+ "homepage": "https://jwhenry3.github.io/atolljs/consumer/#/quickstart",
4
6
  "type": "module",
5
7
  "main": "./dist/index.js",
6
8
  "types": "./dist/index.d.ts",
@@ -28,8 +30,11 @@
28
30
  "serve:all": "node scripts/serve-all.mjs",
29
31
  "build": "tsc --noEmit && vite build",
30
32
  "test": "vitest run",
33
+ "stats": "node scripts/bundle-stats.mjs",
34
+ "stats:islands": "node scripts/island-perf.mjs",
31
35
  "build:pages": "node scripts/build-pages.mjs",
32
- "assemble:pages": "node scripts/assemble-pages.mjs"
36
+ "assemble:pages": "node scripts/assemble-pages.mjs",
37
+ "serve:pages": "node scripts/serve-pages.mjs"
33
38
  },
34
39
  "devDependencies": {
35
40
  "@angular/core": "^22.2.0",
@@ -64,5 +69,12 @@
64
69
  "repository": {
65
70
  "type": "git",
66
71
  "url": "https://github.com/jwhenry3/atolljs"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public",
75
+ "registry": "https://registry.npmjs.org"
76
+ },
77
+ "bugs": {
78
+ "url": "https://github.com/jwhenry3/atolljs/issues"
67
79
  }
68
80
  }
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import { z } from './zod';
2
2
  import type {
3
3
  ScalarKind,
4
4
  ListFieldSpec,
@@ -71,7 +71,7 @@ export function listSchema<F extends ListFields>(fields: F): z.ZodObject<ListZod
71
71
  function memberToSchema(member: ListMember): z.ZodTypeAny {
72
72
  if (member instanceof z.ZodType) {
73
73
  const bytes =
74
- member.def?.type === 'string' ? (member as z.ZodString).meta()?.bytes : undefined;
74
+ member._zod.def?.type === 'string' ? zodMetaBytes(member) : undefined;
75
75
  return typeof bytes === 'number'
76
76
  ? member.refine((s) => utf8.encode(s as string).byteLength <= bytes, `exceeds ${bytes} UTF-8 bytes`)
77
77
  : member;
@@ -160,12 +160,12 @@ export function memberToSpec(member: ListMember, name: string): ListFieldSpec {
160
160
  if ((member as { bool?: unknown }).bool === true) return member as { bool: true };
161
161
  throw new TypeError(`member "${name}": expected a scalar token, { string: n }, { bool: true }, or a zod schema`);
162
162
  }
163
- const def = member.def as ZodMemberDef;
163
+ const def = member._zod.def as unknown as ZodMemberDef;
164
164
  if (def.type === 'number') return numberMemberToSpec(def);
165
165
  if (def.type === 'bigint') return BIGINT_FORMATS[def.format ?? ''] ?? 'i64';
166
166
  if (def.type === 'boolean') return { bool: true };
167
167
  if (def.type === 'string') {
168
- const bytes = (member as z.ZodString).meta()?.bytes;
168
+ const bytes = zodMetaBytes(member);
169
169
  if (typeof bytes === 'number') return { string: bytes };
170
170
  throw new TypeError(
171
171
  `member "${name}": z.string() needs .meta({ bytes: n }) — inline strings are fixed-width`
@@ -183,7 +183,7 @@ export function memberToSpec(member: ListMember, name: string): ListFieldSpec {
183
183
  /** The declared member map of a zod object, or null when `schema` isn't one. */
184
184
  export function zodObjectShape(schema: unknown): ListFields | null {
185
185
  if (!(schema instanceof z.ZodObject)) return null;
186
- return (schema.def as { shape: ListFields }).shape;
186
+ return (schema._zod.def as unknown as { shape: ListFields }).shape;
187
187
  }
188
188
 
189
189
  /**
@@ -193,7 +193,7 @@ export function zodObjectShape(schema: unknown): ListFields | null {
193
193
  */
194
194
  export function zodArrayInfo(schema: unknown): { element: z.ZodTypeAny; capacity: number | null } | null {
195
195
  if (!(schema instanceof z.ZodArray)) return null;
196
- const def = schema.def as ZodMemberDef & { element: z.ZodTypeAny };
196
+ const def = schema._zod.def as unknown as ZodMemberDef & { element: z.ZodTypeAny };
197
197
  let capacity: number | null = null;
198
198
  for (const c of def.checks ?? []) {
199
199
  const cd = checkDef(c);
@@ -203,9 +203,14 @@ export function zodArrayInfo(schema: unknown): { element: z.ZodTypeAny; capacity
203
203
  return { element: def.element, capacity };
204
204
  }
205
205
 
206
+ /** `.meta()?.bytes` — optional-call: mini schemas ($ZodString too) have no `.meta` method. */
207
+ function zodMetaBytes(schema: unknown): number | null {
208
+ const meta = (schema as { meta?: () => { bytes?: unknown } }).meta?.();
209
+ return typeof meta?.bytes === 'number' ? meta.bytes : null;
210
+ }
211
+
206
212
  /** The byte budget carried by `mz.string(n)` / `z.string().meta({ bytes: n })` — null otherwise. */
207
213
  export function zodStringBytes(schema: unknown): number | null {
208
214
  if (!(schema instanceof z.ZodString)) return null;
209
- const bytes = schema.meta()?.bytes;
210
- return typeof bytes === 'number' ? bytes : null;
215
+ return zodMetaBytes(schema);
211
216
  }
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import { z } from './zod';
2
2
 
3
3
  /**
4
4
  * Atoll zod — the fixed-width schema vocabulary. Every helper produces a
@@ -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 } from './zod';
6
6
 
7
7
  const memLog = scoped('memory');
8
8
 
@@ -768,6 +768,7 @@ export class SharedMemory<S extends SharedSpec = SharedSpec> {
768
768
  private readonly connectors = new Map<string, Connector<any>>();
769
769
  private readonly boundListeners = new Set<() => void>();
770
770
  private isBound = false;
771
+ private boundBuffer?: SharedArrayBuffer;
771
772
 
772
773
  constructor(spec: S, options: SharedMemoryOptions = {}) {
773
774
  this.spec = spec;
@@ -818,6 +819,7 @@ export class SharedMemory<S extends SharedSpec = SharedSpec> {
818
819
  });
819
820
  });
820
821
  this.isBound = true;
822
+ this.boundBuffer = buffer;
821
823
  for (const cb of [...this.boundListeners]) cb();
822
824
  }
823
825
 
@@ -826,6 +828,31 @@ export class SharedMemory<S extends SharedSpec = SharedSpec> {
826
828
  return this.isBound;
827
829
  }
828
830
 
831
+ /**
832
+ * The buffer this contract is bound to on this thread — the raw backing
833
+ * store persistence adapters and snapshot tooling read/write through.
834
+ * Throws while unbound.
835
+ */
836
+ public get buffer(): SharedArrayBuffer {
837
+ if (!this.boundBuffer) {
838
+ throw new Error('Shared memory contract is not bound to a buffer on this thread.');
839
+ }
840
+ return this.boundBuffer;
841
+ }
842
+
843
+ /**
844
+ * Every field's address in the layout — `{ path, byteOffset, byteLength }`.
845
+ * Persistence adapters use this to read/write field regions directly and
846
+ * to pair each region with its `connector(path)._version` slot.
847
+ */
848
+ public fields(): { path: string; byteOffset: number; byteLength: number }[] {
849
+ return this.entries.map(({ path, descriptor }) => ({
850
+ path,
851
+ byteOffset: this.offsets.get(path)!,
852
+ byteLength: descriptor.byteLength,
853
+ }));
854
+ }
855
+
829
856
  /**
830
857
  * Fires when the contract is bound on this thread — immediately if already
831
858
  * bound. Returns an unsubscribe function.
@@ -26,6 +26,21 @@ export interface MemoryConfig {
26
26
  growthFactor?: number;
27
27
  }
28
28
 
29
+ /**
30
+ * Runtime handle for a memory-persistence adapter — e.g. the Redis adapter
31
+ * in '@atolljs/node/redis'. The adapter mirrors the bound contract's field
32
+ * regions to external storage; the buffer stays the synchronous source of
33
+ * truth.
34
+ */
35
+ export interface MemoryPersistence {
36
+ /** Resolves when initial state has been restored into the local buffer. */
37
+ readonly ready?: Promise<unknown>;
38
+ /** Push dirty fields now — most adapters also flush on an interval. */
39
+ flush?(): Promise<unknown>;
40
+ /** Final flush + release external resources. Invoked by pool.terminate(). */
41
+ stop(): void | Promise<void>;
42
+ }
43
+
29
44
  export interface WorkerPoolConfig<S extends SharedSpec = SharedSpec, T extends TaskMap = TaskMap> {
30
45
  /** Worker entry as a URL — e.g. `new URL('./worker.ts', import.meta.url)`. */
31
46
  workerUrl?: URL;
@@ -42,6 +57,12 @@ export interface WorkerPoolConfig<S extends SharedSpec = SharedSpec, T extends T
42
57
  * skipped and workers receive a plain `{ type: 'INIT' }` handshake.
43
58
  */
44
59
  sharedMemory?: SharedMemory<S> & Prettify<SharedAccess<S>>;
60
+ /**
61
+ * Persistence adapter for `sharedMemory` — invoked with the contract right
62
+ * after the pool binds it, and its `stop()` runs on `terminate()`.
63
+ * `redisMemoryAdapter(client)` from '@atolljs/node/redis' produces one.
64
+ */
65
+ persistence?: (memory: SharedMemory<S>) => MemoryPersistence | undefined;
45
66
  /** Worker count — a number, or 'auto' (the default) for navigator.hardwareConcurrency ?? 4. */
46
67
  poolSize?: number | 'auto';
47
68
  memory?: MemoryConfig;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The SDK's vendored zod surface — everything the contract layer needs,
3
+ * narrowed.
4
+ *
5
+ * `import { z } from 'zod'` bundles the ENTIRE classic namespace in consumer
6
+ * builds (~440KB minified): the `z` object can't be property-shaken, so it
7
+ * drags in every locale, the JSON-schema processors, and the compile
8
+ * pipeline. Named imports tree-shake (~93KB for the full set below).
9
+ *
10
+ * `instanceof` guards use the `$Zod*` base classes from `zod/v4/core` —
11
+ * classic AND mini schemas both extend them, so user-supplied schemas match
12
+ * either way. Constructors stay classic (`zod` named imports) because `mz`
13
+ * output and `memory.schemas` are documented as ordinary zod — `.meta()`,
14
+ * `.refine()`, `.parse()` must all work and compose with the consumer's own
15
+ * `z.object(...)`.
16
+ */
17
+ import {
18
+ array, bigint, boolean, instanceof as instanceof_,
19
+ int32, int64, float32, float64, number, object, string, uint32, uint64,
20
+ unknown as unknown_,
21
+ } from 'zod';
22
+ import { $ZodArray, $ZodObject, $ZodString, $ZodType } from 'zod/v4/core';
23
+ import type * as zod from 'zod';
24
+
25
+ export const z = {
26
+ number, string, boolean, bigint, object, array,
27
+ instanceof: instanceof_, unknown: unknown_,
28
+ int32, uint32, float32, float64, int64, uint64,
29
+ /** Core classes — instanceof-matching for user schemas (classic or mini). */
30
+ ZodType: $ZodType, ZodObject: $ZodObject, ZodArray: $ZodArray, ZodString: $ZodString,
31
+ };
32
+
33
+ export namespace z {
34
+ /* Type surface — the classic zod types the contract layer annotates with.
35
+ * Value positions above are the runtime; these are the types the public
36
+ * signatures speak in (users pass real classic schemas). */
37
+ export type ZodTypeAny = zod.ZodTypeAny;
38
+ export type ZodType<Output = unknown, Input = unknown> = zod.ZodType<Output, Input>;
39
+ export type ZodString = zod.ZodString;
40
+ export type ZodArray<T extends zod.ZodTypeAny = zod.ZodTypeAny> = zod.ZodArray<T>;
41
+ export type ZodObject<S extends zod.ZodRawShape = zod.ZodRawShape> = zod.ZodObject<S>;
42
+ export type ZodRawShape = zod.ZodRawShape;
43
+ export type output<T extends zod.ZodTypeAny> = zod.output<T>;
44
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /* ── Contract — shared memory declarations (both threads) ───────────────── */
2
2
 
3
- export { defineSharedMemory, field, getDefinedSharedMemoryCount, jsonCodec, registerConnectorFactory } from './contract/sharedMemory';
3
+ export { bindSharedMemories, defineSharedMemory, field, getDefinedSharedMemoryCount, jsonCodec, registerConnectorFactory } from './contract/sharedMemory';
4
4
  export { mz } from './contract/mz';
5
5
  export type {
6
6
  Codec,
@@ -44,6 +44,7 @@ export type {
44
44
  TaskMap,
45
45
  TaskMessage,
46
46
  TaskResult,
47
+ MemoryPersistence,
47
48
  SharedWorkerConfig,
48
49
  ViewType,
49
50
  WorkerPoolConfig,
@@ -1,7 +1,7 @@
1
1
  import { MemoryManager } from './memory';
2
2
  import { PoolQueueFullError, TaskAbortedError, TaskTimeoutError, WorkerCrashedError } from './errors';
3
3
  import { SharedAccess, SharedMemory, SharedSpec } from '../contract/sharedMemory';
4
- import { PoolTasks, TaskContract, TaskMap, TaskResult, WorkerPoolConfig } from '../contract/types';
4
+ import { MemoryPersistence, PoolTasks, TaskContract, TaskMap, TaskResult, WorkerPoolConfig } from '../contract/types';
5
5
  import { fmtBytes, scoped } from '../log';
6
6
 
7
7
  const poolLog = scoped('pool');
@@ -101,6 +101,8 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
101
101
  runMs: agg(),
102
102
  };
103
103
  public readonly sharedMemory: (SharedMemory<S> & SharedAccess<S>) | undefined;
104
+ /** The attached memory-persistence adapter, if the config supplied one. */
105
+ public readonly persistence: MemoryPersistence | undefined;
104
106
 
105
107
  constructor(config: WorkerPoolConfig<S, TaskMap>) {
106
108
  if (config.sharedMemory) {
@@ -125,6 +127,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
125
127
  this.memoryManager = new MemoryManager(config.memory);
126
128
  this.memoryManager.ensureCapacity(this.sharedMemory.totalBytes);
127
129
  this.sharedMemory.bind(this.memoryManager.getBuffer());
130
+ this.persistence = config.persistence?.(config.sharedMemory);
128
131
  }
129
132
 
130
133
  const requestedSize = config.poolSize ?? 'auto';
@@ -367,6 +370,25 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
367
370
  return this.dispatch(contract, args);
368
371
  }
369
372
 
373
+ /**
374
+ * The shared buffer handed to workers via INIT_MEMORY — undefined on
375
+ * message-only pools. Useful for passing the same buffer to auxiliaries
376
+ * (e.g. a second pool of HTTP workers via withSharedBuffer).
377
+ */
378
+ public get sharedBuffer(): SharedArrayBuffer | undefined {
379
+ return this.memoryManager?.getBuffer();
380
+ }
381
+
382
+ /**
383
+ * Live snapshot of the pool's worker slots — for auxiliary messaging that
384
+ * isn't task dispatch (e.g. routing accepted sockets into workers). The
385
+ * array is a copy; respawned workers appear on the next read. Do NOT use
386
+ * this to dispatch tasks — runTask/dispatch own the scheduling protocol.
387
+ */
388
+ public get workers(): readonly Worker[] {
389
+ return this.slots.map((s) => s.worker);
390
+ }
391
+
370
392
  /** Running aggregates — no histograms, just counts and means. */
371
393
  public stats(): PoolStats {
372
394
  return {
@@ -398,6 +420,10 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
398
420
 
399
421
  public terminate(): void {
400
422
  this.closed = true;
423
+ // Final flush + release — fire-and-forget; terminate() stays synchronous.
424
+ void this.persistence?.stop()?.catch?.((e: unknown) =>
425
+ poolLog.warn('memory persistence stop failed', e),
426
+ );
401
427
  poolLog.info(`terminating ${this.slots.length} worker(s)`);
402
428
  const crashed = new WorkerCrashedError('pool terminated');
403
429
  for (const [, entry] of this.pending) {