@zintrust/core 2.2.3 → 2.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/core",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "Production-grade TypeScript backend framework for JavaScript",
5
5
  "homepage": "https://zintrust.com",
6
6
  "repository": {
@@ -215,7 +215,7 @@
215
215
  "jsonwebtoken": "^9.0.3",
216
216
  "mysql2": "^3.22.4",
217
217
  "pg": "^8.21.0",
218
- "redis": "^5.12.1",
218
+ "redis": "^6.0.0",
219
219
  "tsx": "^4.22.3"
220
220
  },
221
221
  "overrides": {
package/src/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @zintrust/core v2.2.3
2
+ * @zintrust/core v2.2.4
3
3
  *
4
4
  * ZinTrust Framework - Production-Grade TypeScript Backend
5
5
  * Built for performance, type safety, and exceptional developer experience
6
6
  *
7
7
  * Build Information:
8
- * Built: 2026-05-28T13:50:35.338Z
8
+ * Built: 2026-05-28T17:28:12.580Z
9
9
  * Node: >=20.0.0
10
10
  * License: MIT
11
11
  *
@@ -21,7 +21,7 @@
21
21
  * Available at runtime for debugging and health checks
22
22
  */
23
23
  export const ZINTRUST_VERSION = '0.1.41';
24
- export const ZINTRUST_BUILD_DATE = '2026-05-28T13:50:35.305Z'; // Replaced during build
24
+ export const ZINTRUST_BUILD_DATE = '2026-05-28T17:28:12.543Z'; // Replaced during build
25
25
  export { Application } from './boot/Application.js';
26
26
  export { AwsSigV4 } from './common/index.js';
27
27
  export { SignedRequest } from './security/SignedRequest.js';
@@ -4,6 +4,14 @@ type ReadAndVerifyJsonOptions = WorkerSigningOptions & Readonly<{
4
4
  defaultMaxBodyBytes: number;
5
5
  }>;
6
6
  type ProxyRequestEnv = object;
7
+ export type ProxyRpcService = 'redis' | 'worker' | 'queue-monitor';
8
+ export type ProxyRpcEnvelope = Readonly<{
9
+ service: ProxyRpcService;
10
+ action: string;
11
+ requestId: string;
12
+ payload: Record<string, unknown>;
13
+ }>;
14
+ export declare const resolveProxyRpcService: (subsystem?: string) => ProxyRpcService;
7
15
  export declare const json: (status: number, body: unknown) => Response;
8
16
  export declare const toErrorResponse: (status: number, code: string, message: string) => Response;
9
17
  export declare const getEnvInt: (env: ProxyRequestEnv, name: string, fallback: number) => number;
@@ -1 +1 @@
1
- {"version":3,"file":"CloudflareProxyShared.d.ts","sourceRoot":"","sources":["../../../src/proxy/CloudflareProxyShared.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGjE,YAAY,EACV,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAE9B,KAAK,wBAAwB,GAAG,oBAAoB,GAClD,QAAQ,CAAC;IACP,mBAAmB,EAAE,MAAM,CAAC;CAC7B,CAAC,CAAC;AAEL,KAAK,eAAe,GAAG,MAAM,CAAC;AAE9B,eAAO,MAAM,IAAI,GAAI,QAAQ,MAAM,EAAE,MAAM,OAAO,KAAG,QAQpD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,QAG/E,CAAC;AAMF,eAAO,MAAM,SAAS,GAAI,KAAK,eAAe,EAAE,MAAM,MAAM,EAAE,UAAU,MAAM,KAAG,MAKhF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,IAI9D,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,SAAS,OAAO,EAChB,UAAU,MAAM,KACf,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAY3F,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,MAAM,MAAM,KACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAezF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,OAAO,EAChB,KAAK,eAAe,EACpB,WAAW,UAAU,EACrB,SAAS,oBAAoB,KAC5B,OAAO,CAAC,QAAQ,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAQjC,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,SAAS,OAAO,EAChB,KAAK,eAAe,EACpB,SAAS,wBAAwB,KAChC,OAAO,CACN;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,UAAU,CAAA;CAAE,GAC5E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAapC,CAAC"}
1
+ {"version":3,"file":"CloudflareProxyShared.d.ts","sourceRoot":"","sources":["../../../src/proxy/CloudflareProxyShared.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGjE,YAAY,EACV,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAE9B,KAAK,wBAAwB,GAAG,oBAAoB,GAClD,QAAQ,CAAC;IACP,mBAAmB,EAAE,MAAM,CAAC;CAC7B,CAAC,CAAC;AAEL,KAAK,eAAe,GAAG,MAAM,CAAC;AAE9B,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,GAAG,eAAe,CAAC;AAEnE,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,eAAe,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,CAAC,CAAC;AAEH,eAAO,MAAM,sBAAsB,GAAI,YAAY,MAAM,KAAG,eAK3D,CAAC;AAEF,eAAO,MAAM,IAAI,GAAI,QAAQ,MAAM,EAAE,MAAM,OAAO,KAAG,QAQpD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,QAG/E,CAAC;AAMF,eAAO,MAAM,SAAS,GAAI,KAAK,eAAe,EAAE,MAAM,MAAM,EAAE,UAAU,MAAM,KAAG,MAKhF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,IAI9D,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,SAAS,OAAO,EAChB,UAAU,MAAM,KACf,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAY3F,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,MAAM,MAAM,KACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAezF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,OAAO,EAChB,KAAK,eAAe,EACpB,WAAW,UAAU,EACrB,SAAS,oBAAoB,KAC5B,OAAO,CAAC,QAAQ,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAQjC,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,SAAS,OAAO,EAChB,KAAK,eAAe,EACpB,SAAS,wBAAwB,KAChC,OAAO,CACN;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,UAAU,CAAA;CAAE,GAC5E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAapC,CAAC"}
@@ -2,6 +2,14 @@ import { isString } from '../helper/index.js';
2
2
  import { ErrorHandler } from './ErrorHandler.js';
3
3
  import { RequestValidator } from './RequestValidator.js';
4
4
  import { WorkerSigning } from './WorkerSigning.js';
5
+ export const resolveProxyRpcService = (subsystem) => {
6
+ const normalized = typeof subsystem === 'string' ? subsystem.trim().toLowerCase() : '';
7
+ if (normalized.startsWith('queue-monitor'))
8
+ return 'queue-monitor';
9
+ if (normalized.startsWith('worker'))
10
+ return 'worker';
11
+ return 'redis';
12
+ };
5
13
  export const json = (status, body) => {
6
14
  return new Response(JSON.stringify(body), {
7
15
  status,
@@ -0,0 +1,15 @@
1
+ import { type QueueDriver } from '@zintrust/queue-monitor/driver';
2
+ import { type Metrics } from '@zintrust/queue-monitor/metrics';
3
+ export type QueueMonitorContext = Readonly<{
4
+ driver: QueueDriver;
5
+ metrics: Metrics;
6
+ }>;
7
+ export type RedisProxyRedisConfig = Readonly<{
8
+ host: string;
9
+ port: number;
10
+ password: string;
11
+ db: number;
12
+ }>;
13
+ export declare const createQueueMonitorContext: (redis: RedisProxyRedisConfig) => QueueMonitorContext;
14
+ export declare const dispatchServiceCommand: (service: string, action: string, payload: Record<string, unknown>, queueMonitor: QueueMonitorContext) => Promise<unknown>;
15
+ //# sourceMappingURL=RedisProxyActions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RedisProxyActions.d.ts","sourceRoot":"","sources":["../../../../src/proxy/redis/RedisProxyActions.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,gCAAgC,CAAC;AACtF,OAAO,EAAiB,KAAK,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAa9E,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC,CAAC;AAEH,eAAO,MAAM,yBAAyB,GAAI,OAAO,qBAAqB,KAAG,mBAKxE,CAAC;AAyHF,eAAO,MAAM,sBAAsB,GACjC,SAAS,MAAM,EACf,QAAQ,MAAM,EACd,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,cAAc,mBAAmB,KAChC,OAAO,CAAC,OAAO,CAUjB,CAAC"}
@@ -0,0 +1,100 @@
1
+ import { ErrorFactory } from '../../exceptions/ZintrustError.js';
2
+ import { createBullMQDriver } from '@zintrust/queue-monitor/driver';
3
+ import { createMetrics } from '@zintrust/queue-monitor/metrics';
4
+ import { getRecentJobsForQueue, getRecentJobsForSelection, } from '../../../packages/queue-monitor/src/QueueMonitoringService.js';
5
+ import { getWorkerDetails, getWorkers, toggleAutoStart, } from '../../../packages/workers/src/dashboard/workers-api.js';
6
+ import { WorkerFactory } from '../../../packages/workers/src/WorkerFactory.js';
7
+ export const createQueueMonitorContext = (redis) => {
8
+ return {
9
+ driver: createBullMQDriver(redis),
10
+ metrics: createMetrics(redis),
11
+ };
12
+ };
13
+ const readString = (value) => {
14
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
15
+ };
16
+ const readBoolean = (value) => value === true;
17
+ const readNumber = (value) => {
18
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
19
+ };
20
+ const readQueueNames = (value) => {
21
+ if (!Array.isArray(value))
22
+ return undefined;
23
+ const names = value
24
+ .filter((entry) => typeof entry === 'string')
25
+ .map((entry) => entry.trim())
26
+ .filter((entry) => entry.length > 0);
27
+ return names.length > 0 ? names : undefined;
28
+ };
29
+ const resolveWorkerPersistenceOverride = (payload) => {
30
+ const driver = readString(payload['driver']);
31
+ if (driver === 'redis')
32
+ return { driver: 'redis' };
33
+ if (driver === 'database')
34
+ return { driver: 'database' };
35
+ if (driver === 'memory')
36
+ return { driver: 'memory' };
37
+ return undefined;
38
+ };
39
+ const workerActionHandlers = {
40
+ getWorkers: async (payload) => {
41
+ return getWorkers(payload);
42
+ },
43
+ getWorkerDetails: async (payload) => {
44
+ return getWorkerDetails(readString(payload['name'] ?? payload['workerName']) ?? '', readString(payload['driver']));
45
+ },
46
+ toggleAutoStart: async (payload) => {
47
+ return toggleAutoStart(readString(payload['name'] ?? payload['workerName']) ?? '', readBoolean(payload['enabled']));
48
+ },
49
+ listPersistedRecords: async (payload) => {
50
+ return WorkerFactory.listPersistedRecords(resolveWorkerPersistenceOverride(payload), {
51
+ offset: readNumber(payload['offset']),
52
+ limit: readNumber(payload['limit']),
53
+ search: readString(payload['search']),
54
+ includeInactive: readBoolean(payload['includeInactive']),
55
+ });
56
+ },
57
+ listFileBackedRecords: async () => {
58
+ return WorkerFactory.listFileBackedRecords();
59
+ },
60
+ getPersisted: async (payload) => {
61
+ return WorkerFactory.getPersisted(readString(payload['name'] ?? payload['workerName']) ?? '', resolveWorkerPersistenceOverride(payload));
62
+ },
63
+ getHealth: async (payload) => {
64
+ return WorkerFactory.getHealth(readString(payload['name'] ?? payload['workerName']) ?? '');
65
+ },
66
+ getMetrics: async (payload) => {
67
+ return WorkerFactory.getMetrics(readString(payload['name'] ?? payload['workerName']) ?? '');
68
+ },
69
+ };
70
+ const queueMonitorActionHandlers = {
71
+ getRecentJobsForQueue: async (payload, queueMonitor) => {
72
+ return getRecentJobsForQueue(readString(payload['queue'] ?? payload['queueName']) ?? '', queueMonitor.metrics, queueMonitor.driver);
73
+ },
74
+ getRecentJobsForSelection: async (payload, queueMonitor) => {
75
+ return getRecentJobsForSelection(readString(payload['queue'] ?? payload['queueName']) ?? '', queueMonitor.metrics, queueMonitor.driver, readQueueNames(payload['queueNames']));
76
+ },
77
+ };
78
+ const dispatchWorkerAction = async (action, payload) => {
79
+ const handler = workerActionHandlers[action];
80
+ if (handler === undefined) {
81
+ throw ErrorFactory.createValidationError(`Unsupported worker action: ${action}`);
82
+ }
83
+ return handler(payload);
84
+ };
85
+ const dispatchQueueMonitorAction = async (action, payload, queueMonitor) => {
86
+ const handler = queueMonitorActionHandlers[action];
87
+ if (handler === undefined) {
88
+ throw ErrorFactory.createValidationError(`Unsupported queue-monitor action: ${action}`);
89
+ }
90
+ return handler(payload, queueMonitor);
91
+ };
92
+ export const dispatchServiceCommand = async (service, action, payload, queueMonitor) => {
93
+ if (service === 'worker') {
94
+ return dispatchWorkerAction(action, payload);
95
+ }
96
+ if (service === 'queue-monitor') {
97
+ return dispatchQueueMonitorAction(action, payload, queueMonitor);
98
+ }
99
+ throw ErrorFactory.createValidationError(`Unsupported RPC service: ${service}`);
100
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"RedisProxyServer.d.ts","sourceRoot":"","sources":["../../../../src/proxy/redis/RedisProxyServer.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AAiBjC,KAAK,cAAc,GAAG,kBAAkB,GACtC,OAAO,CAAC;IACN,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAwSL,eAAO,MAAM,gBAAgB;sBACJ,cAAc,GAAQ,OAAO,CAAC,IAAI,CAAC;EAqB1D,CAAC;AAEH,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"RedisProxyServer.d.ts","sourceRoot":"","sources":["../../../../src/proxy/redis/RedisProxyServer.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AAsBjC,KAAK,cAAc,GAAG,kBAAkB,GACtC,OAAO,CAAC;IACN,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAwZL,eAAO,MAAM,gBAAgB;sBACJ,cAAc,GAAQ,OAAO,CAAC,IAAI,CAAC;EAgC1D,CAAC;AAEH,eAAe,gBAAgB,CAAC"}
@@ -2,52 +2,13 @@ import { SystemTraceBridge } from '../../trace/SystemTraceBridge.js';
2
2
  import { Env } from '../../config/env.js';
3
3
  import { Logger } from '../../config/logger.js';
4
4
  import { ErrorFactory } from '../../exceptions/ZintrustError.js';
5
- import { ErrorHandler } from '../ErrorHandler.js';
5
+ import { resolveProxyRpcService } from '../CloudflareProxyShared.js';
6
6
  import { createProxyServer } from '../ProxyServer.js';
7
7
  import { resolveBaseConfig, resolveBaseSigningConfig, verifyRequestSignature, } from '../ProxyServerUtils.js';
8
8
  import { RequestValidator } from '../RequestValidator.js';
9
- // Module-level script cache for persistent storage across requests
10
- const scriptCache = new Map(); // script SHA -> script content
9
+ import { createQueueMonitorContext, dispatchServiceCommand, } from '../redis/RedisProxyActions.js';
10
+ const scriptCache = new Map();
11
11
  let scriptCacheClient = null;
12
- const getScriptCacheClient = async (config) => {
13
- if (scriptCacheClient !== null && scriptCacheClient.status === 'ready') {
14
- return scriptCacheClient;
15
- }
16
- const module = (await getRedisModule());
17
- const moduleDefault = module['default'];
18
- const candidates = [
19
- module['Redis'],
20
- module['default'],
21
- moduleDefault?.['Redis'],
22
- moduleDefault?.['default'],
23
- module,
24
- ];
25
- const RedisCtor = candidates.find((candidate) => typeof candidate === 'function');
26
- if (typeof RedisCtor !== 'function') {
27
- throw ErrorFactory.createConfigError("Redis proxy could not resolve a Redis constructor from 'ioredis'.");
28
- }
29
- scriptCacheClient = new RedisCtor({
30
- host: config.redis.host,
31
- port: config.redis.port,
32
- password: config.redis.password,
33
- db: config.redis.db,
34
- maxRetriesPerRequest: 3,
35
- enableOfflineQueue: true,
36
- lazyConnect: false,
37
- retryStrategy: (times) => {
38
- if (times > 3)
39
- return null;
40
- return Math.min(times * 100, 1000);
41
- },
42
- });
43
- if (typeof scriptCacheClient.connect === 'function') {
44
- await scriptCacheClient.connect();
45
- }
46
- scriptCacheClient.on('error', (error) => {
47
- Logger.warn('[RedisProxyServer] script cache client error', error);
48
- });
49
- return scriptCacheClient;
50
- };
51
12
  const resolveRedisConfig = (overrides = {}) => {
52
13
  const host = overrides.redisHost ?? Env.get('REDIS_PROXY_TARGET_HOST', Env.get('REDIS_HOST', '127.0.0.1'));
53
14
  const port = overrides.redisPort ?? Env.getInt('REDIS_PROXY_TARGET_PORT', Env.getInt('REDIS_PORT', 6379));
@@ -73,17 +34,8 @@ const resolveConfig = (overrides = {}) => {
73
34
  },
74
35
  };
75
36
  };
76
- const validateCommandPayload = (payload) => {
77
- const command = payload['command'];
78
- const args = Array.isArray(payload['args']) ? payload['args'] : [];
79
- if (typeof command !== 'string' || command.trim() === '') {
80
- return { valid: false, error: { code: 'VALIDATION_ERROR', message: 'command is required' } };
81
- }
82
- return { valid: true, command, args };
83
- };
84
37
  const getRedisModule = async () => {
85
- const mod = await import('ioredis');
86
- return mod;
38
+ return import('ioredis');
87
39
  };
88
40
  const createClient = async (config) => {
89
41
  const module = (await getRedisModule());
@@ -132,112 +84,237 @@ const createClient = async (config) => {
132
84
  }
133
85
  return client;
134
86
  };
135
- const executeCommand = async (client, command, args, config) => {
136
- const trimmed = command.trim();
137
- const lower = trimmed.toLowerCase();
138
- // Handle SCRIPT LOAD - use persistent cache client
139
- if (lower === 'script' && args.length > 0) {
140
- const subCommand = String(args[0]).toLowerCase();
141
- if (subCommand === 'load' && args.length > 1) {
142
- const script = String(args[1]);
143
- const cacheClient = await getScriptCacheClient(config);
144
- const sha = await cacheClient.call('SCRIPT', 'LOAD', script);
145
- if (typeof sha === 'string') {
146
- scriptCache.set(sha, script);
147
- Logger.debug('[RedisProxyServer] Script loaded into cache', { sha });
148
- }
149
- return sha;
150
- }
87
+ const normalizeRpcPayload = (payload) => {
88
+ return payload ?? {};
89
+ };
90
+ const parseRedisCommandArgs = (payload) => {
91
+ return Array.isArray(payload['args']) ? payload['args'] : [];
92
+ };
93
+ const validateCommandPayload = (payload) => {
94
+ const requestId = typeof payload['requestId'] === 'string' && payload['requestId'].trim() !== ''
95
+ ? payload['requestId'].trim()
96
+ : 'unknown';
97
+ const service = resolveProxyRpcService(typeof payload['service'] === 'string' ? payload['service'] : undefined);
98
+ let actionValue;
99
+ if (typeof payload['action'] === 'string') {
100
+ actionValue = payload['action'];
101
+ }
102
+ else if (typeof payload['command'] === 'string') {
103
+ actionValue = payload['command'];
104
+ }
105
+ const normalizedPayload = normalizeRpcPayload(payload['payload'] ?? undefined);
106
+ if (typeof actionValue !== 'string' || actionValue.trim() === '') {
107
+ return {
108
+ valid: false,
109
+ requestId,
110
+ service,
111
+ payload: normalizedPayload,
112
+ error: { code: 'VALIDATION_ERROR', message: 'action is required' },
113
+ };
114
+ }
115
+ return {
116
+ valid: true,
117
+ requestId,
118
+ service,
119
+ action: actionValue.trim(),
120
+ payload: normalizedPayload,
121
+ };
122
+ };
123
+ const handleScriptCommand = async (args, config) => {
124
+ const subCommand = String(args[0]).toLowerCase();
125
+ Logger.info('[RedisProxyServer] SCRIPT command received', {
126
+ subCommand,
127
+ argsCount: args.length,
128
+ });
129
+ if (subCommand !== 'load' || args.length <= 1) {
130
+ return { status: 200, body: { result: null } };
151
131
  }
152
- // Handle EVALSHA - check cache first
132
+ const script = String(args[1]);
133
+ Logger.info('[RedisProxyServer] Loading script into Redis', { scriptLength: script.length });
134
+ const cacheClient = await getScriptCacheClient(config);
135
+ const sha = await cacheClient.call('SCRIPT', 'LOAD', script);
136
+ if (typeof sha === 'string') {
137
+ scriptCache.set(sha, script);
138
+ Logger.info('[RedisProxyServer] Script loaded into cache', {
139
+ sha,
140
+ cacheSize: scriptCache.size,
141
+ });
142
+ }
143
+ else {
144
+ Logger.warn('[RedisProxyServer] Script LOAD returned non-string SHA', {
145
+ sha,
146
+ type: typeof sha,
147
+ });
148
+ }
149
+ return { status: 200, body: { result: sha } };
150
+ };
151
+ const handleStandardRedisCommand = async (client, action, args) => {
152
+ const lower = action.trim().toLowerCase();
153
153
  if (lower === 'evalsha' && args.length > 0) {
154
154
  const sha = String(args[0]);
155
+ Logger.info('[RedisProxyServer] EVALSHA command received', {
156
+ sha,
157
+ cacheSize: scriptCache.size,
158
+ hasScript: scriptCache.has(sha),
159
+ });
155
160
  if (scriptCache.has(sha)) {
156
- Logger.debug('[RedisProxyServer] Using cached script for EVALSHA', { sha });
161
+ Logger.info('[RedisProxyServer] Using cached script for EVALSHA', { sha });
162
+ }
163
+ else {
164
+ Logger.warn('[RedisProxyServer] Script not in cache, will attempt direct EVALSHA', {
165
+ sha,
166
+ availableScripts: Array.from(scriptCache.keys()).slice(0, 5),
167
+ });
157
168
  }
158
169
  }
159
170
  const candidate = client[lower];
160
171
  if (typeof candidate === 'function') {
161
- return candidate.apply(client, args);
172
+ return {
173
+ status: 200,
174
+ body: {
175
+ result: await candidate.apply(client, args),
176
+ },
177
+ };
162
178
  }
163
179
  if (typeof client.call === 'function') {
164
- return client.call(trimmed, ...args);
180
+ return { status: 200, body: { result: await client.call(action.trim(), ...args) } };
165
181
  }
166
- throw ErrorFactory.createValidationError(`Unsupported Redis command: ${trimmed}`);
182
+ throw ErrorFactory.createValidationError(`Unsupported Redis command: ${action}`);
167
183
  };
168
- const createBackend = (config) => ({
169
- name: 'redis',
170
- handle: async (request) => {
171
- const methodError = RequestValidator.requirePost(request.method);
172
- if (methodError) {
173
- return {
174
- status: 405,
175
- body: { code: methodError.code, message: methodError.message },
176
- };
177
- }
178
- if (request.path !== '/zin/redis/command') {
179
- return { status: 404, body: { code: 'NOT_FOUND', message: 'Unknown endpoint' } };
180
- }
181
- const parsed = RequestValidator.parseJson(request.body);
182
- if (!parsed.ok) {
183
- return { status: 400, body: { code: parsed.error.code, message: parsed.error.message } };
184
- }
185
- const validated = validateCommandPayload(parsed.value);
186
- if (!validated.valid) {
187
- return {
188
- status: 400,
189
- body: {
190
- code: validated.error?.code ?? 'VALIDATION_ERROR',
191
- message: validated.error?.message ?? 'Invalid request',
192
- },
193
- };
194
- }
195
- const command = validated.command ?? '';
196
- if (command.trim() === '') {
197
- return {
198
- status: 400,
199
- body: { code: 'VALIDATION_ERROR', message: 'command is required' },
200
- };
201
- }
202
- try {
203
- const client = await createClient(config);
204
- try {
205
- const startedAt = Date.now();
206
- const result = await executeCommand(client, command, validated.args ?? [], config);
207
- SystemTraceBridge.emitRedis(command, Date.now() - startedAt);
208
- return { status: 200, body: { result } };
209
- }
210
- finally {
211
- await client.quit();
212
- }
213
- }
214
- catch (error) {
215
- return ErrorHandler.toProxyError(500, 'REDIS_PROXY_ERROR', String(error));
184
+ const handleServiceRpc = async (validated, queueMonitor) => {
185
+ const startedAt = Date.now();
186
+ const result = await dispatchServiceCommand(validated.service, validated.action ?? '', validated.payload, queueMonitor);
187
+ SystemTraceBridge.emitRedis(`${validated.service}:${validated.action ?? 'unknown'}`, Date.now() - startedAt);
188
+ return { status: 200, body: { result } };
189
+ };
190
+ const handleRedisRequest = async (request, config, queueMonitor) => {
191
+ Logger.info('[RedisProxyServer] Handling request', {
192
+ method: request.method,
193
+ path: request.path,
194
+ scriptCacheSize: scriptCache.size,
195
+ });
196
+ const methodError = RequestValidator.requirePost(request.method);
197
+ if (methodError) {
198
+ return {
199
+ status: 405,
200
+ body: { code: methodError.code, message: methodError.message },
201
+ };
202
+ }
203
+ if (request.path !== '/zin/redis/command') {
204
+ return { status: 404, body: { code: 'NOT_FOUND', message: 'Unknown endpoint' } };
205
+ }
206
+ const parsed = RequestValidator.parseJson(request.body);
207
+ if (!parsed.ok) {
208
+ return { status: 400, body: { code: parsed.error.code, message: parsed.error.message } };
209
+ }
210
+ const validated = validateCommandPayload(parsed.value);
211
+ if (!validated.valid) {
212
+ return {
213
+ status: 400,
214
+ body: {
215
+ code: validated.error?.code ?? 'VALIDATION_ERROR',
216
+ message: validated.error?.message ?? 'Invalid request',
217
+ },
218
+ };
219
+ }
220
+ if (validated.service === 'worker' || validated.service === 'queue-monitor') {
221
+ return handleServiceRpc(validated, queueMonitor);
222
+ }
223
+ const client = await createClient(config);
224
+ try {
225
+ const parsedArgs = parseRedisCommandArgs(validated.payload);
226
+ if (validated.action?.toLowerCase() === 'script') {
227
+ return await handleScriptCommand(parsedArgs, config);
216
228
  }
217
- },
218
- health: async () => {
229
+ return await handleStandardRedisCommand(client, validated.action ?? '', parsedArgs);
230
+ }
231
+ finally {
232
+ await client.quit();
233
+ }
234
+ };
235
+ const handleRedisHealth = async (config) => {
236
+ try {
237
+ const client = await createClient(config);
219
238
  try {
220
- const client = await createClient(config);
221
239
  const pingFn = client.ping;
222
240
  if (typeof pingFn === 'function') {
223
241
  await pingFn.apply(client);
224
242
  }
225
243
  else {
226
- await executeCommand(client, 'PING', [], config);
244
+ await handleStandardRedisCommand(client, 'PING', []);
227
245
  }
228
- await client.quit();
229
246
  return { status: 200, body: { status: 'healthy' } };
230
247
  }
231
- catch (error) {
232
- Logger.warn('[RedisProxyServer] health check failed', error);
233
- return { status: 503, body: { status: 'unhealthy', error: String(error) } };
248
+ finally {
249
+ await client.quit();
234
250
  }
235
- },
236
- });
251
+ }
252
+ catch (error) {
253
+ Logger.warn('[RedisProxyServer] health check failed', error);
254
+ return { status: 503, body: { status: 'unhealthy', error: String(error) } };
255
+ }
256
+ };
257
+ const createBackend = (config) => {
258
+ const queueMonitor = createQueueMonitorContext(config.redis);
259
+ return {
260
+ name: 'redis',
261
+ handle: async (request) => handleRedisRequest(request, config, queueMonitor),
262
+ health: async () => handleRedisHealth(config),
263
+ shutdown: async () => {
264
+ await Promise.all([queueMonitor.driver.close(), queueMonitor.metrics.close()]);
265
+ },
266
+ };
267
+ };
268
+ const getScriptCacheClient = async (config) => {
269
+ if (scriptCacheClient !== null && scriptCacheClient.status === 'ready') {
270
+ return scriptCacheClient;
271
+ }
272
+ const module = (await getRedisModule());
273
+ const moduleDefault = module['default'];
274
+ const candidates = [
275
+ module['Redis'],
276
+ module['default'],
277
+ moduleDefault?.['Redis'],
278
+ moduleDefault?.['default'],
279
+ module,
280
+ ];
281
+ const RedisCtor = candidates.find((candidate) => typeof candidate === 'function');
282
+ if (typeof RedisCtor !== 'function') {
283
+ throw ErrorFactory.createConfigError("Redis proxy could not resolve a Redis constructor from 'ioredis'.");
284
+ }
285
+ scriptCacheClient = new RedisCtor({
286
+ host: config.redis.host,
287
+ port: config.redis.port,
288
+ password: config.redis.password,
289
+ db: config.redis.db,
290
+ maxRetriesPerRequest: 3,
291
+ enableOfflineQueue: true,
292
+ lazyConnect: false,
293
+ retryStrategy: (times) => {
294
+ if (times > 3)
295
+ return null;
296
+ return Math.min(times * 100, 1000);
297
+ },
298
+ });
299
+ if (typeof scriptCacheClient.connect === 'function') {
300
+ await scriptCacheClient.connect();
301
+ }
302
+ scriptCacheClient.on('error', (error) => {
303
+ Logger.warn('[RedisProxyServer] script cache client error', error);
304
+ });
305
+ return scriptCacheClient;
306
+ };
237
307
  export const RedisProxyServer = Object.freeze({
238
308
  async start(overrides = {}) {
239
309
  const config = resolveConfig(overrides);
240
310
  const backend = createBackend(config);
311
+ Logger.info('[RedisProxyServer] Starting Redis proxy', {
312
+ host: config.host,
313
+ port: config.port,
314
+ redisHost: config.redis.host,
315
+ redisPort: config.redis.port,
316
+ redisDb: config.redis.db,
317
+ });
241
318
  const server = createProxyServer({
242
319
  host: config.host,
243
320
  port: config.port,
@@ -252,7 +329,10 @@ export const RedisProxyServer = Object.freeze({
252
329
  },
253
330
  });
254
331
  await server.start();
255
- Logger.info(`[redis-proxy] Listening on http://${config.host}:${config.port}`);
332
+ Logger.info(`[redis-proxy] Listening on http://${config.host}:${config.port}`, {
333
+ scriptCacheSize: scriptCache.size,
334
+ luaScriptSupport: 'enabled',
335
+ });
256
336
  },
257
337
  });
258
338
  export default RedisProxyServer;
@@ -1 +1 @@
1
- {"version":3,"file":"RedisTransport.d.ts","sourceRoot":"","sources":["../../../../src/tools/redis/RedisTransport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RedisTransport.d.ts","sourceRoot":"","sources":["../../../../src/tools/redis/RedisTransport.ts"],"names":[],"mappings":"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"}
@@ -2,10 +2,9 @@ import { Env } from '../../config/env.js';
2
2
  import { Logger } from '../../config/logger.js';
3
3
  import { ErrorFactory } from '../../exceptions/ZintrustError.js';
4
4
  import { parseCustomHeadersFromEnv } from '../../orm/adapters/SqlProxyAdapterUtils.js';
5
+ import { resolveProxyRpcService, } from '../../proxy/CloudflareProxyShared.js';
5
6
  import { SignedRequest } from '../../security/SignedRequest.js';
6
7
  const loggedSelections = new Set();
7
- // Module-level script storage for persistence across connections
8
- const globalScripts = new Map();
9
8
  const readEnvString = (key, fallback = '') => {
10
9
  if (typeof Env.get === 'function') {
11
10
  return Env.get(key, fallback);
@@ -57,11 +56,18 @@ const resolveProxyBaseUrl = () => {
57
56
  return explicit;
58
57
  return `http://${Env.REDIS_PROXY_HOST}:${Env.REDIS_PROXY_PORT}`;
59
58
  };
60
- const resolveProxySettings = () => ({
59
+ const createRequestId = () => {
60
+ const crypto = globalThis.crypto;
61
+ if (typeof crypto?.randomUUID === 'function')
62
+ return crypto.randomUUID();
63
+ return `req_${Date.now()}_${Math.random().toString(16).slice(2)}`;
64
+ };
65
+ const resolveProxySettings = (options) => ({
61
66
  baseUrl: resolveProxyBaseUrl(),
62
67
  keyId: Env.REDIS_PROXY_KEY_ID.trim() === '' ? undefined : Env.REDIS_PROXY_KEY_ID,
63
68
  secret: Env.REDIS_PROXY_SECRET.trim() === '' ? undefined : Env.REDIS_PROXY_SECRET,
64
69
  timeoutMs: Env.REDIS_PROXY_TIMEOUT_MS,
70
+ service: resolveProxyRpcService(options?.subsystem),
65
71
  customHeaders: parseCustomHeadersFromEnv('REDIS'),
66
72
  });
67
73
  const buildHeaders = async (settings, requestUrl, body) => {
@@ -84,11 +90,17 @@ const buildHeaders = async (settings, requestUrl, body) => {
84
90
  }
85
91
  return headers;
86
92
  };
87
- const requestProxyCommand = async (settings, command, args) => {
93
+ const requestProxyCommand = async (settings, action, payload) => {
88
94
  if (settings.baseUrl.trim() === '') {
89
95
  throw ErrorFactory.createConfigError('Redis proxy URL is missing (REDIS_PROXY_URL)');
90
96
  }
91
- const body = JSON.stringify({ command, args });
97
+ const envelope = {
98
+ service: settings.service,
99
+ action,
100
+ requestId: createRequestId(),
101
+ payload,
102
+ };
103
+ const body = JSON.stringify(envelope);
92
104
  const requestUrl = buildRequestUrl(settings.baseUrl, '/zin/redis/command');
93
105
  const headers = await buildHeaders(settings, requestUrl, body);
94
106
  const signal = typeof AbortSignal !== 'undefined' && 'timeout' in AbortSignal
@@ -166,13 +178,9 @@ const createScanStream = (settings, options) => {
166
178
  let cursor = '0';
167
179
  do {
168
180
  // eslint-disable-next-line no-await-in-loop
169
- const result = await requestProxyCommand(settings, 'SCAN', [
170
- cursor,
171
- 'MATCH',
172
- options?.match ?? '*',
173
- 'COUNT',
174
- String(options?.count ?? 200),
175
- ]);
181
+ const result = await requestProxyCommand(settings, 'SCAN', {
182
+ args: [cursor, 'MATCH', options?.match ?? '*', 'COUNT', String(options?.count ?? 200)],
183
+ });
176
184
  const [nextCursor, batch] = normalizeScanResponse(result);
177
185
  cursor = nextCursor;
178
186
  if (batch.length > 0) {
@@ -188,7 +196,7 @@ const createScanStream = (settings, options) => {
188
196
  });
189
197
  return stream;
190
198
  };
191
- const createPipeline = (settings, _runCommand) => {
199
+ const createPipeline = (settings) => {
192
200
  const commands = [];
193
201
  const target = {
194
202
  async exec() {
@@ -200,7 +208,7 @@ const createPipeline = (settings, _runCommand) => {
200
208
  for (const entry of commands) {
201
209
  try {
202
210
  // eslint-disable-next-line no-await-in-loop
203
- const result = await requestProxyCommand(settings, entry.command, entry.args);
211
+ const result = await requestProxyCommand(settings, entry.command, { args: entry.args });
204
212
  results.push([null, result]);
205
213
  }
206
214
  catch (error) {
@@ -218,7 +226,6 @@ const createPipeline = (settings, _runCommand) => {
218
226
  Logger.debug('[redis][proxy][pipeline] runCommand called in pipeline', {
219
227
  commandName: name,
220
228
  argsCount: args.length,
221
- hasRunCommandFunction: _runCommand !== undefined,
222
229
  });
223
230
  commands.push({ command: name, args });
224
231
  return pipeline;
@@ -230,12 +237,8 @@ const createPipeline = (settings, _runCommand) => {
230
237
  return Reflect.get(obj, prop);
231
238
  if (prop in obj)
232
239
  return Reflect.get(obj, prop);
233
- Logger.debug('[redis][proxy][pipeline] Property access in pipeline', {
234
- property: prop,
235
- currentCommandCount: commands.length,
236
- });
237
240
  return (...args) => {
238
- commands.push({ command: prop.toUpperCase(), args });
241
+ commands.push({ command: prop, args });
239
242
  return pipeline;
240
243
  };
241
244
  },
@@ -247,55 +250,19 @@ export const resolveRedisTransportMode = () => {
247
250
  ? 'proxy'
248
251
  : 'direct';
249
252
  };
250
- const logDefineCommandCall = (name, definition) => {
251
- Logger.debug('[redis][proxy][bullmq] defineCommand called', {
252
- commandName: name,
253
- numberOfKeys: definition.numberOfKeys,
254
- luaLength: definition.lua.length,
255
- luaPreview: definition.lua.substring(0, 100) + '...',
256
- });
253
+ const createCommandFunction = (settings, command) => {
254
+ return async (...args) => requestProxyCommand(settings, command, { args });
257
255
  };
258
- const storeScript = (name, definition) => {
259
- globalScripts.set(name, {
260
- numberOfKeys: definition.numberOfKeys,
261
- lua: definition.lua,
262
- });
263
- Logger.debug('[redis][proxy][bullmq] Script stored', {
264
- commandName: name,
265
- totalScripts: globalScripts.size,
266
- allScriptNames: Array.from(globalScripts.keys()),
267
- });
268
- };
269
- const executeScriptViaEval = async (settings, name, script, args) => {
270
- Logger.debug('[redis][proxy][bullmq] Executing script via SCRIPT LOAD + EVALSHA', {
271
- commandName: name,
272
- numberOfKeys: script.numberOfKeys,
273
- luaLength: script.lua.length,
274
- args: args.slice(0, 3), // Log first 3 args to avoid huge logs
275
- });
276
- // Load script into Redis cache
277
- const sha = await requestProxyCommand(settings, 'SCRIPT', ['LOAD', script.lua]);
278
- // Execute using SHA for performance
279
- if (typeof sha === 'string') {
280
- return requestProxyCommand(settings, 'EVALSHA', [sha, script.numberOfKeys, ...args]);
281
- }
282
- // Fallback to EVAL if SCRIPT LOAD fails
283
- Logger.warn('[redis][proxy][bullmq] SCRIPT LOAD failed, falling back to EVAL', {
284
- commandName: name,
256
+ const createScriptsHandler = (settings) => {
257
+ return new Proxy({}, {
258
+ get(_target, prop) {
259
+ if (typeof prop !== 'string')
260
+ return undefined;
261
+ return async (...args) => requestProxyCommand(settings, prop, { args });
262
+ },
285
263
  });
286
- return requestProxyCommand(settings, 'EVAL', [script.lua, script.numberOfKeys, ...args]);
287
- };
288
- const createCommandFunction = (settings, prop) => {
289
- return async (...args) => {
290
- Logger.debug('[redis][proxy][trap] Executing unknown property as command', {
291
- property: prop,
292
- argsCount: args.length,
293
- args: args.slice(0, 3),
294
- });
295
- return requestProxyCommand(settings, typeof prop === 'string' ? prop.toUpperCase() : String(prop), args);
296
- };
297
264
  };
298
- const handlePropertyAccess = (obj, prop, scripts, client, settings) => {
265
+ const handlePropertyAccess = (obj, prop, client, settings) => {
299
266
  if (typeof prop !== 'string')
300
267
  return Reflect.get(obj, prop);
301
268
  if (prop === 'then')
@@ -311,52 +278,11 @@ const handlePropertyAccess = (obj, prop, scripts, client, settings) => {
311
278
  };
312
279
  }
313
280
  if (prop in obj) {
314
- Logger.debug('[redis][proxy][trap] Property access on target object', {
315
- property: prop,
316
- hasScript: scripts.has(prop),
317
- });
318
- return Reflect.get(obj, prop);
319
- }
320
- // Log when BullMQ accesses unknown properties (potential versioned commands)
321
- Logger.debug('[redis][proxy][trap] Unknown property access - potential BullMQ command', {
322
- property: prop,
323
- propertyType: typeof prop,
324
- hasScript: typeof prop === 'string' ? scripts.has(prop) : false,
325
- availableScripts: Array.from(scripts.keys()),
326
- isVersionedCommand: typeof prop === 'string' ? prop.includes(':') : false,
327
- });
328
- if (typeof prop !== 'string') {
329
281
  return Reflect.get(obj, prop);
330
282
  }
331
283
  return createCommandFunction(settings, prop);
332
284
  };
333
- const createScriptsHandler = (scripts, settings) => {
334
- return new Proxy({}, {
335
- get(_target, prop) {
336
- return async (...args) => {
337
- const scriptName = String(prop);
338
- Logger.debug('[redis][proxy][scripts] Script method called', {
339
- scriptName,
340
- argsCount: args.length,
341
- isVersioned: scriptName.includes(':'),
342
- });
343
- // Strip version suffix (e.g., moveToActive:5.77.6 -> moveToActive)
344
- const baseScriptName = scriptName.includes(':') ? scriptName.split(':')[0] : scriptName;
345
- const script = scripts.get(baseScriptName);
346
- if (script === undefined) {
347
- Logger.warn('[redis][proxy][scripts] Script not found, falling back to direct proxy call', {
348
- scriptName,
349
- baseScriptName,
350
- availableScripts: Array.from(scripts.keys()),
351
- });
352
- return requestProxyCommand(settings, scriptName, args);
353
- }
354
- return executeScriptViaEval(settings, scriptName, script, args);
355
- };
356
- },
357
- });
358
- };
359
- const createProxyTarget = (config, options, scripts, runDefinedCommand, settings, client) => {
285
+ const createProxyTarget = (config, options, settings, client) => {
360
286
  const target = {
361
287
  __bullmq_iredis: true,
362
288
  isCluster: false,
@@ -368,66 +294,36 @@ const createProxyTarget = (config, options, scripts, runDefinedCommand, settings
368
294
  disconnect: () => undefined,
369
295
  duplicate: () => createRedisProxyConnection(config, options),
370
296
  defineCommand: (name, definition) => {
371
- logDefineCommandCall(name, definition);
372
- storeScript(name, definition);
297
+ Logger.debug('[redis][proxy][bullmq] defineCommand ignored on frontend proxy', {
298
+ commandName: name,
299
+ numberOfKeys: definition.numberOfKeys,
300
+ });
373
301
  },
374
- runCommand: async (name, args) => runDefinedCommand(name, args),
302
+ runCommand: async (name, args) => requestProxyCommand(settings, name, { args }),
375
303
  on: (_event, _handler) => client ?? target,
376
304
  once: (_event, _handler) => client ?? target,
377
305
  off: (_event, _handler) => client ?? target,
378
306
  removeListener: (_event, _handler) => client ?? target,
379
307
  setMaxListeners: (_count) => client ?? target,
380
308
  getMaxListeners: () => Infinity,
381
- call: async (command, ...args) => {
382
- Logger.debug('[redis][proxy][call] Direct call method invoked', {
383
- command,
384
- argsCount: args.length,
385
- hasScript: scripts.has(command),
386
- isVersionedCommand: command.includes(':'),
387
- args: args.slice(0, 3),
388
- });
389
- return requestProxyCommand(settings, command, args);
390
- },
391
- scripts: createScriptsHandler(scripts, settings),
392
- pipeline: () => createPipeline(settings, runDefinedCommand),
393
- multi: () => createPipeline(settings, runDefinedCommand),
309
+ call: async (command, ...args) => requestProxyCommand(settings, command, { args }),
310
+ scripts: createScriptsHandler(settings),
311
+ pipeline: () => createPipeline(settings),
312
+ multi: () => createPipeline(settings),
394
313
  scanStream: (scanOptions) => createScanStream(settings, scanOptions),
395
314
  };
396
315
  return target;
397
316
  };
398
317
  export const createRedisProxyConnection = (config, options) => {
399
- const settings = resolveProxySettings();
400
- const runDefinedCommand = async (name, args) => {
401
- Logger.debug('[redis][proxy][bullmq] runDefinedCommand called', {
402
- commandName: name,
403
- argsCount: args.length,
404
- hasScript: globalScripts.has(name),
405
- scriptKeys: Array.from(globalScripts.keys()),
406
- });
407
- const script = globalScripts.get(name);
408
- if (script === undefined) {
409
- Logger.warn('[redis][proxy][bullmq] Script not found, falling back to direct proxy call', {
410
- commandName: name,
411
- availableScripts: Array.from(globalScripts.keys()),
412
- });
413
- return requestProxyCommand(settings, name, args);
414
- }
415
- return executeScriptViaEval(settings, name, script, args);
416
- };
318
+ const settings = resolveProxySettings(options);
417
319
  logTransportSelection('proxy', config, options);
418
- Logger.info('[redis][proxy][bullmq] Creating BullMQ-compatible proxy connection', {
419
- hasBullMQFlag: true,
420
- hasIsClusterFlag: true,
421
- hasOptionsFlag: true,
422
- hasDuplicateMethod: true,
423
- hasDefineCommandMethod: true,
424
- hasRunCommandMethod: true,
425
- hasMultiMethod: true,
320
+ Logger.info('[redis][proxy] Creating opaque proxy connection', {
321
+ transport: 'BullMQ',
426
322
  });
427
- const proxyTarget = createProxyTarget(config, options, globalScripts, runDefinedCommand, settings, null);
323
+ const proxyTarget = createProxyTarget(config, options, settings, null);
428
324
  const client = new Proxy(proxyTarget, {
429
325
  get(obj, prop) {
430
- return handlePropertyAccess(obj, prop, globalScripts, client, settings);
326
+ return handlePropertyAccess(obj, prop, client, settings);
431
327
  },
432
328
  });
433
329
  return client;
@@ -437,15 +333,6 @@ export const ensureRedisTransportMode = (config, options) => {
437
333
  if (mode === 'proxy' && options?.requireDirect === true) {
438
334
  throw ErrorFactory.createConfigError(`Redis subsystem '${options.subsystem ?? 'redis'}' requires a direct Redis connection, but proxy mode is enabled.`);
439
335
  }
440
- // If requireDirectForScripts is set (option or env), force direct mode for script operations
441
- const requireDirectForScripts = options?.requireDirectForScripts ?? Env.REDIS_REQUIRE_DIRECT_FOR_SCRIPTS;
442
- if (mode === 'proxy' && requireDirectForScripts === true) {
443
- Logger.warn('[redis][transport] Forcing direct mode for scripts due to requireDirectForScripts', {
444
- subsystem: options?.subsystem ?? 'redis',
445
- source: options?.requireDirectForScripts === true ? 'option' : 'env',
446
- });
447
- return 'direct';
448
- }
449
336
  if (mode === 'direct') {
450
337
  logTransportSelection(mode, config, options);
451
338
  }