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.
Files changed (41) hide show
  1. package/config/secrets.ts +7 -1
  2. package/dist/config/secrets.js +4 -0
  3. package/dist/config/secrets.js.map +1 -1
  4. package/dist/internal/runtime/napi/napi.cjs +3 -1
  5. package/dist/internal/runtime/napi/napi.d.cts +114 -1
  6. package/dist/storage/cache/basic.d.ts +268 -0
  7. package/dist/storage/cache/basic.js +383 -0
  8. package/dist/storage/cache/basic.js.map +1 -0
  9. package/dist/storage/cache/cluster.d.ts +48 -0
  10. package/dist/storage/cache/cluster.js +40 -0
  11. package/dist/storage/cache/cluster.js.map +1 -0
  12. package/dist/storage/cache/errors.d.ts +19 -0
  13. package/dist/storage/cache/errors.js +59 -0
  14. package/dist/storage/cache/errors.js.map +1 -0
  15. package/dist/storage/cache/expiry.d.ts +55 -0
  16. package/dist/storage/cache/expiry.js +74 -0
  17. package/dist/storage/cache/expiry.js.map +1 -0
  18. package/dist/storage/cache/keyspace.d.ts +77 -0
  19. package/dist/storage/cache/keyspace.js +100 -0
  20. package/dist/storage/cache/keyspace.js.map +1 -0
  21. package/dist/storage/cache/list.d.ts +249 -0
  22. package/dist/storage/cache/list.js +376 -0
  23. package/dist/storage/cache/list.js.map +1 -0
  24. package/dist/storage/cache/mod.d.ts +10 -0
  25. package/dist/storage/cache/mod.js +13 -0
  26. package/dist/storage/cache/mod.js.map +1 -0
  27. package/dist/storage/cache/set.d.ts +258 -0
  28. package/dist/storage/cache/set.js +411 -0
  29. package/dist/storage/cache/set.js.map +1 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/internal/runtime/napi/napi.cjs +3 -1
  32. package/internal/runtime/napi/napi.d.cts +114 -1
  33. package/package.json +6 -1
  34. package/storage/cache/basic.ts +511 -0
  35. package/storage/cache/cluster.ts +67 -0
  36. package/storage/cache/errors.ts +66 -0
  37. package/storage/cache/expiry.ts +98 -0
  38. package/storage/cache/keyspace.ts +142 -0
  39. package/storage/cache/list.ts +496 -0
  40. package/storage/cache/mod.ts +36 -0
  41. 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
+ }