@blamejs/core 0.8.43 → 0.8.49
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/CHANGELOG.md +92 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.workerPool — generic worker_threads pool with bounded concurrency.
|
|
4
|
+
*
|
|
5
|
+
* Reusable harness for operator-defined workers that need to run
|
|
6
|
+
* CPU-bound work (compression, hashing, parser fan-out, batch render)
|
|
7
|
+
* off the main event loop without rolling per-feature lifecycle code.
|
|
8
|
+
* Wraps node:worker_threads with:
|
|
9
|
+
*
|
|
10
|
+
* - Bounded concurrency — `size` workers, default
|
|
11
|
+
* `Math.max(2, os.cpus().length)`, clamped to 1..256.
|
|
12
|
+
* - Bounded queue — `maxQueueDepth` (default 1024) refuses new
|
|
13
|
+
* `run()` calls when the in-memory queue is full so a slow worker
|
|
14
|
+
* pool can't accumulate unbounded backlog.
|
|
15
|
+
* - Per-task timeout — `taskTimeoutMs` (default `C.TIME.minutes(5)`)
|
|
16
|
+
* terminates the worker on overrun; the pool spawns a replacement
|
|
17
|
+
* so steady-state size stays stable.
|
|
18
|
+
* - Worker recycle on uncaught error — same: terminate + spawn
|
|
19
|
+
* replacement; in-flight task on that worker rejects.
|
|
20
|
+
* - Audit-everything — every task lifecycle event emits to the
|
|
21
|
+
* audit chain: workerpool.task.completed / .failed / .timeout +
|
|
22
|
+
* workerpool.created / .terminated.
|
|
23
|
+
*
|
|
24
|
+
* var pool = b.workerPool.create("/abs/path/to/worker.js", {
|
|
25
|
+
* size: 4,
|
|
26
|
+
* maxQueueDepth: C.BYTES.kib(1), // 1024 max queued tasks
|
|
27
|
+
* taskTimeoutMs: b.constants.TIME.minutes(2),
|
|
28
|
+
* onExit: function (code, workerId) { ... },
|
|
29
|
+
* });
|
|
30
|
+
* var result = await pool.run({ kind: "hash", payload: buf },
|
|
31
|
+
* [buf.buffer]); // optional transferList
|
|
32
|
+
* await pool.drain();
|
|
33
|
+
* await pool.terminate();
|
|
34
|
+
*
|
|
35
|
+
* Worker contract (operator-supplied script at scriptPath):
|
|
36
|
+
*
|
|
37
|
+
* var { parentPort } = require("node:worker_threads");
|
|
38
|
+
* parentPort.on("message", function (msg) {
|
|
39
|
+
* try {
|
|
40
|
+
* var result = doWork(msg);
|
|
41
|
+
* parentPort.postMessage({ ok: true, result: result });
|
|
42
|
+
* } catch (e) {
|
|
43
|
+
* parentPort.postMessage({ ok: false, message: e.message });
|
|
44
|
+
* }
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* The pool tracks each task by an internal taskId (monotonic), pairs
|
|
48
|
+
* the next reply from the assigned worker with that id, and resolves
|
|
49
|
+
* the run() promise. The worker's reply must be a single
|
|
50
|
+
* `{ ok: true, result }` or `{ ok: false, message }` envelope per
|
|
51
|
+
* inbound message.
|
|
52
|
+
*
|
|
53
|
+
* Failure modes (every one throws WorkerPoolError):
|
|
54
|
+
* - workerpool/bad-script-path — non-string / non-absolute / contains eval marker
|
|
55
|
+
* - workerpool/bad-size — non-int / out of 1..256 range
|
|
56
|
+
* - workerpool/bad-max-queue-depth — non-int / out of range
|
|
57
|
+
* - workerpool/bad-task-timeout — non-positive-finite / out of range
|
|
58
|
+
* - workerpool/bad-on-exit — onExit is not a function
|
|
59
|
+
* - workerpool/queue-full — run() called past maxQueueDepth
|
|
60
|
+
* - workerpool/timeout — task exceeded taskTimeoutMs
|
|
61
|
+
* - workerpool/worker-error — worker emitted "error" mid-task
|
|
62
|
+
* - workerpool/worker-exit — worker exited mid-task
|
|
63
|
+
* - workerpool/worker-bad-message — worker reply was not envelope-shaped
|
|
64
|
+
* - workerpool/task-failed — worker reported { ok: false }
|
|
65
|
+
* - workerpool/terminated — pool.terminate() aborted in-flight tasks
|
|
66
|
+
* - workerpool/no-worker-threads — runtime lacks node:worker_threads
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
var os = require("node:os");
|
|
70
|
+
var path = require("node:path");
|
|
71
|
+
var lazyRequire = require("./lazy-require");
|
|
72
|
+
var validateOpts = require("./validate-opts");
|
|
73
|
+
var numericBounds = require("./numeric-bounds");
|
|
74
|
+
var constants = require("./constants");
|
|
75
|
+
var { WorkerPoolError } = require("./framework-error");
|
|
76
|
+
|
|
77
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
78
|
+
|
|
79
|
+
var MIN_SIZE = 1;
|
|
80
|
+
var MAX_SIZE = 256; // allow:raw-byte-literal — sanity ceiling on worker count, not bytes
|
|
81
|
+
var DEFAULT_MAX_QUEUE_DEPTH = 1024; // allow:raw-byte-literal — task-queue depth, not bytes
|
|
82
|
+
var MAX_QUEUE_DEPTH_CAP = 1048576; // allow:raw-byte-literal — task-queue depth ceiling, not bytes
|
|
83
|
+
var DEFAULT_TASK_TIMEOUT_MS = constants.TIME.minutes(5);
|
|
84
|
+
var MAX_TASK_TIMEOUT_MS = constants.TIME.hours(1);
|
|
85
|
+
|
|
86
|
+
// Refuse operator-supplied `eval`-style script paths. Worker_threads
|
|
87
|
+
// supports `{ eval: true }` to spawn from a string; this primitive
|
|
88
|
+
// only accepts a real absolute filesystem path so a typo / operator-
|
|
89
|
+
// supplied input can't be coerced into eval.
|
|
90
|
+
function _validateScriptPath(scriptPath) {
|
|
91
|
+
validateOpts.requireNonEmptyString(scriptPath,
|
|
92
|
+
"workerPool.create: scriptPath", WorkerPoolError, "workerpool/bad-script-path");
|
|
93
|
+
if (!path.isAbsolute(scriptPath)) {
|
|
94
|
+
throw new WorkerPoolError("workerpool/bad-script-path",
|
|
95
|
+
"workerPool.create: scriptPath must be an absolute path; got " +
|
|
96
|
+
JSON.stringify(scriptPath));
|
|
97
|
+
}
|
|
98
|
+
// Defense-in-depth: refuse any path that looks like a data URL / eval
|
|
99
|
+
// marker. Real filesystem paths never contain these.
|
|
100
|
+
if (/^data:/i.test(scriptPath) || /^eval:/i.test(scriptPath)) {
|
|
101
|
+
throw new WorkerPoolError("workerpool/bad-script-path",
|
|
102
|
+
"workerPool.create: scriptPath must be a filesystem path, not an eval/data URL");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function _emitAudit(action, outcome, metadata) {
|
|
107
|
+
try {
|
|
108
|
+
audit().safeEmit({
|
|
109
|
+
action: action,
|
|
110
|
+
outcome: outcome,
|
|
111
|
+
metadata: metadata || {},
|
|
112
|
+
});
|
|
113
|
+
} catch (_e) { /* drop-silent — audit best-effort */ }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function create(scriptPath, opts) {
|
|
117
|
+
opts = opts || {};
|
|
118
|
+
validateOpts(opts, ["size", "onExit", "maxQueueDepth", "taskTimeoutMs"], "workerPool.create");
|
|
119
|
+
_validateScriptPath(scriptPath);
|
|
120
|
+
|
|
121
|
+
var defaultSize = Math.max(2, (os.cpus() || []).length || 2);
|
|
122
|
+
var size = (opts.size === undefined) ? defaultSize : opts.size;
|
|
123
|
+
if (!numericBounds.isPositiveFiniteInt(size) || size < MIN_SIZE || size > MAX_SIZE) {
|
|
124
|
+
throw new WorkerPoolError("workerpool/bad-size",
|
|
125
|
+
"workerPool.create: opts.size must be a positive finite integer in [" +
|
|
126
|
+
MIN_SIZE + ".." + MAX_SIZE + "]; got " + numericBounds.shape(size));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
var maxQueueDepth = (opts.maxQueueDepth === undefined) ? DEFAULT_MAX_QUEUE_DEPTH : opts.maxQueueDepth;
|
|
130
|
+
if (!numericBounds.isPositiveFiniteInt(maxQueueDepth) || maxQueueDepth > MAX_QUEUE_DEPTH_CAP) {
|
|
131
|
+
throw new WorkerPoolError("workerpool/bad-max-queue-depth",
|
|
132
|
+
"workerPool.create: opts.maxQueueDepth must be a positive finite integer <= " +
|
|
133
|
+
MAX_QUEUE_DEPTH_CAP + "; got " + numericBounds.shape(maxQueueDepth));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
var taskTimeoutMs = (opts.taskTimeoutMs === undefined) ? DEFAULT_TASK_TIMEOUT_MS : opts.taskTimeoutMs;
|
|
137
|
+
if (!numericBounds.isPositiveFiniteInt(taskTimeoutMs) || taskTimeoutMs > MAX_TASK_TIMEOUT_MS) {
|
|
138
|
+
throw new WorkerPoolError("workerpool/bad-task-timeout",
|
|
139
|
+
"workerPool.create: opts.taskTimeoutMs must be a positive finite integer <= " +
|
|
140
|
+
MAX_TASK_TIMEOUT_MS + "; got " + numericBounds.shape(taskTimeoutMs));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
var onExit = opts.onExit;
|
|
144
|
+
if (onExit !== undefined && onExit !== null && typeof onExit !== "function") {
|
|
145
|
+
throw new WorkerPoolError("workerpool/bad-on-exit",
|
|
146
|
+
"workerPool.create: opts.onExit must be a function; got " + typeof onExit);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
var workerThreads;
|
|
150
|
+
try { workerThreads = require("node:worker_threads"); }
|
|
151
|
+
catch (_e) {
|
|
152
|
+
throw new WorkerPoolError("workerpool/no-worker-threads",
|
|
153
|
+
"workerPool.create: node:worker_threads is unavailable in this runtime");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Per-pool state. Workers carry { id, worker, busy, currentTaskId,
|
|
157
|
+
// currentTimer }. Queue holds { message, transferList, resolve,
|
|
158
|
+
// reject } envelopes.
|
|
159
|
+
var workerSlots = [];
|
|
160
|
+
var workerSeq = 0;
|
|
161
|
+
var taskSeq = 0;
|
|
162
|
+
var queue = [];
|
|
163
|
+
var totalTasks = 0;
|
|
164
|
+
var totalErrors = 0;
|
|
165
|
+
var terminated = false;
|
|
166
|
+
var drainResolvers = [];
|
|
167
|
+
|
|
168
|
+
function _spawnWorker() {
|
|
169
|
+
var id = ++workerSeq;
|
|
170
|
+
var worker;
|
|
171
|
+
try {
|
|
172
|
+
worker = new workerThreads.Worker(scriptPath);
|
|
173
|
+
} catch (eSpawn) {
|
|
174
|
+
_emitAudit("workerpool.spawn.failed", "failure", {
|
|
175
|
+
scriptPath: scriptPath,
|
|
176
|
+
message: (eSpawn && eSpawn.message) || String(eSpawn),
|
|
177
|
+
});
|
|
178
|
+
throw new WorkerPoolError("workerpool/spawn-failed",
|
|
179
|
+
"workerPool.create: failed to spawn worker: " + (eSpawn && eSpawn.message));
|
|
180
|
+
}
|
|
181
|
+
var slot = {
|
|
182
|
+
id: id,
|
|
183
|
+
worker: worker,
|
|
184
|
+
busy: false,
|
|
185
|
+
currentTaskId: null,
|
|
186
|
+
currentTimer: null,
|
|
187
|
+
currentTask: null,
|
|
188
|
+
};
|
|
189
|
+
worker.on("message", function (msg) { _onWorkerMessage(slot, msg); });
|
|
190
|
+
worker.on("error", function (err) { _onWorkerError(slot, err); });
|
|
191
|
+
worker.on("exit", function (code) { _onWorkerExit(slot, code); });
|
|
192
|
+
workerSlots.push(slot);
|
|
193
|
+
_emitAudit("workerpool.created", "success", { workerId: id, size: size });
|
|
194
|
+
return slot;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function _findIdleSlot() {
|
|
198
|
+
for (var i = 0; i < workerSlots.length; i += 1) {
|
|
199
|
+
if (!workerSlots[i].busy && !workerSlots[i].recycling) return workerSlots[i];
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function _dispatch(slot, task) {
|
|
205
|
+
slot.busy = true;
|
|
206
|
+
slot.currentTaskId = task.id;
|
|
207
|
+
slot.currentTask = task;
|
|
208
|
+
slot.currentTimer = setTimeout(function () {
|
|
209
|
+
_onTaskTimeout(slot);
|
|
210
|
+
}, taskTimeoutMs);
|
|
211
|
+
if (slot.currentTimer && typeof slot.currentTimer.unref === "function") {
|
|
212
|
+
// Don't keep the event loop open just for the timeout.
|
|
213
|
+
slot.currentTimer.unref();
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
slot.worker.postMessage(task.message, task.transferList || undefined);
|
|
217
|
+
} catch (ePost) {
|
|
218
|
+
_finishTask(slot, true,
|
|
219
|
+
new WorkerPoolError("workerpool/post-failed",
|
|
220
|
+
"workerPool.run: postMessage failed: " + (ePost && ePost.message)));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function _drainQueue() {
|
|
225
|
+
while (!terminated && queue.length > 0) {
|
|
226
|
+
var slot = _findIdleSlot();
|
|
227
|
+
if (!slot) return;
|
|
228
|
+
var task = queue.shift();
|
|
229
|
+
_dispatch(slot, task);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function _finishTask(slot, isError, payloadOrError) {
|
|
234
|
+
var task = slot.currentTask;
|
|
235
|
+
if (!task) return;
|
|
236
|
+
if (slot.currentTimer) { clearTimeout(slot.currentTimer); slot.currentTimer = null; }
|
|
237
|
+
slot.busy = false;
|
|
238
|
+
slot.currentTaskId = null;
|
|
239
|
+
slot.currentTask = null;
|
|
240
|
+
totalTasks += 1;
|
|
241
|
+
if (isError) {
|
|
242
|
+
totalErrors += 1;
|
|
243
|
+
task.reject(payloadOrError);
|
|
244
|
+
} else {
|
|
245
|
+
task.resolve(payloadOrError);
|
|
246
|
+
}
|
|
247
|
+
_maybeResolveDrain();
|
|
248
|
+
_drainQueue();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function _onWorkerMessage(slot, msg) {
|
|
252
|
+
if (!slot.currentTask) {
|
|
253
|
+
// Stray message — worker posted before any task; ignore.
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (!msg || typeof msg !== "object" || typeof msg.ok !== "boolean") {
|
|
257
|
+
_emitAudit("workerpool.task.failed", "failure", {
|
|
258
|
+
workerId: slot.id, taskId: slot.currentTaskId, reason: "workerpool/worker-bad-message",
|
|
259
|
+
});
|
|
260
|
+
_finishTask(slot, true,
|
|
261
|
+
new WorkerPoolError("workerpool/worker-bad-message",
|
|
262
|
+
"workerPool: worker reply was not { ok, ... } envelope-shaped"));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (msg.ok) {
|
|
266
|
+
_emitAudit("workerpool.task.completed", "success", {
|
|
267
|
+
workerId: slot.id, taskId: slot.currentTaskId,
|
|
268
|
+
});
|
|
269
|
+
_finishTask(slot, false, msg.result);
|
|
270
|
+
} else {
|
|
271
|
+
_emitAudit("workerpool.task.failed", "failure", {
|
|
272
|
+
workerId: slot.id, taskId: slot.currentTaskId,
|
|
273
|
+
reason: "workerpool/task-failed",
|
|
274
|
+
message: msg.message || "",
|
|
275
|
+
});
|
|
276
|
+
_finishTask(slot, true,
|
|
277
|
+
new WorkerPoolError("workerpool/task-failed",
|
|
278
|
+
"workerPool: worker reported failure: " +
|
|
279
|
+
(msg.message || "(no message)")));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function _onWorkerError(slot, err) {
|
|
284
|
+
var failingTask = slot.currentTask;
|
|
285
|
+
_emitAudit("workerpool.task.failed", "failure", {
|
|
286
|
+
workerId: slot.id, taskId: slot.currentTaskId,
|
|
287
|
+
reason: "workerpool/worker-error",
|
|
288
|
+
message: (err && err.message) || String(err),
|
|
289
|
+
});
|
|
290
|
+
if (failingTask) {
|
|
291
|
+
_finishTask(slot, true,
|
|
292
|
+
new WorkerPoolError("workerpool/worker-error",
|
|
293
|
+
"workerPool: worker errored: " +
|
|
294
|
+
(err && err.message ? err.message : String(err))));
|
|
295
|
+
}
|
|
296
|
+
// Worker is now in an indeterminate state; recycle it.
|
|
297
|
+
_recycleWorker(slot);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function _onWorkerExit(slot, code) {
|
|
301
|
+
var failingTask = slot.currentTask;
|
|
302
|
+
if (failingTask) {
|
|
303
|
+
_emitAudit("workerpool.task.failed", "failure", {
|
|
304
|
+
workerId: slot.id, taskId: slot.currentTaskId,
|
|
305
|
+
reason: "workerpool/worker-exit", code: code,
|
|
306
|
+
});
|
|
307
|
+
_finishTask(slot, true,
|
|
308
|
+
new WorkerPoolError("workerpool/worker-exit",
|
|
309
|
+
"workerPool: worker exited (code " + code + ") mid-task"));
|
|
310
|
+
}
|
|
311
|
+
_emitAudit("workerpool.terminated", "success", {
|
|
312
|
+
workerId: slot.id, code: code,
|
|
313
|
+
});
|
|
314
|
+
if (typeof onExit === "function") {
|
|
315
|
+
try { onExit(code, slot.id); } catch (_e) { /* drop-silent — operator hook */ }
|
|
316
|
+
}
|
|
317
|
+
// Remove from active set. If the pool is still live, spawn a replacement.
|
|
318
|
+
var idx = workerSlots.indexOf(slot);
|
|
319
|
+
if (idx !== -1) workerSlots.splice(idx, 1);
|
|
320
|
+
if (!terminated && workerSlots.length < size) {
|
|
321
|
+
try { _spawnWorker(); } catch (_e) { /* spawn already audited */ }
|
|
322
|
+
_drainQueue();
|
|
323
|
+
} else {
|
|
324
|
+
_maybeResolveDrain();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function _onTaskTimeout(slot) {
|
|
329
|
+
var taskId = slot.currentTaskId;
|
|
330
|
+
_emitAudit("workerpool.task.timeout", "failure", {
|
|
331
|
+
workerId: slot.id, taskId: taskId, taskTimeoutMs: taskTimeoutMs,
|
|
332
|
+
});
|
|
333
|
+
var failingTask = slot.currentTask;
|
|
334
|
+
if (failingTask) {
|
|
335
|
+
_finishTask(slot, true,
|
|
336
|
+
new WorkerPoolError("workerpool/timeout",
|
|
337
|
+
"workerPool: task " + taskId + " exceeded taskTimeoutMs=" + taskTimeoutMs));
|
|
338
|
+
}
|
|
339
|
+
_recycleWorker(slot);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function _recycleWorker(slot) {
|
|
343
|
+
// Mark the slot as dying so _findIdleSlot skips it before exit
|
|
344
|
+
// fires. Without this, a new run() between terminate() and the
|
|
345
|
+
// exit event would dispatch to a worker that's about to die and
|
|
346
|
+
// surface as workerpool/worker-exit on a freshly-queued task.
|
|
347
|
+
slot.busy = true;
|
|
348
|
+
slot.recycling = true;
|
|
349
|
+
try { slot.worker.terminate(); } catch (_e) { /* terminate best-effort */ }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function _maybeResolveDrain() {
|
|
353
|
+
if (drainResolvers.length === 0) return;
|
|
354
|
+
var anyBusy = false;
|
|
355
|
+
for (var i = 0; i < workerSlots.length; i += 1) {
|
|
356
|
+
if (workerSlots[i].busy) { anyBusy = true; break; }
|
|
357
|
+
}
|
|
358
|
+
if (anyBusy || queue.length > 0) return;
|
|
359
|
+
var pending = drainResolvers.splice(0, drainResolvers.length);
|
|
360
|
+
for (var j = 0; j < pending.length; j += 1) {
|
|
361
|
+
try { pending[j](); } catch (_e) { /* drop-silent — drain best-effort */ }
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function run(message, transferList) {
|
|
366
|
+
if (terminated) {
|
|
367
|
+
return Promise.reject(new WorkerPoolError("workerpool/terminated",
|
|
368
|
+
"workerPool.run: pool has been terminated"));
|
|
369
|
+
}
|
|
370
|
+
if (transferList !== undefined && transferList !== null && !Array.isArray(transferList)) {
|
|
371
|
+
return Promise.reject(new WorkerPoolError("workerpool/bad-transfer-list",
|
|
372
|
+
"workerPool.run: transferList must be an array if supplied"));
|
|
373
|
+
}
|
|
374
|
+
if (queue.length >= maxQueueDepth) {
|
|
375
|
+
return Promise.reject(new WorkerPoolError("workerpool/queue-full",
|
|
376
|
+
"workerPool.run: queue is full (depth=" + queue.length +
|
|
377
|
+
" >= maxQueueDepth=" + maxQueueDepth + ")"));
|
|
378
|
+
}
|
|
379
|
+
var taskId = ++taskSeq;
|
|
380
|
+
return new Promise(function (resolve, reject) {
|
|
381
|
+
var task = {
|
|
382
|
+
id: taskId,
|
|
383
|
+
message: message,
|
|
384
|
+
transferList: transferList || null,
|
|
385
|
+
resolve: resolve,
|
|
386
|
+
reject: reject,
|
|
387
|
+
};
|
|
388
|
+
var slot = _findIdleSlot();
|
|
389
|
+
if (slot) {
|
|
390
|
+
_dispatch(slot, task);
|
|
391
|
+
} else {
|
|
392
|
+
queue.push(task);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function drain() {
|
|
398
|
+
return new Promise(function (resolve) {
|
|
399
|
+
var anyBusy = false;
|
|
400
|
+
for (var i = 0; i < workerSlots.length; i += 1) {
|
|
401
|
+
if (workerSlots[i].busy) { anyBusy = true; break; }
|
|
402
|
+
}
|
|
403
|
+
if (!anyBusy && queue.length === 0) { resolve(); return; }
|
|
404
|
+
drainResolvers.push(resolve);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function terminate() {
|
|
409
|
+
terminated = true;
|
|
410
|
+
// Reject queued tasks first so the caller sees a deterministic error.
|
|
411
|
+
var pending = queue.splice(0, queue.length);
|
|
412
|
+
for (var i = 0; i < pending.length; i += 1) {
|
|
413
|
+
try {
|
|
414
|
+
pending[i].reject(new WorkerPoolError("workerpool/terminated",
|
|
415
|
+
"workerPool.terminate: task aborted before dispatch"));
|
|
416
|
+
} catch (_e) { /* drop-silent — caller already has rejection */ }
|
|
417
|
+
}
|
|
418
|
+
// Then terminate every worker. _onWorkerExit will reject any in-flight task.
|
|
419
|
+
var promises = [];
|
|
420
|
+
for (var j = 0; j < workerSlots.length; j += 1) {
|
|
421
|
+
var slot = workerSlots[j];
|
|
422
|
+
if (slot.currentTimer) { clearTimeout(slot.currentTimer); slot.currentTimer = null; }
|
|
423
|
+
try { promises.push(slot.worker.terminate()); }
|
|
424
|
+
catch (_e) { /* terminate best-effort */ }
|
|
425
|
+
}
|
|
426
|
+
return Promise.all(promises).then(function () { /* swallow undefined returns */ });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function stats() {
|
|
430
|
+
var busy = 0;
|
|
431
|
+
for (var i = 0; i < workerSlots.length; i += 1) {
|
|
432
|
+
if (workerSlots[i].busy) busy += 1;
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
size: workerSlots.length,
|
|
436
|
+
busy: busy,
|
|
437
|
+
idle: workerSlots.length - busy,
|
|
438
|
+
queued: queue.length,
|
|
439
|
+
totalTasks: totalTasks,
|
|
440
|
+
totalErrors: totalErrors,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Bring up the pool eagerly so the first run() doesn't pay spawn cost.
|
|
445
|
+
for (var k = 0; k < size; k += 1) _spawnWorker();
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
run: run,
|
|
449
|
+
drain: drain,
|
|
450
|
+
terminate: terminate,
|
|
451
|
+
stats: stats,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
module.exports = {
|
|
456
|
+
create: create,
|
|
457
|
+
MIN_SIZE: MIN_SIZE,
|
|
458
|
+
MAX_SIZE: MAX_SIZE,
|
|
459
|
+
DEFAULT_MAX_QUEUE_DEPTH: DEFAULT_MAX_QUEUE_DEPTH,
|
|
460
|
+
MAX_QUEUE_DEPTH_CAP: MAX_QUEUE_DEPTH_CAP,
|
|
461
|
+
DEFAULT_TASK_TIMEOUT_MS: DEFAULT_TASK_TIMEOUT_MS,
|
|
462
|
+
MAX_TASK_TIMEOUT_MS: MAX_TASK_TIMEOUT_MS,
|
|
463
|
+
WorkerPoolError: WorkerPoolError,
|
|
464
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.49",
|
|
4
4
|
"description": "The Node framework that owns its stack.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "blamejs contributors",
|
|
7
|
-
"homepage": "https://
|
|
7
|
+
"homepage": "https://blamejs.com",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/blamejs/blamejs.git"
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"owns-its-stack"
|
|
55
55
|
],
|
|
56
56
|
"engines": {
|
|
57
|
-
"node": ">=24.
|
|
57
|
+
"node": ">=24.14.1"
|
|
58
58
|
},
|
|
59
59
|
"files": [
|
|
60
60
|
"index.js",
|
package/sbom.cyclonedx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:34a5042e-06d2-4db8-8f87-c16c95d50c13",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-09T14:56:33.086Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.8.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.49",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.49",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.49",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"type": "website",
|
|
38
|
-
"url": "https://
|
|
38
|
+
"url": "https://blamejs.com"
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"type": "issue-tracker",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.8.
|
|
57
|
+
"ref": "@blamejs/core@0.8.49",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|