@veryfront/ext-redis 0.1.1185 → 0.1.1189

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 CHANGED
@@ -32,6 +32,18 @@ Core Redis facades fail closed with an actionable install recommendation until
32
32
  extension orchestration registers the provider. Workspace and compiled-binary
33
33
  availability do not bypass that activation boundary.
34
34
 
35
+ ## Redis-backed infrastructure primitives
36
+
37
+ The package also exports bounded Redis implementations for distributed cache,
38
+ render-cache, rate-limit, routing-invalidation, and agent-memory composition.
39
+ These primitives are explicit: constructing one does not activate Redis for
40
+ unrelated framework features, and each owner must close or destroy the
41
+ resource it creates.
42
+
43
+ Durable workflow storage is intentionally not part of this release. It will be
44
+ published after the provider-neutral workflow recovery contracts land, rather
45
+ than coupling this package to unpublished core APIs.
46
+
35
47
  ## Configuration
36
48
 
37
49
  The shared client facade reads `REDIS_URL` when an explicit URL is not passed.
@@ -0,0 +1,12 @@
1
+ declare global {
2
+ interface Object {
3
+ /**
4
+ * Determines whether an object has a property with the specified name.
5
+ * @param o An object.
6
+ * @param v A property name.
7
+ */
8
+ hasOwn(o: object, v: PropertyKey): boolean;
9
+ }
10
+ }
11
+ export {};
12
+ //# sourceMappingURL=_dnt.polyfills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_dnt.polyfills.d.ts","sourceRoot":"","sources":["../src/_dnt.polyfills.ts"],"names":[],"mappings":"AAeA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd;;;;WAIG;QACH,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;KAC5C;CACF;AAED,OAAO,EAAE,CAAC"}
@@ -0,0 +1,15 @@
1
+ // https://github.com/tc39/proposal-accessible-object-hasownproperty/blob/main/polyfill.js
2
+ if (!Object.hasOwn) {
3
+ Object.defineProperty(Object, "hasOwn", {
4
+ value: function (object, property) {
5
+ if (object == null) {
6
+ throw new TypeError("Cannot convert undefined or null to object");
7
+ }
8
+ return Object.prototype.hasOwnProperty.call(Object(object), property);
9
+ },
10
+ configurable: true,
11
+ enumerable: false,
12
+ writable: true,
13
+ });
14
+ }
15
+ export {};
@@ -0,0 +1,11 @@
1
+ import { Deno } from "@deno/shim-deno";
2
+ export { Deno } from "@deno/shim-deno";
3
+ export { crypto, type Crypto, type SubtleCrypto, type AlgorithmIdentifier, type Algorithm, type RsaOaepParams, type BufferSource, type AesCtrParams, type AesCbcParams, type AesGcmParams, type CryptoKey, type KeyAlgorithm, type KeyType, type KeyUsage, type EcdhKeyDeriveParams, type HkdfParams, type HashAlgorithmIdentifier, type Pbkdf2Params, type AesDerivedKeyParams, type HmacImportParams, type JsonWebKey, type RsaOtherPrimesInfo, type KeyFormat, type RsaHashedKeyGenParams, type RsaKeyGenParams, type BigInteger, type EcKeyGenParams, type NamedCurve, type CryptoKeyPair, type AesKeyGenParams, type HmacKeyGenParams, type RsaHashedImportParams, type EcKeyImportParams, type AesKeyAlgorithm, type RsaPssParams, type EcdsaParams } from "@deno/shim-crypto";
4
+ export { setInterval, setTimeout } from "@deno/shim-timers";
5
+ export declare const dntGlobalThis: Omit<typeof globalThis, "Deno" | "crypto" | "setInterval" | "setTimeout"> & {
6
+ Deno: typeof Deno;
7
+ crypto: import("@deno/shim-crypto").Crypto;
8
+ setInterval: any;
9
+ setTimeout: any;
10
+ };
11
+ //# sourceMappingURL=_dnt.shims.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_dnt.shims.d.ts","sourceRoot":"","sources":["../src/_dnt.shims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAEvC,OAAO,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,mBAAmB,EAAE,KAAK,UAAU,EAAE,KAAK,uBAAuB,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,KAAK,kBAAkB,EAAE,KAAK,SAAS,EAAE,KAAK,qBAAqB,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErvB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAQ5D,eAAO,MAAM,aAAa;;;;;CAA2C,CAAC"}
@@ -0,0 +1,68 @@
1
+ import { Deno } from "@deno/shim-deno";
2
+ export { Deno } from "@deno/shim-deno";
3
+ import { crypto } from "@deno/shim-crypto";
4
+ export { crypto } from "@deno/shim-crypto";
5
+ import { setInterval, setTimeout } from "@deno/shim-timers";
6
+ export { setInterval, setTimeout } from "@deno/shim-timers";
7
+ const dntGlobals = {
8
+ Deno,
9
+ crypto,
10
+ setInterval,
11
+ setTimeout,
12
+ };
13
+ export const dntGlobalThis = createMergeProxy(globalThis, dntGlobals);
14
+ function createMergeProxy(baseObj, extObj) {
15
+ return new Proxy(baseObj, {
16
+ get(_target, prop, _receiver) {
17
+ if (prop in extObj) {
18
+ return extObj[prop];
19
+ }
20
+ else {
21
+ return baseObj[prop];
22
+ }
23
+ },
24
+ set(_target, prop, value) {
25
+ if (prop in extObj) {
26
+ delete extObj[prop];
27
+ }
28
+ baseObj[prop] = value;
29
+ return true;
30
+ },
31
+ deleteProperty(_target, prop) {
32
+ let success = false;
33
+ if (prop in extObj) {
34
+ delete extObj[prop];
35
+ success = true;
36
+ }
37
+ if (prop in baseObj) {
38
+ delete baseObj[prop];
39
+ success = true;
40
+ }
41
+ return success;
42
+ },
43
+ ownKeys(_target) {
44
+ const baseKeys = Reflect.ownKeys(baseObj);
45
+ const extKeys = Reflect.ownKeys(extObj);
46
+ const extKeysSet = new Set(extKeys);
47
+ return [...baseKeys.filter((k) => !extKeysSet.has(k)), ...extKeys];
48
+ },
49
+ defineProperty(_target, prop, desc) {
50
+ if (prop in extObj) {
51
+ delete extObj[prop];
52
+ }
53
+ Reflect.defineProperty(baseObj, prop, desc);
54
+ return true;
55
+ },
56
+ getOwnPropertyDescriptor(_target, prop) {
57
+ if (prop in extObj) {
58
+ return Reflect.getOwnPropertyDescriptor(extObj, prop);
59
+ }
60
+ else {
61
+ return Reflect.getOwnPropertyDescriptor(baseObj, prop);
62
+ }
63
+ },
64
+ has(_target, prop) {
65
+ return prop in extObj || prop in baseObj;
66
+ },
67
+ });
68
+ }
@@ -0,0 +1,60 @@
1
+ /****
2
+ * Redis Memory Backend
3
+ *
4
+ * Distributed memory implementation using Redis for multi-process deployments.
5
+ * Enables conversation persistence across server restarts and horizontal scaling.
6
+ */
7
+ import { type Memory, type MemoryConfigBase, type MemoryStats, type MinimalMessage } from "veryfront/extensions/distributed/agent-memory-support";
8
+ /**
9
+ * Redis client interface (compatible with ioredis and node-redis)
10
+ */
11
+ export interface RedisEvalOptions {
12
+ keys: string[];
13
+ arguments: string[];
14
+ }
15
+ export interface RedisClient {
16
+ get(key: string): Promise<string | null>;
17
+ set(key: string, value: string, options?: {
18
+ EX?: number;
19
+ }): Promise<unknown>;
20
+ del(key: string): Promise<number>;
21
+ expire(key: string, seconds: number): Promise<number>;
22
+ eval?(script: string, options: RedisEvalOptions): Promise<unknown>;
23
+ sendCommand?(args: string[]): Promise<unknown>;
24
+ call?(command: string, ...args: string[]): Promise<unknown>;
25
+ }
26
+ /**
27
+ * Redis memory configuration
28
+ */
29
+ export interface RedisMemoryConfig extends MemoryConfigBase {
30
+ type: "redis";
31
+ /** Redis client instance */
32
+ client: RedisClient;
33
+ /** Key prefix for namespacing */
34
+ keyPrefix?: string;
35
+ /** User ID for per-user memory isolation */
36
+ userId?: string;
37
+ /** TTL in seconds (default: 24 hours) */
38
+ ttl?: number;
39
+ }
40
+ /** Implement redis memory. */
41
+ export declare class RedisMemory<M extends MinimalMessage = MinimalMessage> implements Memory<M> {
42
+ private client;
43
+ private agentId;
44
+ private userId;
45
+ private keyPrefix;
46
+ private ttl;
47
+ private config;
48
+ constructor(agentId: string, config: RedisMemoryConfig);
49
+ private getKey;
50
+ private parseMessages;
51
+ add(message: M): Promise<void>;
52
+ getMessages(): Promise<M[]>;
53
+ clear(): Promise<void>;
54
+ getStats(): Promise<MemoryStats>;
55
+ touch(): Promise<void>;
56
+ private atomicAdd;
57
+ }
58
+ /** Create redis memory. */
59
+ export declare function createRedisMemory<M extends MinimalMessage = MinimalMessage>(agentId: string, config: RedisMemoryConfig): RedisMemory<M>;
60
+ //# sourceMappingURL=agent-memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-memory.d.ts","sourceRoot":"","sources":["../src/agent-memory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,uDAAuD,CAAC;AAI/D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE,WAAW,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7D;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,IAAI,EAAE,OAAO,CAAC;IACd,4BAA4B;IAC5B,MAAM,EAAE,WAAW,CAAC;IACpB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AA4ED,8BAA8B;AAC9B,qBAAa,WAAW,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,CAAE,YAAW,MAAM,CAAC,CAAC,CAAC;IACtF,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,MAAM,CAAoB;gBAEtB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB;IAStD,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,aAAa;IAgBrB,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAW9B,WAAW,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC;IAQ3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAUtB,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC;IAehC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtB,OAAO,CAAC,SAAS;CA2BlB;AAED,2BAA2B;AAC3B,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EACzE,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,iBAAiB,GACxB,WAAW,CAAC,CAAC,CAAC,CAEhB"}
@@ -0,0 +1,173 @@
1
+ /****
2
+ * Redis Memory Backend
3
+ *
4
+ * Distributed memory implementation using Redis for multi-process deployments.
5
+ * Enables conversation persistence across server restarts and horizontal scaling.
6
+ */
7
+ import { estimateTokens, } from "veryfront/extensions/distributed/agent-memory-support";
8
+ import { withSpan } from "veryfront/observability/otlp-setup";
9
+ import { agentLogger } from "veryfront/utils/logger";
10
+ const DEFAULT_TTL = 86_400; // 24 hours
11
+ const DEFAULT_KEY_PREFIX = "veryfront:agent:memory:";
12
+ const ATOMIC_ADD_SCRIPT = `
13
+ local key = KEYS[1]
14
+ local message_json = ARGV[1]
15
+ local max_messages = tonumber(ARGV[2]) or 0
16
+ local max_tokens = tonumber(ARGV[3]) or 0
17
+ local ttl = tonumber(ARGV[4]) or 0
18
+
19
+ local function decode_json(value, label)
20
+ local ok, decoded = pcall(cjson.decode, value)
21
+ if not ok then
22
+ return nil, redis.error_reply("CORRUPT_REDIS_MEMORY_JSON:" .. label)
23
+ end
24
+ if type(decoded) ~= "table" then
25
+ return nil, redis.error_reply("CORRUPT_REDIS_MEMORY_JSON:" .. label)
26
+ end
27
+ return decoded, nil
28
+ end
29
+
30
+ local messages = {}
31
+ local current = redis.call("GET", key)
32
+ if current and current ~= "" then
33
+ local decoded, decode_error = decode_json(current, "stored")
34
+ if decode_error then return decode_error end
35
+ messages = decoded
36
+ end
37
+
38
+ local message, message_error = decode_json(message_json, "message")
39
+ if message_error then return message_error end
40
+ table.insert(messages, message)
41
+
42
+ if max_messages > 0 then
43
+ while #messages > max_messages do
44
+ table.remove(messages, 1)
45
+ end
46
+ end
47
+
48
+ local function message_text_length(item)
49
+ if type(item) ~= "table" or type(item.parts) ~= "table" then return 0 end
50
+ local total = 0
51
+ for _, part in ipairs(item.parts) do
52
+ if type(part) == "table" and part.type == "text" and type(part.text) == "string" then
53
+ total = total + string.len(part.text)
54
+ end
55
+ end
56
+ return total
57
+ end
58
+
59
+ local function estimate_tokens(items)
60
+ local total = 0
61
+ for _, item in ipairs(items) do
62
+ total = total + message_text_length(item)
63
+ end
64
+ return math.ceil(total / 4)
65
+ end
66
+
67
+ if max_tokens > 0 then
68
+ while #messages > 1 and estimate_tokens(messages) > max_tokens do
69
+ table.remove(messages, 1)
70
+ end
71
+ end
72
+
73
+ local encoded = cjson.encode(messages)
74
+ if ttl > 0 then
75
+ redis.call("SET", key, encoded, "EX", ttl)
76
+ else
77
+ redis.call("SET", key, encoded)
78
+ end
79
+
80
+ return #messages
81
+ `;
82
+ /** Implement redis memory. */
83
+ export class RedisMemory {
84
+ client;
85
+ agentId;
86
+ userId;
87
+ keyPrefix;
88
+ ttl;
89
+ config;
90
+ constructor(agentId, config) {
91
+ this.client = config.client;
92
+ this.agentId = agentId;
93
+ this.userId = config.userId ?? "anonymous";
94
+ this.keyPrefix = config.keyPrefix ?? DEFAULT_KEY_PREFIX;
95
+ this.ttl = config.ttl ?? DEFAULT_TTL;
96
+ this.config = config;
97
+ }
98
+ getKey() {
99
+ return `${this.keyPrefix}${this.agentId.length}:${this.agentId}:${this.userId.length}:${this.userId}`;
100
+ }
101
+ parseMessages(data) {
102
+ if (!data)
103
+ return [];
104
+ try {
105
+ return JSON.parse(data);
106
+ }
107
+ catch (error) {
108
+ // Do NOT swallow-and-return []: add() would then overwrite the key with a
109
+ // single message and permanently destroy the stored history. Surface the
110
+ // corruption so the caller aborts instead of silently truncating.
111
+ agentLogger.error("Corrupted JSON in Redis memory; refusing to overwrite", {
112
+ errorName: error instanceof Error ? error.name : typeof error,
113
+ keyLength: this.getKey().length,
114
+ });
115
+ throw error;
116
+ }
117
+ }
118
+ add(message) {
119
+ return withSpan("agent.memory.redis.add", async () => {
120
+ const key = this.getKey();
121
+ await this.atomicAdd(key, message);
122
+ }, { "memory.type": "redis", "memory.agent_id": this.agentId, "memory.ttl": this.ttl });
123
+ }
124
+ getMessages() {
125
+ return withSpan("agent.memory.redis.getMessages", async () => this.parseMessages(await this.client.get(this.getKey())), { "memory.type": "redis", "memory.agent_id": this.agentId });
126
+ }
127
+ clear() {
128
+ return withSpan("agent.memory.redis.clear", async () => {
129
+ await this.client.del(this.getKey());
130
+ }, { "memory.type": "redis", "memory.agent_id": this.agentId });
131
+ }
132
+ getStats() {
133
+ return withSpan("agent.memory.redis.getStats", async () => {
134
+ const messages = await this.getMessages();
135
+ return {
136
+ totalMessages: messages.length,
137
+ estimatedTokens: estimateTokens(messages),
138
+ type: "redis",
139
+ };
140
+ }, { "memory.type": "redis", "memory.agent_id": this.agentId });
141
+ }
142
+ touch() {
143
+ return withSpan("agent.memory.redis.touch", async () => {
144
+ // TTL <= 0 means no expiration, so touch is a no-op
145
+ if (this.ttl <= 0)
146
+ return;
147
+ await this.client.expire(this.getKey(), this.ttl);
148
+ }, { "memory.type": "redis", "memory.agent_id": this.agentId, "memory.ttl": this.ttl });
149
+ }
150
+ atomicAdd(key, message) {
151
+ const args = [
152
+ JSON.stringify(message),
153
+ String(this.config.maxMessages ?? 0),
154
+ String(this.config.maxTokens ?? 0),
155
+ String(this.ttl),
156
+ ];
157
+ if (typeof this.client.sendCommand === "function") {
158
+ return this.client.sendCommand(["EVAL", ATOMIC_ADD_SCRIPT, "1", key, ...args]).then(() => { });
159
+ }
160
+ if (typeof this.client.call === "function") {
161
+ return this.client.call("EVAL", ATOMIC_ADD_SCRIPT, "1", key, ...args).then(() => { });
162
+ }
163
+ if (typeof this.client.eval === "function") {
164
+ return this.client.eval(ATOMIC_ADD_SCRIPT, { keys: [key], arguments: args }).then(() => { });
165
+ }
166
+ throw new Error("RedisMemory requires Redis sendCommand(), call(), or eval() for atomic add(); " +
167
+ "non-atomic GET/SET append is not supported");
168
+ }
169
+ }
170
+ /** Create redis memory. */
171
+ export function createRedisMemory(agentId, config) {
172
+ return new RedisMemory(agentId, config);
173
+ }
@@ -0,0 +1,5 @@
1
+ import type { DistributedCacheAdministration } from "veryfront/extensions/distributed/cache-support";
2
+ import type { RedisClientManager, RedisClientOptions } from "./redis-client-manager.js";
3
+ /** Keep Redis cursor and command semantics behind the provider contract. */
4
+ export declare function createRedisCacheAdministration(manager: RedisClientManager, clientOptions: RedisClientOptions): DistributedCacheAdministration;
5
+ //# sourceMappingURL=cache-administration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-administration.d.ts","sourceRoot":"","sources":["../src/cache-administration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAG/B,MAAM,gDAAgD,CAAC;AACxD,OAAO,KAAK,EAEV,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,2BAA2B,CAAC;AAiCnC,4EAA4E;AAC5E,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,kBAAkB,GAChC,8BAA8B,CAmGhC"}
@@ -0,0 +1,113 @@
1
+ import { isRedisAtomicCounterKey, REDIS_LOGICAL_DELETE_SCRIPT } from "./cache-backend.js";
2
+ import { parseRevisionedCacheRecord } from "./revisioned-cache-record.js";
3
+ import { REVISIONED_CACHE_KEY_PREFIX, stripOwnedDistributedCacheKeyPrefix, } from "veryfront/extensions/distributed/cache-support";
4
+ const SCAN_COUNT = 100;
5
+ const DELETE_BATCH_SIZE = 1_000;
6
+ const MAX_SCAN_ITERATIONS = 10_000;
7
+ const MAX_SCANNED_KEYS = 100_000;
8
+ function escapeRedisGlobLiteral(value) {
9
+ return value.replace(/[\\*?\[\]]/g, "\\$&");
10
+ }
11
+ function requireScanResult(value, prefix) {
12
+ if (!value ||
13
+ !Number.isSafeInteger(value.cursor) ||
14
+ value.cursor < 0 ||
15
+ !Array.isArray(value.keys) ||
16
+ !value.keys.every((key) => typeof key === "string" && key.startsWith(prefix))) {
17
+ throw new TypeError("Redis returned an invalid cache-administration SCAN result");
18
+ }
19
+ return value;
20
+ }
21
+ /** Keep Redis cursor and command semantics behind the provider contract. */
22
+ export function createRedisCacheAdministration(manager, clientOptions) {
23
+ const connection = Object.freeze({ ...clientOptions });
24
+ return Object.freeze({
25
+ isConfigured() {
26
+ return manager.isConfigured(connection);
27
+ },
28
+ async listKeys({ prefix, limit }) {
29
+ const client = await manager.getClient(connection);
30
+ const keys = new Set();
31
+ const seenPhysicalKeys = new Set();
32
+ const seenCursors = new Set();
33
+ let cursor = 0;
34
+ let iterations = 0;
35
+ let scanned = 0;
36
+ do {
37
+ if (iterations++ >= MAX_SCAN_ITERATIONS) {
38
+ throw new RangeError("Redis cache administration exceeded the SCAN iteration limit");
39
+ }
40
+ if (seenCursors.has(cursor)) {
41
+ throw new Error("Redis cache administration repeated a SCAN cursor");
42
+ }
43
+ seenCursors.add(cursor);
44
+ const page = requireScanResult(await client.scan(cursor, {
45
+ MATCH: `${escapeRedisGlobLiteral(prefix)}*`,
46
+ COUNT: SCAN_COUNT,
47
+ }), prefix);
48
+ cursor = page.cursor;
49
+ scanned += page.keys.length;
50
+ if (scanned > MAX_SCANNED_KEYS) {
51
+ throw new RangeError("Redis cache administration exceeded the key traversal limit");
52
+ }
53
+ const candidates = page.keys.filter((key) => {
54
+ if (seenPhysicalKeys.has(key))
55
+ return false;
56
+ seenPhysicalKeys.add(key);
57
+ return true;
58
+ });
59
+ if (candidates.length === 0)
60
+ continue;
61
+ const values = await client.mGet([...candidates]);
62
+ if (!Array.isArray(values) ||
63
+ values.length !== candidates.length ||
64
+ !values.every((value) => value === null || typeof value === "string")) {
65
+ throw new TypeError("Redis returned an invalid cache-administration MGET result");
66
+ }
67
+ for (let index = 0; index < candidates.length; index++) {
68
+ const key = candidates[index];
69
+ const value = values[index];
70
+ if (value === null)
71
+ continue;
72
+ if (stripOwnedDistributedCacheKeyPrefix(key).startsWith(REVISIONED_CACHE_KEY_PREFIX)) {
73
+ const record = parseRevisionedCacheRecord(value);
74
+ if (record.kind === "absent")
75
+ continue;
76
+ }
77
+ if (keys.size === limit) {
78
+ return Object.freeze({ keys: Object.freeze([...keys]), truncated: true });
79
+ }
80
+ keys.add(key);
81
+ }
82
+ } while (cursor !== 0);
83
+ return Object.freeze({ keys: Object.freeze([...keys]), truncated: false });
84
+ },
85
+ async deleteKeys(keys) {
86
+ if (keys.length === 0)
87
+ return 0;
88
+ if (keys.some(isRedisAtomicCounterKey)) {
89
+ throw new TypeError("Redis atomic counters are protected from cache administration");
90
+ }
91
+ const client = await manager.getClient(connection);
92
+ let deleted = 0;
93
+ for (let index = 0; index < keys.length; index += DELETE_BATCH_SIZE) {
94
+ const batch = keys.slice(index, index + DELETE_BATCH_SIZE);
95
+ const classifications = batch.map((key) => stripOwnedDistributedCacheKeyPrefix(key).startsWith(REVISIONED_CACHE_KEY_PREFIX)
96
+ ? "1"
97
+ : "0").join("");
98
+ const count = await client.eval(REDIS_LOGICAL_DELETE_SCRIPT, {
99
+ keys: batch,
100
+ arguments: [classifications, "vf-logical-delete-v1"],
101
+ });
102
+ if (typeof count !== "number" ||
103
+ !Number.isSafeInteger(count) ||
104
+ count < 0 ||
105
+ count > batch.length) {
106
+ throw new TypeError("Redis returned an invalid cache-administration DEL count");
107
+ }
108
+ deleted += count;
109
+ }
110
+ return deleted;
111
+ },
112
+ });
113
+ }
@@ -0,0 +1,46 @@
1
+ import { isRedisConfigured, type RedisClientManager, type RedisClientOptions } from "./redis-client-manager.js";
2
+ import { type CacheBackend, type RevisionedCacheBackend } from "veryfront/extensions/distributed/cache-support";
3
+ export declare const REDIS_LOGICAL_DELETE_SCRIPT: string;
4
+ /** Test whether a physical Redis key is the protected namespace counter. */
5
+ export declare function isRedisAtomicCounterKey(key: unknown): key is string;
6
+ export interface RedisCacheBackendOptions {
7
+ clientManager?: RedisClientManager;
8
+ clientOptions?: RedisClientOptions;
9
+ }
10
+ export { isRedisConfigured };
11
+ export declare class RedisCacheBackend implements CacheBackend {
12
+ readonly type: "distributed";
13
+ getWithRevision?: RevisionedCacheBackend["getWithRevision"];
14
+ compareExchange?: RevisionedCacheBackend["compareExchange"];
15
+ private readonly keyPrefix;
16
+ private readonly atomicCounterKey;
17
+ private readonly clientManager;
18
+ private readonly clientOptions;
19
+ constructor(keyPrefix?: string, options?: RedisCacheBackendOptions);
20
+ private prefixKey;
21
+ private resetAfterFailure;
22
+ private clearRevisionCapability;
23
+ private publishRevisionCapability;
24
+ private probeRevisionCapability;
25
+ private getClientForRead;
26
+ private requireClient;
27
+ initialize(): Promise<boolean>;
28
+ get(key: string): Promise<string | null>;
29
+ getWithinLimit(key: string, maximumBytes: number): Promise<string | null>;
30
+ private decodeOrdinaryRead;
31
+ getRemainingTtlSeconds(key: string): Promise<number | null>;
32
+ getBatch(keys: string[]): Promise<Map<string, string | null>>;
33
+ set(key: string, value: string, ttlSeconds?: any): Promise<void>;
34
+ setBatch(entries: Array<{
35
+ key: string;
36
+ value: string;
37
+ ttl?: number;
38
+ }>): Promise<void>;
39
+ del(key: string): Promise<void>;
40
+ delByPattern(pattern: string): Promise<number>;
41
+ private assertDeleteCount;
42
+ private requireRevisionedKey;
43
+ private readWithRevision;
44
+ private exchangeRevision;
45
+ }
46
+ //# sourceMappingURL=cache-backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-backend.d.ts","sourceRoot":"","sources":["../src/cache-backend.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,iBAAiB,EAEjB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAKL,KAAK,YAAY,EAUjB,KAAK,sBAAsB,EAE5B,MAAM,gDAAgD,CAAC;AAqNxD,eAAO,MAAM,2BAA2B,QAyCvC,CAAC;AAyIF,4EAA4E;AAC5E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,MAAM,CAEnE;AAQD,MAAM,WAAW,wBAAwB;IACvC,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC;AAGD,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAE7B,qBAAa,iBAAkB,YAAW,YAAY;IACpD,QAAQ,CAAC,IAAI,EAAG,aAAa,CAAU;IAC/B,eAAe,CAAC,EAAE,sBAAsB,CAAC,iBAAiB,CAAC,CAAC;IAC5D,eAAe,CAAC,EAAE,sBAAsB,CAAC,iBAAiB,CAAC,CAAC;IACpE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;gBAEvC,SAAS,SAAsB,EAAE,OAAO,GAAE,wBAA6B;IAOnF,OAAO,CAAC,SAAS;YAIH,iBAAiB;IAS/B,OAAO,CAAC,uBAAuB;IAK/B,OAAO,CAAC,yBAAyB;YAiBnB,uBAAuB;YAyBvB,gBAAgB;YAYhB,aAAa;IAO3B,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IA0BxB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiBxC,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAkC/E,OAAO,CAAC,kBAAkB;IASpB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiB3D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IAsC7D,GAAG,CACP,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,UAAU,MAA4B,GACrC,OAAO,CAAC,IAAI,CAAC;IA0BV,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BrF,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe/B,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiEpD,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,oBAAoB;YAOd,gBAAgB;YAehB,gBAAgB;CA4B/B"}