@leejpsd/nextjs-cache-handler 0.4.1 → 1.0.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/CHANGELOG.md +32 -0
- package/README.md +44 -15
- package/dist/cache-components/index.cjs +59 -37
- package/dist/cache-components/index.cjs.map +1 -1
- package/dist/cache-components/index.d.cts +2 -2
- package/dist/cache-components/index.d.ts +2 -2
- package/dist/cache-components/index.js +59 -37
- package/dist/cache-components/index.js.map +1 -1
- package/dist/incremental/index.cjs.map +1 -1
- package/dist/incremental/index.d.cts +2 -2
- package/dist/incremental/index.d.ts +2 -2
- package/dist/incremental/index.js.map +1 -1
- package/dist/index.cjs +59 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +59 -37
- package/dist/index.js.map +1 -1
- package/dist/ops/index.d.cts +1 -1
- package/dist/ops/index.d.ts +1 -1
- package/dist/otel/index.d.cts +1 -1
- package/dist/otel/index.d.ts +1 -1
- package/dist/seed/index.d.cts +1 -1
- package/dist/seed/index.d.ts +1 -1
- package/dist/shared/client/adapter-ioredis.d.cts +1 -1
- package/dist/shared/client/adapter-ioredis.d.ts +1 -1
- package/dist/shared/client/adapter-redis.d.cts +1 -1
- package/dist/shared/client/adapter-redis.d.ts +1 -1
- package/dist/{types-C1vJ_sED.d.cts → types-YbDAllyp.d.cts} +7 -1
- package/dist/{types-C1vJ_sED.d.ts → types-YbDAllyp.d.ts} +7 -1
- package/package.json +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- [`3c064b5`](https://github.com/leejpsd/nextjs-cache-handler/commit/3c064b549778833a85e817885ff9fd6dde85d66e) Thanks [@leejpsd](https://github.com/leejpsd)! - 1.0 — the public API is now stable under semver.
|
|
8
|
+
|
|
9
|
+
Nothing changes for existing 0.4.x users: there are no breaking changes in this release. The version bump is a commitment, not a rewrite. From here, anything that would break a working setup waits for 2.0.
|
|
10
|
+
|
|
11
|
+
What that commitment covers:
|
|
12
|
+
- The eight entry points and everything they export, all of which are now documented in [`docs/api.md`](./docs/api.md) — including the SWR helpers (`partitionEntry`, `shouldServeStale`) that had been exported since 0.1 without being written down.
|
|
13
|
+
- `CacheHandlerOptions` and its twelve options.
|
|
14
|
+
- Redis key shapes and the tag-marker wire format, so mixed-version fleets stay readable in both directions during a rolling deploy.
|
|
15
|
+
- Metric event names.
|
|
16
|
+
|
|
17
|
+
Why now: the handler has been running against a real multi-instance deployment (24h live-traffic soak, plus a Redis-reboot drill with zero 5xx), both cache interfaces are exercised end-to-end against Next.js 16.3 stable, and the tag-invalidation semantics were re-derived from the Next.js source and corrected in 0.4.2. The remaining roadmap items (Vercel KV / Upstash adapters, `'use cache: remote'` multi-tier, a `neshClassicCache` equivalent) are additive and do not need a breaking change to land.
|
|
18
|
+
|
|
19
|
+
## 0.4.2
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- [`c165563`](https://github.com/leejpsd/nextjs-cache-handler/commit/c16556385a7fb3e8e95dfddc9015dde7a887311a) Thanks [@leejpsd](https://github.com/leejpsd)! - Align `'use cache'` tag invalidation exactly with upstream semantics.
|
|
24
|
+
|
|
25
|
+
**Fixed: hard/soft polarity was inverted for `updateTags` without durations.** Next.js calls `updateTags(tags)` with NO durations for hard invalidation (`updateTag()` server actions, single-arg `revalidateTag()`) and `updateTags(tags, { expire })` for profile-based soft invalidation (`revalidateTag(tag, "max")`). The handler treated no-durations as soft, so `updateTag()` — which must guarantee read-your-own-writes — could serve stale content. No-durations is now hard.
|
|
26
|
+
|
|
27
|
+
Also in this release:
|
|
28
|
+
- Soft invalidations now honor their hard deadline: entries serve stale until `durations.expire` seconds after the invalidation, then miss (`Infinity` = never). Tag markers store a `"<stale>|<expired>"` pair; 0.4.1 readers `parseInt` the same marker safely, and legacy bare-number markers are treated as hard-at-timestamp, so mixed-version fleets stay correct in both directions.
|
|
29
|
+
- Tag-stale entries are served with the truthful timestamp and `revalidate: -1` (the built-in handler's refresh signal) instead of a backdated timestamp, which could cross the `expire` boundary on tight profiles and confused the implicit-tags discard check.
|
|
30
|
+
- Soft invalidations serve-while-revalidating regardless of `staleWhileRevalidate` (that option now governs time-based staleness only, matching the upstream contract).
|
|
31
|
+
- `getExpiration` reports only the hard deadline, mirroring the built-in handler.
|
|
32
|
+
- `expire: 0` entries are no longer persisted in production (mirrors Next 16.3's built-in handler; they are regenerated on every read anyway).
|
|
33
|
+
- Verified end-to-end against Next.js 16.3.0 stable (both interfaces; build, serve, hard/soft invalidation, ISR SWR).
|
|
34
|
+
|
|
3
35
|
## 0.4.1
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -3,18 +3,21 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@leejpsd/nextjs-cache-handler)
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
|
|
6
|
-
> **`
|
|
7
|
-
> `npm install @leejpsd/nextjs-cache-handler
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
-
> with zero 5xx — see
|
|
11
|
-
> [`docs/staging-verification-2026-08-01.md`](./docs/staging-verification-2026-08-01.md))
|
|
6
|
+
> **`v1.0.0`** — install with
|
|
7
|
+
> `npm install @leejpsd/nextjs-cache-handler`, or wire everything with
|
|
8
|
+
> `npx nextjs-cache-handler init --yes`. Production-validated against
|
|
9
|
+
> AWS ECS Fargate with multi-instance Redis (24h live-traffic soak and a
|
|
10
|
+
> live Redis-reboot drill with zero 5xx — see
|
|
11
|
+
> [`docs/staging-verification-2026-08-01.md`](./docs/staging-verification-2026-08-01.md)),
|
|
12
|
+
> and exercised end-to-end against **Next.js 16.3** (both cache interfaces).
|
|
12
13
|
>
|
|
13
|
-
> v0.
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
17
|
-
>
|
|
14
|
+
> v0.4 adds: **build-output cache seeding** (first request after deploy is a
|
|
15
|
+
> HIT), **pub/sub tag propagation** (~3ms cross-instance), **Redis Cluster**
|
|
16
|
+
> e2e in CI, a **CLI** (`init` / `doctor` / `seed`), and agent-native assets
|
|
17
|
+
> (skill, rules, [MCP server](https://www.npmjs.com/package/@leejpsd/nextjs-cache-handler-mcp)).
|
|
18
|
+
> 1.0 freezes the public API under semver — the eight entry points, the
|
|
19
|
+
> `CacheHandlerOptions` surface, the Redis key shapes and tag-marker format,
|
|
20
|
+
> and the metric event names. Upgrading from 0.4.x needs no changes.
|
|
18
21
|
|
|
19
22
|
## 🤖 For AI agents
|
|
20
23
|
|
|
@@ -87,7 +90,7 @@ plural interface as ❌ **"Not yet supported - Help needed"**:
|
|
|
87
90
|
> [`nextjs-turbo-redis-cache`](https://github.com/trieb-work/nextjs-turbo-redis-cache#features)
|
|
88
91
|
> directly before relying on this comparison.
|
|
89
92
|
|
|
90
|
-
| Feature | this (0.
|
|
93
|
+
| Feature | this (0.4.x) | @fortedigital 3.2.1 | nextjs-turbo-redis-cache 1.15 |
|
|
91
94
|
|---|---|---|---|
|
|
92
95
|
| `cacheHandlers` config (plural) | ✅ | ❌ Help needed | ✅ since 1.11 |
|
|
93
96
|
| `'use cache'` directive | ✅ | ❌ Help needed | ✅ since 1.11 |
|
|
@@ -372,8 +375,25 @@ eventual implementation should converge on the same behavior.
|
|
|
372
375
|
|
|
373
376
|
---
|
|
374
377
|
|
|
378
|
+
## Migrating from another cache handler
|
|
379
|
+
|
|
380
|
+
Coming from `@neshca/cache-handler` (or its forks like
|
|
381
|
+
`@jadkins89/next-cache-handler`)? There's a dedicated guide with a full
|
|
382
|
+
API/concept mapping, config diff, and a seeding replacement:
|
|
383
|
+
[docs/migrating-from-neshca.md](docs/migrating-from-neshca.md).
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
375
387
|
## Compatibility
|
|
376
388
|
|
|
389
|
+
> **Upgrading to Next.js 16.3?** Two behavioral notes: (1) 16.3 changed the
|
|
390
|
+
> internal fetch-cache key format (`v3` → `v4`), so 16.2-era fetch entries
|
|
391
|
+
> become unreadable orphans — deploy with a new `DEPLOYMENT_VERSION` and let
|
|
392
|
+
> the old namespace age out via TTL. (2) An ISR entry found past its
|
|
393
|
+
> `cacheLife` **expire** now triggers a blocking revalidation instead of
|
|
394
|
+
> serving stale (matching `'use cache'` semantics); within
|
|
395
|
+
> `revalidate..expire` stale-while-revalidate is unchanged.
|
|
396
|
+
|
|
377
397
|
- **Next.js**: `>=15.0.0 <17`
|
|
378
398
|
- `cacheHandler` (singular, ISR): Next **15 and 16** — the handler accepts
|
|
379
399
|
both ctx shapes (Next 15's `ctx.revalidate` / `kindHint`, Next 16's
|
|
@@ -404,9 +424,18 @@ ESM and CJS dual-published, full TypeScript types, validated via
|
|
|
404
424
|
permanent connect-failure latch), request-scoped GET deduplication,
|
|
405
425
|
gzip/brotli compression, Redis Sentinel, built-in OpenTelemetry emitter
|
|
406
426
|
(`/otel`), LRU-bounded memory fallback, ESM peer-loading fixes ✅
|
|
407
|
-
- **v0.4.0** — Build-
|
|
408
|
-
pub/sub
|
|
409
|
-
|
|
427
|
+
- **v0.4.0** *(2026-08)* — Build-output cache seeding (`seed` CLI, NX-safe),
|
|
428
|
+
pub/sub tag propagation, Redis Cluster e2e in CI, `init`/`doctor` CLI,
|
|
429
|
+
agent assets (skill, rules, MCP server) ✅
|
|
430
|
+
- *0.4.1* — Buffer serialization fix (~2.8x smaller ISR payloads) ✅
|
|
431
|
+
- *0.4.2* — upstream-exact tag invalidation semantics: no-durations
|
|
432
|
+
`updateTags` is hard (`updateTag()` read-your-own-writes), profile
|
|
433
|
+
durations are soft with a real hard deadline; tag-stale entries are
|
|
434
|
+
served with `revalidate: -1` instead of a backdated timestamp ✅
|
|
435
|
+
- **v1.0.0** *(2026-08)* — public API frozen under semver; every export
|
|
436
|
+
documented in [`docs/api.md`](./docs/api.md) ✅
|
|
437
|
+
- **Next** — Vercel KV / Upstash adapters, `'use cache: remote'` multi-tier
|
|
438
|
+
setup, `neshClassicCache` equivalent. All additive; no 2.0 needed.
|
|
410
439
|
|
|
411
440
|
---
|
|
412
441
|
|
|
@@ -525,7 +525,7 @@ return 1
|
|
|
525
525
|
`;
|
|
526
526
|
|
|
527
527
|
// src/shared/lua/revalidate-hard.lua
|
|
528
|
-
var revalidate_hard_default = "-- revalidate-hard.lua\n--\n-- Atomically remove every entry referenced by a tag and stamp the tag's\n-- expiration marker. Used by
|
|
528
|
+
var revalidate_hard_default = "-- revalidate-hard.lua\n--\n-- Atomically remove every entry referenced by a tag and stamp the tag's\n-- expiration marker. Used by the hard invalidation path \u2014 `updateTags(tags)`\n-- with no durations (updateTag(), single-arg revalidateTag()) or an explicit\n-- `{ expire: 0 }`.\n--\n-- KEYS[1] : tag set key (e.g. \"next-cache:tag:posts\")\n-- KEYS[2] : tag-expiration marker key (e.g. \"next-cache:tag-expiration:posts\")\n-- ARGV[1] : marker value \"<staleMs>|<expiredMs>\" (hard: both equal)\n-- ARGV[2] : marker TTL in seconds (default: 1 day so cross-deployment\n-- readers can still observe the invalidation)\n--\n-- Returns: number of entries that were deleted.\n\nlocal tagKey = KEYS[1]\nlocal markerKey = KEYS[2]\nlocal marker = ARGV[1]\nlocal markerTtl = tonumber(ARGV[2])\n\nlocal entries = redis.call('SMEMBERS', tagKey)\nlocal deleted = 0\n\nif #entries > 0 then\n -- DEL accepts varargs; unpack the entry list.\n redis.call('DEL', unpack(entries))\n deleted = #entries\nend\n\n-- Drop the tag set itself; on next SET it'll be recreated by set-with-tags.\nredis.call('DEL', tagKey)\n\n-- Stamp the invalidation timestamp. Readers compare this against an entry's\n-- timestamp inside `getExpiration` to decide whether soft-tag-only entries\n-- should be considered stale.\nredis.call('SET', markerKey, marker, 'EX', markerTtl)\n\nreturn deleted\n";
|
|
529
529
|
|
|
530
530
|
// src/shared/lua/refresh-tag-lock.lua
|
|
531
531
|
var refresh_tag_lock_default = `-- refresh-tag-lock.lua
|
|
@@ -869,6 +869,23 @@ var DEFAULT_LOCK_TTL_SEC = 10;
|
|
|
869
869
|
var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
|
|
870
870
|
var FALLBACK_TTL_SEC = 3600;
|
|
871
871
|
var TAG_EXP_MARKER_TTL_SEC = 60 * 60 * 24 * 7;
|
|
872
|
+
function encodeTagMarker(state) {
|
|
873
|
+
return `${state.s}|${state.e === Infinity ? "inf" : state.e}`;
|
|
874
|
+
}
|
|
875
|
+
function decodeTagMarker(raw) {
|
|
876
|
+
const s = Number.parseInt(raw, 10);
|
|
877
|
+
if (!Number.isFinite(s)) return null;
|
|
878
|
+
const sep = raw.indexOf("|");
|
|
879
|
+
if (sep === -1) return { s, e: s };
|
|
880
|
+
const tail = raw.slice(sep + 1);
|
|
881
|
+
if (tail === "inf") return { s, e: Infinity };
|
|
882
|
+
const e = Number.parseInt(tail, 10);
|
|
883
|
+
return { s, e: Number.isFinite(e) ? e : s };
|
|
884
|
+
}
|
|
885
|
+
function mergeTagState(map, tag, next) {
|
|
886
|
+
const prev = map.get(tag);
|
|
887
|
+
if (!prev || next.s > prev.s) map.set(tag, next);
|
|
888
|
+
}
|
|
872
889
|
function gateRedis(state) {
|
|
873
890
|
const fb = state.opts.fallback;
|
|
874
891
|
const ibp = state.opts.rest.isBuildPhase;
|
|
@@ -1053,21 +1070,17 @@ async function getImpl(state, cacheKey, softTags) {
|
|
|
1053
1070
|
const freshnessTags = softTags.length > 0 ? [...softTags, ...envelope.tags] : envelope.tags;
|
|
1054
1071
|
let tagStale = false;
|
|
1055
1072
|
if (freshnessTags.length > 0) {
|
|
1056
|
-
|
|
1073
|
+
const now = Date.now();
|
|
1057
1074
|
for (const t of freshnessTags) {
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
);
|
|
1062
|
-
if (
|
|
1063
|
-
}
|
|
1064
|
-
if (mostRecent > envelope.timestamp) {
|
|
1065
|
-
if (state.opts.staleWhileRevalidate && partition.freshness !== "expired") {
|
|
1066
|
-
tagStale = true;
|
|
1067
|
-
} else {
|
|
1075
|
+
const local = state.localTagTimestamps.get(t);
|
|
1076
|
+
const mem = state.memTagExp.get(t);
|
|
1077
|
+
const st = local && mem ? local.s >= mem.s ? local : mem : local ?? mem;
|
|
1078
|
+
if (!st || st.s <= envelope.timestamp) continue;
|
|
1079
|
+
if (now >= st.e || partition.freshness === "expired") {
|
|
1068
1080
|
state.emit({ type: "cache.miss", meta: { reason: "tag-invalidated" } });
|
|
1069
1081
|
return void 0;
|
|
1070
1082
|
}
|
|
1083
|
+
tagStale = true;
|
|
1071
1084
|
}
|
|
1072
1085
|
}
|
|
1073
1086
|
const freshness = tagStale ? "stale" : partition.freshness;
|
|
@@ -1094,15 +1107,13 @@ async function getImpl(state, cacheKey, softTags) {
|
|
|
1094
1107
|
meta: { freshness }
|
|
1095
1108
|
});
|
|
1096
1109
|
}
|
|
1097
|
-
const revalidateMs = Math.max(0, envelope.revalidate) * 1e3;
|
|
1098
|
-
const outTimestamp = tagStale ? Math.min(envelope.timestamp, Date.now() - revalidateMs - 1) : envelope.timestamp;
|
|
1099
1110
|
return {
|
|
1100
1111
|
value: bufferToStream(Buffer.from(envelope.value, "base64")),
|
|
1101
1112
|
tags: envelope.tags,
|
|
1102
1113
|
stale: envelope.stale,
|
|
1103
|
-
timestamp:
|
|
1114
|
+
timestamp: envelope.timestamp,
|
|
1104
1115
|
expire: envelope.expire,
|
|
1105
|
-
revalidate: envelope.revalidate
|
|
1116
|
+
revalidate: tagStale ? -1 : envelope.revalidate
|
|
1106
1117
|
};
|
|
1107
1118
|
}
|
|
1108
1119
|
async function setImpl(state, cacheKey, pendingEntry) {
|
|
@@ -1116,6 +1127,10 @@ async function setImpl(state, cacheKey, pendingEntry) {
|
|
|
1116
1127
|
state.emit({ type: "cache.set.failed", meta: { reason: "pending-rejected" } });
|
|
1117
1128
|
return;
|
|
1118
1129
|
}
|
|
1130
|
+
if (entry.expire === 0 && !process.env.__NEXT_DEV_SERVER) {
|
|
1131
|
+
state.emit({ type: "cache.set.skipped", meta: { reason: "dynamic-entry" } });
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1119
1134
|
let buf;
|
|
1120
1135
|
try {
|
|
1121
1136
|
buf = await readStreamFully(entry.value);
|
|
@@ -1221,11 +1236,13 @@ async function ensureTagSubscription(state) {
|
|
|
1221
1236
|
try {
|
|
1222
1237
|
const parsed = JSON.parse(message);
|
|
1223
1238
|
if (!Array.isArray(parsed.t) || typeof parsed.ts !== "number") return;
|
|
1239
|
+
const incoming = {
|
|
1240
|
+
s: parsed.ts,
|
|
1241
|
+
e: typeof parsed.e === "number" ? parsed.e : Infinity
|
|
1242
|
+
};
|
|
1224
1243
|
for (const tag of parsed.t) {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
const prevMem = state.memTagExp.get(tag) ?? 0;
|
|
1228
|
-
if (parsed.ts > prevMem) state.memTagExp.set(tag, parsed.ts);
|
|
1244
|
+
mergeTagState(state.localTagTimestamps, tag, incoming);
|
|
1245
|
+
mergeTagState(state.memTagExp, tag, incoming);
|
|
1229
1246
|
}
|
|
1230
1247
|
pruneOldestByTimestamp(state.localTagTimestamps, MAX_LOCAL_TAG_ENTRIES);
|
|
1231
1248
|
pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
|
|
@@ -1277,17 +1294,17 @@ async function refreshTagsImpl(state) {
|
|
|
1277
1294
|
for (let i = 0; i < keys.length; i += 1) {
|
|
1278
1295
|
const v = values[i];
|
|
1279
1296
|
if (!v) continue;
|
|
1280
|
-
const
|
|
1281
|
-
if (!
|
|
1297
|
+
const st = decodeTagMarker(v);
|
|
1298
|
+
if (!st) continue;
|
|
1282
1299
|
const key = keys[i];
|
|
1283
1300
|
if (key === void 0) continue;
|
|
1284
1301
|
const tag = stripExpPrefix(state, key);
|
|
1285
|
-
if (tag) fresh.set(tag,
|
|
1302
|
+
if (tag) fresh.set(tag, st);
|
|
1286
1303
|
}
|
|
1287
1304
|
}
|
|
1288
|
-
for (const [tag,
|
|
1289
|
-
if (
|
|
1290
|
-
fresh.set(tag,
|
|
1305
|
+
for (const [tag, st] of state.localTagTimestamps) {
|
|
1306
|
+
if (st.s >= scanStartMs && (fresh.get(tag)?.s ?? 0) < st.s) {
|
|
1307
|
+
fresh.set(tag, st);
|
|
1291
1308
|
}
|
|
1292
1309
|
}
|
|
1293
1310
|
state.localTagTimestamps = fresh;
|
|
@@ -1305,7 +1322,7 @@ function escapeMatch(s) {
|
|
|
1305
1322
|
var MAX_LOCAL_TAG_ENTRIES = 1e4;
|
|
1306
1323
|
function pruneOldestByTimestamp(map, maxEntries) {
|
|
1307
1324
|
if (map.size <= maxEntries) return;
|
|
1308
|
-
const byAge = [...map.entries()].sort((a, b) => a[1] - b[1]);
|
|
1325
|
+
const byAge = [...map.entries()].sort((a, b) => a[1].s - b[1].s);
|
|
1309
1326
|
const drop = map.size - maxEntries;
|
|
1310
1327
|
for (let i = 0; i < drop; i += 1) {
|
|
1311
1328
|
const entry = byAge[i];
|
|
@@ -1322,9 +1339,9 @@ async function getExpirationImpl(state, tags) {
|
|
|
1322
1339
|
let max = 0;
|
|
1323
1340
|
for (const t of tags) {
|
|
1324
1341
|
const local = state.localTagTimestamps.get(t);
|
|
1325
|
-
if (local
|
|
1342
|
+
if (local && Number.isFinite(local.e) && local.e > max) max = local.e;
|
|
1326
1343
|
const mem = state.memTagExp.get(t);
|
|
1327
|
-
if (mem
|
|
1344
|
+
if (mem && Number.isFinite(mem.e) && mem.e > max) max = mem.e;
|
|
1328
1345
|
}
|
|
1329
1346
|
if (max > 0) {
|
|
1330
1347
|
state.emit({ type: "tag.expiration.read", meta: { source: "local", count: tags.length } });
|
|
@@ -1345,8 +1362,8 @@ async function getExpirationImpl(state, tags) {
|
|
|
1345
1362
|
let m = 0;
|
|
1346
1363
|
for (const v of values) {
|
|
1347
1364
|
if (!v) continue;
|
|
1348
|
-
const
|
|
1349
|
-
if (Number.isFinite(
|
|
1365
|
+
const st = decodeTagMarker(v);
|
|
1366
|
+
if (st && Number.isFinite(st.e) && st.e > m) m = st.e;
|
|
1350
1367
|
}
|
|
1351
1368
|
return m;
|
|
1352
1369
|
}
|
|
@@ -1357,11 +1374,14 @@ async function getExpirationImpl(state, tags) {
|
|
|
1357
1374
|
}
|
|
1358
1375
|
async function updateTagsImpl(state, tags, durations) {
|
|
1359
1376
|
if (tags.length === 0) return;
|
|
1360
|
-
const isHardExpire = durations
|
|
1377
|
+
const isHardExpire = durations === void 0 || durations.expire === 0;
|
|
1361
1378
|
const now = Date.now();
|
|
1379
|
+
const deadline = isHardExpire ? now : durations.expire !== void 0 && Number.isFinite(durations.expire) ? now + durations.expire * 1e3 : Infinity;
|
|
1380
|
+
const tagState = { s: now, e: deadline };
|
|
1381
|
+
const marker = encodeTagMarker(tagState);
|
|
1362
1382
|
for (const t of tags) {
|
|
1363
|
-
state.localTagTimestamps
|
|
1364
|
-
state.memTagExp
|
|
1383
|
+
mergeTagState(state.localTagTimestamps, t, tagState);
|
|
1384
|
+
mergeTagState(state.memTagExp, t, tagState);
|
|
1365
1385
|
}
|
|
1366
1386
|
pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
|
|
1367
1387
|
if (!gateRedis(state)) {
|
|
@@ -1399,8 +1419,8 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
1399
1419
|
client,
|
|
1400
1420
|
"revalidateHard",
|
|
1401
1421
|
[tagKey(state, t), tagExpKey(state, t)],
|
|
1402
|
-
[
|
|
1403
|
-
) : client.set(tagExpKey(state, t),
|
|
1422
|
+
[marker, String(TAG_EXP_MARKER_TTL_SEC)]
|
|
1423
|
+
) : client.set(tagExpKey(state, t), marker, {
|
|
1404
1424
|
EX: TAG_EXP_MARKER_TTL_SEC
|
|
1405
1425
|
})
|
|
1406
1426
|
)
|
|
@@ -1416,7 +1436,9 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
1416
1436
|
const client = await state.conn.getOrConnect();
|
|
1417
1437
|
await client?.publish?.(
|
|
1418
1438
|
invalChannel(state),
|
|
1419
|
-
JSON.stringify(
|
|
1439
|
+
JSON.stringify(
|
|
1440
|
+
deadline === Infinity ? { t: tags, ts: now } : { t: tags, ts: now, e: deadline }
|
|
1441
|
+
)
|
|
1420
1442
|
);
|
|
1421
1443
|
} catch {
|
|
1422
1444
|
}
|