@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.
- package/CHANGELOG.md +93 -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
package/lib/safe-async.js
CHANGED
|
@@ -1,99 +1,62 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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,
|