@fluojs/queue 1.0.2 → 3.0.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"worker-discovery.d.ts","sourceRoot":"","sources":["../src/worker-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGzE,OAAO,EAA8B,KAAK,qBAAqB,EAAkD,MAAM,cAAc,CAAC;AACtI,OAAO,KAAK,EAAE,4BAA4B,EAAE,YAAY,EAAE,qBAAqB,EAAuB,MAAM,YAAY,CAAC;AAEzH;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAC5C,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,OAAO,EAAE,4BAA4B,EACrC,MAAM,EAAE,iBAAiB,EACzB,YAAY,CAAC,EAAE,qBAAqB,GACnC,GAAG,CAAC,YAAY,EAAE,qBAAqB,CAAC,CA4C1C"}
1
+ {"version":3,"file":"worker-discovery.d.ts","sourceRoot":"","sources":["../src/worker-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,EAA8B,KAAK,qBAAqB,EAAkD,MAAM,cAAc,CAAC;AAEtI,OAAO,KAAK,EAAE,4BAA4B,EAAE,YAAY,EAAE,qBAAqB,EAAuB,MAAM,YAAY,CAAC;AAEzH;;;;;;;;GAQG;AACH,wBAAgB,8BAA8B,CAC5C,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,OAAO,EAAE,4BAA4B,EACrC,MAAM,EAAE,iBAAiB,GAAG,SAAS,EACrC,YAAY,CAAC,EAAE,qBAAqB,GACnC,GAAG,CAAC,YAAY,EAAE,qBAAqB,CAAC,CAoC1C"}
@@ -1,11 +1,12 @@
1
- import { getQueueWorkerMetadata } from './metadata.js';
2
1
  import { collectDiscoveryCandidates, normalizePositiveInteger, normalizeRateLimiter } from './helpers.js';
2
+ import { getQueueWorkerMetadata } from './metadata.js';
3
3
  /**
4
4
  * Discover queue worker descriptors.
5
5
  *
6
6
  * @param compiledModules The compiled modules.
7
7
  * @param options The options.
8
- * @param logger The logger.
8
+ * @param logger Optional logger for skipped worker registrations.
9
+ * @param moduleFilter Optional compiled-module ownership filter.
9
10
  * @returns The discover queue worker descriptors result.
10
11
  */
11
12
  export function discoverQueueWorkerDescriptors(compiledModules, options, logger, moduleFilter) {
@@ -17,18 +18,16 @@ export function discoverQueueWorkerDescriptors(compiledModules, options, logger,
17
18
  continue;
18
19
  }
19
20
  if (candidate.scope !== 'singleton') {
20
- logger.warn(`${candidate.targetType.name} in module ${candidate.moduleName} declares @QueueWorker() but is registered with ${candidate.scope} scope. Queue workers are registered only for singleton providers.`, 'QueueLifecycleService');
21
+ logger?.warn(`${candidate.targetType.name} in module ${candidate.moduleName} declares @QueueWorker() but is registered with ${candidate.scope} scope. Queue workers are registered only for singleton providers.`, 'QueueLifecycleService');
21
22
  continue;
22
23
  }
23
24
  const jobType = metadata.jobType;
24
25
  if (descriptorsByJobType.has(jobType)) {
25
- logger.warn(`Duplicate @QueueWorker() registration for job type ${jobType.name} was ignored in ${candidate.moduleName}.`, 'QueueLifecycleService');
26
- continue;
26
+ throw new Error(`Duplicate @QueueWorker() registration for job type ${jobType.name}.`);
27
27
  }
28
28
  const jobName = metadata.options.jobName ?? jobType.name;
29
29
  if (seenJobNames.has(jobName)) {
30
- logger.warn(`Duplicate queue job name ${jobName} was ignored in ${candidate.moduleName}.`, 'QueueLifecycleService');
31
- continue;
30
+ throw new Error(`Duplicate queue job name ${jobName}.`);
32
31
  }
33
32
  seenJobNames.add(jobName);
34
33
  descriptorsByJobType.set(jobType, createWorkerDescriptor(candidate.moduleName, candidate.token, candidate.targetType.name, metadata, jobName, options));
@@ -0,0 +1,10 @@
1
+ import type { ApplicationLogger, CompiledModule, ModuleType } from '@fluojs/runtime';
2
+ /**
3
+ * Validates queue registrations that would assign one BullMQ queue to workers in different DI scopes.
4
+ *
5
+ * @param compiledModules Complete compiled application module graph.
6
+ * @param logger Application logger used for compatibility diagnostics.
7
+ * @param validationModuleType Queue registration module that triggers this one-time application-wide validation.
8
+ */
9
+ export declare function assertUniqueQueueWorkerOwnership(compiledModules: readonly CompiledModule[], logger: ApplicationLogger, validationModuleType: ModuleType): void;
10
+ //# sourceMappingURL=worker-ownership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-ownership.d.ts","sourceRoot":"","sources":["../src/worker-ownership.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AA+HrF;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAC9C,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB,EACzB,oBAAoB,EAAE,UAAU,GAC/B,IAAI,CAmEN"}
@@ -0,0 +1,102 @@
1
+ import { QUEUE_MODULE_CONTEXT_MARKER } from './tokens.js';
2
+ import { discoverQueueWorkerDescriptors } from './worker-discovery.js';
3
+ const UNCONFIGURED_BACKEND_IDENTITY = '(unconfigured)';
4
+ function isQueueModuleContext(value) {
5
+ return typeof value === 'object' && value !== null && QUEUE_MODULE_CONTEXT_MARKER in value && value[QUEUE_MODULE_CONTEXT_MARKER] === true && 'moduleType' in value && typeof value.moduleType === 'function' && 'options' in value && typeof value.options === 'object' && value.options !== null && 'registrationTokens' in value && Array.isArray(value.registrationTokens) && 'scope' in value && typeof value.scope === 'string';
6
+ }
7
+ function collectQueueModuleContexts(compiledModules) {
8
+ const contexts = [];
9
+ for (const compiledModule of compiledModules) {
10
+ for (const provider of compiledModule.definition.providers ?? []) {
11
+ if (typeof provider !== 'object' || provider === null || !('useValue' in provider)) {
12
+ continue;
13
+ }
14
+ if (isQueueModuleContext(provider.useValue)) {
15
+ contexts.push(provider.useValue);
16
+ }
17
+ }
18
+ }
19
+ return contexts;
20
+ }
21
+ function canReachQueueRegistration(compiledModule, moduleContext, compiledModulesByType, visited = new Set()) {
22
+ if (visited.has(compiledModule.type)) {
23
+ return false;
24
+ }
25
+ visited.add(compiledModule.type);
26
+ for (const importedModuleType of compiledModule.definition.imports ?? []) {
27
+ if (importedModuleType === moduleContext.moduleType) {
28
+ return true;
29
+ }
30
+ const importedModule = compiledModulesByType.get(importedModuleType);
31
+ if (!importedModule || !moduleContext.registrationTokens.some(token => importedModule.exportedTokens.has(token))) {
32
+ continue;
33
+ }
34
+ if (canReachQueueRegistration(importedModule, moduleContext, compiledModulesByType, visited)) {
35
+ return true;
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+ function createQueueDiscoveryModuleFilter(compiledModules, moduleContext) {
41
+ if (moduleContext.options.global) {
42
+ return () => true;
43
+ }
44
+ const compiledModulesByType = new Map(compiledModules.map(compiledModule => [compiledModule.type, compiledModule]));
45
+ return compiledModule => canReachQueueRegistration(compiledModule, moduleContext, compiledModulesByType);
46
+ }
47
+ function assertUniqueQueueScopes(moduleContexts) {
48
+ const seenScopes = new Set();
49
+ for (const moduleContext of moduleContexts) {
50
+ if (seenScopes.has(moduleContext.scope)) {
51
+ throw new Error(`Duplicate @fluojs/queue scope "${moduleContext.scope}" registered. Provide a unique QueueModule.forRoot({ scope }) value for each scoped queue registration.`);
52
+ }
53
+ seenScopes.add(moduleContext.scope);
54
+ }
55
+ }
56
+ function canShareBackend(ownershipNamespace, existingOwner) {
57
+ return ownershipNamespace === undefined || existingOwner.ownershipNamespace === undefined || ownershipNamespace === existingOwner.ownershipNamespace;
58
+ }
59
+
60
+ /**
61
+ * Validates queue registrations that would assign one BullMQ queue to workers in different DI scopes.
62
+ *
63
+ * @param compiledModules Complete compiled application module graph.
64
+ * @param logger Application logger used for compatibility diagnostics.
65
+ * @param validationModuleType Queue registration module that triggers this one-time application-wide validation.
66
+ */
67
+ export function assertUniqueQueueWorkerOwnership(compiledModules, logger, validationModuleType) {
68
+ const moduleContexts = collectQueueModuleContexts(compiledModules);
69
+ assertUniqueQueueScopes(moduleContexts);
70
+ if (moduleContexts[0]?.moduleType !== validationModuleType) {
71
+ return;
72
+ }
73
+ const ownersByJobName = new Map();
74
+ for (const moduleContext of moduleContexts) {
75
+ const ownershipNamespace = moduleContext.options.ownershipNamespace;
76
+ if (ownershipNamespace === undefined) {
77
+ logger.warn(`Queue ownership namespace is unconfigured for scope "${moduleContext.scope}". Set QueueModule.forRoot({ ownershipNamespace }) to a stable identity shared only by registrations that use the same BullMQ backend.`, 'QueueLifecycleService');
78
+ }
79
+ const descriptors = discoverQueueWorkerDescriptors(compiledModules, moduleContext.options, undefined, createQueueDiscoveryModuleFilter(compiledModules, moduleContext));
80
+ for (const descriptor of descriptors.values()) {
81
+ const owners = ownersByJobName.get(descriptor.jobName) ?? [];
82
+ const compatibleOwners = owners.filter(owner => canShareBackend(ownershipNamespace, owner));
83
+ const existingOwner = compatibleOwners.find(owner => owner.ownershipEnforcement === 'reject') ?? compatibleOwners[0];
84
+ if (existingOwner) {
85
+ const backendIdentity = ownershipNamespace === undefined || existingOwner.ownershipNamespace === undefined ? UNCONFIGURED_BACKEND_IDENTITY : ownershipNamespace;
86
+ const message = `Cross-scope @fluojs/queue worker ownership collision for backend identity "${backendIdentity}" and jobName "${descriptor.jobName}" between scopes "${existingOwner.scope}" (${existingOwner.workerName} in ${existingOwner.moduleName}) and "${moduleContext.scope}" (${descriptor.workerName} in ${descriptor.moduleName}).`;
87
+ if (existingOwner.ownershipEnforcement === 'reject' || moduleContext.options.ownershipEnforcement === 'reject') {
88
+ throw new Error(`${message} Configure distinct ownershipNamespace or @QueueWorker(..., { jobName }) values.`);
89
+ }
90
+ logger.warn(`${message} Set matching QueueModule.forRoot({ ownershipNamespace }) values for registrations that share one BullMQ backend, then opt into ownershipEnforcement: "reject" to fail before BullMQ resources are created.`, 'QueueLifecycleService');
91
+ }
92
+ owners.push({
93
+ moduleName: descriptor.moduleName,
94
+ ownershipEnforcement: moduleContext.options.ownershipEnforcement,
95
+ ownershipNamespace,
96
+ scope: moduleContext.scope,
97
+ workerName: descriptor.workerName
98
+ });
99
+ ownersByJobName.set(descriptor.jobName, owners);
100
+ }
101
+ }
102
+ }
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "redis",
11
11
  "dlq"
12
12
  ],
13
- "version": "1.0.2",
13
+ "version": "3.0.0",
14
14
  "private": false,
15
15
  "license": "MIT",
16
16
  "repository": {
@@ -19,7 +19,7 @@
19
19
  "directory": "packages/queue"
20
20
  },
21
21
  "engines": {
22
- "node": ">=20.0.0"
22
+ "node": ">=24.0.0 <27"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"
@@ -37,14 +37,14 @@
37
37
  "dist"
38
38
  ],
39
39
  "dependencies": {
40
- "bullmq": "^5.58.0",
41
- "@fluojs/core": "^1.0.3",
42
- "@fluojs/di": "^1.1.0",
43
- "@fluojs/redis": "^1.0.2",
44
- "@fluojs/runtime": "^1.1.8"
40
+ "bullmq": "^5.81.1",
41
+ "@fluojs/core": "^2.0.0",
42
+ "@fluojs/di": "^3.0.0",
43
+ "@fluojs/redis": "^2.0.0",
44
+ "@fluojs/runtime": "^3.0.0"
45
45
  },
46
46
  "devDependencies": {
47
- "vitest": "^3.2.4"
47
+ "vitest": "^4.1.11"
48
48
  },
49
49
  "scripts": {
50
50
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",