@zintrust/cache-redis 2.1.3 → 2.4.1

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,19 @@ Then set your cache driver config (see docs for the full set of env vars):
32
32
  CACHE_DRIVER=redis
33
33
  ```
34
34
 
35
+ ## Cloudflare Workers
36
+
37
+ When the app runs in Cloudflare Workers or another runtime without Redis TCP access, use Redis RPC:
38
+
39
+ ```bash
40
+ npm i @zintrust/redis-rpc
41
+
42
+ USE_REDIS_PROXY=true
43
+ REDIS_RPC_URL=https://queues.example.com
44
+ ```
45
+
46
+ Run the backend with `zin redis-rpc` or `zin s redis-rpc`. The Redis cache driver will send `get`, `set`, `del`, `exists`, and `flushdb` through the core Redis RPC transport. The older Redis HTTP proxy remains available for simple command forwarding through `REDIS_PROXY_URL`.
47
+
35
48
  ## Docs
36
49
 
37
50
  - https://zintrust.com/cache
@@ -2,8 +2,8 @@ import { Env } from '@zintrust/core/config';
2
2
  import { ErrorFactory } from '@zintrust/core/errors';
3
3
  import { createRedisConnection } from '@zintrust/core/redis';
4
4
  const createProxyClient = () => {
5
- if (Env.REDIS_PROXY_URL.trim() === '' && Env.USE_REDIS_PROXY !== true) {
6
- throw ErrorFactory.createConfigError('Redis proxy URL is missing (REDIS_PROXY_URL)');
5
+ if (Env.USE_REDIS_PROXY !== true) {
6
+ throw ErrorFactory.createConfigError('Redis proxy transport requires USE_REDIS_PROXY=true. Add REDIS_RPC_URL for Redis RPC or REDIS_PROXY_URL/REDIS_PROXY_HOST for the legacy Redis HTTP proxy.');
7
7
  }
8
8
  return createRedisConnection({
9
9
  host: Env.get('REDIS_HOST', 'localhost'),
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@zintrust/cache-redis",
3
- "version": "2.1.3",
4
- "buildDate": "2026-05-27T04:03:00.356Z",
3
+ "version": "2.4.1",
4
+ "buildDate": "2026-05-31T11:36:02.431Z",
5
5
  "buildEnvironment": {
6
6
  "node": "v22.22.1",
7
7
  "platform": "darwin",
8
8
  "arch": "arm64"
9
9
  },
10
10
  "git": {
11
- "commit": "c82f6263",
11
+ "commit": "e97b7b3d",
12
12
  "branch": "release"
13
13
  },
14
14
  "package": {
@@ -28,28 +28,20 @@
28
28
  "sha256": "91df2fbb1916b073c3bafa1dc253ab5ee8998cc924c3cd52308c09798c1d829f"
29
29
  },
30
30
  "RedisProxyAdapter.js": {
31
- "size": 1779,
32
- "sha256": "cb3a74a77feb37753c01317258ee94567d1ad20390239fa120a2c5a28d13abdd"
33
- },
34
- "RedisWorkersDurableObjectAdapter.d.ts": {
35
- "size": 193,
36
- "sha256": "859d3177d302c65f93add57216eae91d8c45874801bb75bdea3d6f6b00fb00c3"
37
- },
38
- "RedisWorkersDurableObjectAdapter.js": {
39
- "size": 4482,
40
- "sha256": "5b0ae2883872ce974ecaf01f07d35b72e6da468439912a57734ccd5222705bfd"
31
+ "size": 1851,
32
+ "sha256": "6cbca2f311ac0573e63a1cd60e7bfe8b3eaaedcf23696ec170b234b2fd56f5a9"
41
33
  },
42
34
  "build-manifest.json": {
43
- "size": 1701,
44
- "sha256": "d60387ced9538e8ff6b054357632554f2572823d1e21c49794fff8bf1803b243"
35
+ "size": 1251,
36
+ "sha256": "6feb4171104bdeedf319a588a822a62e56c1db73f61d4ef5238ad48c828778c3"
45
37
  },
46
38
  "index.d.ts": {
47
- "size": 842,
48
- "sha256": "b47614dfdb736524165785958f4a685818c055315961c2e2b48d99ef6ca491e2"
39
+ "size": 1000,
40
+ "sha256": "889b6f7ba479e62988d08472d823c41bb026c261992ba6ad65df2fc4048e6a72"
49
41
  },
50
42
  "index.js": {
51
- "size": 6988,
52
- "sha256": "1af0670044be451bff237933f3fba3846e0ed30a2e2f3b0a766b3efe7781f192"
43
+ "size": 10002,
44
+ "sha256": "3eefc49885300ddadeecef3ff6af2bc0405c43a5498dbc4efb9888144dc6f330"
53
45
  },
54
46
  "register.d.ts": {
55
47
  "size": 184,
package/dist/index.d.ts CHANGED
@@ -4,6 +4,9 @@ export interface CacheDriver {
4
4
  delete(key: string): Promise<void>;
5
5
  clear(): Promise<void>;
6
6
  has(key: string): Promise<boolean>;
7
+ increment?(key: string, amount?: number): Promise<number>;
8
+ decrement?(key: string, amount?: number): Promise<number>;
9
+ getRedisClient?(): unknown;
7
10
  }
8
11
  export type RedisCacheConfig = {
9
12
  driver: 'redis';
package/dist/index.js CHANGED
@@ -30,7 +30,42 @@ const createCacheFailureState = () => {
30
30
  },
31
31
  };
32
32
  };
33
- const createCacheOperations = (ensureClient, operations, defaultTtl) => {
33
+ const incrementValue = async (ensureClient, operations, failureState, defaultTtl, key, amount) => {
34
+ if (failureState.isDisabled())
35
+ return 0;
36
+ try {
37
+ const client = await ensureClient();
38
+ if (operations.increment)
39
+ return operations.increment(client, key, amount);
40
+ const current = Number(safeJsonParse((await operations.get(client, key)) ?? '0') ?? 0);
41
+ const next = current + amount;
42
+ await operations.set(client, key, JSON.stringify(next), defaultTtl);
43
+ return next;
44
+ }
45
+ catch (error) {
46
+ failureState.disableAfterFailure('SET', error);
47
+ return 0;
48
+ }
49
+ };
50
+ const decrementValue = async (ensureClient, operations, failureState, defaultTtl, key, amount) => {
51
+ if (failureState.isDisabled())
52
+ return 0;
53
+ try {
54
+ const client = await ensureClient();
55
+ if (operations.decrement)
56
+ return operations.decrement(client, key, amount);
57
+ const current = Number(safeJsonParse((await operations.get(client, key)) ?? '0') ?? 0);
58
+ const next = current - amount;
59
+ await operations.set(client, key, JSON.stringify(next), defaultTtl);
60
+ return next;
61
+ }
62
+ catch (error) {
63
+ failureState.disableAfterFailure('SET', error);
64
+ return 0;
65
+ }
66
+ };
67
+ // Driver objects intentionally keep the cache method table in one sealed return value.
68
+ const createCacheOperations = (ensureClient, operations, defaultTtl, getRedisClient) => {
34
69
  const failureState = createCacheFailureState();
35
70
  return {
36
71
  async get(key) {
@@ -96,6 +131,13 @@ const createCacheOperations = (ensureClient, operations, defaultTtl) => {
96
131
  return false;
97
132
  }
98
133
  },
134
+ async increment(key, amount = 1) {
135
+ return incrementValue(ensureClient, operations, failureState, defaultTtl, key, amount);
136
+ },
137
+ async decrement(key, amount = 1) {
138
+ return decrementValue(ensureClient, operations, failureState, defaultTtl, key, amount);
139
+ },
140
+ ...(getRedisClient ? { getRedisClient } : {}),
99
141
  };
100
142
  };
101
143
  const createWorkersCacheDriver = (config) => {
@@ -118,7 +160,7 @@ const createWorkersCacheDriver = (config) => {
118
160
  get: (redisClient, key) => redisClient.get(key),
119
161
  set: (redisClient, key, json, ttl) => {
120
162
  if (Number.isFinite(ttl) && ttl > 0) {
121
- return redisClient.set(key, json, { EX: ttl });
163
+ return redisClient.set(key, json, 'EX', ttl);
122
164
  }
123
165
  else {
124
166
  return redisClient.set(key, json);
@@ -137,7 +179,31 @@ const createWorkersCacheDriver = (config) => {
137
179
  return Promise.resolve();
138
180
  },
139
181
  exists: (redisClient, key) => redisClient.exists(key),
140
- }, config.ttl ?? 300);
182
+ increment: async (redisClient, key, amount) => {
183
+ if (typeof redisClient.incrBy === 'function') {
184
+ return redisClient.incrBy(key, amount);
185
+ }
186
+ if (typeof redisClient.incrby === 'function') {
187
+ return redisClient.incrby(key, amount);
188
+ }
189
+ const current = Number((await redisClient.get(key)) ?? 0);
190
+ const next = current + amount;
191
+ await redisClient.set(key, String(next));
192
+ return next;
193
+ },
194
+ decrement: async (redisClient, key, amount) => {
195
+ if (typeof redisClient.decrBy === 'function') {
196
+ return redisClient.decrBy(key, amount);
197
+ }
198
+ if (typeof redisClient.decrby === 'function') {
199
+ return redisClient.decrby(key, amount);
200
+ }
201
+ const current = Number((await redisClient.get(key)) ?? 0);
202
+ const next = current - amount;
203
+ await redisClient.set(key, String(next));
204
+ return next;
205
+ },
206
+ }, config.ttl ?? 300, () => client);
141
207
  };
142
208
  const createNodeCacheDriver = (config) => {
143
209
  let client;
@@ -172,10 +238,12 @@ const createNodeCacheDriver = (config) => {
172
238
  },
173
239
  clear: (redisClient) => redisClient.flushDb(),
174
240
  exists: (redisClient, key) => redisClient.exists(key),
175
- }, config.ttl ?? 300);
241
+ increment: (redisClient, key, amount) => redisClient.incrBy(key, amount),
242
+ decrement: (redisClient, key, amount) => redisClient.decrBy(key, amount),
243
+ }, config.ttl ?? 300, () => client);
176
244
  };
177
245
  const shouldUseProxy = () => {
178
- return Env.REDIS_PROXY_URL.trim() !== '' || Env.USE_REDIS_PROXY === true;
246
+ return Env.USE_REDIS_PROXY === true;
179
247
  };
180
248
  export const RedisCacheDriver = Object.freeze({
181
249
  create(config) {
@@ -184,7 +252,7 @@ export const RedisCacheDriver = Object.freeze({
184
252
  return createWorkersCacheDriver(config);
185
253
  }
186
254
  if (isWorkers && Cloudflare.isCloudflareSocketsEnabled() === false) {
187
- throw ErrorFactory.createConfigError('Redis cache driver requires ENABLE_CLOUDFLARE_SOCKETS=true in Cloudflare Workers.');
255
+ throw ErrorFactory.createConfigError('Redis cache driver in Cloudflare Workers requires USE_REDIS_PROXY=true with REDIS_RPC_URL/REDIS_PROXY_URL, or ENABLE_CLOUDFLARE_SOCKETS=true.');
188
256
  }
189
257
  return isWorkers ? createWorkersCacheDriver(config) : createNodeCacheDriver(config);
190
258
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/cache-redis",
3
- "version": "2.1.3",
3
+ "version": "2.4.1",
4
4
  "description": "Redis cache driver for ZinTrust.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -1,5 +0,0 @@
1
- import type { CacheDriver } from './index.js';
2
- export declare const RedisWorkersDurableObjectAdapter: Readonly<{
3
- create(): CacheDriver;
4
- }>;
5
- export default RedisWorkersDurableObjectAdapter;
@@ -1,129 +0,0 @@
1
- import { ErrorFactory, Logger } from '@zintrust/core';
2
- const createSendCommandFunction = (getStub, connect) => {
3
- return async (command, args) => {
4
- await connect();
5
- const stub = getStub();
6
- const executePath = 'http://do/execute'; //NOSONAR
7
- const payload = JSON.stringify({
8
- command,
9
- params: args,
10
- });
11
- const response = await stub.fetch(executePath, {
12
- method: 'POST',
13
- headers: { 'Content-Type': 'application/json' },
14
- body: payload,
15
- });
16
- if (!response.ok) {
17
- const text = await response.text();
18
- let errDetail;
19
- try {
20
- errDetail = JSON.parse(text);
21
- }
22
- catch {
23
- errDetail = { error: text };
24
- }
25
- const msg = errDetail.error ||
26
- errDetail.message ||
27
- response.statusText;
28
- throw ErrorFactory.createGeneralError(`DO Command Failed: ${msg}`);
29
- }
30
- const json = (await response.json());
31
- if (json !== null && typeof json === 'object' && 'result' in json) {
32
- return json.result;
33
- }
34
- return json;
35
- };
36
- };
37
- const createConnectionManager = (getNamespace) => {
38
- let connected = false;
39
- const getStub = () => {
40
- const namespace = getNamespace();
41
- if (!namespace) {
42
- throw ErrorFactory.createConfigError('REDIS_POOL binding not found. Cannot connect to Durable Object pool.');
43
- }
44
- const id = namespace.idFromName('default');
45
- return namespace.get(id);
46
- };
47
- const connect = async () => {
48
- if (connected)
49
- return;
50
- try {
51
- const stub = getStub();
52
- const health = 'http://do/health'; //NOSONAR
53
- const res = await stub.fetch(health, {
54
- method: 'POST',
55
- });
56
- if (!res.ok) {
57
- throw ErrorFactory.createGeneralError(`DO health check failed: ${res.status}`);
58
- }
59
- const body = (await res.json());
60
- if (!body.connected) {
61
- Logger.info('[RedisWorkersDurableObjectAdapter] DO not connected yet, will init on first command');
62
- }
63
- connected = true;
64
- }
65
- catch (err) {
66
- Logger.error('[RedisWorkersDurableObjectAdapter] Connection failed', err);
67
- throw ErrorFactory.createGeneralError('Failed to connect to Redis DO', err);
68
- }
69
- };
70
- const sendCommand = createSendCommandFunction(getStub, connect);
71
- return {
72
- connect,
73
- sendCommand,
74
- disconnect: () => {
75
- connected = false;
76
- },
77
- isConnected: () => connected,
78
- };
79
- };
80
- const toNumber = (value) => {
81
- if (typeof value === 'number')
82
- return value;
83
- if (typeof value === 'string') {
84
- const parsed = Number(value);
85
- return Number.isFinite(parsed) ? parsed : 0;
86
- }
87
- return 0;
88
- };
89
- export const RedisWorkersDurableObjectAdapter = Object.freeze({
90
- create() {
91
- const connectionManager = createConnectionManager(() => {
92
- const globalEnv = globalThis.env;
93
- return globalEnv?.['REDIS_POOL'];
94
- });
95
- return {
96
- async get(key) {
97
- const raw = await connectionManager.sendCommand('GET', [key]);
98
- if (raw === null || raw === undefined)
99
- return null;
100
- try {
101
- return JSON.parse(String(raw));
102
- }
103
- catch {
104
- return null;
105
- }
106
- },
107
- async set(key, value, ttl) {
108
- const json = JSON.stringify(value);
109
- if (Number.isFinite(ttl) && (ttl ?? 0) > 0) {
110
- await connectionManager.sendCommand('SET', [key, json, 'EX', ttl]);
111
- }
112
- else {
113
- await connectionManager.sendCommand('SET', [key, json]);
114
- }
115
- },
116
- async delete(key) {
117
- await connectionManager.sendCommand('DEL', [key]);
118
- },
119
- async clear() {
120
- await connectionManager.sendCommand('FLUSHDB', []);
121
- },
122
- async has(key) {
123
- const count = await connectionManager.sendCommand('EXISTS', [key]);
124
- return toNumber(count) > 0;
125
- },
126
- };
127
- },
128
- });
129
- export default RedisWorkersDurableObjectAdapter;