@dee-wan/cache 2.0.6 → 2.1.0
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/README.md +5 -1
- package/dist/scoped-read.d.ts +5 -0
- package/dist/scoped-read.js +43 -1
- package/dist/scoped-read.js.map +1 -1
- package/docs/cache-contract.md +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -347,7 +347,11 @@ Defaults are 48 targets per batch, 4 concurrent requests across all warmup batch
|
|
|
347
347
|
|
|
348
348
|
`createIntentWarmup(cooldownMs?, now?)` warms reads on navigation intent, without storing response values. Call `setScope(siteAndActor)` before use and `warm(key, async isCurrent => ...)` on mouse hover or keyboard focus. One task runs; only the latest waiting intent remains. Check `isCurrent()` between sequential reads. Successful tasks cool for 60 seconds by default. Scope changes and `invalidate()` clear waiting tasks and cooldowns; old completions cannot warm a new scope. Use silent, bounded, read-only requests with the same URL, credentials and headers as normal reads. Do not adopt results into active editor stores or start a startup fanout.
|
|
349
349
|
|
|
350
|
-
`cacheReadValue` from `@dee-wan/cache/scoped-read` provides a Cache API value cache without D1 or a Durable Object. Pass a nonempty `namespace`, a bounded structured `key`, and a `validate` type guard with the source loader. Include every authorization and result-varying scope in the key; do not put credentials in cached values. `ttlMs` defaults to 5000; it cannot exceed 5000 unless `maxTtlMs` explicitly raises the limit (up to 300000). `maxBytes` defaults to 1,500,000 and can be lowered. Optional `cache` and `now` allow a Cache API implementation and clock to be supplied. Cache failures, invalid values, and expired entries fall back to the loader; loader errors propagate. Expiry starts before the loader runs.
|
|
350
|
+
`cacheReadValue` from `@dee-wan/cache/scoped-read` provides a Cache API value cache without D1 or a Durable Object. Pass a nonempty `namespace`, a bounded structured `key`, and a `validate` type guard with the source loader. Include every authorization and result-varying scope in the key; do not put credentials in cached values. `ttlMs` defaults to 5000; it cannot exceed 5000 unless `maxTtlMs` explicitly raises the limit (up to 300000). `maxBytes` defaults to 1,500,000 and can be lowered. Optional `cache` and `now` allow a Cache API implementation and clock to be supplied. Cache failures, invalid values, and expired entries fall back to the loader; loader errors propagate. Expiry starts before the loader runs. Optional `refresh: { aheadMs, waitUntil, load }` refreshes an eligible hit in the background without extending the existing entry; successful reloads receive a new TTL measured from their start.
|
|
351
|
+
|
|
352
|
+
Refresh loader: read fresh source; close discarded response bodies. Failed or invalid refresh keeps
|
|
353
|
+
the old expiry. Concurrent refreshes dedupe per key and cache. A foreground miss invalidates an older
|
|
354
|
+
refresh; an already-started refresh put completes before the newer foreground put.
|
|
351
355
|
|
|
352
356
|
### D1 bookmark client transport
|
|
353
357
|
|
package/dist/scoped-read.d.ts
CHANGED
|
@@ -7,6 +7,11 @@ interface ScopedReadOptions<T> {
|
|
|
7
7
|
maxBytes?: number;
|
|
8
8
|
cache?: Pick<Cache, 'match' | 'put'>;
|
|
9
9
|
now?: () => number;
|
|
10
|
+
refresh?: {
|
|
11
|
+
aheadMs: number;
|
|
12
|
+
waitUntil: (task: Promise<void>) => void;
|
|
13
|
+
load: () => Promise<T>;
|
|
14
|
+
};
|
|
10
15
|
}
|
|
11
16
|
declare function cacheReadValue<T>(options: ScopedReadOptions<T>, load: () => Promise<T>): Promise<T>;
|
|
12
17
|
|
package/dist/scoped-read.js
CHANGED
|
@@ -3,6 +3,9 @@ import { MAX_VALUE_BYTES, createRegionalCache } from './chunk-E6OZZ5UX.js';
|
|
|
3
3
|
import './chunk-ZCMZJKGN.js';
|
|
4
4
|
|
|
5
5
|
// src/scoped-read.ts
|
|
6
|
+
var defaultPending = /* @__PURE__ */ new Map();
|
|
7
|
+
var cachePending = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
var pendingCount = 0;
|
|
6
9
|
async function cacheReadValue(options, load) {
|
|
7
10
|
const { namespace, key, validate, ttlMs = 5e3, maxTtlMs = 5e3, maxBytes = MAX_VALUE_BYTES, cache, now = () => Date.now() } = options;
|
|
8
11
|
if (!Number.isSafeInteger(maxTtlMs) || maxTtlMs < 1 || maxTtlMs > 3e5 || !Number.isSafeInteger(ttlMs) || ttlMs < 1 || ttlMs > maxTtlMs) return load();
|
|
@@ -10,17 +13,56 @@ async function cacheReadValue(options, load) {
|
|
|
10
13
|
const regional = createRegionalCache(maxTtlMs, maxBytes, { cache: cache ? () => cache : void 0, now });
|
|
11
14
|
const cacheKey = await regional.regionalKey(ttlMs, ["scoped-read-value-v1", namespace, key]);
|
|
12
15
|
const cached = await regional.regionalRead(cacheKey, ttlMs);
|
|
16
|
+
let pending = defaultPending;
|
|
17
|
+
if (cache) {
|
|
18
|
+
const existing = cachePending.get(cache);
|
|
19
|
+
if (existing) pending = existing;
|
|
20
|
+
else {
|
|
21
|
+
pending = /* @__PURE__ */ new Map();
|
|
22
|
+
cachePending.set(cache, pending);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
13
25
|
try {
|
|
14
26
|
if (cached && validate(cached.value)) {
|
|
15
27
|
limitRegionalDataUntil(cached.expires);
|
|
28
|
+
const refresh = options.refresh;
|
|
29
|
+
if (cacheKey && refresh && Number.isSafeInteger(refresh.aheadMs) && refresh.aheadMs > 0 && refresh.aheadMs <= ttlMs && cached.expires - now() <= refresh.aheadMs && !pending.has(cacheKey) && pendingCount < 1024) {
|
|
30
|
+
const owner = { valid: true };
|
|
31
|
+
pending.set(cacheKey, owner);
|
|
32
|
+
pendingCount++;
|
|
33
|
+
let accepted = false;
|
|
34
|
+
const task = Promise.resolve().then(async () => {
|
|
35
|
+
if (!accepted) return;
|
|
36
|
+
const started2 = now();
|
|
37
|
+
const value2 = await refresh.load();
|
|
38
|
+
if (owner.valid && validate(value2)) {
|
|
39
|
+
owner.put = regional.regionalWrite(cacheKey, ttlMs, started2, value2);
|
|
40
|
+
await owner.put;
|
|
41
|
+
}
|
|
42
|
+
}).catch(() => {
|
|
43
|
+
}).finally(() => {
|
|
44
|
+
if (pending.get(cacheKey) === owner) pending.delete(cacheKey);
|
|
45
|
+
pendingCount--;
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
refresh.waitUntil(task);
|
|
49
|
+
accepted = true;
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
16
53
|
return cached.value;
|
|
17
54
|
}
|
|
18
55
|
} catch {
|
|
19
56
|
}
|
|
57
|
+
const older = cacheKey ? pending.get(cacheKey) : void 0;
|
|
58
|
+
if (older) older.valid = false;
|
|
20
59
|
const started = now();
|
|
21
60
|
const value = await load();
|
|
22
61
|
try {
|
|
23
|
-
if (validate(value))
|
|
62
|
+
if (validate(value)) {
|
|
63
|
+
if (older?.put) await older.put;
|
|
64
|
+
await regional.regionalWrite(cacheKey, ttlMs, started, value);
|
|
65
|
+
}
|
|
24
66
|
} catch {
|
|
25
67
|
}
|
|
26
68
|
return value;
|
package/dist/scoped-read.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/scoped-read.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"sources":["../src/scoped-read.ts"],"names":["started","value"],"mappings":"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","file":"scoped-read.js","sourcesContent":["import { createRegionalCache, MAX_VALUE_BYTES } from './d1/regional';\nimport { limitRegionalDataUntil } from './d1/regional-deadline';\n\nexport interface ScopedReadOptions<T> {\n namespace: string;\n key: unknown;\n validate: (value: unknown) => value is T;\n ttlMs?: number;\n maxTtlMs?: number;\n maxBytes?: number;\n cache?: Pick<Cache, 'match' | 'put'>;\n now?: () => number;\n refresh?: { aheadMs: number; waitUntil: (task: Promise<void>) => void; load: () => Promise<T> };\n}\n\ntype PendingRefresh = { valid: boolean; put?: Promise<void> };\nconst defaultPending = new Map<string, PendingRefresh>();\nconst cachePending = new WeakMap<object, Map<string, PendingRefresh>>();\nlet pendingCount = 0;\n\nexport async function cacheReadValue<T>(options: ScopedReadOptions<T>, load: () => Promise<T>): Promise<T> {\n const { namespace, key, validate, ttlMs = 5000, maxTtlMs = 5000, maxBytes = MAX_VALUE_BYTES, cache, now = () => Date.now() } = options;\n if (!Number.isSafeInteger(maxTtlMs) || maxTtlMs < 1 || maxTtlMs > 300_000 || !Number.isSafeInteger(ttlMs) || ttlMs < 1 || ttlMs > maxTtlMs) return load();\n if (typeof namespace !== 'string' || !namespace.trim() || !Number.isSafeInteger(maxBytes) || maxBytes <= 0 || maxBytes > MAX_VALUE_BYTES) return load();\n const regional = createRegionalCache(maxTtlMs, maxBytes, { cache: cache ? () => cache : undefined, now });\n const cacheKey = await regional.regionalKey(ttlMs, ['scoped-read-value-v1', namespace, key]);\n const cached = await regional.regionalRead(cacheKey, ttlMs);\n let pending = defaultPending;\n if (cache) {\n const existing = cachePending.get(cache);\n if (existing) pending = existing;\n else { pending = new Map(); cachePending.set(cache, pending); }\n }\n try {\n if (cached && validate(cached.value)) {\n limitRegionalDataUntil(cached.expires);\n const refresh = options.refresh;\n if (cacheKey && refresh && Number.isSafeInteger(refresh.aheadMs) && refresh.aheadMs > 0 && refresh.aheadMs <= ttlMs && cached.expires - now() <= refresh.aheadMs && !pending.has(cacheKey) && pendingCount < 1024) {\n const owner: PendingRefresh = { valid:true };\n pending.set(cacheKey, owner);\n pendingCount++;\n let accepted = false;\n const task = Promise.resolve().then(async () => {\n if (!accepted) return;\n const started = now();\n const value = await refresh.load();\n if (owner.valid && validate(value)) {\n owner.put = regional.regionalWrite(cacheKey, ttlMs, started, value);\n await owner.put;\n }\n }).catch(() => {}).finally(() => {\n if (pending.get(cacheKey) === owner) pending.delete(cacheKey);\n pendingCount--;\n });\n try { refresh.waitUntil(task); accepted = true; } catch { /* A declined task never starts its loader. */ }\n }\n return cached.value;\n }\n } catch { /* Invalid cache values fall through to the source. */ }\n const older = cacheKey ? pending.get(cacheKey) : undefined;\n if (older) older.valid = false;\n const started = now();\n const value = await load();\n try {\n if (validate(value)) {\n if (older?.put) await older.put;\n await regional.regionalWrite(cacheKey, ttlMs, started, value);\n }\n } catch { /* Validation failure must not change the source result. */ }\n return value;\n}\n"]}
|
package/docs/cache-contract.md
CHANGED
|
@@ -59,6 +59,10 @@ request flow. Fail to the database, never to stale cache.
|
|
|
59
59
|
- Warm URLs: same origin, max 8192 characters. Consumer portability rules can set a lower limit.
|
|
60
60
|
- Scoped value reads default to a 5-second TTL cap. Explicit `maxTtlMs` can raise the cap to
|
|
61
61
|
300000 ms; `ttlMs` must fit that cap. Consumer owns scope, validation and accepted staleness.
|
|
62
|
+
- Optional scoped `refresh`: ahead window, host `waitUntil`, explicit fresh loader. Hits return
|
|
63
|
+
immediately. Refreshes dedupe per cache and key; source-start TTL stays bounded. Failed or late
|
|
64
|
+
refresh leaves old expiry unchanged. Foreground misses invalidate older refreshes; an already
|
|
65
|
+
started refresh put completes before the newer foreground put. Loader owns unused body cleanup.
|
|
62
66
|
- No cold-read, write or auth latency guarantee. Sub-200 ms is a measured warm-read acceptance target.
|
|
63
67
|
|
|
64
68
|
## Fences
|