layercache 2.1.0 → 3.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/README.md +22 -6
- package/dist/{chunk-IVX6ABFX.js → chunk-5CIBABDH.js} +62 -19
- package/dist/{chunk-6X7NV5BG.js → chunk-NBMG7DHT.js} +85 -13
- package/dist/cli.cjs +153 -25
- package/dist/cli.js +69 -13
- package/dist/{edge-BCU8D-Yd.d.cts → edge-BDyuPmIq.d.cts} +5 -0
- package/dist/{edge-BCU8D-Yd.d.ts → edge-BDyuPmIq.d.ts} +5 -0
- package/dist/edge.cjs +61 -19
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +1 -1
- package/dist/index.cjs +312 -82
- package/dist/index.d.cts +47 -3
- package/dist/index.d.ts +47 -3
- package/dist/index.js +163 -47
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
|
-
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-
|
|
2
|
-
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheContextOptionsContext, n as CacheDegradationOptions, o as CacheEntryWriteKind, p as CacheEntryWriteOptions, q as CacheFetcher, r as CacheFetcherContext, s as CacheHealthCheckResult, t as CacheHitRateSnapshot, u as CacheInspectResult, v as CacheLayerLatency, w as CacheMGetEntry, x as CacheMSetEntry, y as CacheMetricsSnapshot, z as CacheMissError, A as CacheNamespace, B as CacheRateLimitOptions, D as CacheSnapshotEntry, E as CacheStackEvents, F as CacheStackOptions, G as CacheStatsSnapshot, H as CacheTtlPolicy, J as CacheTtlPolicyContext, K as CacheWarmEntry, L as CacheWarmOptions, M as CacheWarmProgress, N as CacheWriteBehindOptions, O as CacheWriteOptions, P as EvictionPolicy, Q as LayerTtlMap, R as MemoryLayer, S as MemoryLayerOptions, T as MemoryLayerSnapshotEntry, U as PatternMatcher, V as TagIndex, W as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-BDyuPmIq.cjs';
|
|
2
|
+
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheContextOptionsContext, n as CacheDegradationOptions, o as CacheEntryWriteKind, p as CacheEntryWriteOptions, q as CacheFetcher, r as CacheFetcherContext, s as CacheHealthCheckResult, t as CacheHitRateSnapshot, u as CacheInspectResult, v as CacheLayerLatency, w as CacheMGetEntry, x as CacheMSetEntry, y as CacheMetricsSnapshot, z as CacheMissError, A as CacheNamespace, B as CacheRateLimitOptions, D as CacheSnapshotEntry, E as CacheStackEvents, F as CacheStackOptions, G as CacheStatsSnapshot, H as CacheTtlPolicy, J as CacheTtlPolicyContext, K as CacheWarmEntry, L as CacheWarmOptions, M as CacheWarmProgress, N as CacheWriteBehindOptions, O as CacheWriteOptions, P as EvictionPolicy, Q as LayerTtlMap, R as MemoryLayer, S as MemoryLayerOptions, T as MemoryLayerSnapshotEntry, U as PatternMatcher, V as TagIndex, W as createHonoCacheMiddleware } from './edge-BDyuPmIq.cjs';
|
|
3
3
|
import Redis from 'ioredis';
|
|
4
4
|
import 'node:events';
|
|
5
5
|
|
|
6
|
+
interface RedisGenerationClient {
|
|
7
|
+
get(key: string): Promise<string | null>;
|
|
8
|
+
set(key: string, value: string, mode?: 'NX'): Promise<unknown>;
|
|
9
|
+
incr(key: string): Promise<number>;
|
|
10
|
+
}
|
|
11
|
+
interface RedisGenerationStoreOptions {
|
|
12
|
+
/** Redis client used to persist and atomically bump the generation. */
|
|
13
|
+
client: RedisGenerationClient;
|
|
14
|
+
/** Redis key storing the active generation. Defaults to `layercache:generation`. */
|
|
15
|
+
key?: string;
|
|
16
|
+
}
|
|
17
|
+
declare class RedisGenerationStore {
|
|
18
|
+
private readonly client;
|
|
19
|
+
private readonly key;
|
|
20
|
+
constructor(options: RedisGenerationStoreOptions);
|
|
21
|
+
get(): Promise<number | undefined>;
|
|
22
|
+
getOrInitialize(initialGeneration?: number): Promise<number>;
|
|
23
|
+
set(generation: number): Promise<void>;
|
|
24
|
+
bump(): Promise<number>;
|
|
25
|
+
private parseGeneration;
|
|
26
|
+
private assertGeneration;
|
|
27
|
+
private isGeneration;
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
interface RedisInvalidationBusOptions {
|
|
7
31
|
/** Redis client used to publish invalidation messages. */
|
|
8
32
|
publisher: Redis;
|
|
@@ -10,6 +34,11 @@ interface RedisInvalidationBusOptions {
|
|
|
10
34
|
subscriber?: Redis;
|
|
11
35
|
/** Pub/sub channel name. Defaults to `layercache:invalidation`. */
|
|
12
36
|
channel?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Optional shared secret used to sign and verify invalidation messages.
|
|
39
|
+
* When configured, unsigned or invalidly signed messages are rejected.
|
|
40
|
+
*/
|
|
41
|
+
signingSecret?: string | Buffer;
|
|
13
42
|
/** Optional logger for invalid payloads or subscriber errors. */
|
|
14
43
|
logger?: CacheLogger;
|
|
15
44
|
}
|
|
@@ -25,6 +54,7 @@ declare class RedisInvalidationBus implements InvalidationBus {
|
|
|
25
54
|
private readonly publisher;
|
|
26
55
|
private readonly subscriber;
|
|
27
56
|
private readonly logger?;
|
|
57
|
+
private readonly signingKey?;
|
|
28
58
|
private readonly handlers;
|
|
29
59
|
private sharedListener?;
|
|
30
60
|
private subscribePromise;
|
|
@@ -39,6 +69,9 @@ declare class RedisInvalidationBus implements InvalidationBus {
|
|
|
39
69
|
publish(message: InvalidationMessage): Promise<void>;
|
|
40
70
|
private dispatchToHandlers;
|
|
41
71
|
private isInvalidationMessage;
|
|
72
|
+
private signMessage;
|
|
73
|
+
private verifySignedEnvelope;
|
|
74
|
+
private createSignature;
|
|
42
75
|
private reportError;
|
|
43
76
|
}
|
|
44
77
|
|
|
@@ -51,12 +84,19 @@ interface RedisTagIndexOptions {
|
|
|
51
84
|
scanCount?: number;
|
|
52
85
|
/** Number of shards for known-key sets. Defaults to 16. */
|
|
53
86
|
knownKeysShards?: number;
|
|
87
|
+
/** Optional logger for legacy index warnings. */
|
|
88
|
+
logger?: CacheLogger;
|
|
89
|
+
}
|
|
90
|
+
interface RedisTagIndexMigrationResult {
|
|
91
|
+
migratedKeys: number;
|
|
54
92
|
}
|
|
55
93
|
declare class RedisTagIndex implements CacheTagIndex {
|
|
56
94
|
private readonly client;
|
|
57
95
|
private readonly prefix;
|
|
58
96
|
private readonly scanCount;
|
|
59
97
|
private readonly knownKeysShards;
|
|
98
|
+
private readonly logger?;
|
|
99
|
+
private warnedLegacyKnownKeys;
|
|
60
100
|
constructor(options: RedisTagIndexOptions);
|
|
61
101
|
/**
|
|
62
102
|
* Records a key as known without changing tag assignments.
|
|
@@ -102,9 +142,13 @@ declare class RedisTagIndex implements CacheTagIndex {
|
|
|
102
142
|
* Clears all Redis tag-index state under this prefix.
|
|
103
143
|
*/
|
|
104
144
|
clear(): Promise<void>;
|
|
145
|
+
migrateLegacyKnownKeys(): Promise<RedisTagIndexMigrationResult>;
|
|
105
146
|
private scanIndexKeys;
|
|
106
147
|
private knownKeysKeyFor;
|
|
148
|
+
private knownKeysKeysForRead;
|
|
107
149
|
private knownKeysKeys;
|
|
150
|
+
private legacyKnownKeysKey;
|
|
151
|
+
private warnLegacyKnownKeys;
|
|
108
152
|
private keyTagsKey;
|
|
109
153
|
private tagKeysKey;
|
|
110
154
|
}
|
|
@@ -746,4 +790,4 @@ declare function createPrometheusMetricsExporter(stacks: CacheStack | Array<{
|
|
|
746
790
|
name: string;
|
|
747
791
|
}>): () => string;
|
|
748
792
|
|
|
749
|
-
export { CacheGetOptions, CacheLayer, CacheLayerSetManyEntry, CacheLogger, CacheSerializer, CacheSingleFlightCoordinator, CacheSingleFlightExecutionOptions, CacheStack, CacheTagIndex, CacheWrapOptions, DiskLayer, InvalidationBus, InvalidationMessage, JsonSerializer, type MemcachedClient, MemcachedLayer, MsgpackSerializer, RedisInvalidationBus, RedisLayer, RedisSingleFlightCoordinator, RedisTagIndex, StampedeGuard, cacheGraphqlResolver, createCacheStatsHandler, createCachedMethodDecorator, createExpressCacheMiddleware, createFastifyLayercachePlugin, createOpenTelemetryPlugin, createPrometheusMetricsExporter, createTrpcCacheMiddleware };
|
|
793
|
+
export { CacheGetOptions, CacheLayer, CacheLayerSetManyEntry, CacheLogger, CacheSerializer, CacheSingleFlightCoordinator, CacheSingleFlightExecutionOptions, CacheStack, CacheTagIndex, CacheWrapOptions, DiskLayer, InvalidationBus, InvalidationMessage, JsonSerializer, type MemcachedClient, MemcachedLayer, MsgpackSerializer, RedisGenerationStore, RedisInvalidationBus, RedisLayer, RedisSingleFlightCoordinator, RedisTagIndex, StampedeGuard, cacheGraphqlResolver, createCacheStatsHandler, createCachedMethodDecorator, createExpressCacheMiddleware, createFastifyLayercachePlugin, createOpenTelemetryPlugin, createPrometheusMetricsExporter, createTrpcCacheMiddleware };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
|
-
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-
|
|
2
|
-
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheContextOptionsContext, n as CacheDegradationOptions, o as CacheEntryWriteKind, p as CacheEntryWriteOptions, q as CacheFetcher, r as CacheFetcherContext, s as CacheHealthCheckResult, t as CacheHitRateSnapshot, u as CacheInspectResult, v as CacheLayerLatency, w as CacheMGetEntry, x as CacheMSetEntry, y as CacheMetricsSnapshot, z as CacheMissError, A as CacheNamespace, B as CacheRateLimitOptions, D as CacheSnapshotEntry, E as CacheStackEvents, F as CacheStackOptions, G as CacheStatsSnapshot, H as CacheTtlPolicy, J as CacheTtlPolicyContext, K as CacheWarmEntry, L as CacheWarmOptions, M as CacheWarmProgress, N as CacheWriteBehindOptions, O as CacheWriteOptions, P as EvictionPolicy, Q as LayerTtlMap, R as MemoryLayer, S as MemoryLayerOptions, T as MemoryLayerSnapshotEntry, U as PatternMatcher, V as TagIndex, W as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-BDyuPmIq.js';
|
|
2
|
+
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheContextOptionsContext, n as CacheDegradationOptions, o as CacheEntryWriteKind, p as CacheEntryWriteOptions, q as CacheFetcher, r as CacheFetcherContext, s as CacheHealthCheckResult, t as CacheHitRateSnapshot, u as CacheInspectResult, v as CacheLayerLatency, w as CacheMGetEntry, x as CacheMSetEntry, y as CacheMetricsSnapshot, z as CacheMissError, A as CacheNamespace, B as CacheRateLimitOptions, D as CacheSnapshotEntry, E as CacheStackEvents, F as CacheStackOptions, G as CacheStatsSnapshot, H as CacheTtlPolicy, J as CacheTtlPolicyContext, K as CacheWarmEntry, L as CacheWarmOptions, M as CacheWarmProgress, N as CacheWriteBehindOptions, O as CacheWriteOptions, P as EvictionPolicy, Q as LayerTtlMap, R as MemoryLayer, S as MemoryLayerOptions, T as MemoryLayerSnapshotEntry, U as PatternMatcher, V as TagIndex, W as createHonoCacheMiddleware } from './edge-BDyuPmIq.js';
|
|
3
3
|
import Redis from 'ioredis';
|
|
4
4
|
import 'node:events';
|
|
5
5
|
|
|
6
|
+
interface RedisGenerationClient {
|
|
7
|
+
get(key: string): Promise<string | null>;
|
|
8
|
+
set(key: string, value: string, mode?: 'NX'): Promise<unknown>;
|
|
9
|
+
incr(key: string): Promise<number>;
|
|
10
|
+
}
|
|
11
|
+
interface RedisGenerationStoreOptions {
|
|
12
|
+
/** Redis client used to persist and atomically bump the generation. */
|
|
13
|
+
client: RedisGenerationClient;
|
|
14
|
+
/** Redis key storing the active generation. Defaults to `layercache:generation`. */
|
|
15
|
+
key?: string;
|
|
16
|
+
}
|
|
17
|
+
declare class RedisGenerationStore {
|
|
18
|
+
private readonly client;
|
|
19
|
+
private readonly key;
|
|
20
|
+
constructor(options: RedisGenerationStoreOptions);
|
|
21
|
+
get(): Promise<number | undefined>;
|
|
22
|
+
getOrInitialize(initialGeneration?: number): Promise<number>;
|
|
23
|
+
set(generation: number): Promise<void>;
|
|
24
|
+
bump(): Promise<number>;
|
|
25
|
+
private parseGeneration;
|
|
26
|
+
private assertGeneration;
|
|
27
|
+
private isGeneration;
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
interface RedisInvalidationBusOptions {
|
|
7
31
|
/** Redis client used to publish invalidation messages. */
|
|
8
32
|
publisher: Redis;
|
|
@@ -10,6 +34,11 @@ interface RedisInvalidationBusOptions {
|
|
|
10
34
|
subscriber?: Redis;
|
|
11
35
|
/** Pub/sub channel name. Defaults to `layercache:invalidation`. */
|
|
12
36
|
channel?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Optional shared secret used to sign and verify invalidation messages.
|
|
39
|
+
* When configured, unsigned or invalidly signed messages are rejected.
|
|
40
|
+
*/
|
|
41
|
+
signingSecret?: string | Buffer;
|
|
13
42
|
/** Optional logger for invalid payloads or subscriber errors. */
|
|
14
43
|
logger?: CacheLogger;
|
|
15
44
|
}
|
|
@@ -25,6 +54,7 @@ declare class RedisInvalidationBus implements InvalidationBus {
|
|
|
25
54
|
private readonly publisher;
|
|
26
55
|
private readonly subscriber;
|
|
27
56
|
private readonly logger?;
|
|
57
|
+
private readonly signingKey?;
|
|
28
58
|
private readonly handlers;
|
|
29
59
|
private sharedListener?;
|
|
30
60
|
private subscribePromise;
|
|
@@ -39,6 +69,9 @@ declare class RedisInvalidationBus implements InvalidationBus {
|
|
|
39
69
|
publish(message: InvalidationMessage): Promise<void>;
|
|
40
70
|
private dispatchToHandlers;
|
|
41
71
|
private isInvalidationMessage;
|
|
72
|
+
private signMessage;
|
|
73
|
+
private verifySignedEnvelope;
|
|
74
|
+
private createSignature;
|
|
42
75
|
private reportError;
|
|
43
76
|
}
|
|
44
77
|
|
|
@@ -51,12 +84,19 @@ interface RedisTagIndexOptions {
|
|
|
51
84
|
scanCount?: number;
|
|
52
85
|
/** Number of shards for known-key sets. Defaults to 16. */
|
|
53
86
|
knownKeysShards?: number;
|
|
87
|
+
/** Optional logger for legacy index warnings. */
|
|
88
|
+
logger?: CacheLogger;
|
|
89
|
+
}
|
|
90
|
+
interface RedisTagIndexMigrationResult {
|
|
91
|
+
migratedKeys: number;
|
|
54
92
|
}
|
|
55
93
|
declare class RedisTagIndex implements CacheTagIndex {
|
|
56
94
|
private readonly client;
|
|
57
95
|
private readonly prefix;
|
|
58
96
|
private readonly scanCount;
|
|
59
97
|
private readonly knownKeysShards;
|
|
98
|
+
private readonly logger?;
|
|
99
|
+
private warnedLegacyKnownKeys;
|
|
60
100
|
constructor(options: RedisTagIndexOptions);
|
|
61
101
|
/**
|
|
62
102
|
* Records a key as known without changing tag assignments.
|
|
@@ -102,9 +142,13 @@ declare class RedisTagIndex implements CacheTagIndex {
|
|
|
102
142
|
* Clears all Redis tag-index state under this prefix.
|
|
103
143
|
*/
|
|
104
144
|
clear(): Promise<void>;
|
|
145
|
+
migrateLegacyKnownKeys(): Promise<RedisTagIndexMigrationResult>;
|
|
105
146
|
private scanIndexKeys;
|
|
106
147
|
private knownKeysKeyFor;
|
|
148
|
+
private knownKeysKeysForRead;
|
|
107
149
|
private knownKeysKeys;
|
|
150
|
+
private legacyKnownKeysKey;
|
|
151
|
+
private warnLegacyKnownKeys;
|
|
108
152
|
private keyTagsKey;
|
|
109
153
|
private tagKeysKey;
|
|
110
154
|
}
|
|
@@ -746,4 +790,4 @@ declare function createPrometheusMetricsExporter(stacks: CacheStack | Array<{
|
|
|
746
790
|
name: string;
|
|
747
791
|
}>): () => string;
|
|
748
792
|
|
|
749
|
-
export { CacheGetOptions, CacheLayer, CacheLayerSetManyEntry, CacheLogger, CacheSerializer, CacheSingleFlightCoordinator, CacheSingleFlightExecutionOptions, CacheStack, CacheTagIndex, CacheWrapOptions, DiskLayer, InvalidationBus, InvalidationMessage, JsonSerializer, type MemcachedClient, MemcachedLayer, MsgpackSerializer, RedisInvalidationBus, RedisLayer, RedisSingleFlightCoordinator, RedisTagIndex, StampedeGuard, cacheGraphqlResolver, createCacheStatsHandler, createCachedMethodDecorator, createExpressCacheMiddleware, createFastifyLayercachePlugin, createOpenTelemetryPlugin, createPrometheusMetricsExporter, createTrpcCacheMiddleware };
|
|
793
|
+
export { CacheGetOptions, CacheLayer, CacheLayerSetManyEntry, CacheLogger, CacheSerializer, CacheSingleFlightCoordinator, CacheSingleFlightExecutionOptions, CacheStack, CacheTagIndex, CacheWrapOptions, DiskLayer, InvalidationBus, InvalidationMessage, JsonSerializer, type MemcachedClient, MemcachedLayer, MsgpackSerializer, RedisGenerationStore, RedisInvalidationBus, RedisLayer, RedisSingleFlightCoordinator, RedisTagIndex, StampedeGuard, cacheGraphqlResolver, createCacheStatsHandler, createCachedMethodDecorator, createExpressCacheMiddleware, createFastifyLayercachePlugin, createOpenTelemetryPlugin, createPrometheusMetricsExporter, createTrpcCacheMiddleware };
|
package/dist/index.js
CHANGED
|
@@ -12,12 +12,13 @@ import {
|
|
|
12
12
|
validateTag,
|
|
13
13
|
validateTags,
|
|
14
14
|
validateTtlPolicy
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-NBMG7DHT.js";
|
|
16
16
|
import {
|
|
17
17
|
MemoryLayer,
|
|
18
18
|
TagIndex,
|
|
19
|
-
createHonoCacheMiddleware
|
|
20
|
-
|
|
19
|
+
createHonoCacheMiddleware,
|
|
20
|
+
normalizeHttpCacheUrl
|
|
21
|
+
} from "./chunk-5CIBABDH.js";
|
|
21
22
|
import {
|
|
22
23
|
PatternMatcher,
|
|
23
24
|
createStoredValueEnvelope,
|
|
@@ -913,7 +914,9 @@ var CacheStackMaintenance = class {
|
|
|
913
914
|
}
|
|
914
915
|
bumpKeyEpochs(keys) {
|
|
915
916
|
for (const key of keys) {
|
|
916
|
-
|
|
917
|
+
const nextEpoch = this.currentKeyEpoch(key) + 1;
|
|
918
|
+
this.keyEpochs.delete(key);
|
|
919
|
+
this.keyEpochs.set(key, nextEpoch);
|
|
917
920
|
}
|
|
918
921
|
this.pruneKeyEpochsIfNeeded();
|
|
919
922
|
}
|
|
@@ -972,10 +975,13 @@ var CacheStackMaintenance = class {
|
|
|
972
975
|
if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
|
|
973
976
|
return;
|
|
974
977
|
}
|
|
975
|
-
const
|
|
976
|
-
const toDelete = Math.ceil(sorted.length * 0.1);
|
|
978
|
+
const toDelete = Math.ceil(this.keyEpochs.size * 0.1);
|
|
977
979
|
for (let i = 0; i < toDelete; i++) {
|
|
978
|
-
this.keyEpochs.
|
|
980
|
+
const oldestKey = this.keyEpochs.keys().next().value;
|
|
981
|
+
if (oldestKey === void 0) {
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
this.keyEpochs.delete(oldestKey);
|
|
979
985
|
}
|
|
980
986
|
}
|
|
981
987
|
};
|
|
@@ -1109,22 +1115,28 @@ var CacheStackReader = class {
|
|
|
1109
1115
|
if (upToIndex < 0) {
|
|
1110
1116
|
return;
|
|
1111
1117
|
}
|
|
1118
|
+
const operations = [];
|
|
1112
1119
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
1113
1120
|
const layer = this.options.layers[index];
|
|
1114
1121
|
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
1115
1122
|
continue;
|
|
1116
1123
|
}
|
|
1117
1124
|
const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1125
|
+
operations.push(
|
|
1126
|
+
(async () => {
|
|
1127
|
+
try {
|
|
1128
|
+
await layer.set(key, stored, ttl);
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
await this.options.handleLayerFailure(layer, "backfill", error);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
this.options.metricsCollector.increment("backfills");
|
|
1134
|
+
this.options.logger.debug?.("backfill", { key, layer: layer.name });
|
|
1135
|
+
this.options.emit("backfill", { key, layer: layer.name });
|
|
1136
|
+
})()
|
|
1137
|
+
);
|
|
1127
1138
|
}
|
|
1139
|
+
await Promise.all(operations);
|
|
1128
1140
|
}
|
|
1129
1141
|
abortAllRefreshes() {
|
|
1130
1142
|
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
@@ -1248,7 +1260,15 @@ var CacheStackReader = class {
|
|
|
1248
1260
|
}
|
|
1249
1261
|
await this.options.sleep(pollIntervalMs);
|
|
1250
1262
|
}
|
|
1251
|
-
|
|
1263
|
+
if (!this.options.singleFlightCoordinator) {
|
|
1264
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
1265
|
+
}
|
|
1266
|
+
return this.options.singleFlightCoordinator.execute(
|
|
1267
|
+
key,
|
|
1268
|
+
this.resolveSingleFlightOptions(),
|
|
1269
|
+
() => this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext),
|
|
1270
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
|
|
1271
|
+
);
|
|
1252
1272
|
}
|
|
1253
1273
|
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1254
1274
|
key,
|
|
@@ -2163,6 +2183,7 @@ var TtlResolver = class {
|
|
|
2163
2183
|
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
2164
2184
|
profile.hits += 1;
|
|
2165
2185
|
profile.lastAccessAt = Date.now();
|
|
2186
|
+
this.accessProfiles.delete(key);
|
|
2166
2187
|
this.accessProfiles.set(key, profile);
|
|
2167
2188
|
this.pruneIfNeeded();
|
|
2168
2189
|
}
|
|
@@ -2252,12 +2273,12 @@ var TtlResolver = class {
|
|
|
2252
2273
|
return;
|
|
2253
2274
|
}
|
|
2254
2275
|
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
this.accessProfiles.delete(entry[0]);
|
|
2276
|
+
for (let i = 0; i < toRemove; i++) {
|
|
2277
|
+
const oldestKey = this.accessProfiles.keys().next().value;
|
|
2278
|
+
if (oldestKey === void 0) {
|
|
2279
|
+
break;
|
|
2260
2280
|
}
|
|
2281
|
+
this.accessProfiles.delete(oldestKey);
|
|
2261
2282
|
}
|
|
2262
2283
|
}
|
|
2263
2284
|
};
|
|
@@ -3112,6 +3133,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
3112
3133
|
}
|
|
3113
3134
|
return this.currentGeneration;
|
|
3114
3135
|
}
|
|
3136
|
+
/**
|
|
3137
|
+
* Returns the active generation prefix number used for future cache keys.
|
|
3138
|
+
*/
|
|
3139
|
+
getGeneration() {
|
|
3140
|
+
return this.currentGeneration;
|
|
3141
|
+
}
|
|
3115
3142
|
/**
|
|
3116
3143
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
3117
3144
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
@@ -3669,12 +3696,65 @@ var CacheStack = class extends EventEmitter {
|
|
|
3669
3696
|
}
|
|
3670
3697
|
};
|
|
3671
3698
|
|
|
3699
|
+
// src/generation/RedisGenerationStore.ts
|
|
3700
|
+
var DEFAULT_GENERATION_KEY = "layercache:generation";
|
|
3701
|
+
var RedisGenerationStore = class {
|
|
3702
|
+
client;
|
|
3703
|
+
key;
|
|
3704
|
+
constructor(options) {
|
|
3705
|
+
this.client = options.client;
|
|
3706
|
+
this.key = options.key ?? DEFAULT_GENERATION_KEY;
|
|
3707
|
+
}
|
|
3708
|
+
async get() {
|
|
3709
|
+
const stored = await this.client.get(this.key);
|
|
3710
|
+
if (stored === null) {
|
|
3711
|
+
return void 0;
|
|
3712
|
+
}
|
|
3713
|
+
return this.parseGeneration(stored);
|
|
3714
|
+
}
|
|
3715
|
+
async getOrInitialize(initialGeneration = 0) {
|
|
3716
|
+
this.assertGeneration(initialGeneration);
|
|
3717
|
+
await this.client.set(this.key, String(initialGeneration), "NX");
|
|
3718
|
+
const generation = await this.get();
|
|
3719
|
+
if (generation === void 0) {
|
|
3720
|
+
throw new Error(`RedisGenerationStore failed to initialize generation key "${this.key}".`);
|
|
3721
|
+
}
|
|
3722
|
+
return generation;
|
|
3723
|
+
}
|
|
3724
|
+
async set(generation) {
|
|
3725
|
+
this.assertGeneration(generation);
|
|
3726
|
+
await this.client.set(this.key, String(generation));
|
|
3727
|
+
}
|
|
3728
|
+
async bump() {
|
|
3729
|
+
const generation = await this.client.incr(this.key);
|
|
3730
|
+
this.assertGeneration(generation);
|
|
3731
|
+
return generation;
|
|
3732
|
+
}
|
|
3733
|
+
parseGeneration(value) {
|
|
3734
|
+
const generation = Number.parseInt(value, 10);
|
|
3735
|
+
if (String(generation) !== value || !this.isGeneration(generation)) {
|
|
3736
|
+
throw new Error(`RedisGenerationStore found invalid persisted generation value for key "${this.key}".`);
|
|
3737
|
+
}
|
|
3738
|
+
return generation;
|
|
3739
|
+
}
|
|
3740
|
+
assertGeneration(value) {
|
|
3741
|
+
if (!this.isGeneration(value)) {
|
|
3742
|
+
throw new Error("RedisGenerationStore generation must be a non-negative safe integer.");
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
isGeneration(value) {
|
|
3746
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
3747
|
+
}
|
|
3748
|
+
};
|
|
3749
|
+
|
|
3672
3750
|
// src/invalidation/RedisInvalidationBus.ts
|
|
3751
|
+
import { createHash, createHmac, timingSafeEqual } from "crypto";
|
|
3673
3752
|
var RedisInvalidationBus = class {
|
|
3674
3753
|
channel;
|
|
3675
3754
|
publisher;
|
|
3676
3755
|
subscriber;
|
|
3677
3756
|
logger;
|
|
3757
|
+
signingKey;
|
|
3678
3758
|
handlers = /* @__PURE__ */ new Set();
|
|
3679
3759
|
sharedListener;
|
|
3680
3760
|
subscribePromise;
|
|
@@ -3683,6 +3763,7 @@ var RedisInvalidationBus = class {
|
|
|
3683
3763
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
3684
3764
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
3685
3765
|
this.logger = options.logger;
|
|
3766
|
+
this.signingKey = options.signingSecret ? normalizeSigningSecret(options.signingSecret) : void 0;
|
|
3686
3767
|
}
|
|
3687
3768
|
/**
|
|
3688
3769
|
* Subscribes to invalidation messages and returns an unsubscribe function.
|
|
@@ -3722,7 +3803,7 @@ var RedisInvalidationBus = class {
|
|
|
3722
3803
|
* Publishes an invalidation message to other subscribers.
|
|
3723
3804
|
*/
|
|
3724
3805
|
async publish(message) {
|
|
3725
|
-
await this.publisher.publish(this.channel, JSON.stringify(message));
|
|
3806
|
+
await this.publisher.publish(this.channel, JSON.stringify(this.signingKey ? this.signMessage(message) : message));
|
|
3726
3807
|
}
|
|
3727
3808
|
async dispatchToHandlers(payload) {
|
|
3728
3809
|
let message;
|
|
@@ -3733,10 +3814,11 @@ var RedisInvalidationBus = class {
|
|
|
3733
3814
|
maxNodes: 1e4,
|
|
3734
3815
|
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
3735
3816
|
});
|
|
3736
|
-
|
|
3817
|
+
const candidate = this.signingKey ? this.verifySignedEnvelope(parsed) : parsed;
|
|
3818
|
+
if (!this.isInvalidationMessage(candidate)) {
|
|
3737
3819
|
throw new Error("Invalid invalidation payload shape.");
|
|
3738
3820
|
}
|
|
3739
|
-
message =
|
|
3821
|
+
message = candidate;
|
|
3740
3822
|
} catch (error) {
|
|
3741
3823
|
this.reportError("invalid invalidation payload", error);
|
|
3742
3824
|
return;
|
|
@@ -3761,6 +3843,34 @@ var RedisInvalidationBus = class {
|
|
|
3761
3843
|
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
3762
3844
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
3763
3845
|
}
|
|
3846
|
+
signMessage(message) {
|
|
3847
|
+
const payload = JSON.stringify(message);
|
|
3848
|
+
return {
|
|
3849
|
+
payload: message,
|
|
3850
|
+
signature: this.createSignature(payload)
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
verifySignedEnvelope(value) {
|
|
3854
|
+
if (!value || typeof value !== "object") {
|
|
3855
|
+
throw new Error("Signed invalidation envelope must be an object.");
|
|
3856
|
+
}
|
|
3857
|
+
const envelope = value;
|
|
3858
|
+
if (!envelope.payload || typeof envelope.payload !== "object" || typeof envelope.signature !== "string") {
|
|
3859
|
+
throw new Error("Signed invalidation envelope is missing payload or signature.");
|
|
3860
|
+
}
|
|
3861
|
+
const payload = JSON.stringify(envelope.payload);
|
|
3862
|
+
const expected = this.createSignature(payload);
|
|
3863
|
+
if (!isEqualSignature(envelope.signature, expected)) {
|
|
3864
|
+
throw new Error("Invalid invalidation message signature.");
|
|
3865
|
+
}
|
|
3866
|
+
return envelope.payload;
|
|
3867
|
+
}
|
|
3868
|
+
createSignature(payload) {
|
|
3869
|
+
if (!this.signingKey) {
|
|
3870
|
+
throw new Error("RedisInvalidationBus signing key is not configured.");
|
|
3871
|
+
}
|
|
3872
|
+
return createHmac("sha256", this.signingKey).update(payload).digest("hex");
|
|
3873
|
+
}
|
|
3764
3874
|
reportError(message, error) {
|
|
3765
3875
|
if (this.logger?.error) {
|
|
3766
3876
|
this.logger.error(message, { error });
|
|
@@ -3769,6 +3879,15 @@ var RedisInvalidationBus = class {
|
|
|
3769
3879
|
console.error(`[layercache] ${message}`, error);
|
|
3770
3880
|
}
|
|
3771
3881
|
};
|
|
3882
|
+
function normalizeSigningSecret(secret) {
|
|
3883
|
+
const raw = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, "utf8");
|
|
3884
|
+
return createHash("sha256").update(raw).digest();
|
|
3885
|
+
}
|
|
3886
|
+
function isEqualSignature(actual, expected) {
|
|
3887
|
+
const actualBuffer = Buffer.from(actual, "hex");
|
|
3888
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
3889
|
+
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
|
|
3890
|
+
}
|
|
3772
3891
|
|
|
3773
3892
|
// src/http/createCacheStatsHandler.ts
|
|
3774
3893
|
function createCacheStatsHandler(cache, options = {}) {
|
|
@@ -3866,7 +3985,7 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3866
3985
|
return;
|
|
3867
3986
|
}
|
|
3868
3987
|
const rawUrl = req.originalUrl ?? req.url ?? "/";
|
|
3869
|
-
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${
|
|
3988
|
+
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeHttpCacheUrl(rawUrl)}`;
|
|
3870
3989
|
const cached = await cache.get(key, void 0, options);
|
|
3871
3990
|
if (cached !== null) {
|
|
3872
3991
|
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
@@ -3882,12 +4001,14 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3882
4001
|
if (originalJson) {
|
|
3883
4002
|
res.json = (body) => {
|
|
3884
4003
|
res.setHeader?.("x-cache", "MISS");
|
|
3885
|
-
|
|
3886
|
-
cache.
|
|
3887
|
-
|
|
3888
|
-
|
|
4004
|
+
if (isSuccessfulStatus(res.statusCode)) {
|
|
4005
|
+
cache.set(key, body, options).catch((err) => {
|
|
4006
|
+
cache.emit("error", {
|
|
4007
|
+
operation: "set",
|
|
4008
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4009
|
+
});
|
|
3889
4010
|
});
|
|
3890
|
-
}
|
|
4011
|
+
}
|
|
3891
4012
|
return originalJson(body);
|
|
3892
4013
|
};
|
|
3893
4014
|
}
|
|
@@ -3897,14 +4018,8 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3897
4018
|
}
|
|
3898
4019
|
};
|
|
3899
4020
|
}
|
|
3900
|
-
function
|
|
3901
|
-
|
|
3902
|
-
const parsed = new URL(url, "http://localhost");
|
|
3903
|
-
parsed.searchParams.sort();
|
|
3904
|
-
return parsed.pathname + parsed.search;
|
|
3905
|
-
} catch {
|
|
3906
|
-
return url;
|
|
3907
|
-
}
|
|
4021
|
+
function isSuccessfulStatus(statusCode) {
|
|
4022
|
+
return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
|
|
3908
4023
|
}
|
|
3909
4024
|
|
|
3910
4025
|
// src/integrations/graphql.ts
|
|
@@ -4471,12 +4586,12 @@ var RedisLayer = class {
|
|
|
4471
4586
|
};
|
|
4472
4587
|
|
|
4473
4588
|
// src/layers/DiskLayer.ts
|
|
4474
|
-
import { createHash as
|
|
4589
|
+
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
4475
4590
|
import { promises as fs2 } from "fs";
|
|
4476
4591
|
import { join, resolve } from "path";
|
|
4477
4592
|
|
|
4478
4593
|
// src/internal/PayloadProtection.ts
|
|
4479
|
-
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
4594
|
+
import { createCipheriv, createDecipheriv, createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4480
4595
|
var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
|
|
4481
4596
|
var MAGIC_SIGNED = Buffer.from("LCS1:");
|
|
4482
4597
|
var ALGORITHM = "aes-256-gcm";
|
|
@@ -4489,11 +4604,11 @@ var PayloadProtection = class {
|
|
|
4489
4604
|
constructor(options) {
|
|
4490
4605
|
if (options.encryptionKey) {
|
|
4491
4606
|
const raw = Buffer.isBuffer(options.encryptionKey) ? options.encryptionKey : Buffer.from(options.encryptionKey, "utf8");
|
|
4492
|
-
this.encryptionKey =
|
|
4607
|
+
this.encryptionKey = createHash2("sha256").update(raw).digest();
|
|
4493
4608
|
}
|
|
4494
4609
|
if (options.signingKey && !options.encryptionKey) {
|
|
4495
4610
|
const raw = Buffer.isBuffer(options.signingKey) ? options.signingKey : Buffer.from(options.signingKey, "utf8");
|
|
4496
|
-
this.signingKey =
|
|
4611
|
+
this.signingKey = createHash2("sha256").update(raw).digest();
|
|
4497
4612
|
}
|
|
4498
4613
|
}
|
|
4499
4614
|
/** Returns `true` when any protection (encryption or signing) is configured. */
|
|
@@ -4565,15 +4680,15 @@ var PayloadProtection = class {
|
|
|
4565
4680
|
}
|
|
4566
4681
|
// ── Signing (HMAC-SHA256) ─────────────────────────────────────────────
|
|
4567
4682
|
sign(payload, key) {
|
|
4568
|
-
const hmac =
|
|
4683
|
+
const hmac = createHmac2("sha256", key).update(payload).digest();
|
|
4569
4684
|
return Buffer.concat([MAGIC_SIGNED, hmac, payload]);
|
|
4570
4685
|
}
|
|
4571
4686
|
verify(payload, key) {
|
|
4572
4687
|
const headerEnd = MAGIC_SIGNED.length;
|
|
4573
4688
|
const receivedHmac = payload.subarray(headerEnd, headerEnd + HMAC_LENGTH);
|
|
4574
4689
|
const data = payload.subarray(headerEnd + HMAC_LENGTH);
|
|
4575
|
-
const expectedHmac =
|
|
4576
|
-
if (receivedHmac.length !== HMAC_LENGTH || !
|
|
4690
|
+
const expectedHmac = createHmac2("sha256", key).update(data).digest();
|
|
4691
|
+
if (receivedHmac.length !== HMAC_LENGTH || !timingSafeEqual2(receivedHmac, expectedHmac)) {
|
|
4577
4692
|
throw new PayloadProtectionError(
|
|
4578
4693
|
"HMAC verification failed. The data may have been tampered with or the signingKey is incorrect."
|
|
4579
4694
|
);
|
|
@@ -4798,7 +4913,7 @@ var DiskLayer = class {
|
|
|
4798
4913
|
async dispose() {
|
|
4799
4914
|
}
|
|
4800
4915
|
keyToPath(key) {
|
|
4801
|
-
const hash =
|
|
4916
|
+
const hash = createHash3("sha256").update(key).digest("hex");
|
|
4802
4917
|
return join(this.directory, `${hash}.lc`);
|
|
4803
4918
|
}
|
|
4804
4919
|
resolveDirectory(directory) {
|
|
@@ -5297,6 +5412,7 @@ export {
|
|
|
5297
5412
|
MemoryLayer,
|
|
5298
5413
|
MsgpackSerializer,
|
|
5299
5414
|
PatternMatcher,
|
|
5415
|
+
RedisGenerationStore,
|
|
5300
5416
|
RedisInvalidationBus,
|
|
5301
5417
|
RedisLayer,
|
|
5302
5418
|
RedisSingleFlightCoordinator,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "layercache",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Production-ready multi-layer caching for Node.js. Stack memory + Redis + disk behind one API with stampede prevention, tag invalidation, stale serving, and full observability.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cache",
|