@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/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# AtollJS
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jwhenry3/atolljs/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/jwhenry3/atolljs)
|
|
5
|
+
|
|
6
|
+
Typed shared-memory worker pools for TypeScript — deterministic `SharedArrayBuffer`
|
|
7
|
+
layouts, first-class task methods, and cross-thread reactive state.
|
|
8
|
+
|
|
9
|
+
## Packages
|
|
10
|
+
|
|
11
|
+
| Package | What it is |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `@atolljs/core` | Core SDK — `defineWorker`/`connectWorker` typed worker clients over `WorkerPool`, shared-memory contracts, `watch`/`observe`, codecs |
|
|
14
|
+
| `@atolljs/react` | React hooks — `useObservable`, `useSharedValue`, `useTask` |
|
|
15
|
+
| `@atolljs/vue` | Vue composables — `useObservable`, `useSharedValue`, `useTask` |
|
|
16
|
+
| `@atolljs/solidjs` | Solid primitives — `createObservable`, `createSharedValue`, `createTask` |
|
|
17
|
+
| `@atolljs/svelte` | Svelte 5 rune bindings — `observableValue`, `sharedValue`, `taskState` |
|
|
18
|
+
| `@atolljs/angular` | Angular signals — `observableSignal`, `sharedValue`, `taskState` |
|
|
19
|
+
| `@atolljs/nextjs` | Next.js client-component bindings (React re-export) |
|
|
20
|
+
| `@atolljs/node` | `node:worker_threads` runtime adapter |
|
|
21
|
+
| `@atolljs/nestjs` | NestJS module/decorators for worker pools |
|
|
22
|
+
| `@atolljs/islands` | worker-side React reconciler + proxy DOM islands — opt-in DOM rendering |
|
|
23
|
+
| `@atolljs/react-island` | React shell components for islands — `<Island/>`, `islandComponent`, `lazyIsland` |
|
|
24
|
+
| `@atolljs/vue-island` | Vue shell (`useIsland`, `<AtollIsland>`) + Vue worker renderer |
|
|
25
|
+
| `@atolljs/svelte-island` | Svelte shell (`use:island`, `createIslandState`) + Svelte 5 worker renderer |
|
|
26
|
+
| `@atolljs/solid-island` | Solid shell (`createIsland`, `Island`) + `solid-js/universal` worker renderer |
|
|
27
|
+
| `@atolljs/angular-island` | Angular shell (`<atoll-island>`, `[atollIsland]`) + `Renderer2` worker renderer |
|
|
28
|
+
|
|
29
|
+
Framework bindings are published independently — install only the one you use:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @atolljs/core @atolljs/react
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
- **`docs/`** — in-repo markdown documentation covering internals, contracts,
|
|
38
|
+
islands, and per-framework bindings. Start at [`docs/README.md`](docs/README.md)
|
|
39
|
+
(agents: `AGENTS.md` points here before extensive work).
|
|
40
|
+
- **`docs-consumer/`** — the consumer-facing package-usage site; deploys to
|
|
41
|
+
GitHub Pages on every push to `master`:
|
|
42
|
+
[jwhenry3.github.io/atolljs](https://jwhenry3.github.io/atolljs/) (`/consumer/`).
|
|
43
|
+
|
|
44
|
+
## Releasing
|
|
45
|
+
|
|
46
|
+
Create a GitHub Release tagged `v<semver>` — `.github/workflows/publish.yml`
|
|
47
|
+
runs the full test suite, builds the core `dist`, stamps every publishable
|
|
48
|
+
package at the tag's version (lockstep; `packages/incidents` stays private),
|
|
49
|
+
and **stages** each to npm with provenance. Staged versions aren't
|
|
50
|
+
installable until a maintainer approves them — `npm stage list` /
|
|
51
|
+
`npm stage approve <stage-id>` (2FA at approval, not in CI), or the Staged
|
|
52
|
+
Packages tab on npmjs.com. Requires a granular `NPM_TOKEN` repo secret.
|
|
53
|
+
Preview the plan locally: `node scripts/publish.mjs v0.1.0 --dry-run`.
|
|
54
|
+
|
|
55
|
+
Staging needs each package to already exist on the registry, so the first
|
|
56
|
+
release is a manual bootstrap — from the repo root:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm login # once
|
|
60
|
+
npm ci && npx vite build && npx tsc -p tsconfig.build.json
|
|
61
|
+
node scripts/publish.mjs 0.1.0 --direct # prompts for 2FA per package
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Once all nine packages exist, release-driven `npm stage publish` works for
|
|
65
|
+
every subsequent version.
|
|
66
|
+
|
|
67
|
+
### Trusted publishing (OIDC)
|
|
68
|
+
|
|
69
|
+
Prefer OIDC over the `NPM_TOKEN` secret — no long-lived credential, and a
|
|
70
|
+
trust relationship can be **stage-only** so the workflow can't direct-publish
|
|
71
|
+
even if compromised. Configure per package (needs the package to exist on
|
|
72
|
+
npm, and npm CLI ≥ 11.10):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
for p in atoll atoll-node atoll-react atoll-angular atoll-nestjs atoll-nextjs atoll-solidjs atoll-svelte atoll-vue; do
|
|
76
|
+
npm trust github "@atolljs/$p" --repo jwhenry3/atolljs --file publish.yml --allow-stage-publish -y
|
|
77
|
+
sleep 2
|
|
78
|
+
done
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Omit `--allow-publish` — stage-only. First call prompts for 2FA; choose
|
|
82
|
+
"skip for 5 minutes" and the loop finishes hands-free. Verify with
|
|
83
|
+
`npm trust list @atolljs/core`. Once all nine show the relationship,
|
|
84
|
+
delete the `NODE_AUTH_TOKEN` env line in `publish.yml` (npm only uses OIDC
|
|
85
|
+
when no token is present) — the secret can be revoked after.
|
|
86
|
+
|
|
87
|
+
## Layout
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
src/ core SDK — contract/ (shared protocol), pool/, worker/
|
|
91
|
+
packages/<fw>/ independently publishable framework bindings
|
|
92
|
+
packages/incidents/ demo domain package (contract + worker + pool)
|
|
93
|
+
examples/<fw>/ per-framework demo apps
|
|
94
|
+
docs/ in-repo markdown documentation (internals + bindings)
|
|
95
|
+
docs-consumer/ consumer docs site
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Scripts
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm test # vitest — all suites (sdk + packages + example e2e)
|
|
102
|
+
npm run build # typecheck + lib build
|
|
103
|
+
npm run dev:all # launch every example dev server
|
|
104
|
+
npm run build:pages # consumer docs + examples → dist-pages (GitHub Pages artifact)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Worker demos require cross-origin isolation (COOP/COEP) — the dev servers set it;
|
|
108
|
+
GitHub Pages cannot send headers, so the Pages artifact ships `coi-sw.js`, a
|
|
109
|
+
service worker that injects them (first visit reloads once; islands fall back to
|
|
110
|
+
their poll transport where isolation still isn't available).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { msgpackrCodec as e } from "./index2.js";
|
|
2
|
+
import { listSchema as t } from "./index3.js";
|
|
3
|
+
import { getLogLevel as n, log as r, scoped as i, setLogLevel as a, setLogSink as o } from "./index4.js";
|
|
4
|
+
import { defineSharedMemory as s, field as c, getDefinedSharedMemoryCount as l, jsonCodec as u, registerConnectorFactory as d } from "./index5.js";
|
|
5
|
+
import { mz as f } from "./index6.js";
|
|
6
|
+
import { msgpackCodec as p } from "./index7.js";
|
|
7
|
+
import { MemoryManager as m } from "./index8.js";
|
|
8
|
+
import { PoolQueueFullError as h, TaskAbortedError as g, TaskTimeoutError as _, WorkerCrashedError as v } from "./index9.js";
|
|
9
|
+
import { WorkerPool as y } from "./index10.js";
|
|
10
|
+
import { connectWorker as b, workerClient as x } from "./index11.js";
|
|
11
|
+
import { connectSharedWorker as S } from "./index12.js";
|
|
12
|
+
import { TaskRegistry as C } from "./index13.js";
|
|
13
|
+
import { sharedWorkerHost as w } from "./index14.js";
|
|
14
|
+
import { createClient as T, defineService as E, implementService as D, rpc as O, serviceMethod as k } from "./index15.js";
|
|
15
|
+
import { RESERVED_CLIENT_KEYS as A, defineWorker as j } from "./index17.js";
|
|
16
|
+
import { reactive as M, shallowEqual as N, watch as P } from "./index18.js";
|
|
17
|
+
import { observe as F } from "./index19.js";
|
|
18
|
+
import { defineTask as I, isAsyncTask as L, toTask as R } from "./index20.js";
|
|
19
|
+
export { m as MemoryManager, h as PoolQueueFullError, A as RESERVED_CLIENT_KEYS, g as TaskAbortedError, C as TaskRegistry, _ as TaskTimeoutError, v as WorkerCrashedError, y as WorkerPool, S as connectSharedWorker, b as connectWorker, T as createClient, E as defineService, s as defineSharedMemory, I as defineTask, j as defineWorker, c as field, l as getDefinedSharedMemoryCount, n as getLogLevel, D as implementService, L as isAsyncTask, u as jsonCodec, t as listSchema, r as log, p as msgpackCodec, e as msgpackrCodec, f as mz, F as observe, M as reactive, d as registerConnectorFactory, O as rpc, i as scoped, k as serviceMethod, a as setLogLevel, o as setLogSink, N as shallowEqual, w as sharedWorkerHost, R as toTask, P as watch, x as workerClient };
|
package/dist/index10.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { fmtBytes as e, scoped as t } from "./index4.js";
|
|
2
|
+
import { MemoryManager as n } from "./index8.js";
|
|
3
|
+
import { PoolQueueFullError as r, TaskAbortedError as i, TaskTimeoutError as a, WorkerCrashedError as o } from "./index9.js";
|
|
4
|
+
//#region src/pool/workerPool.ts
|
|
5
|
+
var s = t("pool"), c = t("task"), l = () => ({
|
|
6
|
+
count: 0,
|
|
7
|
+
mean: 0,
|
|
8
|
+
max: 0,
|
|
9
|
+
add(e) {
|
|
10
|
+
this.count++, this.mean += (e - this.mean) / this.count, e > this.max && (this.max = e);
|
|
11
|
+
}
|
|
12
|
+
}), u = class {
|
|
13
|
+
slots = [];
|
|
14
|
+
pending = /* @__PURE__ */ new Map();
|
|
15
|
+
nextMessageId = 0;
|
|
16
|
+
queue = [];
|
|
17
|
+
memoryManager;
|
|
18
|
+
workerUrl;
|
|
19
|
+
spawn;
|
|
20
|
+
concurrency;
|
|
21
|
+
maxQueue;
|
|
22
|
+
respawn;
|
|
23
|
+
taskTimeout;
|
|
24
|
+
closed = !1;
|
|
25
|
+
drainWaiters = [];
|
|
26
|
+
statsAgg = {
|
|
27
|
+
completed: 0,
|
|
28
|
+
failed: 0,
|
|
29
|
+
aborted: 0,
|
|
30
|
+
waitMs: l(),
|
|
31
|
+
runMs: l()
|
|
32
|
+
};
|
|
33
|
+
sharedMemory;
|
|
34
|
+
constructor(t) {
|
|
35
|
+
if (t.sharedMemory && (typeof SharedArrayBuffer > "u" || typeof crossOriginIsolated < "u" && !crossOriginIsolated)) throw Error("WorkerPool requires SharedArrayBuffer in a cross-origin isolated context — serve the page with COOP/COEP headers.");
|
|
36
|
+
if (!t.workerUrl && !t.createWorker) throw Error("WorkerPool requires either `workerUrl` or `createWorker` in its config.");
|
|
37
|
+
this.workerUrl = t.workerUrl, this.spawn = t.createWorker ?? (() => new Worker(this.workerUrl, { type: "module" })), this.concurrency = t.concurrency ?? 1, this.maxQueue = t.maxQueue ?? Infinity, this.respawn = t.respawn ?? !0, this.taskTimeout = t.taskTimeout, t.sharedMemory && (this.sharedMemory = t.sharedMemory, this.memoryManager = new n(t.memory), this.memoryManager.ensureCapacity(this.sharedMemory.totalBytes), this.sharedMemory.bind(this.memoryManager.getBuffer()));
|
|
38
|
+
let r = t.poolSize ?? "auto", i = r === "auto" ? navigator.hardwareConcurrency ?? 4 : r;
|
|
39
|
+
s.info(this.sharedMemory ? `spawning ${i} worker(s) with ${e(this.sharedMemory.totalBytes)} shared buffer` : `spawning ${i} worker(s), message-only`, { workerUrl: t.workerUrl ? String(t.workerUrl) : "createWorker()" }), s.debug("tip: setLogLevel('trace') for per-task detail");
|
|
40
|
+
for (let e = 0; e < i; e++) this.addWorker(this.spawn());
|
|
41
|
+
let a = t.tasks ?? {};
|
|
42
|
+
for (let [e, t] of Object.entries(a)) {
|
|
43
|
+
if (e in this) {
|
|
44
|
+
s.warn(`task "${e}" collides with a WorkerPool member — not installed`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
this[e] = (...e) => this.runTask(t, ...e);
|
|
48
|
+
}
|
|
49
|
+
Object.keys(a).length > 0 && s.info(`task methods: ${Object.keys(a).join(", ")}`);
|
|
50
|
+
}
|
|
51
|
+
addWorker(e) {
|
|
52
|
+
let t = {
|
|
53
|
+
worker: e,
|
|
54
|
+
inFlight: 0
|
|
55
|
+
};
|
|
56
|
+
return e.addEventListener("message", (e) => {
|
|
57
|
+
this.handleReply(t, e.data);
|
|
58
|
+
}), e.addEventListener("error", (e) => {
|
|
59
|
+
this.handleWorkerError(t, e);
|
|
60
|
+
}), this.memoryManager ? e.postMessage({
|
|
61
|
+
type: "INIT_MEMORY",
|
|
62
|
+
memory: this.memoryManager.memory
|
|
63
|
+
}) : e.postMessage({ type: "INIT" }), this.slots.push(t), s.debug(`worker spawned, ${this.memoryManager ? "INIT_MEMORY" : "INIT"} sent`), t;
|
|
64
|
+
}
|
|
65
|
+
handleReply(e, t) {
|
|
66
|
+
let n = t.messageId === void 0 ? void 0 : this.pending.get(t.messageId);
|
|
67
|
+
n && (this.pending.delete(t.messageId), e.inFlight--, this.detach(n), n.done || (n.done = !0, this.statsAgg.runMs.add(performance.now() - (n.sentAt ?? n.enqueuedAt)), t.success ? (this.statsAgg.completed++, c.debug(`← ${n.contract.taskId} ok`), n.resolve(t.result)) : (this.statsAgg.failed++, c.warn(`← ${n.contract.taskId} failed: ${t.error}`), n.reject(Error(t.error)))), this.drainQueue(), this.checkDrained());
|
|
68
|
+
}
|
|
69
|
+
handleWorkerError(e, t) {
|
|
70
|
+
if (this.closed) return;
|
|
71
|
+
let n = t, r = n?.message ?? (n?.error ? String(n.error) : "worker error");
|
|
72
|
+
s.warn(`worker error: ${r}`);
|
|
73
|
+
for (let [t, n] of [...this.pending]) if (n.slot === e) {
|
|
74
|
+
if (this.pending.delete(t), this.detach(n), n.done) continue;
|
|
75
|
+
n.done = !0, this.statsAgg.failed++, n.reject(new o(r));
|
|
76
|
+
}
|
|
77
|
+
let i = this.slots.indexOf(e);
|
|
78
|
+
if (i >= 0 && this.slots.splice(i, 1), e.worker.terminate(), this.respawn) s.info("respawning crashed worker"), this.addWorker(this.spawn());
|
|
79
|
+
else if (this.slots.length === 0) for (let e of this.queue.splice(0)) e.done = !0, this.detach(e), this.statsAgg.failed++, e.reject(new o(r));
|
|
80
|
+
this.drainQueue(), this.checkDrained();
|
|
81
|
+
}
|
|
82
|
+
pickWorker() {
|
|
83
|
+
let e;
|
|
84
|
+
for (let t of this.slots) t.inFlight >= this.concurrency || (!e || t.inFlight < e.inFlight) && (e = t);
|
|
85
|
+
return e;
|
|
86
|
+
}
|
|
87
|
+
drainQueue() {
|
|
88
|
+
let e;
|
|
89
|
+
for (; this.queue.length > 0 && (e = this.pickWorker());) {
|
|
90
|
+
let t = this.queue.shift();
|
|
91
|
+
t.done || this.send(e, t);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
send(e, t) {
|
|
95
|
+
let n = this.nextMessageId++;
|
|
96
|
+
t.messageId = n, t.slot = e, t.sentAt = performance.now(), this.statsAgg.waitMs.add(performance.now() - t.enqueuedAt), e.inFlight++, this.pending.set(n, t), c.debug(`→ ${t.contract.taskId}`, {
|
|
97
|
+
worker: this.slots.indexOf(e),
|
|
98
|
+
args: t.args
|
|
99
|
+
}), e.worker.postMessage({
|
|
100
|
+
type: "EXECUTE_TASK",
|
|
101
|
+
messageId: n,
|
|
102
|
+
taskId: t.contract.taskId,
|
|
103
|
+
args: t.args
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
detach(e) {
|
|
107
|
+
e.timer && clearTimeout(e.timer), e.signal && e.onSignalAbort && e.signal.removeEventListener("abort", e.onSignalAbort);
|
|
108
|
+
}
|
|
109
|
+
cancel(e, t) {
|
|
110
|
+
if (e.done) return;
|
|
111
|
+
let n = this.queue.indexOf(e);
|
|
112
|
+
n >= 0 && this.queue.splice(n, 1), e.done = !0, this.detach(e), this.statsAgg.aborted++, e.reject(t), this.drainQueue(), this.checkDrained();
|
|
113
|
+
}
|
|
114
|
+
checkDrained() {
|
|
115
|
+
if (this.queue.length === 0 && this.pending.size === 0) {
|
|
116
|
+
let e = this.drainWaiters.splice(0);
|
|
117
|
+
for (let t of e) t();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
dispatch(e, t, n) {
|
|
121
|
+
return this.closed || this.slots.length === 0 ? Promise.reject(new o("WorkerPool has been terminated — no workers available.")) : n?.signal?.aborted ? Promise.reject(new i()) : new Promise((o, s) => {
|
|
122
|
+
let c = {
|
|
123
|
+
contract: e,
|
|
124
|
+
args: t,
|
|
125
|
+
enqueuedAt: performance.now(),
|
|
126
|
+
resolve: o,
|
|
127
|
+
reject: s,
|
|
128
|
+
done: !1,
|
|
129
|
+
signal: n?.signal
|
|
130
|
+
};
|
|
131
|
+
n?.signal && (c.onSignalAbort = () => this.cancel(c, new i()), n.signal.addEventListener("abort", c.onSignalAbort, { once: !0 }));
|
|
132
|
+
let l = n?.timeout ?? this.taskTimeout;
|
|
133
|
+
l !== void 0 && (c.timer = setTimeout(() => this.cancel(c, new a()), l));
|
|
134
|
+
let u = this.pickWorker();
|
|
135
|
+
if (u) this.send(u, c);
|
|
136
|
+
else if (this.queue.length >= this.maxQueue) {
|
|
137
|
+
c.done = !0, this.detach(c), s(new r());
|
|
138
|
+
return;
|
|
139
|
+
} else this.queue.push(c);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
runTask(e, ...t) {
|
|
143
|
+
return this.dispatch(e, t);
|
|
144
|
+
}
|
|
145
|
+
stats() {
|
|
146
|
+
return {
|
|
147
|
+
workers: this.slots.length,
|
|
148
|
+
idle: this.slots.filter((e) => e.inFlight === 0).length,
|
|
149
|
+
inFlight: this.pending.size,
|
|
150
|
+
queued: this.queue.length,
|
|
151
|
+
completed: this.statsAgg.completed,
|
|
152
|
+
failed: this.statsAgg.failed,
|
|
153
|
+
aborted: this.statsAgg.aborted,
|
|
154
|
+
waitMs: {
|
|
155
|
+
count: this.statsAgg.waitMs.count,
|
|
156
|
+
mean: this.statsAgg.waitMs.mean,
|
|
157
|
+
max: this.statsAgg.waitMs.max
|
|
158
|
+
},
|
|
159
|
+
runMs: {
|
|
160
|
+
count: this.statsAgg.runMs.count,
|
|
161
|
+
mean: this.statsAgg.runMs.mean,
|
|
162
|
+
max: this.statsAgg.runMs.max
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
close() {
|
|
167
|
+
return this.queue.length === 0 && this.pending.size === 0 ? (this.terminate(), Promise.resolve()) : new Promise((e) => {
|
|
168
|
+
this.drainWaiters.push(() => {
|
|
169
|
+
this.terminate(), e();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
terminate() {
|
|
174
|
+
this.closed = !0, s.info(`terminating ${this.slots.length} worker(s)`);
|
|
175
|
+
let e = new o("pool terminated");
|
|
176
|
+
for (let [, t] of this.pending) this.detach(t), !t.done && (t.done = !0, this.statsAgg.failed++, t.reject(e));
|
|
177
|
+
this.pending.clear();
|
|
178
|
+
for (let t of this.queue.splice(0)) t.done = !0, this.detach(t), this.statsAgg.failed++, t.reject(e);
|
|
179
|
+
for (let e of this.slots) e.worker.terminate();
|
|
180
|
+
this.slots = [], this.checkDrained();
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
//#endregion
|
|
184
|
+
export { u as WorkerPool };
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=index10.js.map
|
package/dist/index11.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { WorkerPool as e } from "./index10.js";
|
|
2
|
+
//#region src/pool/workerClient.ts
|
|
3
|
+
var t = (e, n, r, i) => {
|
|
4
|
+
let a = /* @__PURE__ */ new Map();
|
|
5
|
+
return new Proxy(() => {}, {
|
|
6
|
+
get(o, s) {
|
|
7
|
+
if (s === "then" || typeof s == "symbol") return;
|
|
8
|
+
if (s === "with") return (i) => t(e, n, r, i);
|
|
9
|
+
if (e.length === 0 && r && s in r) return Reflect.get(r, s);
|
|
10
|
+
let c = a.get(s);
|
|
11
|
+
return c || (c = t([...e, s], n, r, i), a.set(s, c)), c;
|
|
12
|
+
},
|
|
13
|
+
apply(t, r, a) {
|
|
14
|
+
let o = n(), s = { taskId: e.join(".") };
|
|
15
|
+
return o.dispatch ? o.dispatch(s, a, i) : o.runTask(s, ...a);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
function n(e) {
|
|
20
|
+
let n;
|
|
21
|
+
return t([], typeof e == "function" ? () => n ??= e() : () => e);
|
|
22
|
+
}
|
|
23
|
+
function r(n) {
|
|
24
|
+
let r = null, i = () => r ??= new e({
|
|
25
|
+
sharedMemory: n.sharedMemory,
|
|
26
|
+
createWorker: typeof n.worker == "function" ? n.worker : () => new Worker(n.worker, { type: "module" }),
|
|
27
|
+
poolSize: n.poolSize,
|
|
28
|
+
memory: n.memory,
|
|
29
|
+
concurrency: n.concurrency,
|
|
30
|
+
maxQueue: n.maxQueue,
|
|
31
|
+
respawn: n.respawn,
|
|
32
|
+
taskTimeout: n.taskTimeout
|
|
33
|
+
});
|
|
34
|
+
return n.lazy === !1 && i(), t([], i, {
|
|
35
|
+
start: () => {
|
|
36
|
+
i();
|
|
37
|
+
},
|
|
38
|
+
terminate: () => {
|
|
39
|
+
r?.terminate(), r = null;
|
|
40
|
+
},
|
|
41
|
+
get pool() {
|
|
42
|
+
return r;
|
|
43
|
+
},
|
|
44
|
+
get sharedMemory() {
|
|
45
|
+
return n.sharedMemory;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { r as connectWorker, n as workerClient };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=index11.js.map
|
package/dist/index12.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { scoped as e } from "./index4.js";
|
|
2
|
+
//#region src/shared/sharedWorkerClient.ts
|
|
3
|
+
var t = e("shared-client"), n = e("task"), r = class {
|
|
4
|
+
clientIndex;
|
|
5
|
+
sharedMemory;
|
|
6
|
+
port;
|
|
7
|
+
constructor(e, n, r) {
|
|
8
|
+
this.clientIndex = n, this.port = e, this.sharedMemory = r.sharedMemory;
|
|
9
|
+
let i = r.tasks ?? {};
|
|
10
|
+
for (let [e, n] of Object.entries(i)) {
|
|
11
|
+
if (e in this) {
|
|
12
|
+
t.warn(`task "${e}" collides with a SharedWorkerClient member — not installed`);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
this[e] = (...e) => this.runTask(n, ...e);
|
|
16
|
+
}
|
|
17
|
+
Object.keys(i).length > 0 && t.info(`task methods: ${Object.keys(i).join(", ")}`);
|
|
18
|
+
}
|
|
19
|
+
runTask(e, ...t) {
|
|
20
|
+
return new Promise((r, i) => {
|
|
21
|
+
let a = Math.random().toString(36).substring(2), o = performance.now();
|
|
22
|
+
n.debug(`→ ${e.taskId}`, { args: t });
|
|
23
|
+
let s = (t) => {
|
|
24
|
+
let c = t.data;
|
|
25
|
+
if (c.messageId === a) {
|
|
26
|
+
this.port.removeEventListener("message", s);
|
|
27
|
+
let t = (performance.now() - o).toFixed(1);
|
|
28
|
+
c.success ? (n.debug(`← ${e.taskId} ok`, { ms: +t }), r(c.result)) : (n.warn(`← ${e.taskId} failed: ${c.error}`, { ms: +t }), i(Error(c.error)));
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
this.port.addEventListener("message", s), this.port.postMessage({
|
|
32
|
+
type: "EXECUTE_TASK",
|
|
33
|
+
messageId: a,
|
|
34
|
+
taskId: e.taskId,
|
|
35
|
+
args: t
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
disconnect() {
|
|
40
|
+
try {
|
|
41
|
+
this.port.postMessage({ type: "SHARED_DISCONNECT" });
|
|
42
|
+
} finally {
|
|
43
|
+
this.port.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
async function i(e) {
|
|
48
|
+
if (typeof SharedArrayBuffer > "u" || typeof crossOriginIsolated < "u" && !crossOriginIsolated) throw Error("connectSharedWorker requires SharedArrayBuffer in a cross-origin isolated context — serve the page with COOP/COEP headers.");
|
|
49
|
+
if (!e.port && !e.workerUrl && !e.createWorker) throw Error("connectSharedWorker requires `workerUrl`, `createWorker`, or `port` in its config.");
|
|
50
|
+
let n = e.port ?? (e.createWorker ? e.createWorker().port : new SharedWorker(e.workerUrl, { type: "module" }).port);
|
|
51
|
+
return n.start(), await new Promise((i, a) => {
|
|
52
|
+
let o = setTimeout(() => a(/* @__PURE__ */ Error("connectSharedWorker: timed out waiting for the shared worker — is the host module calling sharedWorkerHost()?")), e.connectTimeoutMs ?? 1e4), s = (a) => {
|
|
53
|
+
let c = a.data;
|
|
54
|
+
c?.type === "SHARED_MEMORY" && (n.removeEventListener("message", s), clearTimeout(o), e.sharedMemory.bind(c.buffer), t.info(`connected as client #${c.clientIndex}`, { bytes: c.buffer.byteLength }), i(new r(n, c.clientIndex, e)));
|
|
55
|
+
};
|
|
56
|
+
n.addEventListener("message", s), n.postMessage({
|
|
57
|
+
type: "SHARED_CONNECT",
|
|
58
|
+
memoryBytes: e.sharedMemory.totalBytes,
|
|
59
|
+
memory: e.memory
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { i as connectSharedWorker };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=index12.js.map
|
package/dist/index13.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { scoped as e } from "./index4.js";
|
|
2
|
+
//#region src/worker/registry.ts
|
|
3
|
+
var t = e("registry"), n = class {
|
|
4
|
+
static tasks = /* @__PURE__ */ new Map();
|
|
5
|
+
static register(e, n) {
|
|
6
|
+
this.tasks.set(e.taskId, {
|
|
7
|
+
contract: e,
|
|
8
|
+
handler: n
|
|
9
|
+
}), t.debug(`registered "${e.taskId}"`, {
|
|
10
|
+
args: e.argsSchema ? "validated" : "unvalidated",
|
|
11
|
+
result: e.resultSchema ? "validated" : "unvalidated"
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
static getContract(e) {
|
|
15
|
+
return this.tasks.get(e)?.contract;
|
|
16
|
+
}
|
|
17
|
+
static async execute(e, ...t) {
|
|
18
|
+
let n = this.tasks.get(e);
|
|
19
|
+
if (!n) throw Error(`Task handler not found for id: ${e}`);
|
|
20
|
+
let { contract: r, handler: i } = n, a = await i(...r.argsSchema ? r.argsSchema.parse(t) : t);
|
|
21
|
+
return r.resultSchema ? r.resultSchema.parse(a) : a;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { n as TaskRegistry };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=index13.js.map
|
package/dist/index14.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { fmtBytes as e, scoped as t } from "./index4.js";
|
|
2
|
+
import { bindSharedMemories as n } from "./index5.js";
|
|
3
|
+
import { MemoryManager as r } from "./index8.js";
|
|
4
|
+
import { TaskRegistry as i } from "./index13.js";
|
|
5
|
+
//#region src/shared/sharedWorkerHost.ts
|
|
6
|
+
var a = t("shared-worker"), o = null, s = 0;
|
|
7
|
+
function c(t, i) {
|
|
8
|
+
if (o) return o.buffer.byteLength < t && a.warn(`client requested ${e(t)} but buffer is ${e(o.buffer.byteLength)} — first client's capacity wins`), o;
|
|
9
|
+
let s = new r(i);
|
|
10
|
+
return s.ensureCapacity(t), o = s.memory, n(s.getBuffer()), a.info(`shared buffer allocated (${e(o.buffer.byteLength)})`), o;
|
|
11
|
+
}
|
|
12
|
+
function l(e) {
|
|
13
|
+
let t = ++s;
|
|
14
|
+
a.info(`client #${t} connected`), e.onmessage = async (n) => {
|
|
15
|
+
let r = n.data;
|
|
16
|
+
if (r?.type === "SHARED_CONNECT") {
|
|
17
|
+
let n = c(r.memoryBytes ?? 0, r.memory);
|
|
18
|
+
e.postMessage({
|
|
19
|
+
type: "SHARED_MEMORY",
|
|
20
|
+
buffer: n.buffer,
|
|
21
|
+
clientIndex: t
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (r?.type === "EXECUTE_TASK") {
|
|
26
|
+
let { messageId: n, taskId: s, args: c } = r;
|
|
27
|
+
if (!o) {
|
|
28
|
+
e.postMessage({
|
|
29
|
+
messageId: n,
|
|
30
|
+
success: !1,
|
|
31
|
+
error: "Shared memory not initialized in worker."
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let l = performance.now();
|
|
36
|
+
try {
|
|
37
|
+
let r = await i.execute(s, ...c || []);
|
|
38
|
+
a.debug(`task ${s} → ${(performance.now() - l).toFixed(1)}ms (client #${t})`), e.postMessage({
|
|
39
|
+
messageId: n,
|
|
40
|
+
success: !0,
|
|
41
|
+
result: r
|
|
42
|
+
});
|
|
43
|
+
} catch (t) {
|
|
44
|
+
a.warn(`task ${s} failed: ${t.message || String(t)}`), e.postMessage({
|
|
45
|
+
messageId: n,
|
|
46
|
+
success: !1,
|
|
47
|
+
error: t.message || String(t)
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
r?.type === "SHARED_DISCONNECT" && (e.close(), a.info(`client #${t} disconnected`));
|
|
53
|
+
}, e.start();
|
|
54
|
+
}
|
|
55
|
+
function u() {
|
|
56
|
+
self.onconnect = (e) => {
|
|
57
|
+
l(e.ports[0]);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { l as attachSharedPort, u as sharedWorkerHost };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=index14.js.map
|
package/dist/index15.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { scoped as e } from "./index4.js";
|
|
2
|
+
import { TaskRegistry as t } from "./index13.js";
|
|
3
|
+
//#region src/service.ts
|
|
4
|
+
var n = e("service");
|
|
5
|
+
function r(e = {}) {
|
|
6
|
+
return e;
|
|
7
|
+
}
|
|
8
|
+
function i(e) {
|
|
9
|
+
return e;
|
|
10
|
+
}
|
|
11
|
+
function a(e, t) {
|
|
12
|
+
let n = {};
|
|
13
|
+
for (let [r, i] of Object.entries(t)) {
|
|
14
|
+
let t = "def" in i ? i.def : i;
|
|
15
|
+
n[r] = {
|
|
16
|
+
taskId: t.taskId ?? `${e}.${r}`,
|
|
17
|
+
...t.dataType ? { dataType: t.dataType } : {},
|
|
18
|
+
...t.argsSchema ? { argsSchema: t.argsSchema } : {},
|
|
19
|
+
...t.resultSchema ? { resultSchema: t.resultSchema } : {}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
name: e,
|
|
24
|
+
tasks: n
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function o(e, r) {
|
|
28
|
+
let i = Object.keys(e.tasks).filter((e) => {
|
|
29
|
+
let t = r[e];
|
|
30
|
+
return typeof t != "function" && typeof t?.run != "function";
|
|
31
|
+
});
|
|
32
|
+
if (i.length > 0) throw Error(`implementService("${e.name}"): missing handlers for ${i.join(", ")}`);
|
|
33
|
+
for (let [n, i] of Object.entries(e.tasks)) {
|
|
34
|
+
let e = r[n];
|
|
35
|
+
t.register(i, typeof e == "function" ? e.bind(r) : e.run.bind(e));
|
|
36
|
+
}
|
|
37
|
+
return n.info(`service "${e.name}" implemented — ${Object.keys(e.tasks).join(", ")}`), {
|
|
38
|
+
service: e,
|
|
39
|
+
handlers: r
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function s(e, t) {
|
|
43
|
+
let n = {};
|
|
44
|
+
for (let [r, i] of Object.entries(e.tasks)) n[r] = (...e) => t.runTask(i, ...e);
|
|
45
|
+
return n;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { s as createClient, a as defineService, o as implementService, r as rpc, i as serviceMethod };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=index15.js.map
|
package/dist/index16.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { fmtBytes as e, scoped as t } from "./index4.js";
|
|
2
|
+
import { bindSharedMemories as n, getDefinedSharedMemoryCount as r } from "./index5.js";
|
|
3
|
+
import { TaskRegistry as i } from "./index13.js";
|
|
4
|
+
//#region src/worker/workerBootstrap.ts
|
|
5
|
+
var a = t("worker"), o = !1;
|
|
6
|
+
typeof self > "u" ? a.warn("workerBootstrap: no worker global found — skipping message wiring") : self.onmessage = async (t) => {
|
|
7
|
+
let s = t.data;
|
|
8
|
+
if (s.type === "INIT") {
|
|
9
|
+
o = !0, r() > 0 && a.warn("pool sent INIT without memory, but shared-memory contracts are defined — field access will throw until bound"), a.info("initialized (message-only pool — no shared memory)");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (s.type === "INIT_MEMORY") {
|
|
13
|
+
let t = s.memory;
|
|
14
|
+
n(t.buffer), o = !0, a.info(`shared memory initialized (${e(t.buffer.byteLength)})`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (s.type === "EXECUTE_TASK") {
|
|
18
|
+
let { messageId: e, taskId: t, args: n } = s;
|
|
19
|
+
if (!o) {
|
|
20
|
+
a.warn(`rejected ${t} — shared memory not initialized`), self.postMessage({
|
|
21
|
+
messageId: e,
|
|
22
|
+
success: !1,
|
|
23
|
+
error: "Shared memory not initialized in worker."
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let r = performance.now();
|
|
28
|
+
try {
|
|
29
|
+
let o = await i.execute(t, ...n || []);
|
|
30
|
+
a.debug(`task ${t} → ${(performance.now() - r).toFixed(1)}ms`), self.postMessage({
|
|
31
|
+
messageId: e,
|
|
32
|
+
success: !0,
|
|
33
|
+
result: o
|
|
34
|
+
});
|
|
35
|
+
} catch (n) {
|
|
36
|
+
a.warn(`task ${t} failed: ${n.message || String(n)}`), self.postMessage({
|
|
37
|
+
messageId: e,
|
|
38
|
+
success: !1,
|
|
39
|
+
error: n.message || String(n)
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=index16.js.map
|
package/dist/index17.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { scoped as e } from "./index4.js";
|
|
2
|
+
import { TaskRegistry as t } from "./index13.js";
|
|
3
|
+
import "./index16.js";
|
|
4
|
+
//#region src/worker/defineWorker.ts
|
|
5
|
+
var n = e("worker"), r = [
|
|
6
|
+
"start",
|
|
7
|
+
"terminate",
|
|
8
|
+
"pool",
|
|
9
|
+
"sharedMemory",
|
|
10
|
+
"then",
|
|
11
|
+
"with"
|
|
12
|
+
], i = (e) => typeof e == "object" && !!e && "run" in e, a = new Set(r);
|
|
13
|
+
function o(e) {
|
|
14
|
+
let r = e.methods ?? {}, o = e.services ?? {}, s = 0, c = (e, n, r, o) => {
|
|
15
|
+
if (a.has(n)) throw Error(`defineWorker: "${n}" is reserved for the client API — rename the method`);
|
|
16
|
+
if (i(r)) {
|
|
17
|
+
let { def: n } = r;
|
|
18
|
+
t.register({
|
|
19
|
+
taskId: n.taskId ?? e,
|
|
20
|
+
...n.dataType ? { dataType: n.dataType } : {},
|
|
21
|
+
...n.argsSchema ? { argsSchema: n.argsSchema } : {},
|
|
22
|
+
...n.resultSchema ? { resultSchema: n.resultSchema } : {}
|
|
23
|
+
}, r.run.bind(r));
|
|
24
|
+
} else t.register({ taskId: e }, r.bind(o));
|
|
25
|
+
s++;
|
|
26
|
+
};
|
|
27
|
+
for (let [e, t] of Object.entries(r)) c(e, e, t, r);
|
|
28
|
+
for (let [e, t] of Object.entries(o)) {
|
|
29
|
+
if (a.has(e)) throw Error(`defineWorker: service name "${e}" is reserved for the client API — rename it`);
|
|
30
|
+
for (let [n, r] of Object.entries(t)) c(`${e}.${n}`, n, r, t);
|
|
31
|
+
}
|
|
32
|
+
n.info(`worker defined — ${s} method(s)`);
|
|
33
|
+
let l = {
|
|
34
|
+
methods: r,
|
|
35
|
+
services: o
|
|
36
|
+
};
|
|
37
|
+
return e.sharedMemory && (l.sharedMemory = e.sharedMemory), l;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { r as RESERVED_CLIENT_KEYS, o as defineWorker };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=index17.js.map
|