encore.dev 1.54.2 → 1.55.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,496 @@
1
+ import { getCurrentRequest } from "../../internal/reqtrack/mod";
2
+ import { CacheCluster } from "./cluster";
3
+ import { Keyspace, KeyspaceConfig, WriteOptions } from "./keyspace";
4
+
5
+ /**
6
+ * Position in a list (left/head or right/tail).
7
+ */
8
+ export type ListPosition = "left" | "right";
9
+
10
+ /**
11
+ * Base class for list keyspaces with all list operations.
12
+ * Subclasses provide typed serialization/deserialization.
13
+ * @internal
14
+ */
15
+ abstract class ListKeyspace<K, V> extends Keyspace<K> {
16
+ constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {
17
+ super(cluster, config);
18
+ }
19
+
20
+ protected abstract serializeItem(value: V): Buffer;
21
+ protected abstract deserializeItem(data: Buffer): V;
22
+
23
+ /**
24
+ * Pushes one or more values at the head of the list stored at key.
25
+ * If the key does not already exist, it is first created as an empty list.
26
+ *
27
+ * If multiple values are given, they are inserted one after another,
28
+ * starting with the leftmost value. For instance,
29
+ * `pushLeft(key, "a", "b", "c")` will result in a list containing
30
+ * "c" as its first element, "b" as its second, and "a" as its third.
31
+ *
32
+ * @returns The length of the list after the operation.
33
+ * @see https://redis.io/commands/lpush/
34
+ */
35
+ async pushLeft(key: K, ...values: V[]): Promise<number> {
36
+ const source = getCurrentRequest();
37
+ const mappedKey = this.mapKey(key);
38
+ const serialized = values.map((v) => this.serializeItem(v));
39
+ const ttlMs = this.resolveTtl();
40
+ const result = await this.cluster.impl.lpush(
41
+ mappedKey,
42
+ serialized,
43
+ ttlMs,
44
+ source
45
+ );
46
+ return Number(result);
47
+ }
48
+
49
+ /**
50
+ * Pushes one or more values at the tail of the list stored at key.
51
+ * If the key does not already exist, it is first created as an empty list.
52
+ *
53
+ * If multiple values are given, they are inserted one after another,
54
+ * starting with the leftmost value. For instance,
55
+ * `pushRight(key, "a", "b", "c")` will result in a list containing
56
+ * "a" as its first element, "b" as its second, and "c" as its third.
57
+ *
58
+ * @returns The length of the list after the operation.
59
+ * @see https://redis.io/commands/rpush/
60
+ */
61
+ async pushRight(key: K, ...values: V[]): Promise<number> {
62
+ const source = getCurrentRequest();
63
+ const mappedKey = this.mapKey(key);
64
+ const serialized = values.map((v) => this.serializeItem(v));
65
+ const ttlMs = this.resolveTtl();
66
+ const result = await this.cluster.impl.rpush(
67
+ mappedKey,
68
+ serialized,
69
+ ttlMs,
70
+ source
71
+ );
72
+ return Number(result);
73
+ }
74
+
75
+ /**
76
+ * Pops a single element off the head of the list stored at key.
77
+ *
78
+ * @returns The popped value, or `undefined` if the key does not exist.
79
+ * @see https://redis.io/commands/lpop/
80
+ */
81
+ async popLeft(key: K, options?: WriteOptions): Promise<V | undefined> {
82
+ const source = getCurrentRequest();
83
+ const mappedKey = this.mapKey(key);
84
+ const ttlMs = this.resolveTtl(options);
85
+ const result = await this.cluster.impl.lpop(mappedKey, ttlMs, source);
86
+ if (result === null) {
87
+ return undefined;
88
+ }
89
+ return this.deserializeItem(result);
90
+ }
91
+
92
+ /**
93
+ * Pops a single element off the tail of the list stored at key.
94
+ *
95
+ * @returns The popped value, or `undefined` if the key does not exist.
96
+ * @see https://redis.io/commands/rpop/
97
+ */
98
+ async popRight(key: K, options?: WriteOptions): Promise<V | undefined> {
99
+ const source = getCurrentRequest();
100
+ const mappedKey = this.mapKey(key);
101
+ const ttlMs = this.resolveTtl(options);
102
+ const result = await this.cluster.impl.rpop(mappedKey, ttlMs, source);
103
+ if (result === null) {
104
+ return undefined;
105
+ }
106
+ return this.deserializeItem(result);
107
+ }
108
+
109
+ /**
110
+ * Returns the length of the list stored at key.
111
+ *
112
+ * Non-existing keys are considered as empty lists.
113
+ *
114
+ * @returns The list length.
115
+ * @see https://redis.io/commands/llen/
116
+ */
117
+ async len(key: K): Promise<number> {
118
+ const source = getCurrentRequest();
119
+ const mappedKey = this.mapKey(key);
120
+ const result = await this.cluster.impl.llen(mappedKey, source);
121
+ return Number(result);
122
+ }
123
+
124
+ /**
125
+ * Trims the list stored at key to only contain the elements between the indices
126
+ * `start` and `stop` (inclusive). Both are zero-based indices.
127
+ *
128
+ * Negative indices can be used to indicate offsets from the end of the list,
129
+ * where -1 is the last element of the list, -2 the penultimate element, and so on.
130
+ *
131
+ * Out of range indices are valid and are treated as if they specify the start or end of the list,
132
+ * respectively. If `start` > `stop` the end result is an empty list.
133
+ *
134
+ * @param key - The cache key.
135
+ * @param start - Start index (inclusive).
136
+ * @param stop - Stop index (inclusive).
137
+ * @see https://redis.io/commands/ltrim/
138
+ */
139
+ async trim(
140
+ key: K,
141
+ start: number,
142
+ stop: number,
143
+ options?: WriteOptions
144
+ ): Promise<void> {
145
+ const source = getCurrentRequest();
146
+ const mappedKey = this.mapKey(key);
147
+ const ttlMs = this.resolveTtl(options);
148
+ await this.cluster.impl.ltrim(mappedKey, start, stop, ttlMs, source);
149
+ }
150
+
151
+ /**
152
+ * Updates the list element at the given index.
153
+ *
154
+ * Negative indices can be used to indicate offsets from the end of the list,
155
+ * where -1 is the last element of the list, -2 the penultimate element, and so on.
156
+ *
157
+ * @param key - The cache key.
158
+ * @param index - Zero-based index of the element to update.
159
+ * @param value - The new value.
160
+ * @throws {Error} If the index is out of range.
161
+ * @see https://redis.io/commands/lset/
162
+ */
163
+ async set(
164
+ key: K,
165
+ index: number,
166
+ value: V,
167
+ options?: WriteOptions
168
+ ): Promise<void> {
169
+ const source = getCurrentRequest();
170
+ const mappedKey = this.mapKey(key);
171
+ const serialized = this.serializeItem(value);
172
+ const ttlMs = this.resolveTtl(options);
173
+ await this.cluster.impl.lset(mappedKey, index, serialized, ttlMs, source);
174
+ }
175
+
176
+ /**
177
+ * Returns the value of the list element at the given index.
178
+ *
179
+ * Negative indices can be used to indicate offsets from the end of the list,
180
+ * where -1 is the last element of the list, -2 the penultimate element, and so on.
181
+ *
182
+ * @param key - The cache key.
183
+ * @param index - Zero-based index of the element to retrieve.
184
+ * @returns The value at the index, or `undefined` if out of range or the key does not exist.
185
+ * @see https://redis.io/commands/lindex/
186
+ */
187
+ async get(key: K, index: number): Promise<V | undefined> {
188
+ const source = getCurrentRequest();
189
+ const mappedKey = this.mapKey(key);
190
+ const result = await this.cluster.impl.lindex(mappedKey, index, source);
191
+
192
+ if (result === null) {
193
+ return undefined;
194
+ }
195
+
196
+ return this.deserializeItem(result);
197
+ }
198
+
199
+ /**
200
+ * Returns all the elements in the list stored at key.
201
+ *
202
+ * If the key does not exist it returns an empty array.
203
+ *
204
+ * @returns All elements in the list.
205
+ * @see https://redis.io/commands/lrange/
206
+ */
207
+ async items(key: K): Promise<V[]> {
208
+ const source = getCurrentRequest();
209
+ const mappedKey = this.mapKey(key);
210
+ const results = await this.cluster.impl.lrangeAll(mappedKey, source);
211
+ return results.map((r) => this.deserializeItem(r));
212
+ }
213
+
214
+ /**
215
+ * Returns the elements in the list stored at key between `start` and `stop` (inclusive).
216
+ * Both are zero-based indices.
217
+ *
218
+ * Negative indices can be used to indicate offsets from the end of the list,
219
+ * where -1 is the last element of the list, -2 the penultimate element, and so on.
220
+ *
221
+ * If the key does not exist it returns an empty array.
222
+ *
223
+ * @param key - The cache key.
224
+ * @param start - Start index (inclusive).
225
+ * @param stop - Stop index (inclusive).
226
+ * @returns The elements in the specified range.
227
+ * @see https://redis.io/commands/lrange/
228
+ */
229
+ async getRange(key: K, start: number, stop: number): Promise<V[]> {
230
+ const source = getCurrentRequest();
231
+ const mappedKey = this.mapKey(key);
232
+ const results = await this.cluster.impl.lrange(
233
+ mappedKey,
234
+ start,
235
+ stop,
236
+ source
237
+ );
238
+ return results.map((r) => this.deserializeItem(r));
239
+ }
240
+
241
+ /**
242
+ * Inserts `value` into the list stored at key, at the position just before `pivot`.
243
+ *
244
+ * If the list does not contain `pivot`, the value is not inserted and -1 is returned.
245
+ *
246
+ * @param key - The cache key.
247
+ * @param pivot - The existing element to insert before.
248
+ * @param value - The value to insert.
249
+ * @returns The new list length, or -1 if `pivot` was not found.
250
+ * @see https://redis.io/commands/linsert/
251
+ */
252
+ async insertBefore(
253
+ key: K,
254
+ pivot: V,
255
+ value: V,
256
+ options?: WriteOptions
257
+ ): Promise<number> {
258
+ const source = getCurrentRequest();
259
+ const mappedKey = this.mapKey(key);
260
+ const pivotSerialized = this.serializeItem(pivot);
261
+ const valueSerialized = this.serializeItem(value);
262
+ const ttlMs = this.resolveTtl(options);
263
+ const result = await this.cluster.impl.linsertBefore(
264
+ mappedKey,
265
+ pivotSerialized,
266
+ valueSerialized,
267
+ ttlMs,
268
+ source
269
+ );
270
+ return Number(result);
271
+ }
272
+
273
+ /**
274
+ * Inserts `value` into the list stored at key, at the position just after `pivot`.
275
+ *
276
+ * If the list does not contain `pivot`, the value is not inserted and -1 is returned.
277
+ *
278
+ * @param key - The cache key.
279
+ * @param pivot - The existing element to insert after.
280
+ * @param value - The value to insert.
281
+ * @returns The new list length, or -1 if `pivot` was not found.
282
+ * @see https://redis.io/commands/linsert/
283
+ */
284
+ async insertAfter(
285
+ key: K,
286
+ pivot: V,
287
+ value: V,
288
+ options?: WriteOptions
289
+ ): Promise<number> {
290
+ const source = getCurrentRequest();
291
+ const mappedKey = this.mapKey(key);
292
+ const pivotSerialized = this.serializeItem(pivot);
293
+ const valueSerialized = this.serializeItem(value);
294
+ const ttlMs = this.resolveTtl(options);
295
+ const result = await this.cluster.impl.linsertAfter(
296
+ mappedKey,
297
+ pivotSerialized,
298
+ valueSerialized,
299
+ ttlMs,
300
+ source
301
+ );
302
+ return Number(result);
303
+ }
304
+
305
+ /**
306
+ * Removes all occurrences of `value` in the list stored at key.
307
+ *
308
+ * If the list does not contain `value`, or the list does not exist, returns 0.
309
+ *
310
+ * @param key - The cache key.
311
+ * @param value - The value to remove.
312
+ * @returns The number of elements removed.
313
+ * @see https://redis.io/commands/lrem/
314
+ */
315
+ async removeAll(key: K, value: V, options?: WriteOptions): Promise<number> {
316
+ const source = getCurrentRequest();
317
+ const mappedKey = this.mapKey(key);
318
+ const valueSerialized = this.serializeItem(value);
319
+ const ttlMs = this.resolveTtl(options);
320
+ const result = await this.cluster.impl.lremAll(
321
+ mappedKey,
322
+ valueSerialized,
323
+ ttlMs,
324
+ source
325
+ );
326
+ return Number(result);
327
+ }
328
+
329
+ /**
330
+ * Removes the first `count` occurrences of `value` in the list stored at key,
331
+ * scanning from head to tail.
332
+ *
333
+ * If the list does not contain `value`, or the list does not exist, returns 0.
334
+ *
335
+ * @param key - The cache key.
336
+ * @param count - Maximum number of occurrences to remove.
337
+ * @param value - The value to remove.
338
+ * @returns The number of elements removed.
339
+ * @see https://redis.io/commands/lrem/
340
+ */
341
+ async removeFirst(
342
+ key: K,
343
+ count: number,
344
+ value: V,
345
+ options?: WriteOptions
346
+ ): Promise<number> {
347
+ if (count < 0) {
348
+ throw new Error("count must be non-negative");
349
+ }
350
+ const source = getCurrentRequest();
351
+ const mappedKey = this.mapKey(key);
352
+ const valueSerialized = this.serializeItem(value);
353
+ const ttlMs = this.resolveTtl(options);
354
+ const result = await this.cluster.impl.lremFirst(
355
+ mappedKey,
356
+ count,
357
+ valueSerialized,
358
+ ttlMs,
359
+ source
360
+ );
361
+ return Number(result);
362
+ }
363
+
364
+ /**
365
+ * Removes the last `count` occurrences of `value` in the list stored at key,
366
+ * scanning from tail to head.
367
+ *
368
+ * If the list does not contain `value`, or the list does not exist, returns 0.
369
+ *
370
+ * @param key - The cache key.
371
+ * @param count - Maximum number of occurrences to remove.
372
+ * @param value - The value to remove.
373
+ * @returns The number of elements removed.
374
+ * @see https://redis.io/commands/lrem/
375
+ */
376
+ async removeLast(
377
+ key: K,
378
+ count: number,
379
+ value: V,
380
+ options?: WriteOptions
381
+ ): Promise<number> {
382
+ if (count < 0) {
383
+ throw new Error("count must be non-negative");
384
+ }
385
+ const source = getCurrentRequest();
386
+ const mappedKey = this.mapKey(key);
387
+ const valueSerialized = this.serializeItem(value);
388
+ const ttlMs = this.resolveTtl(options);
389
+ // Negative count means remove from tail to head
390
+ const result = await this.cluster.impl.lremLast(
391
+ mappedKey,
392
+ count,
393
+ valueSerialized,
394
+ ttlMs,
395
+ source
396
+ );
397
+ return Number(result);
398
+ }
399
+
400
+ /**
401
+ * Atomically moves an element from the list stored at `src` to the list stored at `dst`.
402
+ *
403
+ * The value moved can be either the head (`srcPos === "left"`) or tail (`srcPos === "right"`)
404
+ * of the list at `src`. Similarly, the value can be placed either at the head (`dstPos === "left"`)
405
+ * or tail (`dstPos === "right"`) of the list at `dst`.
406
+ *
407
+ * If `src` and `dst` are the same list, the value is atomically rotated from one end to the other
408
+ * when `srcPos !== dstPos`, or if `srcPos === dstPos` nothing happens.
409
+ *
410
+ * @param src - Source list key.
411
+ * @param dst - Destination list key.
412
+ * @param srcPos - Position to pop from in the source list.
413
+ * @param dstPos - Position to push to in the destination list.
414
+ * @returns The moved element, or `undefined` if the source list does not exist.
415
+ * @see https://redis.io/commands/lmove/
416
+ */
417
+ async move(
418
+ src: K,
419
+ dst: K,
420
+ srcPos: ListPosition,
421
+ dstPos: ListPosition,
422
+ options?: WriteOptions
423
+ ): Promise<V | undefined> {
424
+ const source = getCurrentRequest();
425
+ const srcKey = this.mapKey(src);
426
+ const dstKey = this.mapKey(dst);
427
+ const ttlMs = this.resolveTtl(options);
428
+ const result = await this.cluster.impl.lmove(
429
+ srcKey,
430
+ dstKey,
431
+ srcPos,
432
+ dstPos,
433
+ ttlMs,
434
+ source
435
+ );
436
+ if (result === null || result === undefined) {
437
+ return undefined;
438
+ }
439
+ return this.deserializeItem(result);
440
+ }
441
+ }
442
+
443
+ /**
444
+ * StringListKeyspace stores lists of string values.
445
+ *
446
+ * @example
447
+ * ```ts
448
+ * const recentViews = new StringListKeyspace<string>(cluster, {
449
+ * keyPattern: "recent-views/:userId",
450
+ * defaultExpiry: ExpireIn(86400000), // 24 hours
451
+ * });
452
+ *
453
+ * await recentViews.pushLeft("user1", "product-123", "product-456");
454
+ * const views = await recentViews.items("user1");
455
+ * ```
456
+ */
457
+ export class StringListKeyspace<K> extends ListKeyspace<K, string> {
458
+ constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {
459
+ super(cluster, config);
460
+ }
461
+
462
+ protected serializeItem(value: string): Buffer {
463
+ return Buffer.from(value, "utf-8");
464
+ }
465
+
466
+ protected deserializeItem(data: Buffer): string {
467
+ return data.toString("utf-8");
468
+ }
469
+ }
470
+
471
+ /**
472
+ * NumberListKeyspace stores lists of numeric values.
473
+ *
474
+ * @example
475
+ * ```ts
476
+ * const scores = new NumberListKeyspace<string>(cluster, {
477
+ * keyPattern: "scores/:gameId",
478
+ * });
479
+ *
480
+ * await scores.pushRight("game1", 100, 200, 300);
481
+ * const allScores = await scores.items("game1");
482
+ * ```
483
+ */
484
+ export class NumberListKeyspace<K> extends ListKeyspace<K, number> {
485
+ constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {
486
+ super(cluster, config);
487
+ }
488
+
489
+ protected serializeItem(value: number): Buffer {
490
+ return Buffer.from(String(value), "utf-8");
491
+ }
492
+
493
+ protected deserializeItem(data: Buffer): number {
494
+ return Number(data.toString("utf-8"));
495
+ }
496
+ }
@@ -0,0 +1,36 @@
1
+ // Cache cluster
2
+ export { CacheCluster } from "./cluster";
3
+ export type { CacheClusterConfig, EvictionPolicy } from "./cluster";
4
+
5
+ // Keyspace configuration
6
+ export type { KeyspaceConfig, WriteOptions } from "./keyspace";
7
+
8
+ // Basic keyspaces
9
+ export {
10
+ StringKeyspace,
11
+ IntKeyspace,
12
+ FloatKeyspace,
13
+ StructKeyspace
14
+ } from "./basic";
15
+
16
+ // List keyspaces
17
+ export { StringListKeyspace, NumberListKeyspace } from "./list";
18
+ export type { ListPosition } from "./list";
19
+
20
+ // Set keyspaces
21
+ export { StringSetKeyspace, NumberSetKeyspace } from "./set";
22
+
23
+ // Expiry utilities
24
+ export {
25
+ expireIn,
26
+ expireInSeconds,
27
+ expireInMinutes,
28
+ expireInHours,
29
+ expireDailyAt,
30
+ neverExpire,
31
+ keepTTL
32
+ } from "./expiry";
33
+ export type { Expiry } from "./expiry";
34
+
35
+ // Error types
36
+ export { CacheError, CacheMiss, CacheKeyExists } from "./errors";