@zintrust/core 0.1.24 → 0.1.26
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 +4 -3
- package/src/auth/Auth.d.ts.map +1 -0
- package/src/boot/Application.d.ts.map +1 -1
- package/src/boot/Application.js +8 -0
- package/src/boot/bootstrap.js +34 -15
- package/src/cache/drivers/RedisDriver.d.ts.map +1 -1
- package/src/cache/drivers/RedisDriver.js +10 -5
- package/src/cli/CLI.d.ts.map +1 -1
- package/src/cli/CLI.js +6 -0
- package/src/cli/commands/QueueCommand.d.ts.map +1 -1
- package/src/cli/commands/QueueCommand.js +89 -39
- package/src/cli/commands/QueueLockCommand.d.ts +7 -0
- package/src/cli/commands/QueueLockCommand.d.ts.map +1 -0
- package/src/cli/commands/QueueLockCommand.js +138 -0
- package/src/cli/commands/StartCommand.d.ts.map +1 -1
- package/src/cli/commands/StartCommand.js +15 -3
- package/src/cli/commands/TemplatesCommand.js +1 -1
- package/src/cli/commands/WorkerCommands.d.ts.map +1 -1
- package/src/cli/commands/WorkerCommands.js +46 -22
- package/src/cli/scaffolding/ProjectScaffolder.js +2 -2
- package/src/cli/scaffolding/RouteGenerator.d.ts.map +1 -1
- package/src/cli/scaffolding/RouteGenerator.js +27 -28
- package/src/cli/services/VersionChecker.d.ts +53 -0
- package/src/cli/services/VersionChecker.d.ts.map +1 -0
- package/src/cli/services/VersionChecker.js +180 -0
- package/src/cli/workers/QueueWorkRunner.d.ts.map +1 -1
- package/src/cli/workers/QueueWorkRunner.js +128 -7
- package/src/common/ExternalServiceUtils.d.ts +2 -2
- package/src/config/app.d.ts +4 -0
- package/src/config/app.d.ts.map +1 -1
- package/src/config/app.js +9 -0
- package/src/config/constants.d.ts +140 -10
- package/src/config/constants.d.ts.map +1 -1
- package/src/config/constants.js +86 -5
- package/src/config/index.d.ts +1 -0
- package/src/config/index.d.ts.map +1 -1
- package/src/config/middleware.d.ts +6 -6
- package/src/config/middleware.d.ts.map +1 -1
- package/src/config/middleware.js +6 -7
- package/src/config/queue.d.ts +4 -0
- package/src/config/queue.d.ts.map +1 -1
- package/src/config/queue.js +1 -1
- package/src/config/redis.d.ts +17 -0
- package/src/config/redis.d.ts.map +1 -0
- package/src/config/redis.js +54 -0
- package/src/config/type.d.ts +3 -0
- package/src/config/type.d.ts.map +1 -1
- package/src/http/Request.d.ts +10 -1
- package/src/http/Request.d.ts.map +1 -1
- package/src/http/Request.js +79 -7
- package/src/http/error-pages/ErrorPageRenderer.d.ts.map +1 -1
- package/src/http/error-pages/ErrorPageRenderer.js +4 -3
- package/src/index.d.ts +14 -11
- package/src/index.d.ts.map +1 -1
- package/src/index.js +18 -11
- package/src/lang/lang.d.ts +23 -0
- package/src/lang/lang.d.ts.map +1 -0
- package/src/lang/lang.js +22 -0
- package/src/middleware/ErrorHandlerMiddleware.d.ts.map +1 -1
- package/src/middleware/ErrorHandlerMiddleware.js +9 -1
- package/src/migrations/schema/SchemaCompiler.js +1 -1
- package/src/migrations/schema/types.d.ts +1 -1
- package/src/migrations/schema/types.d.ts.map +1 -1
- package/src/node.d.ts +1 -1
- package/src/node.d.ts.map +1 -1
- package/src/node.js +1 -1
- package/src/orm/Database.d.ts +1 -1
- package/src/orm/Database.d.ts.map +1 -1
- package/src/orm/Database.js +22 -3
- package/src/performance/Optimizer.js +1 -1
- package/src/routing/Router.d.ts +6 -2
- package/src/routing/Router.d.ts.map +1 -1
- package/src/routing/Router.js +19 -4
- package/src/runtime/PluginManager.js +1 -1
- package/src/runtime/PluginRegistry.js +2 -2
- package/src/start.d.ts.map +1 -1
- package/src/start.js +8 -7
- package/src/templates/TemplateRegistry.js +2 -2
- package/src/templates/TemplateRegistry.ts +2 -2
- package/src/templates/feature/Queue.ts.tpl +114 -0
- package/src/templates/project/basic/app/Controllers/UserController.ts.tpl +22 -0
- package/src/templates/project/basic/config/queue.ts.tpl +19 -0
- package/src/templates/project/basic/package.json.tpl +2 -1
- package/src/templates/project/basic/src/index.ts.tpl +0 -3
- package/src/tools/broadcast/drivers/Redis.d.ts.map +1 -1
- package/src/tools/broadcast/drivers/Redis.js +8 -56
- package/src/tools/mail/Mail.d.ts +1 -29
- package/src/tools/mail/Mail.d.ts.map +1 -1
- package/src/tools/mail/Mail.js +1 -111
- package/src/tools/mail/drivers/SendGrid.d.ts.map +1 -1
- package/src/tools/mail/drivers/SendGrid.js +4 -3
- package/src/tools/mail/drivers/Smtp.d.ts.map +1 -1
- package/src/tools/mail/drivers/Smtp.js +32 -10
- package/src/tools/mail/index.d.ts +40 -0
- package/src/tools/mail/index.d.ts.map +1 -0
- package/src/tools/mail/index.js +129 -0
- package/src/tools/mail/template-loader.d.ts +10 -0
- package/src/tools/mail/template-loader.d.ts.map +1 -0
- package/src/tools/mail/template-loader.js +101 -0
- package/src/tools/mail/template-utils.d.ts +10 -0
- package/src/tools/mail/template-utils.d.ts.map +1 -0
- package/src/tools/mail/template-utils.js +16 -0
- package/src/tools/mail/templates/index.d.ts +30 -0
- package/src/tools/mail/templates/index.d.ts.map +1 -1
- package/src/tools/mail/templates/index.js +69 -0
- package/src/tools/queue/AdvancedQueue.d.ts +19 -0
- package/src/tools/queue/AdvancedQueue.d.ts.map +1 -0
- package/src/tools/queue/AdvancedQueue.js +352 -0
- package/src/tools/queue/DeduplicationBuilder.d.ts +20 -0
- package/src/tools/queue/DeduplicationBuilder.d.ts.map +1 -0
- package/src/tools/queue/DeduplicationBuilder.js +77 -0
- package/src/tools/queue/LockProvider.d.ts +22 -0
- package/src/tools/queue/LockProvider.d.ts.map +1 -0
- package/src/tools/queue/LockProvider.js +282 -0
- package/src/tools/queue/Queue.d.ts.map +1 -1
- package/src/tools/queue/Queue.js +2 -1
- package/src/tools/queue/QueueExtensions.d.ts +46 -0
- package/src/tools/queue/QueueExtensions.d.ts.map +1 -0
- package/src/tools/queue/QueueExtensions.js +129 -0
- package/src/tools/queue/QueueRuntimeRegistration.d.ts.map +1 -1
- package/src/tools/queue/QueueRuntimeRegistration.js +2 -2
- package/src/tools/queue/drivers/Database.d.ts +23 -0
- package/src/tools/queue/drivers/Database.d.ts.map +1 -0
- package/src/tools/queue/drivers/Database.js +123 -0
- package/src/tools/queue/drivers/Redis.d.ts.map +1 -1
- package/src/tools/queue/drivers/Redis.js +11 -82
- package/src/tools/queue/index.d.ts +9 -0
- package/src/tools/queue/index.d.ts.map +1 -0
- package/src/tools/queue/index.js +7 -0
- package/src/tools/redis/RedisKeyManager.d.ts +64 -0
- package/src/tools/redis/RedisKeyManager.d.ts.map +1 -0
- package/src/tools/redis/RedisKeyManager.js +124 -0
- package/src/types/Queue.d.ts +62 -0
- package/src/types/Queue.d.ts.map +1 -0
- package/src/types/Queue.js +5 -0
- package/src/features/Auth.d.ts.map +0 -1
- package/src/features/Queue.d.ts +0 -21
- package/src/features/Queue.d.ts.map +0 -1
- package/src/features/Queue.js +0 -33
- package/src/templates/features/Queue.ts.tpl +0 -47
- package/src/tools/mail/templates/markdown/index.d.ts +0 -17
- package/src/tools/mail/templates/markdown/index.d.ts.map +0 -1
- package/src/tools/mail/templates/markdown/index.js +0 -49
- package/src/tools/mail/templates/markdown/registry.d.ts +0 -15
- package/src/tools/mail/templates/markdown/registry.d.ts.map +0 -1
- package/src/tools/mail/templates/markdown/registry.js +0 -34
- package/src/tools/mail/templates/markdown/validator.d.ts +0 -16
- package/src/tools/mail/templates/markdown/validator.d.ts.map +0 -1
- package/src/tools/mail/templates/markdown/validator.js +0 -24
- /package/src/{features → auth}/Auth.d.ts +0 -0
- /package/src/{features → auth}/Auth.js +0 -0
- /package/src/templates/{features → auth}/Auth.ts.tpl +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
2
|
+
/**
|
|
3
|
+
* Lock Provider Implementation
|
|
4
|
+
* Provides distributed lock management for job deduplication
|
|
5
|
+
*/
|
|
6
|
+
import { ErrorFactory } from '../../exceptions/ZintrustError.js';
|
|
7
|
+
import { Logger } from '../../config/logger.js';
|
|
8
|
+
import { createBaseDrivers } from '../../config/queue.js';
|
|
9
|
+
import { createRedisConnection } from '../../config/workers.js';
|
|
10
|
+
import { ZintrustLang } from '../../lang/lang.js';
|
|
11
|
+
// Singleton Redis client for locks to avoid connection spam
|
|
12
|
+
let redisClient = null;
|
|
13
|
+
function getRedisClient() {
|
|
14
|
+
if (!redisClient) {
|
|
15
|
+
const redisConfig = createBaseDrivers().redis;
|
|
16
|
+
// Adapt queue config to worker config format if needed
|
|
17
|
+
redisClient = createRedisConnection({
|
|
18
|
+
host: redisConfig.host,
|
|
19
|
+
port: redisConfig.port,
|
|
20
|
+
password: redisConfig.password,
|
|
21
|
+
db: redisConfig.database,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return redisClient;
|
|
25
|
+
}
|
|
26
|
+
const METRICS_SUFFIX = {
|
|
27
|
+
attempts: 'metrics:attempts',
|
|
28
|
+
acquired: 'metrics:acquired',
|
|
29
|
+
collisions: 'metrics:collisions',
|
|
30
|
+
};
|
|
31
|
+
const recordLockAttempt = async (prefix, acquired) => {
|
|
32
|
+
const client = getRedisClient();
|
|
33
|
+
try {
|
|
34
|
+
const attemptsKey = `${prefix}${METRICS_SUFFIX.attempts}`;
|
|
35
|
+
const acquiredKey = `${prefix}${METRICS_SUFFIX.acquired}`;
|
|
36
|
+
const collisionsKey = `${prefix}${METRICS_SUFFIX.collisions}`;
|
|
37
|
+
const operations = [client.incr(attemptsKey)];
|
|
38
|
+
operations.push(acquired ? client.incr(acquiredKey) : client.incr(collisionsKey));
|
|
39
|
+
await Promise.allSettled(operations);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
Logger.debug('Lock metrics update failed', { error });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function createAcquireMethod(prefix, defaultTtl) {
|
|
46
|
+
return async function acquire(key, options = {}) {
|
|
47
|
+
const lockKey = `${prefix}${key}`;
|
|
48
|
+
const ttl = options.ttl ?? defaultTtl;
|
|
49
|
+
const client = getRedisClient();
|
|
50
|
+
try {
|
|
51
|
+
// Use SET NX PX to acquire lock
|
|
52
|
+
const result = await client.set(lockKey, 'locked', 'PX', ttl, 'NX');
|
|
53
|
+
const acquired = result === 'OK';
|
|
54
|
+
const expires = new Date(Date.now() + ttl);
|
|
55
|
+
Logger.debug(`Lock acquisition attempt`, { key: lockKey, ttl, acquired });
|
|
56
|
+
void recordLockAttempt(prefix, acquired);
|
|
57
|
+
return {
|
|
58
|
+
key: lockKey,
|
|
59
|
+
ttl,
|
|
60
|
+
acquired,
|
|
61
|
+
expires,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
Logger.error(`Failed to acquire lock`, { key: lockKey, error });
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function normalizeLockKey(prefix, key) {
|
|
71
|
+
return key.startsWith(prefix) ? key : `${prefix}${key}`;
|
|
72
|
+
}
|
|
73
|
+
function createReleaseMethod(prefix) {
|
|
74
|
+
return async function release(lock) {
|
|
75
|
+
try {
|
|
76
|
+
const client = getRedisClient();
|
|
77
|
+
const lockKey = normalizeLockKey(prefix, lock.key);
|
|
78
|
+
await client.del(lockKey);
|
|
79
|
+
Logger.debug(`Lock release`, { key: lockKey });
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
Logger.error(`Failed to release lock`, { key: lock.key, error });
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function createExtendMethod(prefix) {
|
|
88
|
+
return async function extend(lock, ttl) {
|
|
89
|
+
try {
|
|
90
|
+
const client = getRedisClient();
|
|
91
|
+
const lockKey = normalizeLockKey(prefix, lock.key);
|
|
92
|
+
// Use PEXPIRE to extend
|
|
93
|
+
const result = await client.pexpire(lockKey, ttl);
|
|
94
|
+
const success = result === 1;
|
|
95
|
+
if (success) {
|
|
96
|
+
const newExpires = new Date(Date.now() + ttl);
|
|
97
|
+
Logger.debug(`Lock extension`, { key: lockKey, ttl, newExpires });
|
|
98
|
+
lock.ttl = ttl;
|
|
99
|
+
lock.expires = newExpires;
|
|
100
|
+
}
|
|
101
|
+
return success;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
Logger.error(`Failed to extend lock`, { key: lock.key, error });
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function createStatusMethod(prefix) {
|
|
110
|
+
return async function status(key) {
|
|
111
|
+
const lockKey = `${prefix}${key}`;
|
|
112
|
+
try {
|
|
113
|
+
const client = getRedisClient();
|
|
114
|
+
const ttl = await client.pttl(lockKey);
|
|
115
|
+
const exists = ttl > 0;
|
|
116
|
+
Logger.debug(`Lock status check`, { key: lockKey, exists, ttl });
|
|
117
|
+
return {
|
|
118
|
+
exists,
|
|
119
|
+
ttl: exists ? ttl : undefined,
|
|
120
|
+
expires: exists ? new Date(Date.now() + ttl) : undefined,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
Logger.error(`Failed to check lock status`, { key: lockKey, error });
|
|
125
|
+
return { exists: false };
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function createListMethod(prefix) {
|
|
130
|
+
return async function list(pattern = '*') {
|
|
131
|
+
try {
|
|
132
|
+
const client = getRedisClient();
|
|
133
|
+
const searchPattern = `${prefix}${pattern}`;
|
|
134
|
+
const keys = [];
|
|
135
|
+
let cursor = '0';
|
|
136
|
+
do {
|
|
137
|
+
// SCAN to avoid blocking Redis in production environments
|
|
138
|
+
// eslint-disable-next-line no-await-in-loop
|
|
139
|
+
const [nextCursor, batch] = await client.scan(cursor, 'MATCH', searchPattern, 'COUNT', '200');
|
|
140
|
+
cursor = nextCursor;
|
|
141
|
+
keys.push(...batch);
|
|
142
|
+
} while (cursor !== '0');
|
|
143
|
+
return keys.map((k) => k.replace(prefix, ''));
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
Logger.error(`Failed to list locks`, { pattern, error });
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Redis-based Lock Provider Implementation
|
|
153
|
+
*/
|
|
154
|
+
export function createRedisLockProvider(config) {
|
|
155
|
+
const prefix = config.prefix ?? ZintrustLang.ZINTRUST_LOCKS_PREFIX;
|
|
156
|
+
const defaultTtl = config.defaultTtl ?? ZintrustLang.ZINTRUST_LOCKS_TTL;
|
|
157
|
+
return {
|
|
158
|
+
acquire: createAcquireMethod(prefix, defaultTtl),
|
|
159
|
+
release: createReleaseMethod(prefix),
|
|
160
|
+
extend: createExtendMethod(prefix),
|
|
161
|
+
status: createStatusMethod(prefix),
|
|
162
|
+
list: createListMethod(prefix),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Memory-based Lock Provider (for testing/sync driver)
|
|
167
|
+
*/
|
|
168
|
+
function globToRegExp(pattern) {
|
|
169
|
+
// Escape all regex metacharacters, then turn '*' into '.*' as a wildcard
|
|
170
|
+
const escaped = pattern.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
|
|
171
|
+
const regexSource = escaped.replaceAll(String.raw `\*`, '.*');
|
|
172
|
+
return new RegExp(`^${regexSource}$`);
|
|
173
|
+
}
|
|
174
|
+
export function createMemoryLockProvider(config) {
|
|
175
|
+
const locks = new Map();
|
|
176
|
+
const prefix = config.prefix ?? ZintrustLang.MEMORY_LOCKS;
|
|
177
|
+
const defaultTtl = config.defaultTtl ?? ZintrustLang.ZINTRUST_LOCKS_TTL;
|
|
178
|
+
return {
|
|
179
|
+
async acquire(key, options = {}) {
|
|
180
|
+
const lockKey = `${prefix}${key}`;
|
|
181
|
+
const ttl = options.ttl ?? defaultTtl;
|
|
182
|
+
const expires = new Date(Date.now() + ttl);
|
|
183
|
+
if (locks.has(lockKey)) {
|
|
184
|
+
const existingLock = locks.get(lockKey);
|
|
185
|
+
if (existingLock !== undefined && existingLock.expires > new Date()) {
|
|
186
|
+
return {
|
|
187
|
+
key: lockKey,
|
|
188
|
+
ttl,
|
|
189
|
+
acquired: false,
|
|
190
|
+
expires,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// Lock expired, remove it
|
|
194
|
+
locks.delete(lockKey);
|
|
195
|
+
}
|
|
196
|
+
const lock = {
|
|
197
|
+
key: lockKey,
|
|
198
|
+
ttl,
|
|
199
|
+
acquired: true,
|
|
200
|
+
expires,
|
|
201
|
+
};
|
|
202
|
+
locks.set(lockKey, lock);
|
|
203
|
+
Logger.debug(`Memory lock acquired`, { key: lockKey, ttl });
|
|
204
|
+
return lock;
|
|
205
|
+
},
|
|
206
|
+
async release(lock) {
|
|
207
|
+
const lockKey = normalizeLockKey(prefix, lock.key);
|
|
208
|
+
locks.delete(lockKey);
|
|
209
|
+
Logger.debug(`Memory lock released`, { key: lockKey });
|
|
210
|
+
},
|
|
211
|
+
async extend(lock, ttl) {
|
|
212
|
+
const lockKey = normalizeLockKey(prefix, lock.key);
|
|
213
|
+
const existingLock = locks.get(lockKey);
|
|
214
|
+
if (!existingLock) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
existingLock.ttl = ttl;
|
|
218
|
+
existingLock.expires = new Date(Date.now() + ttl);
|
|
219
|
+
Logger.debug(`Memory lock extended`, { key: lockKey, ttl });
|
|
220
|
+
return true;
|
|
221
|
+
},
|
|
222
|
+
async status(key) {
|
|
223
|
+
const lockKey = `${prefix}${key}`;
|
|
224
|
+
const lock = locks.get(lockKey);
|
|
225
|
+
if (!lock) {
|
|
226
|
+
return { exists: false };
|
|
227
|
+
}
|
|
228
|
+
if (lock.expires <= new Date()) {
|
|
229
|
+
locks.delete(lockKey);
|
|
230
|
+
return { exists: false };
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
exists: true,
|
|
234
|
+
ttl: lock.ttl,
|
|
235
|
+
expires: lock.expires,
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
async list(pattern = '*') {
|
|
239
|
+
// Simple glob-style match for memory provider ('*' as wildcard)
|
|
240
|
+
const regex = globToRegExp(pattern);
|
|
241
|
+
const keys = [];
|
|
242
|
+
for (const key of locks.keys()) {
|
|
243
|
+
const strippedKey = key.replace(prefix, '');
|
|
244
|
+
if (regex.test(strippedKey)) {
|
|
245
|
+
keys.push(strippedKey);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return keys;
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Lock Provider Registry
|
|
254
|
+
*/
|
|
255
|
+
const lockProviders = new Map();
|
|
256
|
+
export function registerLockProvider(name, provider) {
|
|
257
|
+
lockProviders.set(name, provider);
|
|
258
|
+
Logger.info(`Lock provider registered`, { name });
|
|
259
|
+
}
|
|
260
|
+
export function getLockProvider(name) {
|
|
261
|
+
return lockProviders.get(name);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Clear all registered lock providers (for testing purposes)
|
|
265
|
+
* @internal
|
|
266
|
+
*/
|
|
267
|
+
export function clearLockProviders() {
|
|
268
|
+
lockProviders.clear();
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Create lock provider based on configuration
|
|
272
|
+
*/
|
|
273
|
+
export function createLockProvider(config) {
|
|
274
|
+
switch (config.type) {
|
|
275
|
+
case ZintrustLang.REDIS:
|
|
276
|
+
return createRedisLockProvider(config);
|
|
277
|
+
case ZintrustLang.MEMORY:
|
|
278
|
+
return createMemoryLockProvider(config);
|
|
279
|
+
default:
|
|
280
|
+
throw ErrorFactory.createConfigError(`Unsupported lock provider type: ${config.type}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/Queue.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAErF,UAAU,YAAY;IACpB,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjE,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC1E,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAID,eAAO,MAAM,KAAK;mBACD,MAAM,UAAU,YAAY;aAIlC,IAAI;eAIF,MAAM,GAAG,YAAY;YAUlB,CAAC,mBAAmB,MAAM,WAAW,CAAC,eAAe,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/Queue.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAErF,UAAU,YAAY;IACpB,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjE,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC1E,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAID,eAAO,MAAM,KAAK;mBACD,MAAM,UAAU,YAAY;aAIlC,IAAI;eAIF,MAAM,GAAG,YAAY;YAUlB,CAAC,mBAAmB,MAAM,WAAW,CAAC,eAAe,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAM7E,CAAC,mBACN,MAAM,eACA,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;eAKtB,MAAM,MAAM,MAAM,eAAe,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;kBAKpD,MAAM,eAAe,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;iBAK9C,MAAM,eAAe,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;EAI9D,CAAC;AAEH,eAAe,KAAK,CAAC"}
|
package/src/tools/queue/Queue.js
CHANGED
|
@@ -20,7 +20,8 @@ export const Queue = Object.freeze({
|
|
|
20
20
|
},
|
|
21
21
|
async enqueue(queue, payload, driverName) {
|
|
22
22
|
const driver = Queue.get(driverName);
|
|
23
|
-
|
|
23
|
+
const jobId = await driver.enqueue(queue, payload);
|
|
24
|
+
return jobId;
|
|
24
25
|
},
|
|
25
26
|
async dequeue(queue, driverName) {
|
|
26
27
|
const driver = Queue.get(driverName);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Extensions - Backward Compatible Extensions
|
|
3
|
+
* Extends existing Queue functionality without breaking changes
|
|
4
|
+
*/
|
|
5
|
+
import type { AdvancedJobOptions, QueueConfig } from '../../types/Queue';
|
|
6
|
+
import { createDeduplicationBuilder } from './DeduplicationBuilder';
|
|
7
|
+
/**
|
|
8
|
+
* Extend existing Queue with advanced capabilities
|
|
9
|
+
* This provides a migration path for existing code
|
|
10
|
+
*/
|
|
11
|
+
export declare function extendQueue(config: QueueConfig): void;
|
|
12
|
+
/**
|
|
13
|
+
* Enhanced enqueue method that supports advanced options
|
|
14
|
+
* This can be used as a drop-in replacement for Queue.enqueue
|
|
15
|
+
*/
|
|
16
|
+
export declare function enqueueAdvanced(name: string, payload: unknown, options?: AdvancedJobOptions): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Initialize default lock providers
|
|
19
|
+
*/
|
|
20
|
+
export declare function initializeDefaultLockProviders(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Get deduplication builder instance
|
|
23
|
+
*/
|
|
24
|
+
export declare function getDeduplicationBuilder(): ReturnType<typeof createDeduplicationBuilder>;
|
|
25
|
+
/**
|
|
26
|
+
* Queue utilities for lock management
|
|
27
|
+
*/
|
|
28
|
+
export declare const QueueLocks: {
|
|
29
|
+
release: (key: string) => Promise<void>;
|
|
30
|
+
extend: (key: string, ttl: number) => Promise<boolean>;
|
|
31
|
+
status: (key: string) => Promise<boolean>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Migration helpers for existing queue code
|
|
35
|
+
*/
|
|
36
|
+
export declare const MigrationHelpers: {
|
|
37
|
+
/**
|
|
38
|
+
* Convert existing job options to advanced options
|
|
39
|
+
*/
|
|
40
|
+
toAdvancedOptions(existingOptions: Record<string, unknown>, uniqueId?: string): AdvancedJobOptions;
|
|
41
|
+
/**
|
|
42
|
+
* Add deduplication to existing job patterns
|
|
43
|
+
*/
|
|
44
|
+
withDeduplication(existingOptions: Record<string, unknown>, deduplicationId: string, ttl?: number): AdvancedJobOptions;
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=QueueExtensions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QueueExtensions.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/QueueExtensions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAsB,WAAW,EAAE,MAAM,eAAe,CAAC;AAKzF,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAMzE;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAWrD;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;GAEG;AACH,wBAAgB,8BAA8B,IAAI,IAAI,CAuBrD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAEvF;AAED;;GAEG;AACH,eAAO,MAAM,UAAU,EAAE;IACvB,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAkC3C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,gBAAgB;IAC3B;;OAEG;uCAEgB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,aAC7B,MAAM,GAChB,kBAAkB;IAOrB;;OAEG;uCAEgB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,mBACvB,MAAM,QACjB,MAAM,GACX,kBAAkB;CAStB,CAAC"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Extensions - Backward Compatible Extensions
|
|
3
|
+
* Extends existing Queue functionality without breaking changes
|
|
4
|
+
*/
|
|
5
|
+
import { Logger } from '../../config/logger.js';
|
|
6
|
+
import { createValidationError } from '../../exceptions/ZintrustError.js';
|
|
7
|
+
import { ZintrustLang } from '../../lang/lang.js';
|
|
8
|
+
import { createAdvancedQueue } from './AdvancedQueue.js';
|
|
9
|
+
import { createDeduplicationBuilder } from './DeduplicationBuilder.js';
|
|
10
|
+
import { createLockProvider, registerLockProvider } from './LockProvider.js';
|
|
11
|
+
import { Queue } from './Queue.js';
|
|
12
|
+
let advancedQueueRef = null;
|
|
13
|
+
/**
|
|
14
|
+
* Extend existing Queue with advanced capabilities
|
|
15
|
+
* This provides a migration path for existing code
|
|
16
|
+
*/
|
|
17
|
+
export function extendQueue(config) {
|
|
18
|
+
try {
|
|
19
|
+
const advancedQueue = createAdvancedQueue(config);
|
|
20
|
+
advancedQueueRef = advancedQueue;
|
|
21
|
+
Logger.info(`Queue extended with advanced capabilities`, { queueName: config.name });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
Logger.error(`Failed to extend queue with advanced capabilities`, { error });
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Enhanced enqueue method that supports advanced options
|
|
30
|
+
* This can be used as a drop-in replacement for Queue.enqueue
|
|
31
|
+
*/
|
|
32
|
+
export async function enqueueAdvanced(name, payload, options = {}) {
|
|
33
|
+
if (advancedQueueRef === null) {
|
|
34
|
+
Logger.warn(`Advanced queue not initialized, falling back to standard enqueue`);
|
|
35
|
+
return Queue.enqueue(name, payload);
|
|
36
|
+
}
|
|
37
|
+
return advancedQueueRef.enqueue(name, payload, options);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Initialize default lock providers
|
|
41
|
+
*/
|
|
42
|
+
export function initializeDefaultLockProviders() {
|
|
43
|
+
// Register memory lock provider for sync driver
|
|
44
|
+
registerLockProvider(ZintrustLang.MEMORY, createLockProvider({
|
|
45
|
+
type: ZintrustLang.MEMORY,
|
|
46
|
+
prefix: ZintrustLang.ZINTRUST_LOCKS_PREFIX,
|
|
47
|
+
defaultTtl: ZintrustLang.ZINTRUST_LOCKS_TTL,
|
|
48
|
+
}));
|
|
49
|
+
// Register Redis lock provider if Redis is available
|
|
50
|
+
try {
|
|
51
|
+
const redisConfig = {
|
|
52
|
+
type: ZintrustLang.REDIS,
|
|
53
|
+
prefix: ZintrustLang.ZINTRUST_LOCKS_PREFIX,
|
|
54
|
+
defaultTtl: ZintrustLang.ZINTRUST_LOCKS_TTL,
|
|
55
|
+
};
|
|
56
|
+
registerLockProvider('redis', createLockProvider(redisConfig));
|
|
57
|
+
Logger.info(`Redis lock provider registered`);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
Logger.warn(`Redis lock provider registration failed, using memory provider`, { error });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get deduplication builder instance
|
|
65
|
+
*/
|
|
66
|
+
export function getDeduplicationBuilder() {
|
|
67
|
+
return createDeduplicationBuilder();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Queue utilities for lock management
|
|
71
|
+
*/
|
|
72
|
+
export const QueueLocks = {
|
|
73
|
+
/**
|
|
74
|
+
* Release a lock by key
|
|
75
|
+
*/
|
|
76
|
+
async release(key) {
|
|
77
|
+
if (advancedQueueRef !== null) {
|
|
78
|
+
return advancedQueueRef.releaseLock(key);
|
|
79
|
+
}
|
|
80
|
+
throw createValidationError('Advanced queue not initialized. Call extendQueue() first.');
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Extend a lock's TTL
|
|
84
|
+
*/
|
|
85
|
+
async extend(key, ttl) {
|
|
86
|
+
if (advancedQueueRef !== null) {
|
|
87
|
+
return advancedQueueRef.extendLock(key, ttl);
|
|
88
|
+
}
|
|
89
|
+
throw createValidationError('Advanced queue not initialized. Call extendQueue() first.');
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* Check lock status
|
|
93
|
+
*/
|
|
94
|
+
async status(key) {
|
|
95
|
+
const { getLockProvider } = await import('../../tools/queue/LockProvider.js');
|
|
96
|
+
const lockProvider = getLockProvider(ZintrustLang.MEMORY);
|
|
97
|
+
if (lockProvider !== undefined) {
|
|
98
|
+
const status = await lockProvider.status(key);
|
|
99
|
+
return status.exists;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Migration helpers for existing queue code
|
|
106
|
+
*/
|
|
107
|
+
export const MigrationHelpers = {
|
|
108
|
+
/**
|
|
109
|
+
* Convert existing job options to advanced options
|
|
110
|
+
*/
|
|
111
|
+
toAdvancedOptions(existingOptions, uniqueId) {
|
|
112
|
+
return {
|
|
113
|
+
...existingOptions,
|
|
114
|
+
...(uniqueId !== null && uniqueId !== undefined && uniqueId !== '' && { uniqueId }),
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* Add deduplication to existing job patterns
|
|
119
|
+
*/
|
|
120
|
+
withDeduplication(existingOptions, deduplicationId, ttl) {
|
|
121
|
+
return {
|
|
122
|
+
...existingOptions,
|
|
123
|
+
deduplication: createDeduplicationBuilder()
|
|
124
|
+
.id(deduplicationId)
|
|
125
|
+
.expireAfter(ttl ?? ZintrustLang.ZINTRUST_LOCKS_TTL)
|
|
126
|
+
.build(),
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueueRuntimeRegistration.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/QueueRuntimeRegistration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAOjD;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"QueueRuntimeRegistration.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/QueueRuntimeRegistration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAOjD;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAazE"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ErrorFactory } from '../../exceptions/ZintrustError.js';
|
|
2
|
+
import { DatabaseQueue } from '../queue/drivers/Database.js';
|
|
2
3
|
import { InMemoryQueue } from '../queue/drivers/InMemory.js';
|
|
3
|
-
import { RedisQueue } from '../queue/drivers/Redis.js';
|
|
4
4
|
import { Queue } from '../queue/Queue.js';
|
|
5
5
|
/**
|
|
6
6
|
* Register queue drivers from runtime config.
|
|
@@ -13,9 +13,9 @@ import { Queue } from '../queue/Queue.js';
|
|
|
13
13
|
export function registerQueuesFromRuntimeConfig(config) {
|
|
14
14
|
// Built-in drivers (core)
|
|
15
15
|
Queue.register('inmemory', InMemoryQueue);
|
|
16
|
+
Queue.register('db', DatabaseQueue);
|
|
16
17
|
// Project templates use QUEUE_DRIVER=sync; treat this as an alias of in-memory.
|
|
17
18
|
Queue.register('sync', InMemoryQueue);
|
|
18
|
-
Queue.register('redis', RedisQueue);
|
|
19
19
|
const defaultName = (config.default ?? '').toString().trim().toLowerCase();
|
|
20
20
|
if (defaultName.length === 0) {
|
|
21
21
|
throw ErrorFactory.createConfigError('Queue default driver is not configured');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { QueueMessage } from '../../queue/Queue';
|
|
2
|
+
export interface DatabaseQueueConfig {
|
|
3
|
+
connection?: string;
|
|
4
|
+
table?: string;
|
|
5
|
+
retryAttempts?: number;
|
|
6
|
+
visibilityTimeout?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface IDatabaseQueueDriver {
|
|
9
|
+
enqueue<T = unknown>(queue: string, payload: T, connectionName?: string): Promise<string>;
|
|
10
|
+
dequeue<T = unknown>(queue: string, connectionName?: string): Promise<QueueMessage<T> | undefined>;
|
|
11
|
+
ack(queue: string, id: string, connectionName?: string): Promise<void>;
|
|
12
|
+
length(queue: string, connectionName?: string): Promise<number>;
|
|
13
|
+
drain(queue: string, connectionName?: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare const DatabaseQueue: {
|
|
16
|
+
readonly enqueue: <T = unknown>(queue: string, payload: T, connectionName?: string) => Promise<string>;
|
|
17
|
+
readonly dequeue: <T = unknown>(queue: string, connectionName?: string) => Promise<QueueMessage<T> | undefined>;
|
|
18
|
+
readonly ack: (queue: string, id: string, connectionName?: string) => Promise<void>;
|
|
19
|
+
readonly length: (queue: string, connectionName?: string) => Promise<number>;
|
|
20
|
+
readonly drain: (queue: string, connectionName?: string) => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
export default DatabaseQueue;
|
|
23
|
+
//# sourceMappingURL=Database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Database.d.ts","sourceRoot":"","sources":["../../../../../src/tools/queue/drivers/Database.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1F,OAAO,CAAC,CAAC,GAAG,OAAO,EACjB,KAAK,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACxC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9D;AAED,eAAO,MAAM,aAAa;uBAkBR,CAAC,mBACN,MAAM,WACJ,CAAC,mBACO,MAAM,KACtB,OAAO,CAAC,MAAM,CAAC;uBAkBJ,CAAC,mBACN,MAAM,mBACI,MAAM,KACtB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;0BAsEtB,MAAM,MAAM,MAAM,mBAAmB,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;6BAYxD,MAAM,mBAAmB,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;4BAoBlD,MAAM,mBAAmB,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;CASlE,CAAC;AAEL,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Env } from '../../../config/env.js';
|
|
2
|
+
import { Logger } from '../../../config/logger.js';
|
|
3
|
+
import { ErrorFactory } from '../../../exceptions/ZintrustError.js';
|
|
4
|
+
import { useDatabase } from '../../../orm/Database.js';
|
|
5
|
+
import { QueryBuilder } from '../../../orm/QueryBuilder.js';
|
|
6
|
+
export const DatabaseQueue = (() => {
|
|
7
|
+
const connections = new Map();
|
|
8
|
+
const getConnection = (connectionName) => {
|
|
9
|
+
const name = connectionName ?? Env.get('DB_CONNECTION', 'default');
|
|
10
|
+
if (!connections.has(name)) {
|
|
11
|
+
connections.set(name, useDatabase(undefined, name));
|
|
12
|
+
}
|
|
13
|
+
const connection = connections.get(name);
|
|
14
|
+
if (!connection) {
|
|
15
|
+
throw ErrorFactory.createConfigError(`Database connection '${name}' not found`);
|
|
16
|
+
}
|
|
17
|
+
return connection;
|
|
18
|
+
};
|
|
19
|
+
const getTableName = () => Env.get('QUEUE_DB_TABLE', 'queue_jobs');
|
|
20
|
+
return {
|
|
21
|
+
async enqueue(queue, payload, connectionName) {
|
|
22
|
+
const db = getConnection(connectionName);
|
|
23
|
+
const tableName = getTableName();
|
|
24
|
+
const result = await QueryBuilder.create(tableName, db).insert({
|
|
25
|
+
queue,
|
|
26
|
+
payload: JSON.stringify(payload),
|
|
27
|
+
attempts: 0,
|
|
28
|
+
max_attempts: Env.getInt('QUEUE_DB_RETRY_ATTEMPTS', 3),
|
|
29
|
+
created_at: new Date(),
|
|
30
|
+
available_at: new Date(),
|
|
31
|
+
});
|
|
32
|
+
const jobId = result.id;
|
|
33
|
+
Logger.info(`[DatabaseQueue] Job enqueued: ${jobId} to queue: ${queue}`);
|
|
34
|
+
return jobId;
|
|
35
|
+
},
|
|
36
|
+
async dequeue(queue, connectionName) {
|
|
37
|
+
const db = getConnection(connectionName);
|
|
38
|
+
const tableName = getTableName();
|
|
39
|
+
const visibilityTimeout = Env.getInt('QUEUE_DB_VISIBILITY_TIMEOUT', 30);
|
|
40
|
+
const timeoutDate = new Date(Date.now() - visibilityTimeout * 1000);
|
|
41
|
+
// Try to reserve a job using atomic update
|
|
42
|
+
const result = await db.transaction(async (trx) => {
|
|
43
|
+
const job = await QueryBuilder.create(tableName, trx)
|
|
44
|
+
.select('id', 'payload', 'attempts', 'max_attempts')
|
|
45
|
+
.where('queue', '=', queue)
|
|
46
|
+
.where('available_at', '<=', new Date())
|
|
47
|
+
.where('reserved_at', '=', null)
|
|
48
|
+
.orWhere('reserved_at', '<=', timeoutDate)
|
|
49
|
+
.where('failed_at', '=', null)
|
|
50
|
+
.orderBy('available_at', 'ASC')
|
|
51
|
+
.limit(1)
|
|
52
|
+
.first();
|
|
53
|
+
if (!job)
|
|
54
|
+
return null;
|
|
55
|
+
// Check if job has exceeded max attempts
|
|
56
|
+
if (job.attempts >= job.max_attempts) {
|
|
57
|
+
// Move to dead letter queue
|
|
58
|
+
await QueryBuilder.create('queue_jobs_failed', trx).insert({
|
|
59
|
+
original_id: job.id,
|
|
60
|
+
queue,
|
|
61
|
+
payload: job.payload,
|
|
62
|
+
attempts: job.attempts,
|
|
63
|
+
failed_at: new Date(),
|
|
64
|
+
error_message: 'Max attempts exceeded',
|
|
65
|
+
});
|
|
66
|
+
// Remove from active queue
|
|
67
|
+
await QueryBuilder.create(tableName, trx).where('id', '=', job.id).delete();
|
|
68
|
+
Logger.warn(`[DatabaseQueue] Job ${job.id} exceeded max attempts (${job.max_attempts}), moved to dead letter queue`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Calculate exponential backoff delay
|
|
72
|
+
const backoffDelay = Math.min(Math.pow(2, job.attempts) * 1000, 30000); // Max 30 seconds
|
|
73
|
+
const nextAvailableAt = new Date(Date.now() + backoffDelay);
|
|
74
|
+
// Reserve the job and set next available time
|
|
75
|
+
await QueryBuilder.create(tableName, trx)
|
|
76
|
+
.where('id', '=', job.id)
|
|
77
|
+
.update({
|
|
78
|
+
reserved_at: new Date(),
|
|
79
|
+
attempts: job.attempts + 1,
|
|
80
|
+
available_at: nextAvailableAt,
|
|
81
|
+
});
|
|
82
|
+
Logger.debug(`[DatabaseQueue] Job ${job.id} reserved, attempt ${job.attempts + 1}/${job.max_attempts}, next available at ${nextAvailableAt.toISOString()}`);
|
|
83
|
+
return job;
|
|
84
|
+
});
|
|
85
|
+
if (!result)
|
|
86
|
+
return undefined;
|
|
87
|
+
return {
|
|
88
|
+
id: result.id,
|
|
89
|
+
payload: JSON.parse(result.payload),
|
|
90
|
+
attempts: result.attempts,
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
async ack(queue, id, connectionName) {
|
|
94
|
+
const db = getConnection(connectionName);
|
|
95
|
+
const tableName = getTableName();
|
|
96
|
+
await QueryBuilder.create(tableName, db)
|
|
97
|
+
.where('id', '=', id)
|
|
98
|
+
.where('queue', '=', queue)
|
|
99
|
+
.delete();
|
|
100
|
+
Logger.debug(`[DatabaseQueue] Job acknowledged: ${id} from queue: ${queue}`);
|
|
101
|
+
},
|
|
102
|
+
async length(queue, connectionName) {
|
|
103
|
+
const db = getConnection(connectionName);
|
|
104
|
+
const tableName = getTableName();
|
|
105
|
+
const result = await QueryBuilder.create(tableName, db)
|
|
106
|
+
.select('id')
|
|
107
|
+
.where('queue', '=', queue)
|
|
108
|
+
.where('available_at', '<=', new Date())
|
|
109
|
+
.where('reserved_at', '=', null)
|
|
110
|
+
.orWhere('reserved_at', '<=', new Date(Date.now() - Env.getInt('QUEUE_DB_VISIBILITY_TIMEOUT', 30) * 1000))
|
|
111
|
+
.where('failed_at', '=', null)
|
|
112
|
+
.get();
|
|
113
|
+
return result?.length ?? 0;
|
|
114
|
+
},
|
|
115
|
+
async drain(queue, connectionName) {
|
|
116
|
+
const db = getConnection(connectionName);
|
|
117
|
+
const tableName = getTableName();
|
|
118
|
+
await QueryBuilder.create(tableName, db).where('queue', '=', queue).delete();
|
|
119
|
+
Logger.info(`[DatabaseQueue] Queue drained: ${queue}`);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
})();
|
|
123
|
+
export default DatabaseQueue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Redis.d.ts","sourceRoot":"","sources":["../../../../../src/tools/queue/drivers/Redis.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Redis.d.ts","sourceRoot":"","sources":["../../../../../src/tools/queue/drivers/Redis.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,eAAO,MAAM,UAAU;uBAEL,CAAC,mBAAmB,MAAM,WAAW,CAAC,KAAG,OAAO,CAAC,MAAM,CAAC;uBAKxD,CAAC,mBAAmB,MAAM,KAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;2BAK7D,MAAM,OAAO,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;6BAKjC,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;4BAKzB,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;CAKzC,CAAC;AAEL,eAAe,UAAU,CAAC"}
|