@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
package/src/task.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { scoped } from './log';
|
|
2
|
+
import type { ObservableValue } from './observable';
|
|
3
|
+
|
|
4
|
+
const taskLog = scoped('task');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Point-in-time state of an {@link AsyncTask}. Replaced wholesale on every
|
|
8
|
+
* transition so `get()` stays stable for `useSyncExternalStore`-style
|
|
9
|
+
* adapters.
|
|
10
|
+
*/
|
|
11
|
+
export interface TaskSnapshot<R> {
|
|
12
|
+
/** Result of the last completed run; kept while a newer run is in flight. */
|
|
13
|
+
data: R | null;
|
|
14
|
+
pending: boolean;
|
|
15
|
+
/** True once any run has completed (success or failure). */
|
|
16
|
+
settled: boolean;
|
|
17
|
+
/** Wall-clock duration of the last completed run. */
|
|
18
|
+
elapsedMs: number | null;
|
|
19
|
+
error: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* An async function turned into a subscribable task: `run` starts an
|
|
24
|
+
* invocation, and concurrent runs are latest-wins — a stale result is
|
|
25
|
+
* discarded rather than applied. Framework bindings subscribe to the
|
|
26
|
+
* snapshot; callers trigger `run`.
|
|
27
|
+
*/
|
|
28
|
+
export interface AsyncTask<A, R> extends ObservableValue<TaskSnapshot<R>> {
|
|
29
|
+
run(input: A): void;
|
|
30
|
+
/** No-op if a run is pending or has already settled — for init-style tasks. */
|
|
31
|
+
runOnce(input: A): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function defineTask<A, R>(fn: (input: A) => Promise<R>): AsyncTask<A, R> {
|
|
35
|
+
let reqId = 0;
|
|
36
|
+
let snapshot: TaskSnapshot<R> = { data: null, pending: false, settled: false, elapsedMs: null, error: null };
|
|
37
|
+
const subscribers = new Set<(s: TaskSnapshot<R>) => void>();
|
|
38
|
+
const emit = (next: TaskSnapshot<R>) => {
|
|
39
|
+
snapshot = next;
|
|
40
|
+
for (const cb of subscribers) cb(next);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const task: AsyncTask<A, R> = {
|
|
44
|
+
get: () => snapshot,
|
|
45
|
+
subscribe(onChange) {
|
|
46
|
+
subscribers.add(onChange);
|
|
47
|
+
return () => {
|
|
48
|
+
subscribers.delete(onChange);
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
run(input: A) {
|
|
52
|
+
const id = ++reqId;
|
|
53
|
+
const t0 = performance.now();
|
|
54
|
+
emit({ ...snapshot, pending: true, error: null });
|
|
55
|
+
void fn(input).then(
|
|
56
|
+
(data) => {
|
|
57
|
+
if (id === reqId) emit({ data, pending: false, settled: true, elapsedMs: performance.now() - t0, error: null });
|
|
58
|
+
},
|
|
59
|
+
(error) => {
|
|
60
|
+
if (id === reqId) emit({ ...snapshot, pending: false, settled: true, error });
|
|
61
|
+
taskLog.error('task run failed', error);
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
runOnce(input: A) {
|
|
66
|
+
if (snapshot.pending || snapshot.settled) return;
|
|
67
|
+
task.run(input);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
return task;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Structural check: run/runOnce triggers plus the observable contract. */
|
|
74
|
+
export function isAsyncTask(x: unknown): x is AsyncTask<any, any> {
|
|
75
|
+
return (
|
|
76
|
+
typeof x === 'object' &&
|
|
77
|
+
x !== null &&
|
|
78
|
+
typeof (x as AsyncTask<any, any>).run === 'function' &&
|
|
79
|
+
typeof (x as AsyncTask<any, any>).runOnce === 'function' &&
|
|
80
|
+
typeof (x as AsyncTask<any, any>).get === 'function' &&
|
|
81
|
+
typeof (x as AsyncTask<any, any>).subscribe === 'function'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Accepts an {@link AsyncTask} or a plain async function and always returns
|
|
87
|
+
* a task — framework bindings (useTask, taskState, …) take either, so a
|
|
88
|
+
* worker client's method drops straight in:
|
|
89
|
+
*
|
|
90
|
+
* const seed = useTask(initIncidents); // async fn → defineTask
|
|
91
|
+
* const page = useTask(incidents.queryIncidents); // client method → task
|
|
92
|
+
*/
|
|
93
|
+
export function toTask<R>(source: AsyncTask<void, R> | (() => Promise<R>)): AsyncTask<void, R>;
|
|
94
|
+
export function toTask<A, R>(source: AsyncTask<A, R> | ((input: A) => Promise<R>)): AsyncTask<A, R>;
|
|
95
|
+
export function toTask<A, R>(
|
|
96
|
+
source: AsyncTask<A, R> | ((input: A) => Promise<R>),
|
|
97
|
+
): AsyncTask<A, R> {
|
|
98
|
+
return isAsyncTask(source) ? source : defineTask(source);
|
|
99
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test double for the Worker global that runs the real worker protocol
|
|
3
|
+
* in-process: INIT_MEMORY binds every defined shared-memory contract to the
|
|
4
|
+
* pool's buffer, EXECUTE_TASK dispatches through the real TaskRegistry.
|
|
5
|
+
* Everything except the OS thread boundary is real.
|
|
6
|
+
*
|
|
7
|
+
* Register worker-entry modules via `InProcessWorker.handlerModules` —
|
|
8
|
+
* importing them performs their TaskRegistry.register side effects, exactly
|
|
9
|
+
* what a bundled worker entry does at boot.
|
|
10
|
+
*/
|
|
11
|
+
import { bindSharedMemories } from '../contract/sharedMemory';
|
|
12
|
+
import { TaskRegistry } from '../worker/registry';
|
|
13
|
+
|
|
14
|
+
export class InProcessWorker {
|
|
15
|
+
static created: InProcessWorker[] = [];
|
|
16
|
+
static handlerModules: Array<() => Promise<unknown>> = [];
|
|
17
|
+
|
|
18
|
+
public executed: any[] = [];
|
|
19
|
+
public terminated = false;
|
|
20
|
+
private listeners = {
|
|
21
|
+
message: new Set<(event: { data: any }) => void>(),
|
|
22
|
+
error: new Set<(event: any) => void>(),
|
|
23
|
+
};
|
|
24
|
+
private ready: Promise<unknown> | null = null;
|
|
25
|
+
/** Module-load failure — a crashed worker never executes another task. */
|
|
26
|
+
private dead = false;
|
|
27
|
+
|
|
28
|
+
constructor(public url: URL, public options?: any) {
|
|
29
|
+
InProcessWorker.created.push(this);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
postMessage(data: any) {
|
|
33
|
+
queueMicrotask(() => void this.handle(data));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Imports the registered handler modules — what a real worker's entry does at boot. */
|
|
37
|
+
private boot(): Promise<unknown> {
|
|
38
|
+
return (async () => {
|
|
39
|
+
// workerBootstrap assigns self.onmessage at module load — provide a
|
|
40
|
+
// minimal stand-in so the real worker entry can be imported in-process.
|
|
41
|
+
(globalThis as { self?: unknown }).self ??= {
|
|
42
|
+
postMessage() {},
|
|
43
|
+
onmessage: null,
|
|
44
|
+
};
|
|
45
|
+
for (const load of InProcessWorker.handlerModules) await load();
|
|
46
|
+
})();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Module-load failure = a real worker's 'error' event. */
|
|
50
|
+
private crashOnBoot(err: unknown): void {
|
|
51
|
+
// A real worker whose entry module fails to load fires 'error' and never
|
|
52
|
+
// answers a task — mirror that so pools reject in-flight calls (and
|
|
53
|
+
// respawn) instead of hanging on a ready that never resolves.
|
|
54
|
+
this.dead = true;
|
|
55
|
+
const event = {
|
|
56
|
+
type: 'error',
|
|
57
|
+
message: `worker entry failed to load: ${(err as Error)?.message ?? String(err)}`,
|
|
58
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
59
|
+
};
|
|
60
|
+
for (const l of this.listeners.error) l(event);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async handle(data: any) {
|
|
64
|
+
if (data.type === 'INIT') {
|
|
65
|
+
// Message-only pool — the entry still loads; just no shared buffer.
|
|
66
|
+
this.ready ??= this.boot();
|
|
67
|
+
try {
|
|
68
|
+
await this.ready;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
this.crashOnBoot(err);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (data.type === 'INIT_MEMORY') {
|
|
75
|
+
this.ready ??= this.boot().then(() =>
|
|
76
|
+
bindSharedMemories((data.memory as WebAssembly.Memory).buffer as unknown as SharedArrayBuffer),
|
|
77
|
+
);
|
|
78
|
+
try {
|
|
79
|
+
await this.ready;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
this.crashOnBoot(err);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (data.type === 'EXECUTE_TASK') {
|
|
86
|
+
if (this.dead) return;
|
|
87
|
+
try {
|
|
88
|
+
await this.ready;
|
|
89
|
+
} catch {
|
|
90
|
+
// Boot failed in the meantime — a crashed worker drops the task
|
|
91
|
+
// (the INIT handler already fired 'error').
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (this.dead) return;
|
|
95
|
+
this.executed.push(data);
|
|
96
|
+
try {
|
|
97
|
+
const result = await TaskRegistry.execute(data.taskId, ...(data.args || []));
|
|
98
|
+
this.emit({ messageId: data.messageId, success: true, result });
|
|
99
|
+
} catch (err: any) {
|
|
100
|
+
this.emit({ messageId: data.messageId, success: false, error: err.message || String(err) });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private emit(msg: any) {
|
|
106
|
+
for (const l of this.listeners.message) l({ data: msg });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Simulates a worker crash: fires 'error' listeners like a real worker's error event. */
|
|
110
|
+
crash(message = 'simulated crash') {
|
|
111
|
+
const event = { type: 'error', message, error: new Error(message) };
|
|
112
|
+
for (const l of this.listeners.error) l(event);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
addEventListener(type: string, l: (event: any) => void) {
|
|
116
|
+
if (type === 'error') this.listeners.error.add(l);
|
|
117
|
+
else this.listeners.message.add(l);
|
|
118
|
+
}
|
|
119
|
+
removeEventListener(type: string, l: (event: any) => void) {
|
|
120
|
+
if (type === 'error') this.listeners.error.delete(l);
|
|
121
|
+
else this.listeners.message.delete(l);
|
|
122
|
+
}
|
|
123
|
+
terminate() { this.terminated = true; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Flushes microtasks + the waitAsync observer loop after a shared write. */
|
|
127
|
+
export const flushObservers = () => new Promise((r) => setTimeout(r, 5));
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import './workerBootstrap'; // side-effect: wires self.onmessage (guarded for main-thread import)
|
|
2
|
+
import { TaskRegistry } from './registry';
|
|
3
|
+
import { scoped } from '../log';
|
|
4
|
+
import type { ServiceMethod } from '../service';
|
|
5
|
+
import type { SharedMemory, SharedSpec } from '../contract/sharedMemory';
|
|
6
|
+
|
|
7
|
+
const workerLog = scoped('worker');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One worker-exposed method: a plain function (siblings reachable via `this`)
|
|
11
|
+
* or a {@link ServiceMethod} unit (`{ def, run }` — schemas enforced on the
|
|
12
|
+
* wire).
|
|
13
|
+
*/
|
|
14
|
+
export type WorkerMethod = ((...args: any[]) => any) | ServiceMethod<any[], any>;
|
|
15
|
+
export type WorkerMethodMap = Record<string, WorkerMethod>;
|
|
16
|
+
export type WorkerServiceMap = Record<string, WorkerMethodMap>;
|
|
17
|
+
|
|
18
|
+
type ReservedKey = (typeof RESERVED_CLIENT_KEYS)[number];
|
|
19
|
+
|
|
20
|
+
/** Turns any reserved keys in M into `never`-typed members — a compile error. */
|
|
21
|
+
type NoReserved<M> = [Extract<keyof M, ReservedKey>] extends [never]
|
|
22
|
+
? M
|
|
23
|
+
: M & { [K in Extract<keyof M, ReservedKey>]: never };
|
|
24
|
+
|
|
25
|
+
export interface DefineWorkerConfig<
|
|
26
|
+
S extends SharedSpec,
|
|
27
|
+
M extends WorkerMethodMap,
|
|
28
|
+
Svc extends WorkerServiceMap,
|
|
29
|
+
> {
|
|
30
|
+
/**
|
|
31
|
+
* Optional — carried in the type so connectWorker can check the main-side
|
|
32
|
+
* contract matches. Binding is global via workerBootstrap.
|
|
33
|
+
*/
|
|
34
|
+
sharedMemory?: SharedMemory<S>;
|
|
35
|
+
/** Flat methods → taskId = method name. */
|
|
36
|
+
methods?: NoReserved<M>;
|
|
37
|
+
/** Namespaced methods → taskId = `${service}.${method}`. */
|
|
38
|
+
services?: NoReserved<Svc>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WorkerDefinition<
|
|
42
|
+
S extends SharedSpec = SharedSpec,
|
|
43
|
+
M extends WorkerMethodMap = {},
|
|
44
|
+
Svc extends WorkerServiceMap = {},
|
|
45
|
+
> {
|
|
46
|
+
readonly methods: M;
|
|
47
|
+
readonly services: Svc;
|
|
48
|
+
readonly sharedMemory?: SharedMemory<S>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Client-surface keys a worker method must not claim — a method named
|
|
53
|
+
* `pool` or `then` would collide with {@link connectWorker}'s API (or make
|
|
54
|
+
* the client look like a thenable).
|
|
55
|
+
*/
|
|
56
|
+
export const RESERVED_CLIENT_KEYS = ['start', 'terminate', 'pool', 'sharedMemory', 'then', 'with'] as const;
|
|
57
|
+
|
|
58
|
+
const isUnit = (x: unknown): x is ServiceMethod<any[], any> =>
|
|
59
|
+
typeof x === 'object' && x !== null && 'run' in x;
|
|
60
|
+
|
|
61
|
+
const RESERVED = new Set<string>(RESERVED_CLIENT_KEYS);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The worker-side half of the tRPC-style pair: the worker script owns the
|
|
65
|
+
* runtime (importing this module wires `self.onmessage` via workerBootstrap)
|
|
66
|
+
* and the method list — the main thread imports only `typeof` the returned
|
|
67
|
+
* definition and drives it through a {@link connectWorker} Proxy client.
|
|
68
|
+
*
|
|
69
|
+
* // incidents.worker.ts — the whole worker entry
|
|
70
|
+
* export const incidentsWorker = defineWorker({
|
|
71
|
+
* sharedMemory: incidentsMemory,
|
|
72
|
+
* methods: { seedIncidents, queryIncidents, computeMetrics },
|
|
73
|
+
* });
|
|
74
|
+
* export type IncidentsWorker = typeof incidentsWorker;
|
|
75
|
+
*
|
|
76
|
+
* Flat methods register under their own name; `services: { svc: { m } }`
|
|
77
|
+
* registers `svc.m`. Plain functions are invoked with `this` bound to the
|
|
78
|
+
* methods object (sibling calls); ServiceMethod units contribute their
|
|
79
|
+
* `def` schemas (and a `def.taskId` override) and run bound to the unit.
|
|
80
|
+
*
|
|
81
|
+
* BOOTSTRAP ORDERING (Node): the `@atolljs/node/shim` import must
|
|
82
|
+
* evaluate before this module — it binds `self = parentPort`, which
|
|
83
|
+
* workerBootstrap needs at import time. Keep `import '@atolljs/node/shim'`
|
|
84
|
+
* first in the worker entry.
|
|
85
|
+
*/
|
|
86
|
+
export function defineWorker<
|
|
87
|
+
S extends SharedSpec,
|
|
88
|
+
M extends WorkerMethodMap = {},
|
|
89
|
+
Svc extends WorkerServiceMap = {},
|
|
90
|
+
>(config: DefineWorkerConfig<S, M, Svc>): WorkerDefinition<S, M, Svc> {
|
|
91
|
+
const methods = (config.methods ?? {}) as M;
|
|
92
|
+
const services = (config.services ?? {}) as Svc;
|
|
93
|
+
|
|
94
|
+
let count = 0;
|
|
95
|
+
const register = (taskId: string, name: string, method: WorkerMethod, thisArg: object) => {
|
|
96
|
+
if (RESERVED.has(name)) {
|
|
97
|
+
throw new Error(`defineWorker: "${name}" is reserved for the client API — rename the method`);
|
|
98
|
+
}
|
|
99
|
+
if (isUnit(method)) {
|
|
100
|
+
const { def } = method;
|
|
101
|
+
TaskRegistry.register(
|
|
102
|
+
{
|
|
103
|
+
taskId: def.taskId ?? taskId,
|
|
104
|
+
...(def.dataType ? { dataType: def.dataType } : {}),
|
|
105
|
+
...(def.argsSchema ? { argsSchema: def.argsSchema } : {}),
|
|
106
|
+
...(def.resultSchema ? { resultSchema: def.resultSchema } : {}),
|
|
107
|
+
},
|
|
108
|
+
method.run.bind(method),
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
TaskRegistry.register({ taskId }, method.bind(thisArg));
|
|
112
|
+
}
|
|
113
|
+
count++;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
for (const [name, method] of Object.entries(methods)) {
|
|
117
|
+
register(name, name, method, methods);
|
|
118
|
+
}
|
|
119
|
+
for (const [service, serviceMethods] of Object.entries(services)) {
|
|
120
|
+
if (RESERVED.has(service)) {
|
|
121
|
+
throw new Error(`defineWorker: service name "${service}" is reserved for the client API — rename it`);
|
|
122
|
+
}
|
|
123
|
+
for (const [name, method] of Object.entries(serviceMethods)) {
|
|
124
|
+
register(`${service}.${name}`, name, method, serviceMethods);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
workerLog.info(`worker defined — ${count} method(s)`);
|
|
129
|
+
|
|
130
|
+
const definition: WorkerDefinition<S, M, Svc> = { methods, services };
|
|
131
|
+
if (config.sharedMemory) {
|
|
132
|
+
(definition as { sharedMemory?: SharedMemory<S> }).sharedMemory = config.sharedMemory;
|
|
133
|
+
}
|
|
134
|
+
return definition;
|
|
135
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TaskContract } from '../contract/types';
|
|
2
|
+
import { scoped } from '../log';
|
|
3
|
+
|
|
4
|
+
const regLog = scoped('registry');
|
|
5
|
+
|
|
6
|
+
export type TaskHandler<Args extends any[] = any[], Result = any> =
|
|
7
|
+
(...args: Args) => Result | Promise<Result>;
|
|
8
|
+
|
|
9
|
+
interface RegisteredTask {
|
|
10
|
+
contract: TaskContract;
|
|
11
|
+
handler: TaskHandler;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class TaskRegistry {
|
|
15
|
+
private static tasks: Map<string, RegisteredTask> = new Map();
|
|
16
|
+
|
|
17
|
+
public static register<Args extends any[], Result>(
|
|
18
|
+
contract: TaskContract<Args, Result>,
|
|
19
|
+
handler: TaskHandler<Args, Result>
|
|
20
|
+
): void {
|
|
21
|
+
this.tasks.set(contract.taskId, { contract, handler });
|
|
22
|
+
regLog.debug(`registered "${contract.taskId}"`, {
|
|
23
|
+
args: contract.argsSchema ? 'validated' : 'unvalidated',
|
|
24
|
+
result: contract.resultSchema ? 'validated' : 'unvalidated',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public static getContract(taskId: string): TaskContract | undefined {
|
|
29
|
+
return this.tasks.get(taskId)?.contract;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public static async execute(taskId: string, ...args: any[]): Promise<any> {
|
|
33
|
+
const task = this.tasks.get(taskId);
|
|
34
|
+
if (!task) {
|
|
35
|
+
throw new Error(`Task handler not found for id: ${taskId}`);
|
|
36
|
+
}
|
|
37
|
+
const { contract, handler } = task;
|
|
38
|
+
const validatedArgs = contract.argsSchema ? contract.argsSchema.parse(args) : args;
|
|
39
|
+
const result = await handler(...validatedArgs);
|
|
40
|
+
return contract.resultSchema ? contract.resultSchema.parse(result) : result;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { TaskRegistry } from './registry';
|
|
2
|
+
import { bindSharedMemories, getDefinedSharedMemoryCount } from '../contract/sharedMemory';
|
|
3
|
+
import { fmtBytes, scoped } from '../log';
|
|
4
|
+
|
|
5
|
+
const workerLog = scoped('worker');
|
|
6
|
+
|
|
7
|
+
let memoryInitialized = false;
|
|
8
|
+
|
|
9
|
+
if (typeof self === 'undefined') {
|
|
10
|
+
// Imported outside a worker context (e.g. a Nest entry that shares the
|
|
11
|
+
// atoll-nestjs barrel with the worker entry) — nothing to wire.
|
|
12
|
+
workerLog.warn('workerBootstrap: no worker global found — skipping message wiring');
|
|
13
|
+
} else {
|
|
14
|
+
self.onmessage = async (event) => {
|
|
15
|
+
const data = event.data;
|
|
16
|
+
|
|
17
|
+
if (data.type === 'INIT') {
|
|
18
|
+
memoryInitialized = true;
|
|
19
|
+
if (getDefinedSharedMemoryCount() > 0) {
|
|
20
|
+
workerLog.warn(
|
|
21
|
+
'pool sent INIT without memory, but shared-memory contracts are defined — field access will throw until bound',
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
workerLog.info('initialized (message-only pool — no shared memory)');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (data.type === 'INIT_MEMORY') {
|
|
29
|
+
const memory = data.memory as WebAssembly.Memory;
|
|
30
|
+
bindSharedMemories(memory.buffer as unknown as SharedArrayBuffer);
|
|
31
|
+
memoryInitialized = true;
|
|
32
|
+
workerLog.info(`shared memory initialized (${fmtBytes(memory.buffer.byteLength)})`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (data.type === 'EXECUTE_TASK') {
|
|
37
|
+
const { messageId, taskId, args } = data;
|
|
38
|
+
|
|
39
|
+
if (!memoryInitialized) {
|
|
40
|
+
workerLog.warn(`rejected ${taskId} — shared memory not initialized`);
|
|
41
|
+
self.postMessage({ messageId, success: false, error: 'Shared memory not initialized in worker.' });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const t0 = performance.now();
|
|
46
|
+
try {
|
|
47
|
+
const result = await TaskRegistry.execute(taskId, ...(args || []));
|
|
48
|
+
workerLog.debug(`task ${taskId} → ${(performance.now() - t0).toFixed(1)}ms`);
|
|
49
|
+
self.postMessage({ messageId, success: true, result });
|
|
50
|
+
} catch (err: any) {
|
|
51
|
+
workerLog.warn(`task ${taskId} failed: ${err.message || String(err)}`);
|
|
52
|
+
self.postMessage({ messageId, success: false, error: err.message || String(err) });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|