@zintrust/queue-redis 0.1.20 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,251 @@
1
+ import { Env, ErrorFactory, SignedRequest, ZintrustLang } from '@zintrust/core';
2
+ const resolveSigningPrefix = (baseUrl) => {
3
+ try {
4
+ const parsed = new URL(baseUrl);
5
+ const path = parsed.pathname.endsWith('/') ? parsed.pathname.slice(0, -1) : parsed.pathname;
6
+ if (path === '' || path === '/')
7
+ return undefined;
8
+ return path;
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ };
14
+ const buildRequestUrl = (baseUrl, path) => {
15
+ const url = new URL(baseUrl);
16
+ const basePath = url.pathname.endsWith('/') ? url.pathname.slice(0, -1) : url.pathname;
17
+ const requestPath = path.startsWith('/') ? path : `/${path}`;
18
+ url.pathname = `${basePath}${requestPath}`;
19
+ return url;
20
+ };
21
+ const buildSigningUrl = (requestUrl, baseUrl) => {
22
+ const prefix = resolveSigningPrefix(baseUrl);
23
+ if (!prefix)
24
+ return requestUrl;
25
+ if (requestUrl.pathname === prefix || requestUrl.pathname.startsWith(`${prefix}/`)) {
26
+ const signingUrl = new URL(requestUrl.toString());
27
+ const stripped = requestUrl.pathname.slice(prefix.length);
28
+ signingUrl.pathname = stripped.startsWith('/') ? stripped : `/${stripped}`;
29
+ return signingUrl;
30
+ }
31
+ return requestUrl;
32
+ };
33
+ let publishClientInstance = null;
34
+ let publishClientConnected = false;
35
+ const resolveProxyBaseUrl = () => {
36
+ const explicit = Env.REDIS_PROXY_URL.trim();
37
+ if (explicit !== '')
38
+ return explicit;
39
+ if (Env.USE_REDIS_PROXY === false)
40
+ return '';
41
+ const host = Env.REDIS_PROXY_HOST || '127.0.0.1';
42
+ const port = Env.REDIS_PROXY_PORT;
43
+ return `http://${host}:${port}`;
44
+ };
45
+ const buildProxySettings = () => {
46
+ const baseUrl = resolveProxyBaseUrl();
47
+ const keyId = Env.REDIS_PROXY_KEY_ID || undefined;
48
+ const secret = Env.REDIS_PROXY_SECRET || Env.APP_KEY || undefined;
49
+ const timeoutMs = Env.REDIS_PROXY_TIMEOUT_MS;
50
+ return { baseUrl, keyId, secret, timeoutMs };
51
+ };
52
+ const buildHeaders = async (settings, requestUrl, body) => {
53
+ const headers = {
54
+ 'Content-Type': 'application/json',
55
+ };
56
+ if (settings.keyId && settings.secret) {
57
+ const signingUrl = buildSigningUrl(requestUrl, settings.baseUrl);
58
+ const signed = await SignedRequest.createHeaders({
59
+ method: 'POST',
60
+ url: signingUrl,
61
+ body,
62
+ keyId: settings.keyId,
63
+ secret: settings.secret,
64
+ });
65
+ Object.assign(headers, signed);
66
+ }
67
+ return headers;
68
+ };
69
+ const requestProxy = async (settings, path, payload) => {
70
+ if (settings.baseUrl.trim() === '') {
71
+ throw ErrorFactory.createConfigError('Redis proxy URL is missing (REDIS_PROXY_URL)');
72
+ }
73
+ const body = JSON.stringify(payload);
74
+ const url = buildRequestUrl(settings.baseUrl, path);
75
+ const headers = await buildHeaders(settings, url, body);
76
+ const timeoutSignal = typeof AbortSignal !== 'undefined' && 'timeout' in AbortSignal;
77
+ const signal = timeoutSignal ? AbortSignal.timeout(settings.timeoutMs) : undefined;
78
+ const response = await fetch(url.toString(), {
79
+ method: 'POST',
80
+ headers,
81
+ body,
82
+ signal,
83
+ });
84
+ if (!response.ok) {
85
+ const text = await response.text();
86
+ throw ErrorFactory.createTryCatchError(`Redis proxy request failed (${response.status})`, text);
87
+ }
88
+ return (await response.json());
89
+ };
90
+ const toNumber = (value) => {
91
+ if (typeof value === 'number')
92
+ return value;
93
+ if (typeof value === 'string') {
94
+ const parsed = Number(value);
95
+ return Number.isFinite(parsed) ? parsed : 0;
96
+ }
97
+ return 0;
98
+ };
99
+ const tryCreateProxyPublishClient = async () => {
100
+ const settings = buildProxySettings();
101
+ if (settings.baseUrl.trim() === '')
102
+ return null;
103
+ return {
104
+ publish: async (channel, message) => {
105
+ const response = await requestProxy(settings, '/zin/redis/command', {
106
+ command: 'PUBLISH',
107
+ args: [channel, message],
108
+ });
109
+ return toNumber(response.result);
110
+ },
111
+ };
112
+ };
113
+ /**
114
+ * Build Redis URL from environment variables
115
+ */
116
+ const buildRedisUrl = () => {
117
+ // Get REDIS_URL from environment
118
+ const redisUrl = getRedisUrlFromEnv();
119
+ if (redisUrl)
120
+ return redisUrl;
121
+ // Build URL from individual components
122
+ return buildRedisUrlFromComponents();
123
+ };
124
+ /**
125
+ * Get REDIS_URL from environment variables
126
+ */
127
+ const getRedisUrlFromEnv = () => {
128
+ const anyEnv = process.env;
129
+ const fromEnv = typeof anyEnv.get === 'function' ? anyEnv.get('REDIS_URL', '') : '';
130
+ const hasProcess = typeof process === 'object' && process !== null;
131
+ const fallback = hasProcess ? (process.env?.['REDIS_URL'] ?? '') : '';
132
+ const trimmed = fromEnv.trim();
133
+ const url = (trimmed.length > 0 ? fromEnv : String(fallback)).trim();
134
+ return url.length > 0 ? url : null;
135
+ };
136
+ /**
137
+ * Build Redis URL from individual environment components
138
+ */
139
+ const buildRedisUrlFromComponents = () => {
140
+ const host = process.env?.['REDIS_HOST'] ?? 'localhost';
141
+ const port = Number(process.env?.['REDIS_PORT'] ?? ZintrustLang.REDIS_DEFAULT_PORT);
142
+ const password = process.env?.['REDIS_PASSWORD'];
143
+ const database = Number(process.env?.['REDIS_QUEUE_DB'] ?? ZintrustLang.REDIS_DEFAULT_DB);
144
+ let redisUrl = `redis://`;
145
+ if (password)
146
+ redisUrl += `:${password}@`;
147
+ redisUrl += `${host}:${port}`;
148
+ if (database > 0)
149
+ redisUrl += `/${database}`;
150
+ return redisUrl;
151
+ };
152
+ /**
153
+ * Singleton Redis publish client factory
154
+ * Creates and caches a Redis publish client for broadcasting
155
+ */
156
+ export const createRedisPublishClient = async () => {
157
+ // Return cached instance if available
158
+ if (publishClientConnected && publishClientInstance !== null) {
159
+ return publishClientInstance;
160
+ }
161
+ const proxyClient = await tryCreateProxyPublishClient();
162
+ if (proxyClient) {
163
+ return cacheAndReturnClient(proxyClient);
164
+ }
165
+ const url = buildRedisUrl();
166
+ if (url === null)
167
+ throw ErrorFactory.createConfigError('Redis publish client requires REDIS_URL');
168
+ // Try different Redis clients in order of preference
169
+ const redisClient = (await tryCreateRedisClient(url)) || (await tryCreateIoRedisClient(url)) || getFallbackClient();
170
+ return redisClient;
171
+ };
172
+ /**
173
+ * Try to create client using 'redis' package
174
+ */
175
+ const tryCreateRedisClient = async (url) => {
176
+ try {
177
+ const mod = (await import('redis'));
178
+ const client = mod.createClient({ url });
179
+ if (typeof client.connect === 'function') {
180
+ await connectClient(client, 'Redis publish client failed to connect');
181
+ }
182
+ return cacheAndReturnClient(client);
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ };
188
+ /**
189
+ * Try to create client using 'ioredis' package
190
+ */
191
+ const tryCreateIoRedisClient = async (url) => {
192
+ try {
193
+ const mod = (await import('ioredis'));
194
+ const redis = mod.default(url);
195
+ const client = {
196
+ publish: (channel, message) => redis.publish(channel, message),
197
+ connect: redis.connect
198
+ ? async () => {
199
+ const connectFn = redis.connect;
200
+ await connectFn();
201
+ }
202
+ : undefined,
203
+ };
204
+ if (typeof client.connect === 'function') {
205
+ await connectClient(client, 'Redis publish client (ioredis) failed to connect');
206
+ }
207
+ return cacheAndReturnClient(client);
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ };
213
+ /**
214
+ * Get fallback client from global or throw error
215
+ */
216
+ const getFallbackClient = () => {
217
+ const globalFake = globalThis
218
+ .__fakeRedisClient;
219
+ if (globalFake === undefined) {
220
+ throw ErrorFactory.createConfigError("Redis publish client requires the 'redis' or 'ioredis' package (run `zin add broadcast:redis' / `zin plugin install broadcast:redis`, or `npm install redis` / `npm install ioredis`) or a test fake client set in globalThis.__fakeRedisClient");
221
+ }
222
+ return cacheAndReturnClient(globalFake);
223
+ };
224
+ /**
225
+ * Connect to Redis client with error handling
226
+ */
227
+ const connectClient = async (client, errorMessage) => {
228
+ try {
229
+ if (client.connect) {
230
+ await client.connect();
231
+ }
232
+ }
233
+ catch (err) {
234
+ throw ErrorFactory.createTryCatchError(errorMessage, err);
235
+ }
236
+ };
237
+ /**
238
+ * Cache and return the client instance
239
+ */
240
+ const cacheAndReturnClient = (client) => {
241
+ publishClientInstance = client;
242
+ publishClientConnected = true;
243
+ return client;
244
+ };
245
+ /**
246
+ * Reset the singleton publish client (useful for testing)
247
+ */
248
+ export const resetPublishClient = () => {
249
+ publishClientInstance = null;
250
+ publishClientConnected = false;
251
+ };
@@ -0,0 +1,10 @@
1
+ import type { QueueMessage } from '@zintrust/core';
2
+ interface IQueueDriver {
3
+ enqueue<T = unknown>(queue: string, payload: T): Promise<string>;
4
+ dequeue<T = unknown>(queue: string): Promise<QueueMessage<T> | undefined>;
5
+ ack(queue: string, id: string): Promise<void>;
6
+ length(queue: string): Promise<number>;
7
+ drain(queue: string): Promise<void>;
8
+ }
9
+ export declare const RedisQueue: IQueueDriver;
10
+ export default RedisQueue;
@@ -0,0 +1,92 @@
1
+ import { ErrorFactory, generateUuid, getRedisUrl, Logger } from '@zintrust/core';
2
+ export const RedisQueue = (() => {
3
+ let client = null;
4
+ let connected = false;
5
+ const ensureClient = async () => {
6
+ if (connected && client !== null)
7
+ return client;
8
+ const url = getRedisUrl();
9
+ if (url === null)
10
+ throw ErrorFactory.createConfigError('Redis queue driver requires REDIS_URL');
11
+ // Import lazily so package is optional for environments that don't use Redis
12
+ try {
13
+ // Prefer the redis package when available
14
+ try {
15
+ const mod = (await import('redis'));
16
+ const createClient = mod.createClient;
17
+ client = createClient({ url });
18
+ if (typeof client.connect === 'function') {
19
+ try {
20
+ await client.connect();
21
+ connected = true;
22
+ }
23
+ catch (connectionError) {
24
+ connected = false;
25
+ Logger.warn('Redis client connect failed:', String(connectionError));
26
+ }
27
+ }
28
+ else {
29
+ connected = true;
30
+ }
31
+ }
32
+ catch {
33
+ // Fallback to ioredis when available (used by queue-monitor)
34
+ const mod = (await import('ioredis'));
35
+ const redis = mod.default(url);
36
+ client = {
37
+ rPush: (queue, value) => redis.rpush(queue, value),
38
+ lPop: (queue) => redis.lpop(queue),
39
+ lLen: (queue) => redis.llen(queue),
40
+ del: (queue) => redis.del(queue),
41
+ };
42
+ connected = true;
43
+ }
44
+ }
45
+ catch (error) {
46
+ const globalFake = globalThis
47
+ .__fakeRedisClient;
48
+ if (globalFake === undefined) {
49
+ throw ErrorFactory.createConfigError("Redis queue driver requires the 'redis' or 'ioredis' package (run `zin add queue:redis` / `zin plugin install queue:redis`, or `npm install redis` / `npm install ioredis`) or a test fake client set in globalThis.__fakeRedisClient", error);
50
+ }
51
+ client = globalFake;
52
+ connected = true;
53
+ }
54
+ if (client === null)
55
+ throw ErrorFactory.createConfigError('Redis client could not be initialized');
56
+ return client;
57
+ };
58
+ return {
59
+ async enqueue(queue, payload) {
60
+ const cli = await ensureClient();
61
+ const id = generateUuid();
62
+ const msg = JSON.stringify({ id, payload, attempts: 0 });
63
+ await cli.rPush(queue, msg);
64
+ return id;
65
+ },
66
+ async dequeue(queue) {
67
+ const cli = await ensureClient();
68
+ const raw = await cli.lPop(queue);
69
+ if (raw === null)
70
+ return undefined;
71
+ try {
72
+ const parsed = JSON.parse(raw);
73
+ return parsed;
74
+ }
75
+ catch (err) {
76
+ throw ErrorFactory.createTryCatchError('Failed to parse queue message', err);
77
+ }
78
+ },
79
+ async ack(_queue, _id) {
80
+ return Promise.resolve(); // NOSONAR
81
+ },
82
+ async length(queue) {
83
+ const cli = await ensureClient();
84
+ return cli.lLen(queue);
85
+ },
86
+ async drain(queue) {
87
+ const cli = await ensureClient();
88
+ await cli.del(queue);
89
+ },
90
+ };
91
+ })();
92
+ export default RedisQueue;
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@zintrust/queue-redis",
3
+ "version": "0.1.27",
4
+ "buildDate": "2026-03-26T09:12:06.897Z",
5
+ "buildEnvironment": {
6
+ "node": "v25.6.1",
7
+ "platform": "darwin",
8
+ "arch": "arm64"
9
+ },
10
+ "git": {
11
+ "commit": "597f453f",
12
+ "branch": "dev"
13
+ },
14
+ "package": {
15
+ "engines": {
16
+ "node": ">=20.0.0"
17
+ },
18
+ "dependencies": [
19
+ "ioredis",
20
+ "redis"
21
+ ],
22
+ "peerDependencies": [
23
+ "@zintrust/core"
24
+ ]
25
+ },
26
+ "files": {
27
+ "BullMQRedisQueue.d.ts": {
28
+ "size": 1062,
29
+ "sha256": "52fb0f688cd17cc7d8e8128e60df6f5eea4c803282382ac75550e6aacee7cbb0"
30
+ },
31
+ "BullMQRedisQueue.js": {
32
+ "size": 20130,
33
+ "sha256": "25e6fb6928236fe10959d2d6a3849ee7845d89e7469b12e57d4f945d32f7304f"
34
+ },
35
+ "HttpQueueDriver.d.ts": {
36
+ "size": 835,
37
+ "sha256": "31735e10a83cf905ca48f3bf1c79f40029b9ac7c0ce6c26751ad646fa77bc764"
38
+ },
39
+ "HttpQueueDriver.js": {
40
+ "size": 8967,
41
+ "sha256": "1ed2cbcee0df24c42760222eb2cab2fac21d4547c153453ce4a8623e5b83b5c3"
42
+ },
43
+ "QueueHttpGateway.d.ts": {
44
+ "size": 432,
45
+ "sha256": "16c32e2179c3286569714233a9f84f32a64056593fe502a381dc9bc45e972e3a"
46
+ },
47
+ "QueueHttpGateway.js": {
48
+ "size": 8066,
49
+ "sha256": "ad4b78c5a40221aa6cf1f5959729f7ad9f6b0321c443723fff481c7dbfb91239"
50
+ },
51
+ "RedisPublishClient.d.ts": {
52
+ "size": 451,
53
+ "sha256": "341a68a3b8603b453146dc721f9d2eaf0d65bb6a1acb67908a9c6f9542b9fd2d"
54
+ },
55
+ "RedisPublishClient.js": {
56
+ "size": 8688,
57
+ "sha256": "4252875ce0326c74fb96bd6901e328bff4a4f6cd894681a3378f990a528880ff"
58
+ },
59
+ "RedisQueue.d.ts": {
60
+ "size": 438,
61
+ "sha256": "eefad366ae71d63044b23038265bf3eb60a0277e41ac70f57c5973d79f2af5ce"
62
+ },
63
+ "RedisQueue.js": {
64
+ "size": 3593,
65
+ "sha256": "dc8b2c28b2e288e048423067f90ffbe0389ac813086246a1c8fafeeeab5c142d"
66
+ },
67
+ "build-manifest.json": {
68
+ "size": 2508,
69
+ "sha256": "4c5170b80a1f21694c96af04dd5c10b7597ba3d52780a577eff04458b727e571"
70
+ },
71
+ "index.d.ts": {
72
+ "size": 565,
73
+ "sha256": "7bc063419989a39530b3a4213627e0aea33fbaacac8c307d9e7e654c99bd5b8d"
74
+ },
75
+ "index.js": {
76
+ "size": 677,
77
+ "sha256": "aa8fbba3ba7a6f0a603c3e57dd49cbc43a028273ceba9c45eb584baaa20a37bd"
78
+ },
79
+ "register.d.ts": {
80
+ "size": 169,
81
+ "sha256": "1ba6de37e0d3276ed9b9e6de02541aab03d3d1b7d3246013af27b66179c38948"
82
+ },
83
+ "register.js": {
84
+ "size": 513,
85
+ "sha256": "3c5de15f10641521cac21e0a26112bb7bde615c60652327210e77b5c90307409"
86
+ }
87
+ }
88
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,11 @@
1
- declare const RedisQueue: any;
2
- export { RedisQueue };
1
+ export { BullMQRedisQueue } from './BullMQRedisQueue';
2
+ export { HttpQueueDriver } from './HttpQueueDriver';
3
+ export { QueueHttpGateway } from './QueueHttpGateway';
4
+ export { createRedisPublishClient, resetPublishClient, type RedisPublishClient, } from './RedisPublishClient';
3
5
  export type { QueueMessage } from '@zintrust/core';
6
+ /**
7
+ * Package version and build metadata
8
+ * Available at runtime for debugging and health checks
9
+ */
10
+ export declare const _ZINTRUST_QUEUE_REDIS_VERSION = "0.1.15";
11
+ export declare const _ZINTRUST_QUEUE_REDIS_BUILD_DATE = "__BUILD_DATE__";
package/dist/index.js CHANGED
@@ -1,3 +1,10 @@
1
- import { RedisQueue as CoreRedisQueue } from '@zintrust/core';
2
- const RedisQueue = CoreRedisQueue;
3
- export { RedisQueue };
1
+ export { BullMQRedisQueue } from './BullMQRedisQueue.js';
2
+ export { HttpQueueDriver } from './HttpQueueDriver.js';
3
+ export { QueueHttpGateway } from './QueueHttpGateway.js';
4
+ export { createRedisPublishClient, resetPublishClient, } from './RedisPublishClient.js';
5
+ /**
6
+ * Package version and build metadata
7
+ * Available at runtime for debugging and health checks
8
+ */
9
+ export const _ZINTRUST_QUEUE_REDIS_VERSION = '0.1.15';
10
+ export const _ZINTRUST_QUEUE_REDIS_BUILD_DATE = '__BUILD_DATE__';
package/dist/register.js CHANGED
@@ -1,8 +1,6 @@
1
1
  export async function registerRedisQueueDriver(queue) {
2
- const core = (await importCore());
3
- if (core.RedisQueue === undefined)
4
- return;
5
- queue.register('redis', core.RedisQueue);
2
+ const mod = await import('./BullMQRedisQueue.js');
3
+ queue.register('redis', mod.default);
6
4
  }
7
5
  const importCore = async () => {
8
6
  try {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@zintrust/queue-redis",
3
- "version": "0.1.20",
3
+ "version": "0.1.27",
4
+ "description": "Redis queue driver for ZinTrust.",
4
5
  "private": false,
5
6
  "type": "module",
6
7
  "main": "./dist/index.js",
@@ -22,13 +23,27 @@
22
23
  "node": ">=20.0.0"
23
24
  },
24
25
  "peerDependencies": {
25
- "@zintrust/core": "^0.1.20"
26
+ "@zintrust/core": "^0.4.21"
27
+ },
28
+ "devDependencies": {
29
+ "@zintrust/core": "file:../../dist"
26
30
  },
27
31
  "publishConfig": {
28
32
  "access": "public"
29
33
  },
34
+ "keywords": [
35
+ "zintrust",
36
+ "queue",
37
+ "redis",
38
+ "jobs",
39
+ "adapter"
40
+ ],
30
41
  "scripts": {
31
- "build": "tsc -p tsconfig.json",
42
+ "build": "tsc -p tsconfig.json && node ../../scripts/fix-dist-esm-imports.mjs dist",
32
43
  "prepublishOnly": "npm run build"
44
+ },
45
+ "dependencies": {
46
+ "ioredis": "^5.10.0",
47
+ "redis": "^5.11.0"
33
48
  }
34
49
  }