encore.dev 1.54.2 → 1.56.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/config/secrets.ts +7 -1
- package/dist/config/secrets.js +4 -0
- package/dist/config/secrets.js.map +1 -1
- package/dist/internal/runtime/napi/napi.cjs +3 -1
- package/dist/internal/runtime/napi/napi.d.cts +114 -1
- package/dist/storage/cache/basic.d.ts +268 -0
- package/dist/storage/cache/basic.js +383 -0
- package/dist/storage/cache/basic.js.map +1 -0
- package/dist/storage/cache/cluster.d.ts +48 -0
- package/dist/storage/cache/cluster.js +40 -0
- package/dist/storage/cache/cluster.js.map +1 -0
- package/dist/storage/cache/errors.d.ts +19 -0
- package/dist/storage/cache/errors.js +59 -0
- package/dist/storage/cache/errors.js.map +1 -0
- package/dist/storage/cache/expiry.d.ts +55 -0
- package/dist/storage/cache/expiry.js +74 -0
- package/dist/storage/cache/expiry.js.map +1 -0
- package/dist/storage/cache/keyspace.d.ts +77 -0
- package/dist/storage/cache/keyspace.js +100 -0
- package/dist/storage/cache/keyspace.js.map +1 -0
- package/dist/storage/cache/list.d.ts +249 -0
- package/dist/storage/cache/list.js +376 -0
- package/dist/storage/cache/list.js.map +1 -0
- package/dist/storage/cache/mod.d.ts +10 -0
- package/dist/storage/cache/mod.js +13 -0
- package/dist/storage/cache/mod.js.map +1 -0
- package/dist/storage/cache/set.d.ts +258 -0
- package/dist/storage/cache/set.js +411 -0
- package/dist/storage/cache/set.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/internal/runtime/napi/napi.cjs +3 -1
- package/internal/runtime/napi/napi.d.cts +114 -1
- package/package.json +6 -1
- package/storage/cache/basic.ts +511 -0
- package/storage/cache/cluster.ts +67 -0
- package/storage/cache/errors.ts +66 -0
- package/storage/cache/expiry.ts +98 -0
- package/storage/cache/keyspace.ts +142 -0
- package/storage/cache/list.ts +496 -0
- package/storage/cache/mod.ts +36 -0
- package/storage/cache/set.ts +491 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as runtime from "../../internal/runtime/mod";
|
|
2
|
+
import { StringLiteral } from "../../internal/utils/constraints";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Redis eviction policy that determines how keys are evicted when memory is full.
|
|
6
|
+
*/
|
|
7
|
+
export type EvictionPolicy =
|
|
8
|
+
| "noeviction"
|
|
9
|
+
| "allkeys-lru"
|
|
10
|
+
| "allkeys-lfu"
|
|
11
|
+
| "allkeys-random"
|
|
12
|
+
| "volatile-lru"
|
|
13
|
+
| "volatile-lfu"
|
|
14
|
+
| "volatile-ttl"
|
|
15
|
+
| "volatile-random";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration options for a cache cluster.
|
|
19
|
+
*/
|
|
20
|
+
export interface CacheClusterConfig {
|
|
21
|
+
/**
|
|
22
|
+
* The eviction policy to use when the cache is full.
|
|
23
|
+
* Defaults to "allkeys-lru".
|
|
24
|
+
*/
|
|
25
|
+
evictionPolicy?: EvictionPolicy;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* CacheCluster represents a Redis cache cluster.
|
|
30
|
+
*
|
|
31
|
+
* Create a new cluster using `new CacheCluster(name)`.
|
|
32
|
+
* Reference an existing cluster using `CacheCluster.named(name)`.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { CacheCluster } from "encore.dev/storage/cache";
|
|
37
|
+
*
|
|
38
|
+
* const myCache = new CacheCluster("my-cache", {
|
|
39
|
+
* evictionPolicy: "allkeys-lru",
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export class CacheCluster {
|
|
44
|
+
/** @internal */
|
|
45
|
+
readonly impl: runtime.CacheCluster;
|
|
46
|
+
/** @internal */
|
|
47
|
+
readonly clusterName: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a new cache cluster with the given name and configuration.
|
|
51
|
+
* @param name - The unique name for this cache cluster
|
|
52
|
+
* @param cfg - Optional configuration for the cluster
|
|
53
|
+
*/
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
55
|
+
constructor(name: string, cfg?: CacheClusterConfig) {
|
|
56
|
+
this.clusterName = name;
|
|
57
|
+
this.impl = runtime.RT.cacheCluster(name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reference an existing cache cluster by name.
|
|
62
|
+
* To create a new cache cluster, use `new CacheCluster(...)` instead.
|
|
63
|
+
*/
|
|
64
|
+
static named<Name extends string>(name: StringLiteral<Name>): CacheCluster {
|
|
65
|
+
return new CacheCluster(name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CacheError is the base class for all cache-related errors.
|
|
3
|
+
*/
|
|
4
|
+
export class CacheError extends Error {
|
|
5
|
+
constructor(msg: string) {
|
|
6
|
+
// extending errors causes issues after you construct them, unless you apply the following fixes
|
|
7
|
+
super(msg);
|
|
8
|
+
|
|
9
|
+
// set error name as constructor name, make it not enumerable to keep native Error behavior
|
|
10
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors
|
|
11
|
+
Object.defineProperty(this, "name", {
|
|
12
|
+
value: "CacheError",
|
|
13
|
+
enumerable: false,
|
|
14
|
+
configurable: true
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Fix the prototype chain, capture stack trace.
|
|
18
|
+
Object.setPrototypeOf(this, CacheError.prototype);
|
|
19
|
+
Error.captureStackTrace(this, this.constructor);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* CacheMiss is thrown when a cache key is not found.
|
|
25
|
+
*/
|
|
26
|
+
export class CacheMiss extends CacheError {
|
|
27
|
+
constructor(key: string) {
|
|
28
|
+
// extending errors causes issues after you construct them, unless you apply the following fixes
|
|
29
|
+
super(`cache key "${key}" not found`);
|
|
30
|
+
|
|
31
|
+
// set error name as constructor name, make it not enumerable to keep native Error behavior
|
|
32
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors
|
|
33
|
+
Object.defineProperty(this, "name", {
|
|
34
|
+
value: "CacheMiss",
|
|
35
|
+
enumerable: false,
|
|
36
|
+
configurable: true
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Fix the prototype chain, capture stack trace.
|
|
40
|
+
Object.setPrototypeOf(this, CacheMiss.prototype);
|
|
41
|
+
Error.captureStackTrace(this, this.constructor);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* CacheKeyExists is thrown when attempting to set a key that already exists
|
|
47
|
+
* using setIfNotExists.
|
|
48
|
+
*/
|
|
49
|
+
export class CacheKeyExists extends CacheError {
|
|
50
|
+
constructor(key: string) {
|
|
51
|
+
// extending errors causes issues after you construct them, unless you apply the following fixes
|
|
52
|
+
super(`cache key "${key}" already exists`);
|
|
53
|
+
|
|
54
|
+
// set error name as constructor name, make it not enumerable to keep native Error behavior
|
|
55
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors
|
|
56
|
+
Object.defineProperty(this, "name", {
|
|
57
|
+
value: "CacheKeyExists",
|
|
58
|
+
enumerable: false,
|
|
59
|
+
configurable: true
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Fix the prototype chain, capture stack trace.
|
|
63
|
+
Object.setPrototypeOf(this, CacheKeyExists.prototype);
|
|
64
|
+
Error.captureStackTrace(this, this.constructor);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expiry represents a cache key expiration configuration.
|
|
3
|
+
* Use the helper functions to create expiry configurations.
|
|
4
|
+
*/
|
|
5
|
+
export type Expiry =
|
|
6
|
+
| { type: "duration"; durationMs: number }
|
|
7
|
+
| { type: "time"; hours: number; minutes: number; seconds: number }
|
|
8
|
+
| "never"
|
|
9
|
+
| "keep-ttl";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* expireIn sets the cache entry to expire after the specified duration.
|
|
13
|
+
* @param ms - Duration in milliseconds
|
|
14
|
+
*/
|
|
15
|
+
export function expireIn(ms: number): Expiry {
|
|
16
|
+
return { type: "duration", durationMs: ms };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* expireInSeconds sets the cache entry to expire after the specified seconds.
|
|
21
|
+
* @param seconds - Duration in seconds
|
|
22
|
+
*/
|
|
23
|
+
export function expireInSeconds(seconds: number): Expiry {
|
|
24
|
+
return { type: "duration", durationMs: seconds * 1000 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* expireInMinutes sets the cache entry to expire after the specified minutes.
|
|
29
|
+
* @param minutes - Duration in minutes
|
|
30
|
+
*/
|
|
31
|
+
export function expireInMinutes(minutes: number): Expiry {
|
|
32
|
+
return { type: "duration", durationMs: minutes * 60 * 1000 };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* expireInHours sets the cache entry to expire after the specified hours.
|
|
37
|
+
* @param hours - Duration in hours
|
|
38
|
+
*/
|
|
39
|
+
export function expireInHours(hours: number): Expiry {
|
|
40
|
+
return { type: "duration", durationMs: hours * 60 * 60 * 1000 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* expireDailyAt sets the cache entry to expire at a specific time each day (UTC).
|
|
45
|
+
* @param hours - Hour (0-23)
|
|
46
|
+
* @param minutes - Minutes (0-59)
|
|
47
|
+
* @param seconds - Seconds (0-59)
|
|
48
|
+
*/
|
|
49
|
+
export function expireDailyAt(
|
|
50
|
+
hours: number,
|
|
51
|
+
minutes: number,
|
|
52
|
+
seconds: number
|
|
53
|
+
): Expiry {
|
|
54
|
+
return { type: "time", hours, minutes, seconds };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* neverExpire sets the cache entry to never expire.
|
|
59
|
+
* Note: Redis may still evict the key based on the eviction policy.
|
|
60
|
+
*/
|
|
61
|
+
export const neverExpire: Expiry = "never";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* keepTTL preserves the existing TTL when updating a cache entry.
|
|
65
|
+
* If the key doesn't exist, no TTL is set.
|
|
66
|
+
*/
|
|
67
|
+
export const keepTTL: Expiry = "keep-ttl";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolves an Expiry to a duration in milliseconds, "never", or "keep-ttl".
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
export function resolveExpiry(expiry: Expiry): number | "never" | "keep-ttl" {
|
|
74
|
+
switch (expiry) {
|
|
75
|
+
case "never":
|
|
76
|
+
return "never";
|
|
77
|
+
case "keep-ttl":
|
|
78
|
+
return "keep-ttl";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
switch (expiry.type) {
|
|
82
|
+
case "duration":
|
|
83
|
+
return expiry.durationMs;
|
|
84
|
+
|
|
85
|
+
case "time": {
|
|
86
|
+
const now = new Date();
|
|
87
|
+
const target = new Date(now);
|
|
88
|
+
target.setUTCHours(expiry.hours, expiry.minutes, expiry.seconds, 0);
|
|
89
|
+
|
|
90
|
+
// If target time has passed today, set for tomorrow
|
|
91
|
+
if (target.getTime() <= now.getTime()) {
|
|
92
|
+
target.setUTCDate(target.getUTCDate() + 1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return target.getTime() - now.getTime();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { getCurrentRequest } from "../../internal/reqtrack/mod";
|
|
2
|
+
import { CacheCluster } from "./cluster";
|
|
3
|
+
import { Expiry, keepTTL, neverExpire, resolveExpiry } from "./expiry";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for a cache keyspace.
|
|
7
|
+
*/
|
|
8
|
+
export interface KeyspaceConfig<K> {
|
|
9
|
+
/**
|
|
10
|
+
* The pattern for generating cache keys.
|
|
11
|
+
* Use `:fieldName` to include a field from the key type.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // For a simple key type (string, number)
|
|
15
|
+
* keyPattern: "user/:id"
|
|
16
|
+
*
|
|
17
|
+
* // For a struct key type
|
|
18
|
+
* keyPattern: "user/:userId/region/:region"
|
|
19
|
+
*/
|
|
20
|
+
keyPattern: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default expiry for cache entries in this keyspace.
|
|
24
|
+
* If not set, entries do not expire.
|
|
25
|
+
*/
|
|
26
|
+
defaultExpiry?: Expiry;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Options for write operations.
|
|
31
|
+
*/
|
|
32
|
+
export interface WriteOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Expiry for this specific write operation.
|
|
35
|
+
* Overrides the keyspace's defaultExpiry.
|
|
36
|
+
*/
|
|
37
|
+
expiry?: Expiry;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Base class for all keyspace types (basic, list, set).
|
|
42
|
+
* Provides key mapping, TTL resolution, with(), and delete().
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
export abstract class Keyspace<K> {
|
|
46
|
+
protected readonly cluster: CacheCluster;
|
|
47
|
+
protected readonly config: KeyspaceConfig<K>;
|
|
48
|
+
protected readonly keyMapper: (key: K) => string;
|
|
49
|
+
private _effectiveExpiry?: Expiry;
|
|
50
|
+
|
|
51
|
+
constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {
|
|
52
|
+
this.cluster = cluster;
|
|
53
|
+
this.config = config;
|
|
54
|
+
this.keyMapper = this.createKeyMapper(config.keyPattern);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a key mapper by parsing the key pattern.
|
|
59
|
+
*/
|
|
60
|
+
private createKeyMapper(pattern: string): (key: K) => string {
|
|
61
|
+
const segments = pattern.split("/").map((seg) => {
|
|
62
|
+
if (seg.startsWith(":")) {
|
|
63
|
+
return { isLiteral: false, value: seg.slice(1), field: seg.slice(1) };
|
|
64
|
+
}
|
|
65
|
+
return { isLiteral: true, value: seg };
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return (key: K) => {
|
|
69
|
+
return segments
|
|
70
|
+
.map((seg) => {
|
|
71
|
+
if (seg.isLiteral) return seg.value;
|
|
72
|
+
|
|
73
|
+
let val: unknown;
|
|
74
|
+
if (typeof key === "object" && key !== null && seg.field) {
|
|
75
|
+
val = (key as Record<string, unknown>)[seg.field];
|
|
76
|
+
} else {
|
|
77
|
+
val = key;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Escape forward slashes in string values
|
|
81
|
+
const str = String(val);
|
|
82
|
+
return str.replace(/\//g, "\\/");
|
|
83
|
+
})
|
|
84
|
+
.join("/");
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Maps a key to its Redis key string.
|
|
90
|
+
*/
|
|
91
|
+
protected mapKey(key: K): string {
|
|
92
|
+
const mapped = this.keyMapper(key);
|
|
93
|
+
if (mapped.startsWith("__encore")) {
|
|
94
|
+
throw new Error('use of reserved key prefix "__encore"');
|
|
95
|
+
}
|
|
96
|
+
return mapped;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolves the TTL for a write operation.
|
|
101
|
+
* Returns i64 sentinel for NAPI: undefined=no config, -1=KeepTTL, -2=Persist/NeverExpire, >=0=ms
|
|
102
|
+
*/
|
|
103
|
+
protected resolveTtl(options?: WriteOptions): number | undefined {
|
|
104
|
+
const expiry =
|
|
105
|
+
options?.expiry ?? this._effectiveExpiry ?? this.config.defaultExpiry;
|
|
106
|
+
if (!expiry) return undefined;
|
|
107
|
+
|
|
108
|
+
const resolved = resolveExpiry(expiry);
|
|
109
|
+
if (resolved === "keep-ttl") return -1; // KeepTTL
|
|
110
|
+
if (resolved === "never") return -2; // NeverExpire → Persist
|
|
111
|
+
return resolved; // milliseconds
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns a shallow clone of this keyspace with the specified write options applied.
|
|
116
|
+
* This allows setting expiry for a chain of operations.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* await myKeyspace.with({ expiry: expireIn(5000) }).set(key, value);
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
with(options: WriteOptions): this {
|
|
124
|
+
const clone = Object.create(Object.getPrototypeOf(this)) as this;
|
|
125
|
+
Object.assign(clone, this);
|
|
126
|
+
(clone as any)._effectiveExpiry = options.expiry ?? this._effectiveExpiry;
|
|
127
|
+
return clone;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Deletes the specified keys.
|
|
132
|
+
* If a key does not exist it is ignored.
|
|
133
|
+
*
|
|
134
|
+
* @returns The number of keys that were deleted.
|
|
135
|
+
* @see https://redis.io/commands/del/
|
|
136
|
+
*/
|
|
137
|
+
async delete(...keys: K[]): Promise<number> {
|
|
138
|
+
const source = getCurrentRequest();
|
|
139
|
+
const mappedKeys = keys.map((k) => this.mapKey(k));
|
|
140
|
+
return await this.cluster.impl.delete(mappedKeys, source);
|
|
141
|
+
}
|
|
142
|
+
}
|