@blamejs/core 0.6.31 → 0.6.32
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 +1 -0
- package/lib/cache-redis.js +261 -0
- package/lib/cache.js +30 -2
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.6.x
|
|
10
10
|
|
|
11
|
+
- **0.6.32** (2026-05-02) — E1 from the v0.6.x scope plan. **`b.cache` Redis backend** — first-class `backend: "redis"` for `b.cache.create({...})`, no operator-supplied glue needed. New module `lib/cache-redis.js` consumes `lib/redis-client.js` directly; storage layout: `<namespace>:e:<key>` (STRING, JSON-encoded value, PEXPIREAT-bounded), `<namespace>:t:<tag>` (SET, cacheKeys carrying that tag — powers `invalidateTag` fan-out), `<namespace>:k:<key>:tags` (SET, tags this key carries — powers per-key tag cleanup on `del`/`set`-overwrite, expires alongside the entry). TTL is enforced by Redis itself (PEXPIREAT) so the framework's sweeper is a no-op for this backend; sliding TTL bumps the entry's expiry on every read when `slidingTtl: true` AND `ttlMs` is finite. New opts on `cache.create`: `redisUrl` (required when `backend: "redis"`), `redisPassword`, `redisUsername`, `redisTls`, `redisCa` (private-CA trust pinning per v0.6.28), `redisServername`, `redisConnectTimeoutMs`, `redisCommandTimeoutMs`, `redisMaxReconnectAttempts`. Concurrency: invalidateTag filters out ghost entries (key already PEXPIRE'd from the keyspace but lingering in a tag SET) by EXISTS-checking before del. Lazy connect — `cache.create({backend:"redis"})` stays sync-safe; first op opens the socket. Tests: 10 new live integration assertions on top of the existing memory + custom-backend coverage (set+get round-trip, has, del, complex JSON value round-trip, short-TTL Redis-side expiry, multi-tag invalidateTag fan-out + preservation of untagged entries, wrap() single-flight memoization through Redis). Smoke 6780 / wiki e2e 178 / per-primitive integration 14 files (cache up from 15→25 checks) / wiki integration green.
|
|
11
12
|
- **0.6.31** (2026-05-02) — lazy-deferral cleanup wave from v0.6.27. **A1: queue-redis priority ordering** — `b.queue.enqueue({ priority })` on the Redis backend now matches queue-local's `ORDER BY priority DESC, availableAt ASC, enqueuedAt ASC` semantics. The Redis ZSET stays scored by availableAt only (so ZRANGEBYSCORE 0..nowMs cleanly filters ready vs not-yet-ready jobs), but `LEASE_LUA` now over-fetches `maxRows*5` candidates by score, HMGETs priority + availableAt + enqueuedAt for each, sorts server-side in Lua by the same triple queue-local sorts on, and leases the top `maxRows`. Closes the v0.6.27 deferral that punted priority ordering as "ZSET-incompatible". **A2: queue-redis flow `dependsOn` cascade** — `b.queue.enqueue({ flowId, flowChildName, dependsOn })` on the Redis backend now releases dependent jobs when their parents complete, mirroring queue-local's `_maybeReleaseFlowChildren`. A per-flow `<prefix>:flow:<flowId>` Redis SET tracks every job in the flow; `complete()` walks the set, looks up siblings whose `dependsOn` includes the just-completed job (by id OR `flowChildName`) AND every other dep is also satisfied, and HSET-bumps their `availableAt` to now + ZADDs them into the ready zset. The flow set persists across `complete()` calls so a later sibling's deps check can still find earlier-done parents by name; `purge()` cleans the set on queue teardown. Closes the v0.6.27 deferral. **A4: WebSocket per-message-deflate (RFC 7692)** — `b.websocket.handleUpgrade` negotiates the `permessage-deflate` extension when the client offers it, accepting `client_max_window_bits` / `server_max_window_bits` constraints (8-15 range, default 15) and always asserting `client_no_context_takeover` + `server_no_context_takeover` so every message uses a fresh zlib state. The connection's send path compresses TEXT/BINARY frames via `zlib.deflateRawSync`, strips the 4-byte `0x00 0x00 0xff 0xff` trailer per RFC 7692 §7.2.1, and sets RSV1 on the first frame of each compressed message. The receive path appends the trailer back and inflates via `zlib.inflateRawSync`. RSV1 on a continuation frame, RSV1 without negotiated extension, RSV2/RSV3 set, and decompressed payload exceeding `maxMessageBytes` all close with the appropriate RFC 6455 status (`PROTOCOL_ERROR`, `INVALID_PAYLOAD`, `MESSAGE_TOO_BIG`). Operator opt-out: pass `permessageDeflate: false` to `handleUpgrade` to refuse the extension even when offered. Tests cover the 101-handshake echo of negotiated params, real compression on a redundant payload (4000 bytes → ~30 bytes on the wire), uncompressed-frame round-trip on the same connection (mixed-mode per RFC 7692 §6), and unknown-extension graceful ignore. v0.6.27's third lazy deferral A3 (TLS-redis live test) was already covered by `test/integration/redis-client-tls.test.js` shipped in v0.6.28. Smoke 6780 / wiki e2e 178 / per-primitive integration 14 files green / wiki integration green.
|
|
12
13
|
- **0.6.30** (2026-05-02) — wiki-app integration gate + framework follow-ups surfaced by it. **`scripts/test-wiki-integration.js`** boots `examples/wiki` against the docker-compose fixture stack (real Redis / MinIO / Mailpit / CoreDNS / NTP / mtls-ca) and drives every backend through the wiki's HTTP surface AND the underlying framework primitives — validates that each primitive routes through the configured backend (cache → custom backend, queue → Redis Streams, mail → Mailpit SMTP, object-store → MinIO sigv4, log-stream → webhook receiver) AND that every fix shipped in v0.6.28 holds in a real-app context, not just in unit-test isolation. **Wiki gains test-only routes** under `/test/*` (gated by `WIKI_INTEGRATION_TEST=1`, mounted before CSRF/staticServe so test POSTs don't have to round-trip a token cookie) covering cache get/set/del, queue enqueue/size, mail send, object-store put/get, http-client fetch, log-stream emit, mtls-ca issue, ntp query, dns lookup, ssrf classify+check, plus `/test/diagnostic` that surfaces the active backend posture (network snapshot, queue backends, log sinks, mtls algorithm). **External-integration two-gate rule** — when a release diff touches a primitive that talks to an external service (`lib/redis-client.js`, `lib/queue-redis.js`, `lib/mail.js`, `lib/network-dns.js`, `lib/object-store/*`, `lib/log-stream*.js`, `lib/external-db.js`, `lib/cluster-*.js`, `lib/mtls-ca.js`, `lib/ssrf-guard.js`, `lib/http-client.js`, `lib/ntp-check.js`, `lib/cache.js`, `lib/webhook.js`), operators MUST run BOTH `scripts/test-integration.js` (per-primitive against real backends) AND `scripts/test-wiki-integration.js` (wiki app exercising the same backends end-to-end) before pushing. Documented in CONTRIBUTING.md's pre-push gate list and CLAUDE.md's release workflow as step 4a. The smoke + wiki-e2e gates above stay PURE (no docker dependency, runs in CI / on a developer laptop / inside prepack-guard); the two-gate is operator-run, opt-in, and surfaces bugs that mocks miss — fire-and-forget races, shutdown drains, TLS pinning, real DNS resolution, real protocol handshakes. **Framework fixes surfaced by the new gate**: `b.mtlsCa.create` now auto-creates `opts.dataDir` with `mode: 0o700` if it doesn't exist (matches `b.logStream`'s local sink, `b.backup`, `b.restoreBundle` — without it the first `initCA()` call hit `ENOENT` writing `ca.key.tmp`); `b.logStream` webhook-sink `close()` now waits for the in-flight `_flush()` before draining the buffer (the v0.6.28 drain fix only tracked emit-time wrapper promises, not the actual HTTP POST in flight; a record arriving mid-flush would buffer + the emit-time `_flush` early-returned on `if (inFlight) return`, and shutdown would then strand it). `b.queue.bootFromEnv()` is now wired in the wiki app's `build-app.js` unconditionally so the wiki picks up `BLAMEJS_QUEUE_PROTOCOL=redis` from env without code changes (was previously a documented-but-commented call site). **Test architecture rule (extends the v0.6.28 rule)** — live tests for the framework's primitives go under `test/integration/`; live tests for the wiki app exercising the framework end-to-end go under `examples/wiki/test/integration.js`. Smoke (`test/smoke.js`) and the existing wiki e2e (`examples/wiki/test/e2e.js`) stay pure. Smoke / wiki-e2e green; per-primitive integration suite green; wiki integration green. Eslint pin alignment + shellcheck added to the documented gate list (carry-over from v0.6.29).
|
|
13
14
|
- **0.6.29** (2026-05-02) — CI cleanup. **shellcheck gate green**: `docker/init/generate-certs.sh`'s footer line counted output via `ls "$CERT_DIR" | wc -l` (SC2012 — fragile on filenames with newlines or quoting metacharacters); replaced with `find "$CERT_DIR" -maxdepth 1 -mindepth 1 | wc -l`. Same semantics, robust to any cert filename pki-init might end up writing. **shellcheck added to the documented pre-push gate list** in CONTRIBUTING.md alongside smoke / wiki e2e / eslint / api-snapshot / primitive-section validators — CI's `Lint summary` job has always run it, but the local-dev recipe didn't, so contributors hit it post-push instead of catching it locally. **eslint pin alignment**: `.github/PULL_REQUEST_TEMPLATE.md` and CONTRIBUTING.md still pinned `eslint@10` even though v0.6.16 swapped CI runners to `eslint@latest`; both bumped to `@latest` to match the forward-track posture of every other CI tool. No framework code changes; the script + doc updates never ship in the npm package (excluded by `files` array).
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Redis-backed cache backend for b.cache.
|
|
4
|
+
*
|
|
5
|
+
* Storage layout (operator-overridable namespace; default = "blamejs:cache"):
|
|
6
|
+
* <namespace>:e:<key> STRING — JSON-encoded value
|
|
7
|
+
* PEXPIREAT honours the cache's TTL
|
|
8
|
+
* <namespace>:t:<tag> SET — cacheKeys carrying that tag
|
|
9
|
+
* (powers invalidateTag fan-out)
|
|
10
|
+
* <namespace>:k:<key>:tags SET — tags this key carries
|
|
11
|
+
* (powers per-key tag cleanup on
|
|
12
|
+
* del / set-overwrite, expires
|
|
13
|
+
* alongside the entry)
|
|
14
|
+
*
|
|
15
|
+
* Same backend contract as the framework's other cache backends (memory,
|
|
16
|
+
* cluster, custom): get / set / del / has / clear / size / close, plus
|
|
17
|
+
* the optional invalidateTag + getTags + bytes + _startSweep hooks the
|
|
18
|
+
* `_customBackend` wrapper looks for.
|
|
19
|
+
*
|
|
20
|
+
* TTL is enforced by Redis itself (PEXPIREAT) — the framework does NOT
|
|
21
|
+
* run a sweep timer for this backend; expired entries vanish from the
|
|
22
|
+
* key space without app-side intervention.
|
|
23
|
+
*
|
|
24
|
+
* Sliding TTL: bumps the entry's expiry to `now + defaultTtlMs` on every
|
|
25
|
+
* read when the cache was created with `slidingTtl: true` and the cache
|
|
26
|
+
* has a finite defaultTtlMs. Same shape as the cluster backend.
|
|
27
|
+
*
|
|
28
|
+
* Concurrency: Redis is single-threaded for command execution; our get
|
|
29
|
+
* + set + tag updates are sequential, not pipelined inside a MULTI, so
|
|
30
|
+
* a worst-case interleaving might leave a tag set with a key that no
|
|
31
|
+
* longer exists. The reverse `keyTags` set + `del()` cleanup means the
|
|
32
|
+
* stale entry is reaped on the next `invalidateTag` (the SET membership
|
|
33
|
+
* is filtered against actual key existence — see invalidateTag below).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
var redisClient = require("./redis-client");
|
|
37
|
+
var { CacheError } = require("./framework-error");
|
|
38
|
+
|
|
39
|
+
var _err = CacheError.factory;
|
|
40
|
+
|
|
41
|
+
function _toStr(v) {
|
|
42
|
+
if (v === null || v === undefined) return null;
|
|
43
|
+
return Buffer.isBuffer(v) ? v.toString("utf8") : String(v);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function create(cfg) {
|
|
47
|
+
cfg = cfg || {};
|
|
48
|
+
if (typeof cfg.url !== "string" || cfg.url.length === 0) {
|
|
49
|
+
throw _err("BAD_OPT", "cache-redis: opts.url is required (e.g. redis://localhost:6379/0)");
|
|
50
|
+
}
|
|
51
|
+
var namespace = cfg.namespace;
|
|
52
|
+
var clock = cfg.clock || function () { return Date.now(); };
|
|
53
|
+
var emitObs = cfg.emitObs || function () {};
|
|
54
|
+
var slidingTtl = cfg.slidingTtl;
|
|
55
|
+
var defaultTtlMs = cfg.defaultTtlMs;
|
|
56
|
+
|
|
57
|
+
var client = redisClient.create({
|
|
58
|
+
url: cfg.url,
|
|
59
|
+
password: cfg.password,
|
|
60
|
+
username: cfg.username,
|
|
61
|
+
tls: cfg.tls,
|
|
62
|
+
ca: cfg.ca,
|
|
63
|
+
servername: cfg.servername,
|
|
64
|
+
connectTimeoutMs: cfg.connectTimeoutMs,
|
|
65
|
+
commandTimeoutMs: cfg.commandTimeoutMs,
|
|
66
|
+
maxReconnectAttempts: cfg.maxReconnectAttempts,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Lazy connect — defer until first op so cache.create stays sync-safe.
|
|
70
|
+
var connectPromise = null;
|
|
71
|
+
function _ensureConnected() {
|
|
72
|
+
if (client.isOpen()) return Promise.resolve();
|
|
73
|
+
if (!connectPromise) connectPromise = client.connect();
|
|
74
|
+
return connectPromise;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _key(k) { return namespace + ":e:" + k; }
|
|
78
|
+
function _tagKey(t) { return namespace + ":t:" + t; }
|
|
79
|
+
function _keyTagsKey(k) { return namespace + ":k:" + k + ":tags"; }
|
|
80
|
+
|
|
81
|
+
async function get(key) {
|
|
82
|
+
await _ensureConnected();
|
|
83
|
+
var v = await client.command("GET", _key(key));
|
|
84
|
+
var s = _toStr(v);
|
|
85
|
+
if (s === null) return undefined;
|
|
86
|
+
var parsed;
|
|
87
|
+
try { parsed = JSON.parse(s); }
|
|
88
|
+
catch (_e) { return undefined; }
|
|
89
|
+
// Sliding TTL: extend the entry's life on every read. Best-effort
|
|
90
|
+
// (PEXPIREAT may race with delete; if it fails the operator just
|
|
91
|
+
// sees the original TTL play out).
|
|
92
|
+
if (slidingTtl && typeof defaultTtlMs === "number" && isFinite(defaultTtlMs) && defaultTtlMs > 0) {
|
|
93
|
+
var newExp = clock() + defaultTtlMs;
|
|
94
|
+
client.command("PEXPIREAT", _key(key), String(Math.floor(newExp)))
|
|
95
|
+
.catch(function () { /* best-effort */ });
|
|
96
|
+
client.command("PEXPIREAT", _keyTagsKey(key), String(Math.floor(newExp)))
|
|
97
|
+
.catch(function () { /* best-effort */ });
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function set(key, value, expiresAt, meta) {
|
|
103
|
+
await _ensureConnected();
|
|
104
|
+
var json = JSON.stringify(value);
|
|
105
|
+
|
|
106
|
+
// Drop any prior tag membership for this key (tags may have changed
|
|
107
|
+
// across sets). The reverse-tag set tells us which tag SETs need
|
|
108
|
+
// pruning without scanning every tag in the namespace.
|
|
109
|
+
var oldTagsRv = await client.command("SMEMBERS", _keyTagsKey(key));
|
|
110
|
+
var oldTags = (oldTagsRv || []).map(_toStr).filter(Boolean);
|
|
111
|
+
for (var ot = 0; ot < oldTags.length; ot++) {
|
|
112
|
+
try { await client.command("SREM", _tagKey(oldTags[ot]), key); }
|
|
113
|
+
catch (_e) { /* best-effort */ }
|
|
114
|
+
}
|
|
115
|
+
if (oldTags.length > 0) {
|
|
116
|
+
try { await client.command("DEL", _keyTagsKey(key)); }
|
|
117
|
+
catch (_e) { /* best-effort */ }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Write the value with PEXPIREAT for finite TTLs; un-expiring SET
|
|
121
|
+
// for Infinity (operator opted for "cache forever until evicted").
|
|
122
|
+
if (typeof expiresAt === "number" && isFinite(expiresAt)) {
|
|
123
|
+
await client.command("SET", _key(key), json, "PXAT", String(Math.floor(expiresAt)));
|
|
124
|
+
} else {
|
|
125
|
+
await client.command("SET", _key(key), json);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Wire the new tags. SADD is idempotent on duplicate tag names.
|
|
129
|
+
var tags = meta && Array.isArray(meta.tags) ? meta.tags : null;
|
|
130
|
+
if (tags && tags.length > 0) {
|
|
131
|
+
for (var t = 0; t < tags.length; t++) {
|
|
132
|
+
await client.command("SADD", _tagKey(tags[t]), key);
|
|
133
|
+
await client.command("SADD", _keyTagsKey(key), tags[t]);
|
|
134
|
+
}
|
|
135
|
+
// Match the entry's lifetime on the keyTags reverse-set so
|
|
136
|
+
// membership doesn't outlive the value.
|
|
137
|
+
if (typeof expiresAt === "number" && isFinite(expiresAt)) {
|
|
138
|
+
try { await client.command("PEXPIREAT", _keyTagsKey(key), String(Math.floor(expiresAt))); }
|
|
139
|
+
catch (_e) { /* best-effort */ }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
emitObs("cache.redis.set", { namespace: namespace });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function del(key) {
|
|
146
|
+
await _ensureConnected();
|
|
147
|
+
var oldTagsRv = await client.command("SMEMBERS", _keyTagsKey(key));
|
|
148
|
+
var oldTags = (oldTagsRv || []).map(_toStr).filter(Boolean);
|
|
149
|
+
for (var i = 0; i < oldTags.length; i++) {
|
|
150
|
+
try { await client.command("SREM", _tagKey(oldTags[i]), key); }
|
|
151
|
+
catch (_e) { /* best-effort */ }
|
|
152
|
+
}
|
|
153
|
+
var dels = await Promise.all([
|
|
154
|
+
client.command("DEL", _key(key)),
|
|
155
|
+
client.command("DEL", _keyTagsKey(key)),
|
|
156
|
+
]);
|
|
157
|
+
return Number(dels[0]) === 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function has(key) {
|
|
161
|
+
await _ensureConnected();
|
|
162
|
+
var rv = await client.command("EXISTS", _key(key));
|
|
163
|
+
return Number(rv) === 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function clear() {
|
|
167
|
+
await _ensureConnected();
|
|
168
|
+
// SCAN-and-DEL every key under this namespace. Using SCAN avoids
|
|
169
|
+
// KEYS' O(N) blocking pass and lets a busy Redis serve other
|
|
170
|
+
// commands between cursor batches.
|
|
171
|
+
var cursor = "0";
|
|
172
|
+
do {
|
|
173
|
+
var rv = await client.command("SCAN", cursor, "MATCH", namespace + ":*", "COUNT", "200");
|
|
174
|
+
cursor = _toStr(rv[0]) || "0";
|
|
175
|
+
var keys = (rv[1] || []).map(_toStr).filter(Boolean);
|
|
176
|
+
if (keys.length > 0) {
|
|
177
|
+
await client.command.apply(client, ["DEL"].concat(keys));
|
|
178
|
+
}
|
|
179
|
+
} while (cursor !== "0");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function size() {
|
|
183
|
+
await _ensureConnected();
|
|
184
|
+
var cursor = "0";
|
|
185
|
+
var n = 0;
|
|
186
|
+
do {
|
|
187
|
+
var rv = await client.command("SCAN", cursor, "MATCH", namespace + ":e:*", "COUNT", "200");
|
|
188
|
+
cursor = _toStr(rv[0]) || "0";
|
|
189
|
+
n += (rv[1] || []).length;
|
|
190
|
+
} while (cursor !== "0");
|
|
191
|
+
return n;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function bytes() {
|
|
195
|
+
// Per-entry byte accounting on Redis would mean a MEMORY USAGE call
|
|
196
|
+
// per key, which is operator-expensive and Redis-version-dependent.
|
|
197
|
+
// Operators wanting this use the memory backend or run their own
|
|
198
|
+
// monitoring against MEMORY STATS at the cluster level.
|
|
199
|
+
return Promise.resolve(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function invalidateTag(tag) {
|
|
203
|
+
await _ensureConnected();
|
|
204
|
+
var rv = await client.command("SMEMBERS", _tagKey(tag));
|
|
205
|
+
var keys = (rv || []).map(_toStr).filter(Boolean);
|
|
206
|
+
var dropped = 0;
|
|
207
|
+
for (var i = 0; i < keys.length; i++) {
|
|
208
|
+
// Filter out stale tag-membership entries (key already expired
|
|
209
|
+
// from PEXPIREAT but lingered in the tag SET) by checking
|
|
210
|
+
// EXISTS before del. Saves an unnecessary DEL round-trip on
|
|
211
|
+
// ghost keys.
|
|
212
|
+
var existsRv = await client.command("EXISTS", _key(keys[i]));
|
|
213
|
+
if (Number(existsRv) === 1) {
|
|
214
|
+
await del(keys[i]);
|
|
215
|
+
dropped += 1;
|
|
216
|
+
} else {
|
|
217
|
+
// Ghost — just SREM from this tag set + drop the reverse.
|
|
218
|
+
try { await client.command("SREM", _tagKey(tag), keys[i]); }
|
|
219
|
+
catch (_e) {}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Drop the tag set itself if it's now empty (or empty after the
|
|
223
|
+
// ghost cleanup above).
|
|
224
|
+
var remaining = await client.command("SCARD", _tagKey(tag));
|
|
225
|
+
if (Number(remaining) === 0) {
|
|
226
|
+
try { await client.command("DEL", _tagKey(tag)); }
|
|
227
|
+
catch (_e) {}
|
|
228
|
+
}
|
|
229
|
+
emitObs("cache.redis.invalidateTag", { namespace: namespace, tag: tag, dropped: dropped });
|
|
230
|
+
return dropped;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function getTags(key) {
|
|
234
|
+
await _ensureConnected();
|
|
235
|
+
var rv = await client.command("SMEMBERS", _keyTagsKey(key));
|
|
236
|
+
return (rv || []).map(_toStr).filter(Boolean);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function close() {
|
|
240
|
+
try { await client.close(); }
|
|
241
|
+
catch (_e) { /* best-effort */ }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
name: "redis",
|
|
246
|
+
get: get,
|
|
247
|
+
set: set,
|
|
248
|
+
del: del,
|
|
249
|
+
has: has,
|
|
250
|
+
clear: clear,
|
|
251
|
+
size: size,
|
|
252
|
+
bytes: bytes,
|
|
253
|
+
invalidateTag: invalidateTag,
|
|
254
|
+
getTags: getTags,
|
|
255
|
+
close: close,
|
|
256
|
+
// Redis enforces TTL itself; the framework's sweeper is a no-op here.
|
|
257
|
+
_startSweep: function () {},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = { create: create };
|
package/lib/cache.js
CHANGED
|
@@ -184,10 +184,15 @@ function _validateCreateOpts(opts) {
|
|
|
184
184
|
}
|
|
185
185
|
if (opts.backend !== undefined) {
|
|
186
186
|
if (typeof opts.backend === "string") {
|
|
187
|
-
if (opts.backend !== "memory" && opts.backend !== "cluster") {
|
|
188
|
-
throw _err("BAD_OPT", "cache.create: backend string must be 'memory'
|
|
187
|
+
if (opts.backend !== "memory" && opts.backend !== "cluster" && opts.backend !== "redis") {
|
|
188
|
+
throw _err("BAD_OPT", "cache.create: backend string must be 'memory' | 'cluster' | 'redis', got " +
|
|
189
189
|
JSON.stringify(opts.backend));
|
|
190
190
|
}
|
|
191
|
+
if (opts.backend === "redis") {
|
|
192
|
+
if (typeof opts.redisUrl !== "string" || opts.redisUrl.length === 0) {
|
|
193
|
+
throw _err("BAD_OPT", "cache.create: backend='redis' requires opts.redisUrl (e.g. redis://localhost:6379/0)");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
191
196
|
} else {
|
|
192
197
|
_validateBackendObject(opts.backend);
|
|
193
198
|
}
|
|
@@ -729,6 +734,11 @@ function create(opts) {
|
|
|
729
734
|
"sweepIntervalMs", "staleWhileRevalidate", "slidingTtl",
|
|
730
735
|
"auditFailures", "auditClear",
|
|
731
736
|
"audit", "observability", "clock",
|
|
737
|
+
// backend === "redis" connection options. Ignored for memory /
|
|
738
|
+
// cluster / custom-backend modes.
|
|
739
|
+
"redisUrl", "redisPassword", "redisUsername", "redisTls", "redisCa",
|
|
740
|
+
"redisServername", "redisConnectTimeoutMs", "redisCommandTimeoutMs",
|
|
741
|
+
"redisMaxReconnectAttempts",
|
|
732
742
|
], "cache");
|
|
733
743
|
_validateCreateOpts(opts);
|
|
734
744
|
|
|
@@ -792,6 +802,24 @@ function create(opts) {
|
|
|
792
802
|
backend = _memoryBackend(cfg);
|
|
793
803
|
} else if (backendKind === "cluster") {
|
|
794
804
|
backend = _clusterBackend(cfg);
|
|
805
|
+
} else if (backendKind === "redis") {
|
|
806
|
+
var cacheRedis = require("./cache-redis");
|
|
807
|
+
backend = _customBackend(cacheRedis.create({
|
|
808
|
+
namespace: namespace,
|
|
809
|
+
url: opts.redisUrl,
|
|
810
|
+
password: opts.redisPassword,
|
|
811
|
+
username: opts.redisUsername,
|
|
812
|
+
tls: opts.redisTls,
|
|
813
|
+
ca: opts.redisCa,
|
|
814
|
+
servername: opts.redisServername,
|
|
815
|
+
connectTimeoutMs: opts.redisConnectTimeoutMs,
|
|
816
|
+
commandTimeoutMs: opts.redisCommandTimeoutMs,
|
|
817
|
+
maxReconnectAttempts: opts.redisMaxReconnectAttempts,
|
|
818
|
+
slidingTtl: slidingTtl,
|
|
819
|
+
defaultTtlMs: defaultTtlMs,
|
|
820
|
+
clock: clock,
|
|
821
|
+
emitObs: emitObs,
|
|
822
|
+
}), cfg);
|
|
795
823
|
} else {
|
|
796
824
|
backend = _customBackend(opts.backend, cfg);
|
|
797
825
|
}
|
package/package.json
CHANGED
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:bf737d55-37d5-49a4-95fb-c1149d627468",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-02T19:
|
|
8
|
+
"timestamp": "2026-05-02T19:10:22.247Z",
|
|
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.6.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.6.32",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.6.
|
|
25
|
+
"version": "0.6.32",
|
|
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.6.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.6.32",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.6.
|
|
57
|
+
"ref": "@blamejs/core@0.6.32",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|