@blamejs/core 0.8.42 → 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.
Files changed (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
package/lib/safe-async.js CHANGED
@@ -1,99 +1,62 @@
1
1
  "use strict";
2
2
  /**
3
- * Async resilience + safety primitives.
4
- *
5
- * The framework's async surfaces (external-db queries, cluster
6
- * coordination, queue operations, audit chain writes) all share the
7
- * same hazards: races between interleaved awaits, unbounded retries
8
- * masking real failures, hangs from unresponsive backends, and partial
9
- * results from operator-supplied drivers. This module collects the
10
- * primitives the framework uses to handle those hazards consistently.
11
- *
12
- * Surface includes:
13
- * - Async coordination: withTimeout, withSignal, sleep, repeating,
14
- * flushLoop, safeAwait, asyncRetry
15
- * - Async state objects: Mutex, Semaphore, Once, CircuitBreaker
16
- * - Sync helpers used by async pipelines: safeInvoke (callback
17
- * wrapper with optional onError), makeDropCallback (factory for
18
- * log-stream-style onDrop callbacks), makeScheduledFlush
19
- * (idempotent setTimeout coalesce-and-flush helper)
20
- *
21
- * Design posture:
22
- *
23
- * - **AbortSignal everywhere.** Every primitive that takes time
24
- * accepts an `AbortSignal` and aborts cleanly when the signal
25
- * fires. This is the modern Node.js convention (Node 18+) and
26
- * replaces the older "cancellation token" pattern. Operators who
27
- * don't pass a signal get the legacy non-cancellable behaviour.
28
- *
29
- * - **Error.cause preserved.** Wrapper errors set `.cause` to the
30
- * original failure so debugging traces back to the root. Callers
31
- * who walk `.cause` chains see the full picture.
32
- *
33
- * - **No leaked Promises.** Mutex / Semaphore release on path-out
34
- * in finally blocks even cancellation. No pending acquirer
35
- * stays referenced after its abort.
36
- *
37
- * - **Bounded by default.** Semaphore / Queue have explicit limits
38
- * and reject acquisitions over the limit rather than growing
39
- * unboundedly. Operators size limits explicitly for their workload.
40
- *
41
- * - **Fail loud.** Errors propagate; primitives never silently
42
- * swallow. safeAwait() opt-in for callers who need {error, value}
43
- * tuples; everything else throws / rejects.
44
- *
45
- * Public API:
46
- *
47
- * withTimeout(promise, ms, opts?) promise; rejects on timeout
48
- * withSignal(promise, signal) promise; rejects on abort
49
- * withTimeoutSignal(signal, ms) AbortSignal composing user
50
- * signal + a fresh timeout. Used
51
- * by I/O primitives that already
52
- * accept a signal and want to
53
- * add a wall-clock deadline.
54
- * sleep(ms, opts?) promise that resolves after ms;
55
- * opts.signal aborts mid-sleep,
56
- * timer is unref'd so a pending
57
- * sleep doesn't keep the process
58
- * alive
59
- * safeAwait(promise) [error, value] never throws
60
- *
61
- * Mutex class; .runExclusive(fn)
62
- * Semaphore(limit) class; .runWith(fn)
63
- * Once(fn) class; .invoke()
64
- *
65
- * asyncRetry(fn, opts?) re-export from object-store-retry
66
- * CircuitBreaker(name, opts?) re-export from object-store-retry
67
- *
68
- * SafeAsyncError error class
69
- *
70
- * Best-practice notes for callers:
71
- *
72
- * - Always pair `withTimeout` with the external-db / network calls
73
- * where operator-supplied drivers might hang. The framework's
74
- * external-db wrapper already retries; timeout puts a ceiling on
75
- * each individual attempt.
76
- *
77
- * - Wrap chain-writes with Mutex.runExclusive. Audit chain hashing
78
- * reads the previous tip and writes a successor; without
79
- * serialization, concurrent awaiting record() calls can hash
80
- * against the same prev-tip and produce a forked chain. Mutex
81
- * prevents this in single-process; for cross-process coordination
82
- * the cluster module's leader election is the correct primitive.
83
- *
84
- * - Use Once for boot-time lazy init (counter primer, schema
85
- * check). Multiple concurrent first-callers correctly all wait
86
- * on the same in-flight init Promise rather than each starting
87
- * their own.
88
- *
89
- * - Use safeAwait for fire-and-forget paths (audit hooks in
90
- * middleware) that previously used try/catch — preserves the
91
- * "log + continue" pattern without unhandled-rejection warnings.
92
- *
93
- * - Prefer Promise.allSettled over Promise.all when partial failure
94
- * is acceptable (e.g. emitting to multiple log sinks; one sink
95
- * down shouldn't block the others). The framework's log-stream
96
- * dispatcher already does this.
3
+ * @module b.safeAsync
4
+ * @nav Validation
5
+ * @title Safe Async
6
+ *
7
+ * @intro
8
+ * Timeout-bounded promises, AbortSignal-aware coordination,
9
+ * Promise.race-shaped helpers, and settled-state queries for the
10
+ * framework's async surfaces (external-db queries, cluster
11
+ * coordination, queue operations, audit chain writes).
12
+ *
13
+ * Hazards this module addresses: races between interleaved awaits,
14
+ * unbounded retries masking real failures, hangs from unresponsive
15
+ * backends, and partial results from operator-supplied drivers.
16
+ *
17
+ * Surface:
18
+ * - Async coordination: withTimeout, withSignal, withTimeoutSignal,
19
+ * sleep, repeating, flushLoop, safeAwait, parallel, asyncRetry
20
+ * - Async state objects: Mutex, Semaphore, Once, CircuitBreaker
21
+ * - Sync helpers used by async pipelines: safeInvoke (callback
22
+ * wrapper with optional onError), makeDropCallback (factory for
23
+ * log-stream-style onDrop callbacks), makeScheduledFlush
24
+ * (idempotent setTimeout coalesce-and-flush helper)
25
+ *
26
+ * Design posture:
27
+ * - AbortSignal everywhere. Every time-bounded primitive accepts
28
+ * an AbortSignal and aborts cleanly when it fires.
29
+ * - Error.cause preserved. Wrapper errors set `.cause` to the
30
+ * original failure so debugging traces back to the root.
31
+ * - No leaked Promises. Mutex / Semaphore release on path-out
32
+ * in finally blocks — even on cancellation.
33
+ * - Bounded by default. Semaphore / parallel have explicit limits
34
+ * and reject over-the-limit acquisitions rather than growing
35
+ * unboundedly.
36
+ * - Fail loud. Errors propagate; primitives never silently
37
+ * swallow. safeAwait is the opt-in `{error, value}` tuple form
38
+ * for callers that want to log-and-continue.
39
+ *
40
+ * Best-practice notes for callers:
41
+ * - Pair `withTimeout` with external-db / network calls where
42
+ * operator-supplied drivers might hang. Puts a ceiling on each
43
+ * individual attempt.
44
+ * - Wrap chain-writes with `Mutex.runExclusive`. Audit chain
45
+ * hashing reads the previous tip and writes a successor; without
46
+ * serialization, concurrent record() calls can hash against the
47
+ * same prev-tip and fork the chain.
48
+ * - Use `Once` for boot-time lazy init (counter primer, schema
49
+ * check). Multiple concurrent first-callers correctly wait on
50
+ * the same in-flight init Promise.
51
+ * - Use `safeAwait` for fire-and-forget paths (audit hooks in
52
+ * middleware) preserves "log + continue" without unhandled-
53
+ * rejection warnings.
54
+ * - Prefer Promise.allSettled over Promise.all when partial
55
+ * failure is acceptable (multiple log sinks; one down shouldn't
56
+ * block the others).
57
+ *
58
+ * @card
59
+ * Timeout-bounded promises, AbortSignal-aware coordination, Promise.race-shaped helpers, and settled-state queries for the framework's async surfaces (external-db queries, cluster coordination, queue operations, audit chain writes).
97
60
  */
98
61
 
99
62
  var { FrameworkError } = require("./framework-error");
@@ -119,6 +82,41 @@ class SafeAsyncError extends FrameworkError {
119
82
  // with code=async/aborted.
120
83
  // opts.name: diagnostic label included in the timeout message.
121
84
 
85
+ /**
86
+ * @primitive b.safeAsync.withTimeout
87
+ * @signature b.safeAsync.withTimeout(promise, ms, opts?)
88
+ * @since 0.1.0
89
+ * @status stable
90
+ * @related b.safeAsync.withSignal, b.safeAsync.withTimeoutSignal, b.safeAsync.sleep
91
+ *
92
+ * Race a Promise against a wall-clock deadline. On timeout the
93
+ * wrapper rejects with `SafeAsyncError` (`.code = "async/timeout"`);
94
+ * the underlying Promise keeps running in the background since the
95
+ * framework cannot cancel an arbitrary async operation. Pair with
96
+ * AbortSignal-aware I/O when the caller also wants the work itself
97
+ * to stop. `opts.signal` aborts the wrapper with
98
+ * `.code = "async/aborted"`; `opts.name` is included in the timeout
99
+ * message for diagnostics.
100
+ *
101
+ * @opts
102
+ * signal: AbortSignal, // aborts the wrapper with async/aborted
103
+ * name: string, // diagnostic label baked into error messages
104
+ *
105
+ * @example
106
+ * var b = require("blamejs");
107
+ *
108
+ * // Bound an HTTP call to 5s.
109
+ * var fetchUser = Promise.resolve({ id: 42, name: "alice" });
110
+ * var user = await b.safeAsync.withTimeout(fetchUser, 5000, { name: "fetchUser" });
111
+ * user.id;
112
+ * // → 42
113
+ *
114
+ * // Timeout surfaces as SafeAsyncError(async/timeout).
115
+ * var hang = new Promise(function () {});
116
+ * try { await b.safeAsync.withTimeout(hang, 10, { name: "stuck" }); }
117
+ * catch (e) { e.code; }
118
+ * // → "async/timeout"
119
+ */
122
120
  function withTimeout(promise, ms, opts) {
123
121
  opts = opts || {};
124
122
  if (typeof ms !== "number" || ms <= 0 || !Number.isFinite(ms)) {
@@ -176,6 +174,32 @@ function withTimeout(promise, ms, opts) {
176
174
  // running in the background; only the wrapper's resolution is short-
177
175
  // circuited. Useful for plumbing a single signal through a chain of awaits.
178
176
 
177
+ /**
178
+ * @primitive b.safeAsync.withSignal
179
+ * @signature b.safeAsync.withSignal(promise, signal)
180
+ * @since 0.1.0
181
+ * @status stable
182
+ * @related b.safeAsync.withTimeout, b.safeAsync.withTimeoutSignal
183
+ *
184
+ * Race a Promise against an AbortSignal. When the signal aborts the
185
+ * wrapper rejects with `SafeAsyncError` (`.code = "async/aborted"`,
186
+ * `.cause = signal.reason`). The underlying Promise continues
187
+ * running in the background — only the wrapper's resolution is
188
+ * short-circuited. Useful for plumbing one signal through a chain
189
+ * of awaits where some intermediates aren't signal-aware.
190
+ *
191
+ * @example
192
+ * var b = require("blamejs");
193
+ *
194
+ * // Propagate an AbortSignal through a non-signal-aware Promise.
195
+ * var ctrl = new AbortController();
196
+ * var slow = new Promise(function (resolve) { setTimeout(resolve, 50, "done"); });
197
+ * var wrapped = b.safeAsync.withSignal(slow, ctrl.signal);
198
+ * ctrl.abort();
199
+ * try { await wrapped; }
200
+ * catch (e) { e.code; }
201
+ * // → "async/aborted"
202
+ */
179
203
  function withSignal(promise, signal) {
180
204
  if (!signal) return Promise.resolve(promise);
181
205
  return new Promise(function (resolve, reject) {
@@ -222,6 +246,40 @@ function withSignal(promise, signal) {
222
246
  // ms <= 0 resolves immediately (matches setTimeout's clamp-to-1ms but
223
247
  // without the wasted tick). Non-finite ms rejects.
224
248
 
249
+ /**
250
+ * @primitive b.safeAsync.sleep
251
+ * @signature b.safeAsync.sleep(ms, opts?)
252
+ * @since 0.1.0
253
+ * @status stable
254
+ * @related b.safeAsync.withTimeout, b.safeAsync.repeating
255
+ *
256
+ * Promise that resolves after `ms` milliseconds. `opts.signal`
257
+ * aborts the sleep cleanly — the wrapper rejects with
258
+ * `SafeAsyncError` (`.code = "async/aborted"`). `opts.unref` flips
259
+ * the timer to non-process-holding (default `false`, so
260
+ * `await sleep(ms)` reads naturally as "I'm waiting, this IS my
261
+ * work"). `ms <= 0` resolves immediately; non-finite `ms` rejects.
262
+ *
263
+ * @opts
264
+ * signal: AbortSignal, // aborts mid-sleep with async/aborted
265
+ * unref: boolean, // default false; true to not keep the process alive
266
+ *
267
+ * @example
268
+ * var b = require("blamejs");
269
+ *
270
+ * // Backoff between retries.
271
+ * var t0 = Date.now();
272
+ * await b.safeAsync.sleep(20);
273
+ * (Date.now() - t0) >= 18;
274
+ * // → true
275
+ *
276
+ * // Abort mid-sleep — propagates as SafeAsyncError(async/aborted).
277
+ * var ctrl = new AbortController();
278
+ * setTimeout(function () { ctrl.abort(); }, 5);
279
+ * try { await b.safeAsync.sleep(1000, { signal: ctrl.signal }); }
280
+ * catch (e) { e.code; }
281
+ * // → "async/aborted"
282
+ */
225
283
  function sleep(ms, opts) {
226
284
  if (typeof ms !== "number" || !Number.isFinite(ms)) {
227
285
  return Promise.reject(new SafeAsyncError(
@@ -279,6 +337,35 @@ function sleep(ms, opts) {
279
337
  // the result straight to APIs that treat null as "no abort" (the http
280
338
  // `signal` option does), with no special-case branching needed.
281
339
 
340
+ /**
341
+ * @primitive b.safeAsync.withTimeoutSignal
342
+ * @signature b.safeAsync.withTimeoutSignal(signal, ms)
343
+ * @since 0.7.4
344
+ * @status stable
345
+ * @related b.safeAsync.withTimeout, b.safeAsync.withSignal
346
+ *
347
+ * Compose an existing AbortSignal with a fresh wall-clock timeout.
348
+ * Returns an AbortSignal that fires when EITHER the input signal
349
+ * aborts OR `ms` milliseconds elapse — exactly the shape I/O
350
+ * primitives like `fetch({ signal })` already accept. Edge cases:
351
+ * neither argument supplied returns `null` (a naturally falsy "no
352
+ * signal needed" value most signal-accepting APIs treat as no-op);
353
+ * only `signal` returns it unchanged; only `ms` returns
354
+ * `AbortSignal.timeout(ms)`.
355
+ *
356
+ * @example
357
+ * var b = require("blamejs");
358
+ *
359
+ * // Add a 5s deadline on top of the user's existing AbortSignal.
360
+ * var userCtrl = new AbortController();
361
+ * var sig = b.safeAsync.withTimeoutSignal(userCtrl.signal, 5000);
362
+ * sig instanceof AbortSignal;
363
+ * // → true
364
+ *
365
+ * // No user signal + no timeout returns null (no-abort sentinel).
366
+ * b.safeAsync.withTimeoutSignal(null, 0);
367
+ * // → null
368
+ */
282
369
  function withTimeoutSignal(signal, ms) {
283
370
  var hasTimeout = typeof ms === "number" && ms > 0 && Number.isFinite(ms);
284
371
  if (!signal && !hasTimeout) return null;
@@ -296,6 +383,46 @@ function withTimeoutSignal(signal, ms) {
296
383
  // var [err, value] = await safeAwait(somePromise);
297
384
  // if (err) { /* log + continue */ }
298
385
 
386
+ /**
387
+ * @primitive b.safeAsync.safeAwait
388
+ * @signature b.safeAsync.safeAwait(promise)
389
+ * @since 0.1.0
390
+ * @status stable
391
+ * @related b.safeAsync.withTimeout, b.safeAsync.parallel
392
+ *
393
+ * Go-style `[error, value]` tuple wrapper. Never throws — a rejected
394
+ * Promise becomes `[error, null]`, a resolved Promise becomes
395
+ * `[null, value]`. Replaces try/catch scaffolding around
396
+ * fire-and-forget paths (audit hooks in middleware, optional
397
+ * lookups) where the caller wants to log-and-continue without
398
+ * unhandled-rejection warnings. For settled-state inspection of
399
+ * many concurrent Promises the standard `Promise.allSettled` pairs
400
+ * naturally with this idiom.
401
+ *
402
+ * @example
403
+ * var b = require("blamejs");
404
+ *
405
+ * // Resolved Promise → [null, value].
406
+ * var ok = await b.safeAsync.safeAwait(Promise.resolve(42));
407
+ * ok[0];
408
+ * // → null
409
+ * ok[1];
410
+ * // → 42
411
+ *
412
+ * // Rejected Promise → [error, null].
413
+ * var bad = await b.safeAsync.safeAwait(Promise.reject(new Error("nope")));
414
+ * bad[0].message;
415
+ * // → "nope"
416
+ *
417
+ * // Pair with Promise.allSettled for bulk settled-state inspection.
418
+ * var results = await Promise.all([
419
+ * b.safeAsync.safeAwait(Promise.resolve("a")),
420
+ * b.safeAsync.safeAwait(Promise.reject(new Error("b-failed"))),
421
+ * b.safeAsync.safeAwait(Promise.resolve("c")),
422
+ * ]);
423
+ * results.filter(function (r) { return r[0] === null; }).length;
424
+ * // → 2
425
+ */
299
426
  async function safeAwait(promise) {
300
427
  try {
301
428
  var v = await promise;
@@ -314,6 +441,40 @@ async function safeAwait(promise) {
314
441
  //
315
442
  // safeInvoke(opts.onDrop, { reason: "buffer-full", batch: rows },
316
443
  // function (e) { log.warn("onDrop threw: " + e.message); });
444
+ /**
445
+ * @primitive b.safeAsync.safeInvoke
446
+ * @signature b.safeAsync.safeInvoke(callback, payload, onError)
447
+ * @since 0.6.0
448
+ * @status stable
449
+ * @related b.safeAsync.makeDropCallback
450
+ *
451
+ * Drop-silent operator-callback invoker. Calls `callback(payload)`
452
+ * if `callback` is a function, routes any throw to `onError(e)` if
453
+ * supplied, and silently swallows nested throws from `onError`
454
+ * itself. Used by every drop-callback / completion-callback /
455
+ * failure-callback site in the framework so a buggy operator
456
+ * callback can never crash the request that triggered the audit
457
+ * hook. Hot-path observability sink — drop-silent by design.
458
+ *
459
+ * @example
460
+ * var b = require("blamejs");
461
+ *
462
+ * // Happy path: callback runs with the payload.
463
+ * var seen = null;
464
+ * b.safeAsync.safeInvoke(function (p) { seen = p; }, { reason: "buffer-full", batch: [1, 2] });
465
+ * seen.reason;
466
+ * // → "buffer-full"
467
+ *
468
+ * // Throw routed to onError; original caller never sees it.
469
+ * var caught = null;
470
+ * b.safeAsync.safeInvoke(
471
+ * function () { throw new Error("boom"); },
472
+ * { batch: [] },
473
+ * function (e) { caught = e.message; }
474
+ * );
475
+ * caught;
476
+ * // → "boom"
477
+ */
317
478
  function safeInvoke(callback, payload, onError) {
318
479
  if (typeof callback !== "function") return;
319
480
  try { callback(payload); }
@@ -334,6 +495,35 @@ function safeInvoke(callback, payload, onError) {
334
495
  // var _emitDrop = safeAsync.makeDropCallback(onDrop,
335
496
  // function (e) { log.warn("onDrop-callback-failed: " + e.message); });
336
497
  // _emitDrop("buffer-full", batch, err);
498
+ /**
499
+ * @primitive b.safeAsync.makeDropCallback
500
+ * @signature b.safeAsync.makeDropCallback(onDrop, onError)
501
+ * @since 0.6.0
502
+ * @status stable
503
+ * @related b.safeAsync.safeInvoke, b.safeAsync.makeScheduledFlush
504
+ *
505
+ * Factory for the canonical log-stream-sink onDrop wrapper. Returns
506
+ * a closure `(reason, batch, err) => void` that calls `onDrop` with
507
+ * the framework-canonical payload shape `{ reason, batch, error }`,
508
+ * routing any throw from the operator callback to `onError`. Every
509
+ * sink (cloudwatch / otlp-grpc / otlp-http / syslog / webhook)
510
+ * previously rolled its own three-line `_emitDrop` wrapper — this
511
+ * factory removes that duplication.
512
+ *
513
+ * @example
514
+ * var b = require("blamejs");
515
+ *
516
+ * var dropped = [];
517
+ * var emit = b.safeAsync.makeDropCallback(
518
+ * function (info) { dropped.push(info); },
519
+ * function (e) { console.warn("onDrop threw: " + e.message); }
520
+ * );
521
+ * emit("buffer-full", [{ id: 1 }], new Error("queue overflow"));
522
+ * dropped[0].reason;
523
+ * // → "buffer-full"
524
+ * dropped[0].error.message;
525
+ * // → "queue overflow"
526
+ */
337
527
  function makeDropCallback(onDrop, onError) {
338
528
  return function (reason, batch, err) {
339
529
  safeInvoke(onDrop, { reason: reason, batch: batch, error: err || null }, onError);
@@ -355,6 +545,37 @@ function makeDropCallback(onDrop, onError) {
355
545
  // Returns { schedule, cancel, isPending }. flushFn may be sync or
356
546
  // async — async rejections are swallowed (best-effort sink — operators
357
547
  // see drops via onDrop, not via a sea of unhandled promise rejections).
548
+ /**
549
+ * @primitive b.safeAsync.makeScheduledFlush
550
+ * @signature b.safeAsync.makeScheduledFlush(delayMs, flushFn)
551
+ * @since 0.6.0
552
+ * @status stable
553
+ * @related b.safeAsync.flushLoop, b.safeAsync.makeDropCallback
554
+ *
555
+ * Idempotent setTimeout coalesce-and-flush scheduler used by every
556
+ * log-stream sink to batch buffered writes. Returns
557
+ * `{ schedule, cancel, isPending }` — calling `schedule()` repeatedly
558
+ * within `delayMs` collapses to a single deferred `flushFn()` call.
559
+ * The timer is unref'd so a pending flush never keeps the process
560
+ * alive; async rejections from `flushFn` are swallowed (best-effort
561
+ * sink — operators see drops via the sink's own onDrop). Throws
562
+ * `TypeError` on bad arguments at construction time.
563
+ *
564
+ * @example
565
+ * var b = require("blamejs");
566
+ *
567
+ * // Coalesce many schedule() calls into one flush after delayMs.
568
+ * var flushed = 0;
569
+ * var sched = b.safeAsync.makeScheduledFlush(20, function () { flushed += 1; });
570
+ * sched.schedule();
571
+ * sched.schedule();
572
+ * sched.schedule();
573
+ * sched.isPending();
574
+ * // → true
575
+ * await b.safeAsync.sleep(40);
576
+ * flushed;
577
+ * // → 1
578
+ */
358
579
  function makeScheduledFlush(delayMs, flushFn) {
359
580
  if (typeof delayMs !== "number" || !isFinite(delayMs) || delayMs < 0) {
360
581
  throw new TypeError("safeAsync.makeScheduledFlush: delayMs must be a non-negative finite number");
@@ -384,6 +605,163 @@ function makeScheduledFlush(delayMs, flushFn) {
384
605
  };
385
606
  }
386
607
 
608
+ // ---- parallel ----
609
+ //
610
+ // Bounded-concurrency mapAsync. Runs `fn(item, index)` over `items`
611
+ // with at most opts.concurrency in-flight at once; resolves with
612
+ // results in input order (NOT completion order). The first rejection
613
+ // from any `fn` invocation is propagated (other in-flight calls finish
614
+ // in the background; the wrapper does not cancel them — operator-
615
+ // supplied promises may not be signal-aware).
616
+ //
617
+ // var results = await b.safeAsync.parallel(urls, fetchOne, {
618
+ // concurrency: 16,
619
+ // signal: controller.signal,
620
+ // });
621
+ //
622
+ // Worker-loop pattern: a fixed pool of `concurrency` workers each pull
623
+ // the next available index from a shared cursor. Avoids the
624
+ // Promise.all-batched-chunks pitfall where the next batch can't start
625
+ // until the slowest item in the current batch finishes (long-pole
626
+ // stragglers leave workers idle). See feedback_lpt_scheduling_for_
627
+ // parallel_tests.md — same shape applied to operator workloads.
628
+ //
629
+ // opts.concurrency: 1..256 (default 8). Throws at config time on
630
+ // out-of-range so operator typos surface immediately.
631
+ // opts.signal: AbortSignal — cancels by refusing to dispatch
632
+ // further items; in-flight promises run to settle.
633
+
634
+ var PARALLEL_DEFAULT_CONCURRENCY = 8; // allow:raw-byte-literal — worker pool count, not bytes
635
+ var PARALLEL_MAX_CONCURRENCY = 256; // allow:raw-byte-literal — worker pool ceiling, not bytes
636
+
637
+ /**
638
+ * @primitive b.safeAsync.parallel
639
+ * @signature b.safeAsync.parallel(items, fn, opts?)
640
+ * @since 0.7.0
641
+ * @status stable
642
+ * @related b.safeAsync.safeAwait, b.safeAsync.withTimeout
643
+ *
644
+ * Bounded-concurrency `mapAsync`. Runs `fn(item, index)` over `items`
645
+ * with at most `opts.concurrency` in-flight at a time and resolves
646
+ * with results in INPUT order (not completion order). Worker-loop
647
+ * scheduling: a fixed pool of workers each pull the next index from
648
+ * a shared cursor as soon as their previous task settles — avoids
649
+ * the Promise.all-batched-chunks pitfall where a long-pole straggler
650
+ * leaves workers idle. The first rejection is propagated;
651
+ * still-in-flight calls finish in the background (operator-supplied
652
+ * promises may not be signal-aware). `opts.concurrency` validates at
653
+ * config time (1..256, default 8) and throws on out-of-range so
654
+ * typos surface immediately.
655
+ *
656
+ * @opts
657
+ * concurrency: number, // 1..256; default 8
658
+ * signal: AbortSignal, // refuses to dispatch further items; in-flight run to settle
659
+ *
660
+ * @example
661
+ * var b = require("blamejs");
662
+ *
663
+ * var urls = ["a", "b", "c", "d"];
664
+ * var fetchOne = function (u) { return Promise.resolve("loaded:" + u); };
665
+ * var results = await b.safeAsync.parallel(urls, fetchOne, { concurrency: 2 });
666
+ * results;
667
+ * // → ["loaded:a", "loaded:b", "loaded:c", "loaded:d"]
668
+ *
669
+ * // First rejection wins; remaining workers drain.
670
+ * try {
671
+ * await b.safeAsync.parallel([1, 2, 3], function (n) {
672
+ * if (n === 2) return Promise.reject(new Error("bad-2"));
673
+ * return Promise.resolve(n);
674
+ * }, { concurrency: 1 });
675
+ * } catch (e) {
676
+ * e.message;
677
+ * // → "bad-2"
678
+ * }
679
+ */
680
+ function parallel(items, fn, opts) {
681
+ if (!Array.isArray(items)) {
682
+ throw new SafeAsyncError("parallel: items must be an array", "async/bad-arg");
683
+ }
684
+ if (typeof fn !== "function") {
685
+ throw new SafeAsyncError("parallel: fn must be a function", "async/bad-arg");
686
+ }
687
+ opts = opts || {};
688
+ var concurrency = opts.concurrency != null ? opts.concurrency : PARALLEL_DEFAULT_CONCURRENCY;
689
+ if (typeof concurrency !== "number" || !Number.isInteger(concurrency) ||
690
+ concurrency < 1 || concurrency > PARALLEL_MAX_CONCURRENCY) {
691
+ throw new SafeAsyncError(
692
+ "parallel: concurrency must be an integer in [1.." +
693
+ PARALLEL_MAX_CONCURRENCY + "], got " + concurrency,
694
+ "async/bad-arg"
695
+ );
696
+ }
697
+ var signal = opts.signal;
698
+ if (signal && signal.aborted) {
699
+ return Promise.reject(new SafeAsyncError(
700
+ "parallel aborted before start", "async/aborted", signal.reason
701
+ ));
702
+ }
703
+ if (items.length === 0) return Promise.resolve([]);
704
+
705
+ return new Promise(function (resolve, reject) {
706
+ var results = new Array(items.length);
707
+ var cursor = 0;
708
+ var settled = false;
709
+ var firstError = null;
710
+ var activeWorkers = 0;
711
+ var workerCount = Math.min(concurrency, items.length);
712
+ var onAbort = null;
713
+
714
+ function _finish(err) {
715
+ if (settled) return;
716
+ settled = true;
717
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
718
+ if (err) reject(err); else resolve(results);
719
+ }
720
+
721
+ if (signal) {
722
+ onAbort = function () {
723
+ if (firstError) return;
724
+ firstError = new SafeAsyncError(
725
+ "parallel aborted", "async/aborted", signal.reason
726
+ );
727
+ // In-flight workers finish their current item; new pulls
728
+ // observe firstError and exit. _finish fires when the last
729
+ // worker drains.
730
+ };
731
+ signal.addEventListener("abort", onAbort, { once: true });
732
+ }
733
+
734
+ function _workerLoop() {
735
+ // Continuous worker queue — each worker pulls the next index
736
+ // from the shared cursor as soon as its previous task settles.
737
+ // No batched chunks: a slow item never blocks unrelated items
738
+ // from entering the pool.
739
+ if (firstError || cursor >= items.length) {
740
+ activeWorkers -= 1;
741
+ if (activeWorkers === 0) _finish(firstError);
742
+ return;
743
+ }
744
+ var idx = cursor++;
745
+ var item = items[idx];
746
+ var p;
747
+ try { p = Promise.resolve(fn(item, idx)); }
748
+ catch (e) { p = Promise.reject(e); }
749
+ p.then(function (value) {
750
+ results[idx] = value;
751
+ _workerLoop();
752
+ }, function (e) {
753
+ if (!firstError) firstError = e;
754
+ _workerLoop();
755
+ });
756
+ }
757
+
758
+ for (var i = 0; i < workerCount; i++) {
759
+ activeWorkers += 1;
760
+ _workerLoop();
761
+ }
762
+ });
763
+ }
764
+
387
765
  // ---- Mutex ----
388
766
  //
389
767
  // Async mutex — only one async region holds the lock at a time. Acquirers
@@ -606,6 +984,43 @@ class Once {
606
984
  // that should NOT keep the process alive. Cluster heartbeat etc. set
607
985
  // `unref: false` so the lease keeps the leader from exiting silently.
608
986
 
987
+ /**
988
+ * @primitive b.safeAsync.repeating
989
+ * @signature b.safeAsync.repeating(fn, intervalMs, opts?)
990
+ * @since 0.6.0
991
+ * @status stable
992
+ * @related b.safeAsync.flushLoop, b.safeAsync.sleep
993
+ *
994
+ * Bounded-cadence interval timer with consistent unref + cancel
995
+ * semantics. Replaces the scattered `setInterval` ceremony where
996
+ * each caller hand-rolled `t.unref()` and a corresponding
997
+ * `clearInterval` in shutdown. `fn` may be sync or async; if async,
998
+ * the next tick fires `intervalMs` after the prior fn() STARTED
999
+ * (fixed-rate, matching `setInterval`). Promise rejections are
1000
+ * captured by `opts.onError` if provided, otherwise silently
1001
+ * dropped — a repeating timer is fire-and-forget by definition and
1002
+ * an unhandled rejection here would crash the process. `opts.unref`
1003
+ * defaults `true`; set `false` for cluster heartbeat-style timers
1004
+ * that must hold the loop open. Returns `{ stop }`.
1005
+ *
1006
+ * @opts
1007
+ * unref: boolean, // default true
1008
+ * onError: function(error), // captures sync throws + Promise rejections
1009
+ * name: string, // diagnostic label
1010
+ *
1011
+ * @example
1012
+ * var b = require("blamejs");
1013
+ *
1014
+ * var ticks = 0;
1015
+ * var sweep = b.safeAsync.repeating(function () { ticks += 1; }, 10, {
1016
+ * unref: true,
1017
+ * name: "tick-counter",
1018
+ * });
1019
+ * await b.safeAsync.sleep(35);
1020
+ * sweep.stop();
1021
+ * ticks >= 2;
1022
+ * // → true
1023
+ */
609
1024
  function repeating(fn, intervalMs, opts) {
610
1025
  if (typeof fn !== "function") {
611
1026
  throw new SafeAsyncError("repeating: fn must be a function", "async/bad-arg");
@@ -661,6 +1076,39 @@ function repeating(fn, intervalMs, opts) {
661
1076
  // (the operator's b.appShutdown drives the final drain explicitly).
662
1077
  // onError catches rejections; without one, they're silently dropped.
663
1078
 
1079
+ /**
1080
+ * @primitive b.safeAsync.flushLoop
1081
+ * @signature b.safeAsync.flushLoop(fn, intervalMs, opts?)
1082
+ * @since 0.6.0
1083
+ * @status stable
1084
+ * @related b.safeAsync.repeating, b.safeAsync.makeScheduledFlush
1085
+ *
1086
+ * After-completion background flusher. Schedules `fn()`, awaits its
1087
+ * settle (resolve OR reject), then schedules the next call
1088
+ * `intervalMs` later. Differs from `repeating` (fixed-rate, no
1089
+ * overlap protection) — `flushLoop` is the right shape for
1090
+ * background flushers that must never overlap two flushes and
1091
+ * shouldn't accumulate backlog when one flush is slow. Always
1092
+ * unref'd; `opts.onError` catches rejections, otherwise they're
1093
+ * silently dropped. Returns `{ stop }`.
1094
+ *
1095
+ * @opts
1096
+ * onError: function(error), // captures sync throws + Promise rejections
1097
+ * name: string, // diagnostic label
1098
+ *
1099
+ * @example
1100
+ * var b = require("blamejs");
1101
+ *
1102
+ * var flushes = 0;
1103
+ * var loop = b.safeAsync.flushLoop(function () {
1104
+ * flushes += 1;
1105
+ * return Promise.resolve();
1106
+ * }, 10, { name: "telemetry-flush" });
1107
+ * await b.safeAsync.sleep(35);
1108
+ * loop.stop();
1109
+ * flushes >= 1;
1110
+ * // → true
1111
+ */
664
1112
  function flushLoop(fn, intervalMs, opts) {
665
1113
  if (typeof fn !== "function") {
666
1114
  throw new SafeAsyncError("flushLoop: fn must be a function", "async/bad-arg");
@@ -726,6 +1174,7 @@ module.exports = {
726
1174
  safeInvoke: safeInvoke,
727
1175
  makeDropCallback: makeDropCallback,
728
1176
  makeScheduledFlush: makeScheduledFlush,
1177
+ parallel: parallel,
729
1178
  Mutex: Mutex,
730
1179
  Semaphore: Semaphore,
731
1180
  Once: Once,