@zintrust/core 0.1.24 → 0.1.25
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 +176 -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 +25 -0
- package/src/tools/queue/LockProvider.d.ts.map +1 -0
- package/src/tools/queue/LockProvider.js +276 -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
|
@@ -33,3 +33,72 @@ export const MailTemplates = Object.freeze({
|
|
|
33
33
|
}),
|
|
34
34
|
}),
|
|
35
35
|
});
|
|
36
|
+
import { ErrorFactory } from '../../../exceptions/ZintrustError.js';
|
|
37
|
+
/**
|
|
38
|
+
* Markdown template compatibility exports
|
|
39
|
+
* Mail templates are now pure HTML files.
|
|
40
|
+
* These exports provide backward compatibility for CLI and node.ts exports.
|
|
41
|
+
*/
|
|
42
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
43
|
+
import { join } from 'node:path';
|
|
44
|
+
/**
|
|
45
|
+
* List all available HTML mail templates.
|
|
46
|
+
* Returns template names without the .html extension.
|
|
47
|
+
*/
|
|
48
|
+
export function listTemplates() {
|
|
49
|
+
try {
|
|
50
|
+
const templatesDir = join(process.cwd(), 'src/tools/mail/templates');
|
|
51
|
+
const files = readdirSync(templatesDir);
|
|
52
|
+
return files
|
|
53
|
+
.filter((file) => file.endsWith('.html'))
|
|
54
|
+
.map((file) => file.replace('.html', ''))
|
|
55
|
+
.sort((a, b) => a.localeCompare(b));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Load a mail template by name.
|
|
63
|
+
* Note: HTML templates don't have embedded metadata like markdown templates did.
|
|
64
|
+
* This function returns the raw HTML content.
|
|
65
|
+
*/
|
|
66
|
+
export function loadTemplate(name) {
|
|
67
|
+
try {
|
|
68
|
+
const templatesDir = join(process.cwd(), 'src/tools/mail/templates');
|
|
69
|
+
const filePath = join(templatesDir, `${name}.html`);
|
|
70
|
+
const content = readFileSync(filePath, 'utf8');
|
|
71
|
+
return {
|
|
72
|
+
subject: undefined,
|
|
73
|
+
preheader: undefined,
|
|
74
|
+
variables: undefined,
|
|
75
|
+
content,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw ErrorFactory.createTryCatchError(`Template "${name}" not found: ${error.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Render a mail template with variables.
|
|
84
|
+
* Note: This is a compatibility function. For production use, use MailTemplateRenderer instead.
|
|
85
|
+
*/
|
|
86
|
+
export function renderTemplate(name, variables) {
|
|
87
|
+
const template = loadTemplate(name);
|
|
88
|
+
let html = template.content;
|
|
89
|
+
// Simple variable interpolation
|
|
90
|
+
if (variables) {
|
|
91
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
92
|
+
const regex = new RegExp(String.raw `{{\s*${key}\s*}}`, 'g');
|
|
93
|
+
html = html.replace(regex, String(value ?? ''));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
html,
|
|
98
|
+
meta: {
|
|
99
|
+
subject: template.subject,
|
|
100
|
+
content: template.content,
|
|
101
|
+
variables: template.variables,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Queue Implementation
|
|
3
|
+
* Extends ZinTrust queue functionality with deduplication and lock management
|
|
4
|
+
*/
|
|
5
|
+
import type { AdvancedJobOptions, JobResult, QueueConfig } from '../../types/Queue';
|
|
6
|
+
import { type DeduplicationBuilder } from './DeduplicationBuilder';
|
|
7
|
+
export interface AdvancedQueue {
|
|
8
|
+
enqueue(name: string, payload: unknown, options: AdvancedJobOptions): Promise<string>;
|
|
9
|
+
deduplicate(id: string, builder: DeduplicationBuilder): Promise<JobResult>;
|
|
10
|
+
releaseLock(key: string): Promise<void>;
|
|
11
|
+
extendLock(key: string, ttl: number): Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates an advanced queue instance with deduplication capabilities
|
|
15
|
+
* @param config - Queue configuration
|
|
16
|
+
* @returns {AdvancedQueue} Advanced queue instance
|
|
17
|
+
*/
|
|
18
|
+
export declare function createAdvancedQueue(config: QueueConfig): AdvancedQueue;
|
|
19
|
+
//# sourceMappingURL=AdvancedQueue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdvancedQueue.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/AdvancedQueue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAElB,SAAS,EAGT,WAAW,EACZ,MAAM,eAAe,CAAC;AAKvB,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAIxE,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACtF,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAC3E,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACxD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa,CAiBtE"}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Queue Implementation
|
|
3
|
+
* Extends ZinTrust queue functionality with deduplication and lock management
|
|
4
|
+
*/
|
|
5
|
+
import { Env } from '../../config/env.js';
|
|
6
|
+
import { Logger } from '../../config/logger.js';
|
|
7
|
+
import { createValidationError } from '../../exceptions/ZintrustError.js';
|
|
8
|
+
import { ZintrustLang } from '../../lang/lang.js';
|
|
9
|
+
import { createLockProvider, getLockProvider, registerLockProvider } from './LockProvider.js';
|
|
10
|
+
import { Queue } from './Queue.js';
|
|
11
|
+
/**
|
|
12
|
+
* Creates an advanced queue instance with deduplication capabilities
|
|
13
|
+
* @param config - Queue configuration
|
|
14
|
+
* @returns {AdvancedQueue} Advanced queue instance
|
|
15
|
+
*/
|
|
16
|
+
export function createAdvancedQueue(config) {
|
|
17
|
+
const lockProviderName = resolveLockProviderName(config);
|
|
18
|
+
const lockProvider = initializeLockProvider(lockProviderName, config);
|
|
19
|
+
return {
|
|
20
|
+
enqueue: async (name, payload, options) => enqueueWithDeduplication(name, payload, options, lockProvider),
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
22
|
+
deduplicate: async (_id, _builder) => {
|
|
23
|
+
throw createValidationError('deduplicate() method should be used via enqueue() with deduplication options');
|
|
24
|
+
},
|
|
25
|
+
releaseLock: async (key) => releaseLock(key, lockProvider),
|
|
26
|
+
extendLock: async (key, ttl) => extendLock(key, ttl, lockProvider),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Initialize lock provider for the advanced queue
|
|
31
|
+
*/
|
|
32
|
+
function initializeLockProvider(lockProviderName, config) {
|
|
33
|
+
// Initialize default lock provider if not exists
|
|
34
|
+
const getLock = getLockProvider(lockProviderName);
|
|
35
|
+
if (getLock === undefined) {
|
|
36
|
+
const lockConfig = {
|
|
37
|
+
type: lockProviderName === ZintrustLang.REDIS ? ZintrustLang.REDIS : ZintrustLang.MEMORY,
|
|
38
|
+
prefix: resolveLockPrefix(),
|
|
39
|
+
defaultTtl: resolveDefaultLockTtl(config),
|
|
40
|
+
};
|
|
41
|
+
registerLockProvider(lockProviderName, createLockProvider(lockConfig));
|
|
42
|
+
}
|
|
43
|
+
const provider = getLockProvider(lockProviderName);
|
|
44
|
+
if (provider === undefined) {
|
|
45
|
+
throw createValidationError(`Failed to initialize lock provider: ${lockProviderName}`);
|
|
46
|
+
}
|
|
47
|
+
return provider;
|
|
48
|
+
}
|
|
49
|
+
function resolveLockProviderName(config) {
|
|
50
|
+
const envProvider = Env.get('QUEUE_LOCK_PROVIDER', '').trim();
|
|
51
|
+
if (config.lockProvider !== undefined &&
|
|
52
|
+
config.lockProvider !== null &&
|
|
53
|
+
config.lockProvider.length > 0)
|
|
54
|
+
return config.lockProvider;
|
|
55
|
+
if (envProvider.length > 0)
|
|
56
|
+
return envProvider;
|
|
57
|
+
return ZintrustLang.MEMORY;
|
|
58
|
+
}
|
|
59
|
+
function resolveLockPrefix() {
|
|
60
|
+
const fromEnv = Env.get('QUEUE_LOCK_PREFIX', '').trim();
|
|
61
|
+
return fromEnv.length > 0 ? fromEnv : ZintrustLang.ZINTRUST_LOCKS_PREFIX;
|
|
62
|
+
}
|
|
63
|
+
function resolveDefaultLockTtl(config) {
|
|
64
|
+
return Env.getInt('QUEUE_DEFAULT_DEDUP_TTL', config.defaultDedupTtl ?? ZintrustLang.ZINTRUST_LOCKS_TTL);
|
|
65
|
+
}
|
|
66
|
+
function resolveMaxLockTtl() {
|
|
67
|
+
const max = Env.getInt('QUEUE_MAX_LOCK_TTL', 0);
|
|
68
|
+
if (max <= 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
return max;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Handle uniqueId validation
|
|
74
|
+
*/
|
|
75
|
+
function validateUniqueIdOptions(options) {
|
|
76
|
+
if (options.uniqueId !== null && options.uniqueId !== undefined && options.uniqueId !== '') {
|
|
77
|
+
const validation = validateUniqueId(options.uniqueId);
|
|
78
|
+
if (!validation.valid) {
|
|
79
|
+
throw createValidationError(`Invalid uniqueId: ${validation.reason}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function validateDeduplicationOptions(options) {
|
|
84
|
+
if (!options.deduplication)
|
|
85
|
+
return;
|
|
86
|
+
const maxTtl = resolveMaxLockTtl();
|
|
87
|
+
if (maxTtl !== undefined && options.deduplication.ttl !== undefined) {
|
|
88
|
+
if (options.deduplication.ttl > maxTtl) {
|
|
89
|
+
throw createValidationError(`Deduplication TTL exceeds QUEUE_MAX_LOCK_TTL (${options.deduplication.ttl} > ${maxTtl})`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const QUEUE_META_KEY = '__zintrustQueueMeta';
|
|
94
|
+
function shouldAttachReleaseAfterMeta(options) {
|
|
95
|
+
if (options.deduplication?.releaseAfter === undefined)
|
|
96
|
+
return false;
|
|
97
|
+
return typeof options.deduplication.releaseAfter !== 'number';
|
|
98
|
+
}
|
|
99
|
+
function attachQueueMeta(payload, options) {
|
|
100
|
+
if (!shouldAttachReleaseAfterMeta(options)) {
|
|
101
|
+
return { payload, metaAttached: false };
|
|
102
|
+
}
|
|
103
|
+
if (payload === null || payload === undefined || typeof payload !== 'object') {
|
|
104
|
+
return { payload, metaAttached: false };
|
|
105
|
+
}
|
|
106
|
+
const meta = {
|
|
107
|
+
deduplicationId: options.deduplication?.id,
|
|
108
|
+
releaseAfter: options.deduplication?.releaseAfter,
|
|
109
|
+
uniqueId: options.uniqueId,
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
payload: {
|
|
113
|
+
...payload,
|
|
114
|
+
[QUEUE_META_KEY]: meta,
|
|
115
|
+
},
|
|
116
|
+
metaAttached: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Handle deduplication logic
|
|
121
|
+
*/
|
|
122
|
+
async function handleDeduplicationLogic(options, lockProvider, name, startTime) {
|
|
123
|
+
if (!options.deduplication) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const deduplicationResult = await handleDeduplication(options.deduplication, lockProvider);
|
|
127
|
+
if (deduplicationResult.deduplicated) {
|
|
128
|
+
Logger.info('Job deduplicated', {
|
|
129
|
+
queueName: name,
|
|
130
|
+
deduplicationId: options.deduplication.id,
|
|
131
|
+
duration: Date.now() - startTime,
|
|
132
|
+
});
|
|
133
|
+
return deduplicationResult.lockId ?? ZintrustLang.DEDUPLICATED;
|
|
134
|
+
}
|
|
135
|
+
// Handle releaseAfter (numeric delay)
|
|
136
|
+
if (options.deduplication?.releaseAfter !== undefined &&
|
|
137
|
+
typeof options.deduplication.releaseAfter === 'number' &&
|
|
138
|
+
deduplicationResult.lockId !== null &&
|
|
139
|
+
deduplicationResult.lockId !== undefined &&
|
|
140
|
+
deduplicationResult.lockId !== '') {
|
|
141
|
+
const delay = options.deduplication.releaseAfter;
|
|
142
|
+
// lockId from handleDeduplication (via acquire) already has prefix?
|
|
143
|
+
// acquire returns lock.key which INCLUDES prefix.
|
|
144
|
+
// releaseLock() calls lockProvider.status(key) which ADDS prefix.
|
|
145
|
+
// So if we pass lockId (with prefix) to releaseLock, lockProvider.status will double prefix?
|
|
146
|
+
// Let's check LockProvider.ts.
|
|
147
|
+
// acquire: lockKey = `${prefix}${key}`. Returns lock.key = lockKey.
|
|
148
|
+
// releaseLock (AdvancedQueue): calls lockProvider.status(key).
|
|
149
|
+
// lockProvider.status: lockKey = `${prefix}${key}`.
|
|
150
|
+
// So releaseLock expects 'id' (without prefix).
|
|
151
|
+
// deduplicationResult.lockId is lock.key (WITH prefix).
|
|
152
|
+
// deduplicationResult.id is the original ID.
|
|
153
|
+
const lockId = options.deduplication.id; // Use the original ID
|
|
154
|
+
// Create timeout with proper cleanup tracking
|
|
155
|
+
// Using unref() to prevent blocking process exit
|
|
156
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
157
|
+
const timeoutId = setTimeout(async () => {
|
|
158
|
+
try {
|
|
159
|
+
await releaseLock(lockId, lockProvider);
|
|
160
|
+
Logger.debug('Released lock after delay', { lockId, delay });
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
Logger.error('Failed to release lock after delay', { lockId, error });
|
|
164
|
+
}
|
|
165
|
+
}, delay);
|
|
166
|
+
// Prevent Node.js from keeping the event loop alive
|
|
167
|
+
timeoutId.unref();
|
|
168
|
+
}
|
|
169
|
+
// Store lock reference for manual release info
|
|
170
|
+
if (deduplicationResult.lockId !== null &&
|
|
171
|
+
deduplicationResult.lockId !== undefined &&
|
|
172
|
+
deduplicationResult.lockId !== '') {
|
|
173
|
+
// In real implementation, this would be stored for later reference
|
|
174
|
+
Logger.debug('Lock created for job', {
|
|
175
|
+
lockId: deduplicationResult.lockId,
|
|
176
|
+
deduplicationId: options.deduplication.id,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Enqueue a job with advanced deduplication options
|
|
183
|
+
*/
|
|
184
|
+
async function enqueueWithDeduplication(name, payload, options, defaultLockProvider) {
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
try {
|
|
187
|
+
// Handle uniqueId validation
|
|
188
|
+
validateUniqueIdOptions(options);
|
|
189
|
+
validateDeduplicationOptions(options);
|
|
190
|
+
// Determine lock provider (uniqueVia override)
|
|
191
|
+
let lockProvider = defaultLockProvider;
|
|
192
|
+
if (options.uniqueVia !== null && options.uniqueVia !== undefined && options.uniqueVia !== '') {
|
|
193
|
+
const customProvider = getLockProvider(options.uniqueVia);
|
|
194
|
+
if (!customProvider) {
|
|
195
|
+
throw createValidationError(`Lock provider not found: ${options.uniqueVia}`);
|
|
196
|
+
}
|
|
197
|
+
lockProvider = customProvider;
|
|
198
|
+
}
|
|
199
|
+
// Handle deduplication
|
|
200
|
+
const deduplicationResult = await handleDeduplicationLogic(options, lockProvider, name, startTime);
|
|
201
|
+
if (deduplicationResult !== null) {
|
|
202
|
+
return deduplicationResult;
|
|
203
|
+
}
|
|
204
|
+
const { payload: payloadToSend, metaAttached } = attachQueueMeta(payload, options);
|
|
205
|
+
if (!metaAttached && shouldAttachReleaseAfterMeta(options)) {
|
|
206
|
+
Logger.warn('releaseAfter condition metadata could not be attached; payload is not an object', {
|
|
207
|
+
queueName: name,
|
|
208
|
+
releaseAfter: options.deduplication?.releaseAfter,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Enqueue the job using existing queue system
|
|
212
|
+
const jobId = await Queue.enqueue(name, payloadToSend);
|
|
213
|
+
Logger.info('Job enqueued successfully', {
|
|
214
|
+
queueName: name,
|
|
215
|
+
jobId,
|
|
216
|
+
uniqueId: options.uniqueId,
|
|
217
|
+
duration: Date.now() - startTime,
|
|
218
|
+
});
|
|
219
|
+
return jobId;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
Logger.error('Failed to enqueue job', {
|
|
223
|
+
queueName: name,
|
|
224
|
+
uniqueId: options.uniqueId,
|
|
225
|
+
error,
|
|
226
|
+
});
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Release a lock by key
|
|
232
|
+
*/
|
|
233
|
+
async function releaseLock(key, lockProvider) {
|
|
234
|
+
try {
|
|
235
|
+
const lockStatus = await lockProvider.status(key);
|
|
236
|
+
if (!lockStatus.exists) {
|
|
237
|
+
Logger.warn('Attempted to release non-existent lock', { key });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// Create lock object for release
|
|
241
|
+
const lock = {
|
|
242
|
+
key,
|
|
243
|
+
ttl: lockStatus.ttl ?? 0,
|
|
244
|
+
acquired: true,
|
|
245
|
+
expires: lockStatus.expires ?? new Date(),
|
|
246
|
+
};
|
|
247
|
+
await lockProvider.release(lock);
|
|
248
|
+
Logger.info('Lock released successfully', { key });
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
Logger.error('Failed to release lock', { key, error });
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Extend a lock's TTL
|
|
257
|
+
*/
|
|
258
|
+
async function extendLock(key, ttl, lockProvider) {
|
|
259
|
+
try {
|
|
260
|
+
const lockStatus = await lockProvider.status(key);
|
|
261
|
+
if (!lockStatus.exists) {
|
|
262
|
+
Logger.warn('Attempted to extend non-existent lock', { key });
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
// Create lock object for extension
|
|
266
|
+
const lock = {
|
|
267
|
+
key,
|
|
268
|
+
ttl: lockStatus.ttl ?? 0,
|
|
269
|
+
acquired: true,
|
|
270
|
+
expires: lockStatus.expires ?? new Date(),
|
|
271
|
+
};
|
|
272
|
+
const extended = await lockProvider.extend(lock, ttl);
|
|
273
|
+
if (extended) {
|
|
274
|
+
Logger.info('Lock extended successfully', { key, ttl });
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
Logger.warn('Failed to extend lock', { key, ttl });
|
|
278
|
+
}
|
|
279
|
+
return extended;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
Logger.error('Error extending lock', { key, ttl, error });
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Validate unique ID format and check for potential issues
|
|
288
|
+
*/
|
|
289
|
+
function validateUniqueId(uniqueId) {
|
|
290
|
+
if (!uniqueId || typeof uniqueId !== 'string') {
|
|
291
|
+
return { valid: false, reason: 'uniqueId must be a non-empty string' };
|
|
292
|
+
}
|
|
293
|
+
if (uniqueId.length > 255) {
|
|
294
|
+
return { valid: false, reason: 'uniqueId must be less than 255 characters' };
|
|
295
|
+
}
|
|
296
|
+
if (uniqueId.includes(' ') || uniqueId.includes('\n') || uniqueId.includes('\r')) {
|
|
297
|
+
return { valid: false, reason: 'uniqueId cannot contain whitespace characters' };
|
|
298
|
+
}
|
|
299
|
+
// Check for invalid characters
|
|
300
|
+
const invalidChars = /[<>:"\\|?*]/;
|
|
301
|
+
if (invalidChars.test(uniqueId)) {
|
|
302
|
+
return { valid: false, reason: 'uniqueId contains invalid characters' };
|
|
303
|
+
}
|
|
304
|
+
return { valid: true };
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Handle deduplication logic
|
|
308
|
+
*/
|
|
309
|
+
async function handleDeduplication(deduplicationOptions, // DeduplicationOptions - using any to avoid circular import
|
|
310
|
+
lockProvider) {
|
|
311
|
+
const { id, ttl, replace } = deduplicationOptions;
|
|
312
|
+
try {
|
|
313
|
+
// Check if lock already exists
|
|
314
|
+
const lockStatus = await lockProvider.status(id);
|
|
315
|
+
if (lockStatus.exists) {
|
|
316
|
+
if (replace === true) {
|
|
317
|
+
// Replace existing lock
|
|
318
|
+
const newLock = await lockProvider.acquire(id, { ttl });
|
|
319
|
+
return {
|
|
320
|
+
id,
|
|
321
|
+
deduplicated: false,
|
|
322
|
+
lockId: newLock.key,
|
|
323
|
+
status: ZintrustLang.QUEUED,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
// Job is deduplicated
|
|
328
|
+
return {
|
|
329
|
+
id,
|
|
330
|
+
deduplicated: true,
|
|
331
|
+
status: ZintrustLang.DEDUPLICATED,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Acquire new lock
|
|
336
|
+
const lock = await lockProvider.acquire(id, { ttl });
|
|
337
|
+
return {
|
|
338
|
+
id,
|
|
339
|
+
deduplicated: false,
|
|
340
|
+
lockId: lock.key,
|
|
341
|
+
status: ZintrustLang.QUEUED,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
Logger.error(`Deduplication handling failed`, { id, error });
|
|
346
|
+
return {
|
|
347
|
+
id,
|
|
348
|
+
deduplicated: false,
|
|
349
|
+
status: ZintrustLang.FAILED,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deduplication Builder - Plain function implementation for BullMQ job deduplication
|
|
3
|
+
* Follows ZinTrust's preference for functions over classes
|
|
4
|
+
*/
|
|
5
|
+
import type { DeduplicationOptions, ReleaseCondition } from '../../types/Queue';
|
|
6
|
+
export type ReleaseStrategy = string | number | ReleaseCondition;
|
|
7
|
+
export interface DeduplicationBuilder {
|
|
8
|
+
id(id: string): DeduplicationBuilder;
|
|
9
|
+
expireAfter(ms: number): DeduplicationBuilder;
|
|
10
|
+
dontRelease(): DeduplicationBuilder;
|
|
11
|
+
replace(): DeduplicationBuilder;
|
|
12
|
+
releaseAfter(strategy: ReleaseStrategy): DeduplicationBuilder;
|
|
13
|
+
build(): DeduplicationOptions;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Creates a deduplication builder for configuring job deduplication options
|
|
17
|
+
* @returns {DeduplicationBuilder} Builder instance with fluent interface
|
|
18
|
+
*/
|
|
19
|
+
export declare function createDeduplicationBuilder(): DeduplicationBuilder;
|
|
20
|
+
//# sourceMappingURL=DeduplicationBuilder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DeduplicationBuilder.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/DeduplicationBuilder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAG5E,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAAC;AAEjE,MAAM,WAAW,oBAAoB;IACnC,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,oBAAoB,CAAC;IACrC,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,oBAAoB,CAAC;IAC9C,WAAW,IAAI,oBAAoB,CAAC;IACpC,OAAO,IAAI,oBAAoB,CAAC;IAChC,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,oBAAoB,CAAC;IAC9D,KAAK,IAAI,oBAAoB,CAAC;CAC/B;AAUD;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,oBAAoB,CA+EjE"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deduplication Builder - Plain function implementation for BullMQ job deduplication
|
|
3
|
+
* Follows ZinTrust's preference for functions over classes
|
|
4
|
+
*/
|
|
5
|
+
import { createValidationError } from '../../exceptions/ZintrustError.js';
|
|
6
|
+
/**
|
|
7
|
+
* Creates a deduplication builder for configuring job deduplication options
|
|
8
|
+
* @returns {DeduplicationBuilder} Builder instance with fluent interface
|
|
9
|
+
*/
|
|
10
|
+
export function createDeduplicationBuilder() {
|
|
11
|
+
const state = {};
|
|
12
|
+
return {
|
|
13
|
+
/**
|
|
14
|
+
* Set the unique identifier for deduplication
|
|
15
|
+
* @param id - Unique identifier string
|
|
16
|
+
*/
|
|
17
|
+
id(id) {
|
|
18
|
+
state.id = id;
|
|
19
|
+
return this;
|
|
20
|
+
},
|
|
21
|
+
/**
|
|
22
|
+
* Set time-to-live for deduplication lock in milliseconds
|
|
23
|
+
* @param ms - TTL in milliseconds
|
|
24
|
+
*/
|
|
25
|
+
expireAfter(ms) {
|
|
26
|
+
state.ttl = ms;
|
|
27
|
+
return this;
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* Prevent automatic lock release - requires manual release
|
|
31
|
+
*/
|
|
32
|
+
dontRelease() {
|
|
33
|
+
state.dontRelease = true;
|
|
34
|
+
return this;
|
|
35
|
+
},
|
|
36
|
+
/**
|
|
37
|
+
* Replace existing job with same ID instead of ignoring
|
|
38
|
+
*/
|
|
39
|
+
replace() {
|
|
40
|
+
state.replace = true;
|
|
41
|
+
return this;
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* Set release strategy for the deduplication lock
|
|
45
|
+
* @param strategy - Release strategy (delay, 'success', or condition object)
|
|
46
|
+
*/
|
|
47
|
+
releaseAfter(strategy) {
|
|
48
|
+
state.releaseAfter = strategy;
|
|
49
|
+
return this;
|
|
50
|
+
},
|
|
51
|
+
/**
|
|
52
|
+
* Build the final deduplication options
|
|
53
|
+
* @returns {DeduplicationOptions} Configured deduplication options
|
|
54
|
+
*/
|
|
55
|
+
build() {
|
|
56
|
+
if (state.id === null || state.id === undefined) {
|
|
57
|
+
throw createValidationError('Deduplication ID is required. Call .id() before .build()');
|
|
58
|
+
}
|
|
59
|
+
const options = {
|
|
60
|
+
id: state.id,
|
|
61
|
+
};
|
|
62
|
+
if (state.ttl !== undefined) {
|
|
63
|
+
options.ttl = state.ttl;
|
|
64
|
+
}
|
|
65
|
+
if (state.dontRelease === true) {
|
|
66
|
+
options.dontRelease = true;
|
|
67
|
+
}
|
|
68
|
+
if (state.replace === true) {
|
|
69
|
+
options.replace = true;
|
|
70
|
+
}
|
|
71
|
+
if (state.releaseAfter !== undefined) {
|
|
72
|
+
options.releaseAfter = state.releaseAfter;
|
|
73
|
+
}
|
|
74
|
+
return options;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock Provider Implementation
|
|
3
|
+
* Provides distributed lock management for job deduplication
|
|
4
|
+
*/
|
|
5
|
+
import type { LockProvider, LockProviderConfig } from '../../types/Queue';
|
|
6
|
+
/**
|
|
7
|
+
* Redis-based Lock Provider Implementation
|
|
8
|
+
*/
|
|
9
|
+
export declare function createRedisLockProvider(config: LockProviderConfig): LockProvider;
|
|
10
|
+
/**
|
|
11
|
+
* Memory-based Lock Provider (for testing/sync driver)
|
|
12
|
+
*/
|
|
13
|
+
export declare function createMemoryLockProvider(config: LockProviderConfig): LockProvider;
|
|
14
|
+
export declare function registerLockProvider(name: string, provider: LockProvider): void;
|
|
15
|
+
export declare function getLockProvider(name: string): LockProvider | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Clear all registered lock providers (for testing purposes)
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export declare function clearLockProviders(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Create lock provider based on configuration
|
|
23
|
+
*/
|
|
24
|
+
export declare function createLockProvider(config: LockProviderConfig): LockProvider;
|
|
25
|
+
//# sourceMappingURL=LockProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LockProvider.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/LockProvider.ts"],"names":[],"mappings":"AACA;;;GAGG;AAGH,OAAO,KAAK,EAGV,YAAY,EACZ,kBAAkB,EAEnB,MAAM,eAAe,CAAC;AAwKvB;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAWhF;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CA4FjF;AAOD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAG/E;AACD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAS3E"}
|