@prisma-next/middleware-cache 0.9.0-dev.6

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.
@@ -0,0 +1,139 @@
1
+ /**
2
+ * A cached set of rows produced by a single execution.
3
+ *
4
+ * - `rows` are stored raw (undecoded). The SQL runtime's `decodeRow` pass
5
+ * wraps the orchestrator output, so intercepted rows go through the
6
+ * same codec decoding as driver rows on the way to the consumer. The
7
+ * cache stores wire-format values; decoding happens once per consumer
8
+ * read regardless of where the rows came from.
9
+ * - `storedAt` is the clock value at the moment the entry was committed
10
+ * to the store. It is informational metadata for callers (debugging,
11
+ * telemetry) and is **not** used by the in-memory store itself for
12
+ * expiry — TTL is driven by the store's own clock plus the `ttlMs`
13
+ * passed to `set`. Custom stores may use it differently.
14
+ */
15
+ export interface CachedEntry {
16
+ readonly rows: readonly Record<string, unknown>[];
17
+ readonly storedAt: number;
18
+ }
19
+
20
+ /**
21
+ * Pluggable cache backend used by the cache middleware.
22
+ *
23
+ * The default implementation is an in-memory LRU with TTL produced by
24
+ * `createInMemoryCacheStore`. Users can supply Redis, Memcached, or any
25
+ * other backend by implementing this interface.
26
+ *
27
+ * The interface is intentionally minimal:
28
+ *
29
+ * - `get` returns the entry if it exists and has not expired, or
30
+ * `undefined` otherwise. Implementations that gate on TTL should
31
+ * treat an expired entry as absent (return `undefined`) and may
32
+ * evict it as a side effect.
33
+ * - `set` writes the entry under the key with an associated TTL in
34
+ * milliseconds. Implementations may evict other entries to make
35
+ * room (LRU, LFU, etc.) and may treat the operation as fire-and-
36
+ * forget at scale; the cache middleware does not rely on `set`
37
+ * completing before subsequent `get`s.
38
+ *
39
+ * Both methods are async to leave the door open for I/O-backed stores
40
+ * (Redis, S3, etc.). The default in-memory store completes
41
+ * synchronously and wraps the result in `Promise.resolve` for type
42
+ * conformance.
43
+ */
44
+ export interface CacheStore {
45
+ get(key: string): Promise<CachedEntry | undefined>;
46
+ set(key: string, entry: CachedEntry, ttlMs: number): Promise<void>;
47
+ }
48
+
49
+ /**
50
+ * Options accepted by `createInMemoryCacheStore`.
51
+ *
52
+ * - `maxEntries` — hard cap on the number of live entries. Once the cap
53
+ * is exceeded, the least recently used entry is evicted. Reads and
54
+ * writes both count as "uses" for ordering purposes.
55
+ * - `clock` — injectable time source for TTL math. Defaults to
56
+ * `Date.now`. Tests inject a controlled clock to verify expiry without
57
+ * real-time waits.
58
+ */
59
+ export interface InMemoryCacheStoreOptions {
60
+ readonly maxEntries: number;
61
+ readonly clock?: () => number;
62
+ }
63
+
64
+ interface StoredRecord {
65
+ readonly entry: CachedEntry;
66
+ readonly expiresAt: number;
67
+ }
68
+
69
+ /**
70
+ * Default cache backend. An LRU with per-entry TTL, backed by a `Map`.
71
+ *
72
+ * Eviction policy:
73
+ *
74
+ * - On `set` of a fresh key whose insertion would push the live count
75
+ * above `maxEntries`, the least recently used entry is evicted.
76
+ * Setting an existing key updates the entry in place and refreshes its
77
+ * recency without changing the live count.
78
+ * - On `get` of an existing key, recency is bumped (so the entry is no
79
+ * longer the LRU candidate).
80
+ * - On `get` of an expired entry, the entry is removed from the map and
81
+ * `undefined` is returned. The slot becomes available for new writes
82
+ * without counting against `maxEntries`.
83
+ *
84
+ * `Map` insertion order is the LRU order: the first key is the LRU
85
+ * candidate; the last key is the most recently used. Bumping recency is
86
+ * a delete-then-set on the underlying map.
87
+ *
88
+ * The default store is **not** coherent across processes or replicas —
89
+ * each process holds its own Map. Users who need a shared cache supply
90
+ * their own `CacheStore` (Redis, Memcached, etc.).
91
+ */
92
+ export function createInMemoryCacheStore(options: InMemoryCacheStoreOptions): CacheStore {
93
+ const maxEntries = options.maxEntries;
94
+ const clock = options.clock ?? Date.now;
95
+ const map = new Map<string, StoredRecord>();
96
+
97
+ function get(key: string): Promise<CachedEntry | undefined> {
98
+ const record = map.get(key);
99
+ if (record === undefined) {
100
+ return Promise.resolve(undefined);
101
+ }
102
+ if (clock() >= record.expiresAt) {
103
+ map.delete(key);
104
+ return Promise.resolve(undefined);
105
+ }
106
+ // Bump recency: re-insert at the end of the iteration order.
107
+ map.delete(key);
108
+ map.set(key, record);
109
+ return Promise.resolve(record.entry);
110
+ }
111
+
112
+ function set(key: string, entry: CachedEntry, ttlMs: number): Promise<void> {
113
+ const expiresAt = clock() + ttlMs;
114
+ // Re-set semantics: if the key is already present, deleting first
115
+ // ensures the new value lands at the end of the iteration order
116
+ // (most recently used) rather than retaining the old slot's
117
+ // position. This matters for LRU correctness when the same key is
118
+ // re-cached after a refresh.
119
+ if (map.has(key)) {
120
+ map.delete(key);
121
+ }
122
+ map.set(key, { entry, expiresAt });
123
+
124
+ // Evict LRU entries until the live count is within bounds. The
125
+ // iterator yields keys in insertion order; the first one is the
126
+ // oldest (LRU).
127
+ while (map.size > maxEntries) {
128
+ const oldest = map.keys().next();
129
+ if (oldest.done) {
130
+ break;
131
+ }
132
+ map.delete(oldest.value);
133
+ }
134
+
135
+ return Promise.resolve();
136
+ }
137
+
138
+ return { get, set };
139
+ }
@@ -0,0 +1,6 @@
1
+ export type { CachePayload } from '../cache-annotation';
2
+ export { cacheAnnotation } from '../cache-annotation';
3
+ export type { CacheMiddlewareOptions } from '../cache-middleware';
4
+ export { createCacheMiddleware } from '../cache-middleware';
5
+ export type { CachedEntry, CacheStore, InMemoryCacheStoreOptions } from '../cache-store';
6
+ export { createInMemoryCacheStore } from '../cache-store';