@atolljs/core 0.1.0 → 0.1.2

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 +167 -61
  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 +17 -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,30 @@
1
+ import type { Connector, PathConnector, SharedMemory, SharedSpec, SpecPath } from './contract/sharedMemory';
2
+ import { type SliceOptions } from './reactive';
3
+ /**
4
+ * A minimal subscribable snapshot — `get()` plus `subscribe()`. This is the
5
+ * interface every framework binding adapts (useSyncExternalStore, refs,
6
+ * signals, runes). `get()` must return a stable value between notifications.
7
+ */
8
+ export interface ObservableValue<T> {
9
+ get(): T;
10
+ subscribe(onChange: (value: T) => void): () => void;
11
+ }
12
+ type FieldValue<C> = C extends Connector<infer T> ? T : never;
13
+ /**
14
+ * Observe one field of a shared memory contract as a subscribable snapshot.
15
+ *
16
+ * Safe to call before the contract is bound — `get()` returns `undefined`
17
+ * until binding, and a subscriber registered early activates its watch when
18
+ * `bind()` lands (useful for SSR, where renders happen without a buffer).
19
+ * The underlying `watch` starts on the first subscriber and stops when the
20
+ * last one leaves.
21
+ */
22
+ export declare function observe<S extends SharedSpec, K extends SpecPath<S>>(memory: SharedMemory<S>, key: K): ObservableValue<FieldValue<PathConnector<S, K>> | undefined>;
23
+ /**
24
+ * Slice form: `observe(memory, 'metrics', m => m.critical)` — the observable
25
+ * emits only when the selected slice changes. Slice equality defaults to
26
+ * `Object.is`; pass `{ equals }` to define what counts as a change —
27
+ * `shallowEqual` for object slices, or any `(prev, next) => boolean`.
28
+ */
29
+ export declare function observe<S extends SharedSpec, K extends SpecPath<S>, Sel>(memory: SharedMemory<S>, key: K, select: (value: FieldValue<PathConnector<S, K>>) => Sel, options?: SliceOptions<Sel>): ObservableValue<Sel | undefined>;
30
+ export {};
@@ -0,0 +1,16 @@
1
+ /** Task queue is at `maxQueue` — dispatch rejected without ever running. */
2
+ export declare class PoolQueueFullError extends Error {
3
+ constructor(message?: string);
4
+ }
5
+ /** The task's AbortSignal fired — never dispatched, or reply discarded. */
6
+ export declare class TaskAbortedError extends Error {
7
+ constructor(message?: string);
8
+ }
9
+ /** The task exceeded its `timeout` / pool `taskTimeout` budget. */
10
+ export declare class TaskTimeoutError extends Error {
11
+ constructor(message?: string);
12
+ }
13
+ /** The worker errored/exited, or the pool was terminated mid-flight. */
14
+ export declare class WorkerCrashedError extends Error {
15
+ constructor(message?: string);
16
+ }
@@ -0,0 +1,19 @@
1
+ import { MemoryConfig, ViewType } from '../contract/types';
2
+ export declare class MemoryManager {
3
+ memory: WebAssembly.Memory;
4
+ private maxPages;
5
+ constructor(config?: MemoryConfig);
6
+ /**
7
+ * Automatically checks if memory needs to grow to accommodate new bytes,
8
+ * and expands via memory.grow() if necessary. Thread-safe.
9
+ */
10
+ ensureCapacity(requiredBytes: number): void;
11
+ /**
12
+ * Creates a typed view over the shared memory buffer at a specific byte offset.
13
+ */
14
+ getView<T extends Int32Array | Float64Array | BigInt64Array | Uint8Array>(type: ViewType, byteOffset?: number, length?: number): T;
15
+ /**
16
+ * Exposes the raw SharedArrayBuffer
17
+ */
18
+ getBuffer(): SharedArrayBuffer;
19
+ }
@@ -0,0 +1,80 @@
1
+ import type { ServiceMethod, TaskRunner } from '../service';
2
+ import type { SharedAccess, SharedMemory, SharedSpec } from '../contract/sharedMemory';
3
+ import type { MemoryConfig, Prettify } from '../contract/types';
4
+ import { WorkerPool, type RunOptions } from './workerPool';
5
+ import type { WorkerDefinition, WorkerMethodMap } from '../worker/defineWorker';
6
+ /** Maps a worker method's declared type to its async client signature. */
7
+ type Sig<F> = F extends ServiceMethod<infer A extends any[], infer R> ? (...args: A) => Promise<R> : F extends {
8
+ run: (...args: infer A extends any[]) => infer R;
9
+ } ? (...args: A) => Promise<Awaited<R>> : F extends (...args: infer A extends any[]) => infer R ? (...args: A) => Promise<Awaited<R>> : never;
10
+ /** Flat method map → client signatures. */
11
+ export type ClientMethods<M extends WorkerMethodMap> = {
12
+ [K in keyof M]: Sig<M[K]>;
13
+ };
14
+ /**
15
+ * The typed client surface a {@link WorkerDefinition} exposes: flat methods
16
+ * plus one nested client per service namespace.
17
+ */
18
+ export type WorkerMethods<W extends WorkerDefinition> = ClientMethods<W['methods']> & {
19
+ [K in keyof W['services']]: W['services'][K] extends infer M extends WorkerMethodMap ? ClientMethods<M> : never;
20
+ } & {
21
+ /** Per-call options — `client.with({ signal, timeout }).method(args)`. */
22
+ with(options: RunOptions): WorkerMethods<W>;
23
+ };
24
+ /**
25
+ * Typed Proxy over any {@link TaskRunner} (WorkerPool, SharedWorkerClient,
26
+ * test stub). The path joins with '.', so `client.query(q)` dispatches
27
+ * taskId "query" and `client.pricing.reprice(x)` dispatches
28
+ * "pricing.reprice" — no runtime knowledge of the shape needed.
29
+ */
30
+ export declare function workerClient<W extends WorkerDefinition>(runner: TaskRunner | (() => TaskRunner)): WorkerMethods<W>;
31
+ export interface ConnectWorkerConfig<S extends SharedSpec> {
32
+ /** Omit for a message-only pool — the SharedArrayBuffer check is skipped. */
33
+ sharedMemory?: SharedMemory<S> & Prettify<SharedAccess<S>>;
34
+ /**
35
+ * Bundler-detectable factory `() => new Worker(new URL('./x.worker.ts', import.meta.url), { type: 'module' })`,
36
+ * or a URL.
37
+ */
38
+ worker: (() => Worker) | URL;
39
+ poolSize?: number | 'auto';
40
+ memory?: MemoryConfig;
41
+ /** Max in-flight tasks per worker (default 1); excess calls queue FIFO. */
42
+ concurrency?: number;
43
+ /** Max queued calls (default Infinity); a full queue rejects with PoolQueueFullError. */
44
+ maxQueue?: number;
45
+ /** Default true — a crashed worker is replaced and its in-flight calls reject. */
46
+ respawn?: boolean;
47
+ /** Default per-call timeout ms (`client.with({ timeout })` overrides). */
48
+ taskTimeout?: number;
49
+ /** Default true: pool spawns on first method call (or start()). False spawns immediately. */
50
+ lazy?: boolean;
51
+ }
52
+ /** A typed worker client plus pool lifecycle controls. */
53
+ export type WorkerClient<W extends WorkerDefinition, S extends SharedSpec> = WorkerMethods<W> & {
54
+ /** Spawn the pool now (idempotent). */
55
+ start(): void;
56
+ /** Terminate the pool; the next method call re-spawns it. */
57
+ terminate(): void;
58
+ readonly pool: WorkerPool<S> | null;
59
+ readonly sharedMemory: ConnectWorkerConfig<S>['sharedMemory'] | undefined;
60
+ };
61
+ /**
62
+ * One-line worker hookup for the main thread — the WorkerPool boilerplate
63
+ * (worker factory, shared memory, sizing) collapses into the client itself:
64
+ *
65
+ * import type { IncidentsWorker } from './worker/incidents.worker';
66
+ *
67
+ * export const incidents = connectWorker<IncidentsWorker>({
68
+ * sharedMemory: incidentsMemory,
69
+ * worker: () => new Worker(new URL('./worker/incidents.worker.ts', import.meta.url), { type: 'module' }),
70
+ * poolSize: 1,
71
+ * });
72
+ *
73
+ * await incidents.queryIncidents(q); // typed dispatch to the pool
74
+ * incidents.terminate(); // next call lazily re-spawns
75
+ *
76
+ * Lazy by default: no Worker is touched until the first method call (or
77
+ * `start()`), so importing the client is SSR-safe.
78
+ */
79
+ export declare function connectWorker<W extends WorkerDefinition, S extends SharedSpec = W extends WorkerDefinition<infer X> ? X : SharedSpec>(config: ConnectWorkerConfig<S>): WorkerClient<W, S>;
80
+ export {};
@@ -0,0 +1,115 @@
1
+ import { SharedAccess, SharedMemory, SharedSpec } from '../contract/sharedMemory';
2
+ import { MemoryPersistence, PoolTasks, TaskContract, TaskMap, WorkerPoolConfig } from '../contract/types';
3
+ /** Per-call dispatch controls. */
4
+ export interface RunOptions {
5
+ /**
6
+ * Cancels the call: a queued task is removed and rejects with
7
+ * TaskAbortedError; an in-flight task rejects immediately and the worker's
8
+ * eventual reply is discarded. JS cannot be interrupted — the worker keeps
9
+ * running, and the worker's slot is released only when its reply arrives.
10
+ * Callers needing a hard cancel should `terminate()`.
11
+ */
12
+ signal?: AbortSignal;
13
+ /**
14
+ * Per-call timeout in ms — overrides the pool's `taskTimeout`. The timer
15
+ * starts at enqueue, so it bounds queue-wait + execution time together.
16
+ */
17
+ timeout?: number;
18
+ }
19
+ export interface PoolStats {
20
+ workers: number;
21
+ idle: number;
22
+ inFlight: number;
23
+ queued: number;
24
+ completed: number;
25
+ failed: number;
26
+ aborted: number;
27
+ /** queue→dispatch timings. */
28
+ waitMs: {
29
+ count: number;
30
+ mean: number;
31
+ max: number;
32
+ };
33
+ /** dispatch→reply timings. */
34
+ runMs: {
35
+ count: number;
36
+ mean: number;
37
+ max: number;
38
+ };
39
+ }
40
+ declare class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
41
+ private slots;
42
+ private pending;
43
+ private nextMessageId;
44
+ private queue;
45
+ private memoryManager?;
46
+ private workerUrl?;
47
+ private spawn;
48
+ private readonly concurrency;
49
+ private readonly maxQueue;
50
+ private readonly respawn;
51
+ private readonly taskTimeout?;
52
+ private closed;
53
+ private drainWaiters;
54
+ private statsAgg;
55
+ readonly sharedMemory: (SharedMemory<S> & SharedAccess<S>) | undefined;
56
+ /** The attached memory-persistence adapter, if the config supplied one. */
57
+ readonly persistence: MemoryPersistence | undefined;
58
+ constructor(config: WorkerPoolConfig<S, TaskMap>);
59
+ /** Adds a spawned worker: one message + one error listener, then the INIT handshake. */
60
+ private addWorker;
61
+ private handleReply;
62
+ private handleWorkerError;
63
+ /** Least-busy worker below its concurrency cap, if any. */
64
+ private pickWorker;
65
+ private drainQueue;
66
+ private send;
67
+ /** Detaches signal/timeout listeners from a finalized entry. */
68
+ private detach;
69
+ /**
70
+ * Abort/timeout path: a queued task is removed outright; an in-flight task
71
+ * stays in `pending` as an orphan — the worker is still running it, so the
72
+ * slot remains occupied until the reply arrives (handleReply frees it).
73
+ */
74
+ private cancel;
75
+ private checkDrained;
76
+ /**
77
+ * Full dispatch: least-busy scheduling, `concurrency` cap per worker,
78
+ * FIFO queue bounded by `maxQueue`, abort/timeout, and queue→reply timing.
79
+ */
80
+ dispatch<Args extends any[], Result>(contract: TaskContract<Args, Result>, args: Args, options?: RunOptions): Promise<Result>;
81
+ /**
82
+ * Dispatches a task to the least-busy worker in the pool.
83
+ * The task contract guarantees the argument and result types across the thread boundary.
84
+ */
85
+ runTask<Args extends any[], Result>(contract: TaskContract<Args, Result>, ...args: Args): Promise<Result>;
86
+ /**
87
+ * The shared buffer handed to workers via INIT_MEMORY — undefined on
88
+ * message-only pools. Useful for passing the same buffer to auxiliaries
89
+ * (e.g. a second pool of HTTP workers via withSharedBuffer).
90
+ */
91
+ get sharedBuffer(): SharedArrayBuffer | undefined;
92
+ /**
93
+ * Live snapshot of the pool's worker slots — for auxiliary messaging that
94
+ * isn't task dispatch (e.g. routing accepted sockets into workers). The
95
+ * array is a copy; respawned workers appear on the next read. Do NOT use
96
+ * this to dispatch tasks — runTask/dispatch own the scheduling protocol.
97
+ */
98
+ get workers(): readonly Worker[];
99
+ /** Running aggregates — no histograms, just counts and means. */
100
+ stats(): PoolStats;
101
+ /** Resolves once the queue is empty and nothing is in flight, then terminates. */
102
+ close(): Promise<void>;
103
+ terminate(): void;
104
+ }
105
+ /**
106
+ * A WorkerPool with first-class task methods baked in from `config.tasks`.
107
+ * Constructed as `new WorkerPool({ workerUrl, sharedMemory, tasks })` —
108
+ * `tasks: { queryIncidents: QueryIncidents }` exposes `pool.queryIncidents(q)`
109
+ * with the contract's declared args/result types.
110
+ */
111
+ export type WorkerPool<S extends SharedSpec = SharedSpec, T extends TaskMap = TaskMap> = WorkerPoolImpl<S> & PoolTasks<T>;
112
+ export declare const WorkerPool: {
113
+ new <S extends SharedSpec = SharedSpec, T extends TaskMap = TaskMap>(config: WorkerPoolConfig<S, T>): WorkerPool<S, T>;
114
+ };
115
+ export {};
@@ -0,0 +1,46 @@
1
+ import { Connector } from './contract/sharedMemory';
2
+ /** Emission control for slices — return `true` when `next` should be treated
3
+ * as unchanged (no emit). Defaults to `Object.is`. */
4
+ export type EqualityFn<T> = (prev: T, next: T) => boolean;
5
+ export interface SliceOptions<S> {
6
+ /** Distinct comparator for the selected slice: `true` = unchanged. */
7
+ equals?: EqualityFn<S>;
8
+ }
9
+ /** Reference-equality object comparator — emits when any own property value
10
+ * changes. Handy for object slices: `{ equals: shallowEqual }`. */
11
+ export declare function shallowEqual<T extends object>(prev: T | undefined, next: T | undefined): boolean;
12
+ /**
13
+ * A reactive view over a shared memory connector. `get()` is signal-tracked,
14
+ * so Solid effects and memos re-run when the field changes. `set()` writes
15
+ * through the connector and notifies subscribers on this thread.
16
+ */
17
+ export interface ReactiveConnector<T> {
18
+ get(): T;
19
+ set(value: T): void;
20
+ peek(): T;
21
+ /**
22
+ * Start observing writes made by the other thread (via the shared version
23
+ * counter). Returns a function to stop observing. Local writes already
24
+ * notify through `set()`, so this is only needed for remote changes.
25
+ */
26
+ observeRemote(): () => void;
27
+ }
28
+ export declare function reactive<T>(connector: Connector<T>): ReactiveConnector<T>;
29
+ /**
30
+ * Observe a shared-memory field and invoke `onChange` with its value on
31
+ * every write — local or remote (both bump the shared version counter).
32
+ * Fires once immediately with the current value unless the field has never
33
+ * been written (structured fields read `undefined` until then).
34
+ *
35
+ * Pass a selector to watch a slice: `watch(conn, m => m.critical, cb)` —
36
+ * the selected value is memoized, so the callback only runs when the slice
37
+ * itself changes, not on every field write. Slice equality defaults to
38
+ * `Object.is`; pass `options.equals` to decide what emits — e.g.
39
+ * `{ equals: shallowEqual }` for object slices, or a custom comparator like
40
+ * `(a, b) => Math.abs(a - b) < 0.01` to mute noise.
41
+ *
42
+ * Returns an unsubscribe function. Requires the contract to be bound on
43
+ * this thread (the pool binds it on construction).
44
+ */
45
+ export declare function watch<T>(connector: Connector<T>, onChange: (value: T) => void): () => void;
46
+ export declare function watch<T, S>(connector: Connector<T>, select: (value: T) => S, onChange: (slice: S, previous: S | undefined) => void, options?: SliceOptions<S>): () => void;
@@ -0,0 +1,181 @@
1
+ import type { Schema, TaskContract, ViewType } from './contract/types';
2
+ /**
3
+ * One RPC method's declaration. Identical fields to {@link TaskContract}
4
+ * except `taskId` is optional — when omitted the wire id is derived as
5
+ * `${serviceName}.${methodName}` so it can't drift between threads.
6
+ *
7
+ * Most methods need no wrapper — write the object inline and defineService
8
+ * infers Args/Result from the schemas:
9
+ *
10
+ * seedIncidents: { resultSchema: z.number() }, // → () => number
11
+ * queryIncidents: { argsSchema, resultSchema }, // → (q) => Result
12
+ *
13
+ * Use {@link rpc} only to declare types a method's schemas can't carry —
14
+ * e.g. typed args with no validation: `rpc<[QueryArgs], QueryResult>()`.
15
+ */
16
+ export interface RpcMethodDef<Args extends any[] = any[], Result = any> {
17
+ /** Explicit wire id — only needed to interop with pre-existing taskIds. */
18
+ taskId?: string;
19
+ /** Element datatype of the shared-memory region this method operates on. */
20
+ dataType?: ViewType;
21
+ /** Validates args as they cross the thread boundary. */
22
+ argsSchema?: Schema<Args>;
23
+ /** Validates the handler's return value. */
24
+ resultSchema?: Schema<Result>;
25
+ }
26
+ /**
27
+ * Explicit type carrier for method declarations — an identity function.
28
+ * Needed only when schemas are absent or don't express the full signature;
29
+ * schema-typed methods infer automatically inside defineService.
30
+ */
31
+ export declare function rpc<Args extends any[] = any[], Result = any>(def?: RpcMethodDef<Args, Result>): RpcMethodDef<Args, Result>;
32
+ /**
33
+ * One method's whole unit — the pattern for one-file-per-task modules. The
34
+ * file's top-level export satisfies this interface: `def` is the wire
35
+ * declaration {@link defineService} composes, `run` is the worker-side
36
+ * implementation {@link implementService} registers. Both entry points
37
+ * accept the unit directly and unwrap the part they need:
38
+ *
39
+ * // service/seedIncidents.ts — one file, the whole stack
40
+ * export const seedIncidents = {
41
+ * def: { resultSchema: z.number() },
42
+ * run() { …; return ms; },
43
+ * } satisfies ServiceMethod<[], number>;
44
+ */
45
+ export interface ServiceMethod<Args extends any[] = any[], Result = any> {
46
+ /** The wire declaration fed into {@link defineService}. */
47
+ def: RpcMethodDef<Args, Result>;
48
+ /** The worker-side implementation fed into {@link implementService}. */
49
+ run(...args: Args): Result | Promise<Result>;
50
+ }
51
+ /** The args a method def declares — argsSchema's tuple, else an rpc<A,R>() declaration's type args, else []. */
52
+ type DefArgs<D> = D extends {
53
+ argsSchema: Schema<infer A extends any[]>;
54
+ } ? A : D extends {
55
+ resultSchema: Schema<any>;
56
+ } ? [] : D extends RpcMethodDef<infer A extends any[], any> ? A : [];
57
+ /** The result a method def declares — resultSchema's output, else an rpc<A,R>() declaration's type args, else unknown. */
58
+ type DefResult<D> = D extends {
59
+ resultSchema: Schema<infer R>;
60
+ } ? R : D extends RpcMethodDef<any[], infer R> ? R : unknown;
61
+ /**
62
+ * Builds a {@link ServiceMethod} unit — the factory form of
63
+ * `{ def, run } satisfies ServiceMethod<…>`. The def's schemas (or an
64
+ * `rpc<A,R>()` declaration's type args) supply the signature, so `run`
65
+ * gets contextual parameter types with nothing to annotate:
66
+ *
67
+ * export const queryIncidents = serviceMethod({
68
+ * def: { argsSchema: z.tuple([queryArgsSchema]), resultSchema: queryResultSchema },
69
+ * run(q) { … }, // q: QueryArgs — inferred
70
+ * });
71
+ *
72
+ * The unit's exact type is returned unchanged, so extra fields (config,
73
+ * counters) ride along for implementService's `this`-binding — `run`'s
74
+ * `this` is the unit; annotate it to type extra fields:
75
+ * `run(this: { prefix: string }, name) { … }`.
76
+ */
77
+ export declare function serviceMethod<D extends RpcMethodDef, U extends {
78
+ def: D;
79
+ }>(unit: U & {
80
+ run: (...args: DefArgs<D>) => DefResult<D> | Promise<DefResult<D>>;
81
+ }): U;
82
+ /** A service method entry: a bare RpcMethodDef or a full {@link ServiceMethod} unit. */
83
+ export type RpcMethodInput = RpcMethodDef<any[], any> | ServiceMethod<any[], any>;
84
+ export type RpcMethodMap = Record<string, RpcMethodDef<any[], any>>;
85
+ export type RpcMethodInputMap = Record<string, RpcMethodInput>;
86
+ /**
87
+ * Resolves a method declaration to its TaskContract type. Schemas win when
88
+ * present (argsSchema+resultSchema → full signature; resultSchema alone →
89
+ * a no-arg method); an rpc<A,R>() declaration's type args carry the rest.
90
+ */
91
+ type MethodContract<D> = D extends {
92
+ def: infer Def;
93
+ } ? MethodContract<Def> : D extends {
94
+ argsSchema: Schema<infer A extends any[]>;
95
+ } ? D extends {
96
+ resultSchema: Schema<infer R>;
97
+ } ? TaskContract<A, R> : TaskContract<A, any> : D extends {
98
+ resultSchema: Schema<infer R>;
99
+ } ? TaskContract<[], R> : D extends RpcMethodDef<infer A extends any[], infer R> ? TaskContract<A, R> : TaskContract<any[], any>;
100
+ type MethodsToContracts<M extends RpcMethodInputMap> = {
101
+ [K in keyof M]: MethodContract<M[K]>;
102
+ };
103
+ /**
104
+ * A named bundle of RPC method contracts shared by both threads. `tasks` is
105
+ * a fully-typed {@link TaskMap} — hand it to `WorkerPool`/`SharedWorker`
106
+ * config or iterate it for registration.
107
+ */
108
+ export interface ServiceContract<M extends RpcMethodInputMap = RpcMethodInputMap> {
109
+ readonly name: string;
110
+ readonly tasks: MethodsToContracts<M>;
111
+ }
112
+ /**
113
+ * Declares a worker service's RPC surface. The same object is imported by
114
+ * the worker entry (for `implementService`) and the main thread (for
115
+ * `createClient` / pool config):
116
+ *
117
+ * export const incidentsService = defineService('incidents', {
118
+ * seedIncidents: { resultSchema: z.number() },
119
+ * queryIncidents: {
120
+ * argsSchema: z.tuple([queryArgsSchema]),
121
+ * resultSchema: queryResultSchema,
122
+ * },
123
+ * });
124
+ */
125
+ export declare function defineService<M extends RpcMethodInputMap>(name: string, methods: M): ServiceContract<M>;
126
+ type ServiceHandlerFn<A extends any[], R> = (...args: A) => R | Promise<R>;
127
+ /** The handler signatures a service implementation must provide. */
128
+ export type ServiceHandlers<M extends RpcMethodInputMap> = {
129
+ [K in keyof M]: MethodContract<M[K]> extends TaskContract<infer A, infer R> ? ServiceHandlerFn<A, R> | {
130
+ run: ServiceHandlerFn<A, R>;
131
+ } : never;
132
+ };
133
+ /**
134
+ * A service contract bound to handler implementations — the worker-side
135
+ * "service" that owns the RPC methods. Registration happens eagerly inside
136
+ * {@link implementService}; the returned handle is for composition/testing.
137
+ */
138
+ export interface ServiceImpl<M extends RpcMethodInputMap = RpcMethodInputMap> {
139
+ readonly service: ServiceContract<M>;
140
+ readonly handlers: ServiceHandlers<M>;
141
+ }
142
+ /**
143
+ * Binds a service contract to handler functions and registers every method
144
+ * with the {@link TaskRegistry} — the worker-side half of the contract:
145
+ *
146
+ * export const incidentsImpl = implementService(incidentsService, {
147
+ * seedIncidents: () => { …; return ms; },
148
+ * queryIncidents: (q) => { …; return page; },
149
+ * });
150
+ *
151
+ * Missing methods throw at bind time (a silent omission would surface as a
152
+ * runtime "handler not found" on the far thread). Handlers are invoked with
153
+ * `this` bound to the handlers object, so methods may call siblings via
154
+ * `this`. When `handlers` is a class instance (e.g. a DI-resolved provider
155
+ * whose methods match the service surface), binding preserves its `this`.
156
+ */
157
+ export declare function implementService<M extends RpcMethodInputMap>(service: ServiceContract<M>, handlers: ServiceHandlers<M>): ServiceImpl<M>;
158
+ /** Anything that can dispatch a TaskContract — WorkerPool, SharedWorkerClient. */
159
+ export interface TaskRunner {
160
+ runTask<Args extends any[], Result>(contract: TaskContract<Args, Result>, ...args: Args): Promise<Result>;
161
+ /**
162
+ * Options-aware dispatch (abort/timeout). When present, `workerClient`
163
+ * `client.with(options)` calls route through it; otherwise they fall back
164
+ * to runTask and the options are ignored.
165
+ */
166
+ dispatch?<Args extends any[], Result>(contract: TaskContract<Args, Result>, args: Args, options?: {
167
+ signal?: AbortSignal;
168
+ timeout?: number;
169
+ }): Promise<Result>;
170
+ }
171
+ /** The callable RPC surface a service client exposes. */
172
+ export type ServiceClient<M extends RpcMethodMap> = {
173
+ [K in keyof M]: MethodContract<M[K]> extends TaskContract<infer A, infer R> ? (...args: A) => Promise<R> : never;
174
+ };
175
+ /**
176
+ * The main-thread half of a service contract: a typed proxy that turns
177
+ * `client.seedIncidents()` into `runner.runTask(contract)`. Works over any
178
+ * {@link TaskRunner} — a WorkerPool, a SharedWorkerClient, or a test stub.
179
+ */
180
+ export declare function createClient<M extends RpcMethodMap>(service: ServiceContract<M>, runner: TaskRunner): ServiceClient<M>;
181
+ export {};
@@ -0,0 +1,35 @@
1
+ import { SharedAccess, SharedMemory, SharedSpec } from '../contract/sharedMemory';
2
+ import { PoolTasks, TaskContract, TaskMap, SharedWorkerConfig } from '../contract/types';
3
+ declare class SharedWorkerClientImpl<S extends SharedSpec = SharedSpec> {
4
+ readonly clientIndex: number;
5
+ readonly sharedMemory: SharedMemory<S> & SharedAccess<S>;
6
+ private readonly port;
7
+ constructor(port: MessagePort, clientIndex: number, config: SharedWorkerConfig<S, TaskMap>);
8
+ /** Dispatches a task to the shared worker; the reply comes back on this client's port. */
9
+ runTask<Args extends any[], Result>(contract: TaskContract<Args, Result>, ...args: Args): Promise<Result>;
10
+ /** Detaches this client. The shared worker (and its buffer) stay alive for other clients. */
11
+ disconnect(): void;
12
+ }
13
+ /**
14
+ * A SharedWorkerClient with first-class task methods baked in from
15
+ * `config.tasks` — same shape as `WorkerPool`.
16
+ */
17
+ export type SharedWorkerClient<S extends SharedSpec = SharedSpec, T extends TaskMap = TaskMap> = SharedWorkerClientImpl<S> & PoolTasks<T>;
18
+ /**
19
+ * Connects this page to a `SharedWorker`. The worker owns the SharedArrayBuffer
20
+ * and shares it with every connecting client — so every page/tab/iframe bound
21
+ * to the same contract reads and writes the same memory. State propagation
22
+ * needs no messaging: writes bump shared version counters via Atomics, which
23
+ * `observe`/`watch` subscribers in every context are already parked on.
24
+ * Ports carry only the connect handshake and task dispatch.
25
+ *
26
+ * ```ts
27
+ * const worker = await connectSharedWorker({
28
+ * createWorker: () => new SharedWorker(new URL('./incidents.sharedWorker.ts', import.meta.url), { type: 'module' }),
29
+ * sharedMemory: incidentsMemory,
30
+ * tasks: { initIncidents: InitIncidents, queryIncidents: QueryIncidents },
31
+ * });
32
+ * ```
33
+ */
34
+ export declare function connectSharedWorker<S extends SharedSpec = SharedSpec, T extends TaskMap = TaskMap>(config: SharedWorkerConfig<S, T>): Promise<SharedWorkerClient<S, T>>;
35
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Attaches the shared-worker protocol to one client port. Exported separately
3
+ * so tests can drive it with a `MessageChannel` without a real SharedWorker.
4
+ */
5
+ export declare function attachSharedPort(port: MessagePort): void;
6
+ /**
7
+ * SharedWorker-side entry point — call once at the top of the shared worker
8
+ * module, after registering task handlers:
9
+ *
10
+ * ```ts
11
+ * import { sharedWorkerHost, TaskRegistry } from '@atolljs/core';
12
+ * import './tasks'; // TaskRegistry.register(...) calls
13
+ * sharedWorkerHost();
14
+ * ```
15
+ *
16
+ * Each page/tab/iframe that opens this SharedWorker gets a MessagePort here;
17
+ * all of them bind the same contract to the same SharedArrayBuffer. Ports are
18
+ * the control plane (handshake + task dispatch) — state itself never crosses
19
+ * them; clients observe shared fields directly via Atomics version counters.
20
+ */
21
+ export declare function sharedWorkerHost(): void;
package/dist/task.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { ObservableValue } from './observable';
2
+ /**
3
+ * Point-in-time state of an {@link AsyncTask}. Replaced wholesale on every
4
+ * transition so `get()` stays stable for `useSyncExternalStore`-style
5
+ * adapters.
6
+ */
7
+ export interface TaskSnapshot<R> {
8
+ /** Result of the last completed run; kept while a newer run is in flight. */
9
+ data: R | null;
10
+ pending: boolean;
11
+ /** True once any run has completed (success or failure). */
12
+ settled: boolean;
13
+ /** Wall-clock duration of the last completed run. */
14
+ elapsedMs: number | null;
15
+ error: unknown;
16
+ }
17
+ /**
18
+ * An async function turned into a subscribable task: `run` starts an
19
+ * invocation, and concurrent runs are latest-wins — a stale result is
20
+ * discarded rather than applied. Framework bindings subscribe to the
21
+ * snapshot; callers trigger `run`.
22
+ */
23
+ export interface AsyncTask<A, R> extends ObservableValue<TaskSnapshot<R>> {
24
+ run(input: A): void;
25
+ /** No-op if a run is pending or has already settled — for init-style tasks. */
26
+ runOnce(input: A): void;
27
+ }
28
+ export declare function defineTask<A, R>(fn: (input: A) => Promise<R>): AsyncTask<A, R>;
29
+ /** Structural check: run/runOnce triggers plus the observable contract. */
30
+ export declare function isAsyncTask(x: unknown): x is AsyncTask<any, any>;
31
+ /**
32
+ * Accepts an {@link AsyncTask} or a plain async function and always returns
33
+ * a task — framework bindings (useTask, taskState, …) take either, so a
34
+ * worker client's method drops straight in:
35
+ *
36
+ * const seed = useTask(initIncidents); // async fn → defineTask
37
+ * const page = useTask(incidents.queryIncidents); // client method → task
38
+ */
39
+ export declare function toTask<R>(source: AsyncTask<void, R> | (() => Promise<R>)): AsyncTask<void, R>;
40
+ export declare function toTask<A, R>(source: AsyncTask<A, R> | ((input: A) => Promise<R>)): AsyncTask<A, R>;
@@ -0,0 +1,63 @@
1
+ import './workerBootstrap';
2
+ import type { ServiceMethod } from '../service';
3
+ import type { SharedMemory, SharedSpec } from '../contract/sharedMemory';
4
+ /**
5
+ * One worker-exposed method: a plain function (siblings reachable via `this`)
6
+ * or a {@link ServiceMethod} unit (`{ def, run }` — schemas enforced on the
7
+ * wire).
8
+ */
9
+ export type WorkerMethod = ((...args: any[]) => any) | ServiceMethod<any[], any>;
10
+ export type WorkerMethodMap = Record<string, WorkerMethod>;
11
+ export type WorkerServiceMap = Record<string, WorkerMethodMap>;
12
+ type ReservedKey = (typeof RESERVED_CLIENT_KEYS)[number];
13
+ /** Turns any reserved keys in M into `never`-typed members — a compile error. */
14
+ type NoReserved<M> = [Extract<keyof M, ReservedKey>] extends [never] ? M : M & {
15
+ [K in Extract<keyof M, ReservedKey>]: never;
16
+ };
17
+ export interface DefineWorkerConfig<S extends SharedSpec, M extends WorkerMethodMap, Svc extends WorkerServiceMap> {
18
+ /**
19
+ * Optional — carried in the type so connectWorker can check the main-side
20
+ * contract matches. Binding is global via workerBootstrap.
21
+ */
22
+ sharedMemory?: SharedMemory<S>;
23
+ /** Flat methods → taskId = method name. */
24
+ methods?: NoReserved<M>;
25
+ /** Namespaced methods → taskId = `${service}.${method}`. */
26
+ services?: NoReserved<Svc>;
27
+ }
28
+ export interface WorkerDefinition<S extends SharedSpec = SharedSpec, M extends WorkerMethodMap = {}, Svc extends WorkerServiceMap = {}> {
29
+ readonly methods: M;
30
+ readonly services: Svc;
31
+ readonly sharedMemory?: SharedMemory<S>;
32
+ }
33
+ /**
34
+ * Client-surface keys a worker method must not claim — a method named
35
+ * `pool` or `then` would collide with {@link connectWorker}'s API (or make
36
+ * the client look like a thenable).
37
+ */
38
+ export declare const RESERVED_CLIENT_KEYS: readonly ['start', 'terminate', 'pool', 'sharedMemory', 'then', 'with'];
39
+ /**
40
+ * The worker-side half of the tRPC-style pair: the worker script owns the
41
+ * runtime (importing this module wires `self.onmessage` via workerBootstrap)
42
+ * and the method list — the main thread imports only `typeof` the returned
43
+ * definition and drives it through a {@link connectWorker} Proxy client.
44
+ *
45
+ * // incidents.worker.ts — the whole worker entry
46
+ * export const incidentsWorker = defineWorker({
47
+ * sharedMemory: incidentsMemory,
48
+ * methods: { seedIncidents, queryIncidents, computeMetrics },
49
+ * });
50
+ * export type IncidentsWorker = typeof incidentsWorker;
51
+ *
52
+ * Flat methods register under their own name; `services: { svc: { m } }`
53
+ * registers `svc.m`. Plain functions are invoked with `this` bound to the
54
+ * methods object (sibling calls); ServiceMethod units contribute their
55
+ * `def` schemas (and a `def.taskId` override) and run bound to the unit.
56
+ *
57
+ * BOOTSTRAP ORDERING (Node): the `@atolljs/node/shim` import must
58
+ * evaluate before this module — it binds `self = parentPort`, which
59
+ * workerBootstrap needs at import time. Keep `import '@atolljs/node/shim'`
60
+ * first in the worker entry.
61
+ */
62
+ export declare function defineWorker<S extends SharedSpec, M extends WorkerMethodMap = {}, Svc extends WorkerServiceMap = {}>(config: DefineWorkerConfig<S, M, Svc>): WorkerDefinition<S, M, Svc>;
63
+ export {};
@@ -0,0 +1,8 @@
1
+ import { TaskContract } from '../contract/types';
2
+ export type TaskHandler<Args extends any[] = any[], Result = any> = (...args: Args) => Result | Promise<Result>;
3
+ export declare class TaskRegistry {
4
+ private static tasks;
5
+ static register<Args extends any[], Result>(contract: TaskContract<Args, Result>, handler: TaskHandler<Args, Result>): void;
6
+ static getContract(taskId: string): TaskContract | undefined;
7
+ static execute(taskId: string, ...args: any[]): Promise<any>;
8
+ }