@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 +2 -2
- package/src/index.js +3 -3
- package/src/proxy/CloudflareProxyShared.d.ts +8 -0
- package/src/proxy/CloudflareProxyShared.d.ts.map +1 -1
- package/src/proxy/CloudflareProxyShared.js +8 -0
- package/src/proxy/redis/RedisProxyActions.d.ts +15 -0
- package/src/proxy/redis/RedisProxyActions.d.ts.map +1 -0
- package/src/proxy/redis/RedisProxyActions.js +100 -0
- package/src/proxy/redis/RedisProxyServer.d.ts.map +1 -1
- package/src/proxy/redis/RedisProxyServer.js +212 -132
- package/src/tools/redis/RedisTransport.d.ts.map +1 -1
- package/src/tools/redis/RedisTransport.js +48 -161
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/core",
|
|
3
|
-
"version": "2.2.
|
|
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": "^
|
|
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.
|
|
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-
|
|
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-
|
|
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;
|
|
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 {
|
|
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
|
-
|
|
10
|
-
const scriptCache = new Map();
|
|
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
|
-
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
180
|
+
return { status: 200, body: { result: await client.call(action.trim(), ...args) } };
|
|
165
181
|
}
|
|
166
|
-
throw ErrorFactory.createValidationError(`Unsupported Redis command: ${
|
|
182
|
+
throw ErrorFactory.createValidationError(`Unsupported Redis command: ${action}`);
|
|
167
183
|
};
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
|
244
|
+
await handleStandardRedisCommand(client, 'PING', []);
|
|
227
245
|
}
|
|
228
|
-
await client.quit();
|
|
229
246
|
return { status: 200, body: { status: 'healthy' } };
|
|
230
247
|
}
|
|
231
|
-
|
|
232
|
-
|
|
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":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
251
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
372
|
-
|
|
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) =>
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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]
|
|
419
|
-
|
|
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,
|
|
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,
|
|
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
|
}
|