@zintrust/workers 0.4.43 → 0.4.50
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/dist/WorkerFactory.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Central factory for creating workers with all advanced features
|
|
4
4
|
* Sealed namespace for immutability
|
|
5
5
|
*/
|
|
6
|
-
import
|
|
6
|
+
import * as ZintrustCoreModule from '@zintrust/core';
|
|
7
|
+
import { Cloudflare, createRedisConnection, databaseConfig, DatabaseConnectionRegistry, Env, ErrorFactory, generateUuid, getBullMQSafeQueueName, isFunction, isNonEmptyString, isObject, JobStateTracker, Logger, NodeSingletons, queueConfig, registerDatabasesFromRuntimeConfig, useEnsureDbConnected, workersConfig, ZintrustLang, } from '@zintrust/core';
|
|
7
8
|
import { Worker } from 'bullmq';
|
|
8
9
|
import { AutoScaler } from './AutoScaler.js';
|
|
9
10
|
import { CanaryController } from './CanaryController.js';
|
|
@@ -34,6 +35,52 @@ const canUseProjectFileImports = () => typeof NodeSingletons?.fs?.writeFileSync
|
|
|
34
35
|
typeof NodeSingletons?.fs?.existsSync === 'function' &&
|
|
35
36
|
typeof NodeSingletons?.url?.pathToFileURL === 'function' &&
|
|
36
37
|
typeof NodeSingletons?.path?.join === 'function';
|
|
38
|
+
const getProcessorPackageBridgeGlobal = () => {
|
|
39
|
+
return globalThis;
|
|
40
|
+
};
|
|
41
|
+
const isValidBridgeExportName = (value) => {
|
|
42
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(value);
|
|
43
|
+
};
|
|
44
|
+
const resolveRuntimeBridgeModule = (specifier) => {
|
|
45
|
+
if (specifier === '@zintrust/core') {
|
|
46
|
+
return ZintrustCoreModule;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
const resolveRuntimeBridgeUrl = (specifier) => {
|
|
51
|
+
if (!isNodeRuntime() || !canUseProjectFileImports())
|
|
52
|
+
return null;
|
|
53
|
+
const bridgeModule = resolveRuntimeBridgeModule(specifier);
|
|
54
|
+
if (bridgeModule === null)
|
|
55
|
+
return null;
|
|
56
|
+
const dir = ensureProcessorSpecDir();
|
|
57
|
+
if (dir === null)
|
|
58
|
+
return null;
|
|
59
|
+
const bridgeGlobal = getProcessorPackageBridgeGlobal();
|
|
60
|
+
bridgeGlobal.__zintrustProcessorPackageBridges__ ??= new Map();
|
|
61
|
+
bridgeGlobal.__zintrustProcessorPackageBridges__.set(specifier, bridgeModule);
|
|
62
|
+
const safeName = specifier.replaceAll('@', '').replaceAll('/', '-');
|
|
63
|
+
const filePath = path.join(dir, `${safeName}.bridge.mjs`);
|
|
64
|
+
const exportLines = Object.keys(bridgeModule)
|
|
65
|
+
.filter((key) => key !== 'default' && isValidBridgeExportName(key))
|
|
66
|
+
.sort((a, b) => a.localeCompare(b))
|
|
67
|
+
.map((key) => `export const ${key} = bridge[${JSON.stringify(key)}];`);
|
|
68
|
+
const code = [
|
|
69
|
+
'const bridgeMap = globalThis.__zintrustProcessorPackageBridges__;',
|
|
70
|
+
`const bridge = bridgeMap?.get(${JSON.stringify(specifier)}) ?? {};`,
|
|
71
|
+
'export default bridge;',
|
|
72
|
+
...exportLines,
|
|
73
|
+
'',
|
|
74
|
+
].join('\n');
|
|
75
|
+
try {
|
|
76
|
+
NodeSingletons.fs.writeFileSync(filePath, code, 'utf8');
|
|
77
|
+
return NodeSingletons.url.pathToFileURL(filePath).href;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
Logger.debug(`Failed to write processor bridge for ${specifier}`, error);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
37
84
|
const buildCandidatesForSpecifier = (specifier, root) => {
|
|
38
85
|
if (specifier === '@zintrust/core') {
|
|
39
86
|
return [
|
|
@@ -80,6 +127,9 @@ const resolveLocalPackageFallback = (specifier) => {
|
|
|
80
127
|
const resolvePackageSpecifierUrl = (specifier) => {
|
|
81
128
|
if (!isNodeRuntime() || !canUseProjectFileImports())
|
|
82
129
|
return null;
|
|
130
|
+
const bridgeUrl = resolveRuntimeBridgeUrl(specifier);
|
|
131
|
+
if (bridgeUrl)
|
|
132
|
+
return bridgeUrl;
|
|
83
133
|
if (typeof NodeSingletons?.module?.createRequire !== 'function') {
|
|
84
134
|
return resolveLocalPackageFallback(specifier);
|
|
85
135
|
}
|
|
@@ -1494,19 +1544,25 @@ const resolvePersistenceConfig = (config) => {
|
|
|
1494
1544
|
throw ErrorFactory.createConfigError('WORKER_PERSISTENCE_DRIVER must be one of memory, redis, or database');
|
|
1495
1545
|
};
|
|
1496
1546
|
const resolveDbClientFromEnv = async (connectionName = 'default') => {
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1547
|
+
// Eagerly populate the registry when the requested connection is not yet
|
|
1548
|
+
// registered. On both the Cloudflare Workers runtime and Node, the registry
|
|
1549
|
+
// may be empty when called from the workers-persistence path because
|
|
1550
|
+
// registerDatabasesFromRuntimeConfig has not yet run for this connection. The
|
|
1551
|
+
// old pattern of connecting first (which always fails) then registering as a
|
|
1552
|
+
// fallback produced spurious [DEBUG] noise on every fresh start.
|
|
1553
|
+
if (DatabaseConnectionRegistry.get(connectionName) === undefined) {
|
|
1554
|
+
try {
|
|
1555
|
+
registerDatabasesFromRuntimeConfig(databaseConfig);
|
|
1556
|
+
}
|
|
1557
|
+
catch (registrationError) {
|
|
1558
|
+
Logger.warn(`[WorkerPersistence] Runtime database registration failed for connection '${connectionName}'`, registrationError);
|
|
1559
|
+
}
|
|
1503
1560
|
}
|
|
1504
1561
|
try {
|
|
1505
|
-
|
|
1506
|
-
return await connect();
|
|
1562
|
+
return await useEnsureDbConnected(undefined, connectionName);
|
|
1507
1563
|
}
|
|
1508
1564
|
catch (error) {
|
|
1509
|
-
Logger.error('Worker persistence failed
|
|
1565
|
+
Logger.error('Worker persistence failed to resolve database connection', error);
|
|
1510
1566
|
throw ErrorFactory.createConfigError(`Worker persistence requires a database client. Register connection '${connectionName}' or pass infrastructure.persistence.client.`);
|
|
1511
1567
|
}
|
|
1512
1568
|
};
|
|
@@ -457,6 +457,12 @@ async function getRedisQueueData() {
|
|
|
457
457
|
throw ErrorFactory.createConfigError('Redis driver not configured');
|
|
458
458
|
}
|
|
459
459
|
const monitor = QueueMonitor.create({
|
|
460
|
+
knownQueues: async () => {
|
|
461
|
+
const records = await WorkerFactory.listPersistedRecords();
|
|
462
|
+
return Array.from(new Set(records
|
|
463
|
+
.map((record) => record.queueName)
|
|
464
|
+
.filter((queueName) => typeof queueName === 'string'))).sort((left, right) => left.localeCompare(right));
|
|
465
|
+
},
|
|
460
466
|
redis: {
|
|
461
467
|
host: redisConfig.host || 'localhost',
|
|
462
468
|
port: redisConfig.port || 6379,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/workers",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.50",
|
|
4
4
|
"description": "Worker orchestration and background job management for ZinTrust.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=20.0.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@zintrust/core": "
|
|
43
|
+
"@zintrust/core": ">=0.4.0 <0.5.0",
|
|
44
44
|
"@zintrust/queue-monitor": "*",
|
|
45
45
|
"@zintrust/queue-redis": "*"
|
|
46
46
|
},
|
|
@@ -73,4 +73,4 @@
|
|
|
73
73
|
"prom-client": "^15.1.3",
|
|
74
74
|
"simple-statistics": "^7.8.9"
|
|
75
75
|
}
|
|
76
|
-
}
|
|
76
|
+
}
|
package/src/WorkerFactory.ts
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Sealed namespace for immutability
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import * as ZintrustCoreModule from '@zintrust/core';
|
|
7
8
|
import {
|
|
8
9
|
Cloudflare,
|
|
9
10
|
createRedisConnection,
|
|
10
11
|
databaseConfig,
|
|
12
|
+
DatabaseConnectionRegistry,
|
|
11
13
|
Env,
|
|
12
14
|
ErrorFactory,
|
|
13
15
|
generateUuid,
|
|
@@ -55,6 +57,10 @@ import {
|
|
|
55
57
|
|
|
56
58
|
const path = NodeSingletons.path;
|
|
57
59
|
|
|
60
|
+
type ProcessorPackageBridgeGlobal = typeof globalThis & {
|
|
61
|
+
__zintrustProcessorPackageBridges__?: Map<string, Record<string, unknown>>;
|
|
62
|
+
};
|
|
63
|
+
|
|
58
64
|
const isNodeRuntime = (): boolean =>
|
|
59
65
|
typeof process !== 'undefined' && Boolean(process.versions?.node);
|
|
60
66
|
|
|
@@ -70,6 +76,59 @@ const canUseProjectFileImports = (): boolean =>
|
|
|
70
76
|
typeof NodeSingletons?.url?.pathToFileURL === 'function' &&
|
|
71
77
|
typeof NodeSingletons?.path?.join === 'function';
|
|
72
78
|
|
|
79
|
+
const getProcessorPackageBridgeGlobal = (): ProcessorPackageBridgeGlobal => {
|
|
80
|
+
return globalThis as ProcessorPackageBridgeGlobal;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const isValidBridgeExportName = (value: string): boolean => {
|
|
84
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(value);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const resolveRuntimeBridgeModule = (specifier: string): Record<string, unknown> | null => {
|
|
88
|
+
if (specifier === '@zintrust/core') {
|
|
89
|
+
return ZintrustCoreModule as Record<string, unknown>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const resolveRuntimeBridgeUrl = (specifier: string): string | null => {
|
|
96
|
+
if (!isNodeRuntime() || !canUseProjectFileImports()) return null;
|
|
97
|
+
|
|
98
|
+
const bridgeModule = resolveRuntimeBridgeModule(specifier);
|
|
99
|
+
if (bridgeModule === null) return null;
|
|
100
|
+
|
|
101
|
+
const dir = ensureProcessorSpecDir();
|
|
102
|
+
if (dir === null) return null;
|
|
103
|
+
|
|
104
|
+
const bridgeGlobal = getProcessorPackageBridgeGlobal();
|
|
105
|
+
bridgeGlobal.__zintrustProcessorPackageBridges__ ??= new Map<string, Record<string, unknown>>();
|
|
106
|
+
bridgeGlobal.__zintrustProcessorPackageBridges__.set(specifier, bridgeModule);
|
|
107
|
+
|
|
108
|
+
const safeName = specifier.replaceAll('@', '').replaceAll('/', '-');
|
|
109
|
+
const filePath = path.join(dir, `${safeName}.bridge.mjs`);
|
|
110
|
+
const exportLines = Object.keys(bridgeModule)
|
|
111
|
+
.filter((key) => key !== 'default' && isValidBridgeExportName(key))
|
|
112
|
+
.sort((a, b) => a.localeCompare(b))
|
|
113
|
+
.map((key) => `export const ${key} = bridge[${JSON.stringify(key)}];`);
|
|
114
|
+
|
|
115
|
+
const code = [
|
|
116
|
+
'const bridgeMap = globalThis.__zintrustProcessorPackageBridges__;',
|
|
117
|
+
`const bridge = bridgeMap?.get(${JSON.stringify(specifier)}) ?? {};`,
|
|
118
|
+
'export default bridge;',
|
|
119
|
+
...exportLines,
|
|
120
|
+
'',
|
|
121
|
+
].join('\n');
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
NodeSingletons.fs.writeFileSync(filePath, code, 'utf8');
|
|
125
|
+
return NodeSingletons.url.pathToFileURL(filePath).href;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
Logger.debug(`Failed to write processor bridge for ${specifier}`, error);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
73
132
|
const buildCandidatesForSpecifier = (specifier: string, root: string): string[] => {
|
|
74
133
|
if (specifier === '@zintrust/core') {
|
|
75
134
|
return [
|
|
@@ -117,6 +176,10 @@ const resolveLocalPackageFallback = (specifier: string): string | null => {
|
|
|
117
176
|
|
|
118
177
|
const resolvePackageSpecifierUrl = (specifier: string): string | null => {
|
|
119
178
|
if (!isNodeRuntime() || !canUseProjectFileImports()) return null;
|
|
179
|
+
|
|
180
|
+
const bridgeUrl = resolveRuntimeBridgeUrl(specifier);
|
|
181
|
+
if (bridgeUrl) return bridgeUrl;
|
|
182
|
+
|
|
120
183
|
if (typeof NodeSingletons?.module?.createRequire !== 'function') {
|
|
121
184
|
return resolveLocalPackageFallback(specifier);
|
|
122
185
|
}
|
|
@@ -2164,20 +2227,27 @@ const resolvePersistenceConfig = (
|
|
|
2164
2227
|
};
|
|
2165
2228
|
|
|
2166
2229
|
const resolveDbClientFromEnv = async (connectionName = 'default'): Promise<IDatabase> => {
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2230
|
+
// Eagerly populate the registry when the requested connection is not yet
|
|
2231
|
+
// registered. On both the Cloudflare Workers runtime and Node, the registry
|
|
2232
|
+
// may be empty when called from the workers-persistence path because
|
|
2233
|
+
// registerDatabasesFromRuntimeConfig has not yet run for this connection. The
|
|
2234
|
+
// old pattern of connecting first (which always fails) then registering as a
|
|
2235
|
+
// fallback produced spurious [DEBUG] noise on every fresh start.
|
|
2236
|
+
if (DatabaseConnectionRegistry.get(connectionName) === undefined) {
|
|
2237
|
+
try {
|
|
2238
|
+
registerDatabasesFromRuntimeConfig(databaseConfig);
|
|
2239
|
+
} catch (registrationError) {
|
|
2240
|
+
Logger.warn(
|
|
2241
|
+
`[WorkerPersistence] Runtime database registration failed for connection '${connectionName}'`,
|
|
2242
|
+
registrationError
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2174
2245
|
}
|
|
2175
2246
|
|
|
2176
2247
|
try {
|
|
2177
|
-
|
|
2178
|
-
return await connect();
|
|
2248
|
+
return await useEnsureDbConnected(undefined, connectionName);
|
|
2179
2249
|
} catch (error) {
|
|
2180
|
-
Logger.error('Worker persistence failed
|
|
2250
|
+
Logger.error('Worker persistence failed to resolve database connection', error);
|
|
2181
2251
|
throw ErrorFactory.createConfigError(
|
|
2182
2252
|
`Worker persistence requires a database client. Register connection '${connectionName}' or pass infrastructure.persistence.client.`
|
|
2183
2253
|
);
|
|
@@ -591,6 +591,16 @@ async function getRedisQueueData(): Promise<QueueData> {
|
|
|
591
591
|
}
|
|
592
592
|
|
|
593
593
|
const monitor = QueueMonitor.create({
|
|
594
|
+
knownQueues: async () => {
|
|
595
|
+
const records = await WorkerFactory.listPersistedRecords();
|
|
596
|
+
return Array.from(
|
|
597
|
+
new Set(
|
|
598
|
+
records
|
|
599
|
+
.map((record) => record.queueName)
|
|
600
|
+
.filter((queueName) => typeof queueName === 'string')
|
|
601
|
+
)
|
|
602
|
+
).sort((left, right) => left.localeCompare(right));
|
|
603
|
+
},
|
|
594
604
|
redis: {
|
|
595
605
|
host: redisConfig.host || 'localhost',
|
|
596
606
|
port: redisConfig.port || 6379,
|
|
@@ -24,6 +24,9 @@ declare module '@zintrust/queue-monitor' {
|
|
|
24
24
|
autoRefresh?: boolean;
|
|
25
25
|
refreshIntervalMs?: number;
|
|
26
26
|
redis?: Record<string, unknown>;
|
|
27
|
+
knownQueues?:
|
|
28
|
+
| ReadonlyArray<string>
|
|
29
|
+
| (() => Promise<ReadonlyArray<string>> | ReadonlyArray<string>);
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
export type QueueMonitorApi = {
|