@decocms/blocks 7.62.4 → 7.62.5
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/package.json
CHANGED
package/src/sdk/cachedLoader.ts
CHANGED
|
@@ -78,7 +78,6 @@ export interface LoaderModule<TProps = any, TResult = any> {
|
|
|
78
78
|
cacheKey?: (props: TProps, req?: Request) => string | null;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
82
81
|
interface CacheEntry<T = unknown> {
|
|
83
82
|
value: T;
|
|
84
83
|
createdAt: number;
|
|
@@ -102,16 +101,41 @@ const DEFAULT_MAX_AGE = 60_000;
|
|
|
102
101
|
const DEFAULT_MAX_CACHE_BYTES = 32 * 1024 * 1024;
|
|
103
102
|
|
|
104
103
|
function resolveMaxBytes(): number {
|
|
105
|
-
const env = typeof globalThis.process !== "undefined"
|
|
106
|
-
? globalThis.process.env
|
|
107
|
-
: undefined;
|
|
104
|
+
const env = typeof globalThis.process !== "undefined" ? globalThis.process.env : undefined;
|
|
108
105
|
const raw = env?.DECO_LOADER_CACHE_MAX_BYTES;
|
|
109
106
|
if (!raw) return DEFAULT_MAX_CACHE_BYTES;
|
|
110
107
|
const parsed = Number.parseInt(raw, 10);
|
|
111
108
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CACHE_BYTES;
|
|
112
109
|
}
|
|
113
110
|
|
|
114
|
-
|
|
111
|
+
let maxCacheBytes = resolveMaxBytes();
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Override the loader cache byte cap at runtime.
|
|
115
|
+
*
|
|
116
|
+
* The `DECO_LOADER_CACHE_MAX_BYTES` env override above only lands when the
|
|
117
|
+
* Worker's `compatibility_date` is new enough for `nodejs_compat` to populate
|
|
118
|
+
* `process.env`. Verified in workerd: with `2024-09-23` it is empty both at
|
|
119
|
+
* module scope and inside the handler; with `2025-06-01` it is populated. Sites
|
|
120
|
+
* pinned to an older date — a common thing to do, since the date also gates
|
|
121
|
+
* behaviour flags they may have tuned deliberately — had a documented knob that
|
|
122
|
+
* silently did nothing.
|
|
123
|
+
*
|
|
124
|
+
* Call this from the site's `setup.ts`, before any loader is wrapped.
|
|
125
|
+
*
|
|
126
|
+
* Sizing note: the cap is per ISOLATE, not per site, and a Worker isolate has
|
|
127
|
+
* 128 MB total, shared with the bundle, the V8 heap and the render working set.
|
|
128
|
+
* Setting it at or near 128 MB does not give headroom — it removes the ceiling.
|
|
129
|
+
*/
|
|
130
|
+
export function setLoaderCacheMaxBytes(bytes: number): void {
|
|
131
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return;
|
|
132
|
+
maxCacheBytes = bytes;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Current byte cap — exported for diagnostics and tests. */
|
|
136
|
+
export function getLoaderCacheMaxBytes(): number {
|
|
137
|
+
return maxCacheBytes;
|
|
138
|
+
}
|
|
115
139
|
|
|
116
140
|
const cache = new Map<string, CacheEntry>();
|
|
117
141
|
let cacheBytes = 0;
|
|
@@ -162,13 +186,11 @@ function deleteCacheEntry(key: string) {
|
|
|
162
186
|
}
|
|
163
187
|
|
|
164
188
|
function evictIfNeeded() {
|
|
165
|
-
if (cacheBytes <=
|
|
166
|
-
const oldest = [...cache.entries()].sort(
|
|
167
|
-
(a, b) => a[1].createdAt - b[1].createdAt,
|
|
168
|
-
);
|
|
189
|
+
if (cacheBytes <= maxCacheBytes) return;
|
|
190
|
+
const oldest = [...cache.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
169
191
|
for (const [key] of oldest) {
|
|
170
192
|
deleteCacheEntry(key);
|
|
171
|
-
if (cacheBytes <=
|
|
193
|
+
if (cacheBytes <= maxCacheBytes) break;
|
|
172
194
|
}
|
|
173
195
|
}
|
|
174
196
|
|
|
@@ -246,11 +268,10 @@ export function createCachedLoader<TProps, TResult>(
|
|
|
246
268
|
recordCacheMetric(false, name, undefined, "cachedLoader");
|
|
247
269
|
const devStart = performance.now();
|
|
248
270
|
const promise = withInflightTimeout(
|
|
249
|
-
withTracing(
|
|
250
|
-
"deco.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
),
|
|
271
|
+
withTracing("deco.cachedLoader", () => loaderFn(props), {
|
|
272
|
+
"deco.loader": name,
|
|
273
|
+
"deco.cache.policy": "no-cache-dev",
|
|
274
|
+
}),
|
|
254
275
|
`cachedLoader:dev ${cacheKey}`,
|
|
255
276
|
)
|
|
256
277
|
.then((r) => {
|
|
@@ -374,7 +395,6 @@ export function createCachedLoader<TProps, TResult>(
|
|
|
374
395
|
};
|
|
375
396
|
}
|
|
376
397
|
|
|
377
|
-
|
|
378
398
|
// ---------------------------------------------------------------------------
|
|
379
399
|
// Module loader dedup (the `@decocms/apps` cache/cacheKey convention)
|
|
380
400
|
//
|
|
@@ -404,11 +424,12 @@ export function createCachedLoaderFromModule<TProps, TResult>(
|
|
|
404
424
|
name: string,
|
|
405
425
|
mod: LoaderModule<TProps, TResult>,
|
|
406
426
|
): (props: TProps, req?: Request) => Promise<TResult> {
|
|
407
|
-
const policy: CachePolicy =
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
427
|
+
const policy: CachePolicy =
|
|
428
|
+
typeof mod.cache === "string"
|
|
429
|
+
? mod.cache
|
|
430
|
+
: mod.cache && typeof mod.cache === "object"
|
|
431
|
+
? "stale-while-revalidate"
|
|
432
|
+
: "no-store";
|
|
412
433
|
|
|
413
434
|
// Only `stale-while-revalidate` (or `{ maxAge }`) opts into dedup — matching
|
|
414
435
|
// `@decocms/apps`/deco, where `no-store` (the default) and `no-cache` both
|
|
@@ -459,9 +480,7 @@ export function createLoaderEntry<TProps = any, TResult = any>(
|
|
|
459
480
|
// first-calls — exactly the N-sections-per-render case — share one import
|
|
460
481
|
// instead of racing into N. Reset on failure so a transient import error can
|
|
461
482
|
// retry rather than poisoning the entry forever.
|
|
462
|
-
let wrappedPromise:
|
|
463
|
-
| Promise<(props: TProps, req?: Request) => Promise<TResult>>
|
|
464
|
-
| undefined;
|
|
483
|
+
let wrappedPromise: Promise<(props: TProps, req?: Request) => Promise<TResult>> | undefined;
|
|
465
484
|
return (props: TProps, req?: Request): Promise<TResult> => {
|
|
466
485
|
if (!wrappedPromise) {
|
|
467
486
|
wrappedPromise = importFn()
|
|
@@ -498,6 +517,6 @@ export function getLoaderCacheStats() {
|
|
|
498
517
|
entries: cache.size,
|
|
499
518
|
inflight: inflightRequests.size,
|
|
500
519
|
estimatedBytes: cacheBytes,
|
|
501
|
-
maxBytes:
|
|
520
|
+
maxBytes: maxCacheBytes,
|
|
502
521
|
};
|
|
503
522
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getLoaderCacheMaxBytes, setLoaderCacheMaxBytes } from "./cachedLoader";
|
|
3
|
+
|
|
4
|
+
describe("loader cache byte cap", () => {
|
|
5
|
+
it("defaults to 32 MB", () => {
|
|
6
|
+
// process.env is empty in workerd unless compatibility_date is new enough,
|
|
7
|
+
// which is exactly why the setter below exists.
|
|
8
|
+
expect(getLoaderCacheMaxBytes()).toBe(32 * 1024 * 1024);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("can be raised programmatically", () => {
|
|
12
|
+
setLoaderCacheMaxBytes(64 * 1024 * 1024);
|
|
13
|
+
expect(getLoaderCacheMaxBytes()).toBe(64 * 1024 * 1024);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("ignores nonsense instead of disabling eviction", () => {
|
|
17
|
+
setLoaderCacheMaxBytes(64 * 1024 * 1024);
|
|
18
|
+
for (const bad of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
19
|
+
setLoaderCacheMaxBytes(bad);
|
|
20
|
+
expect(getLoaderCacheMaxBytes()).toBe(64 * 1024 * 1024);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
});
|