@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.
@@ -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 { Cloudflare, createRedisConnection, databaseConfig, Env, ErrorFactory, generateUuid, getBullMQSafeQueueName, isFunction, isNonEmptyString, isObject, JobStateTracker, Logger, NodeSingletons, queueConfig, registerDatabasesFromRuntimeConfig, useEnsureDbConnected, workersConfig, ZintrustLang, } from '@zintrust/core';
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
- const connect = async () => await useEnsureDbConnected(undefined, connectionName);
1498
- try {
1499
- return await connect();
1500
- }
1501
- catch (error) {
1502
- Logger.error('Worker persistence failed to resolve database connection', error);
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
- registerDatabasesFromRuntimeConfig(databaseConfig);
1506
- return await connect();
1562
+ return await useEnsureDbConnected(undefined, connectionName);
1507
1563
  }
1508
1564
  catch (error) {
1509
- Logger.error('Worker persistence failed after registering runtime databases', error);
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.43",
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": "^0.4.43",
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
+ }
@@ -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
- const connect = async (): Promise<IDatabase> =>
2168
- await useEnsureDbConnected(undefined, connectionName);
2169
-
2170
- try {
2171
- return await connect();
2172
- } catch (error) {
2173
- Logger.error('Worker persistence failed to resolve database connection', error);
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
- registerDatabasesFromRuntimeConfig(databaseConfig);
2178
- return await connect();
2248
+ return await useEnsureDbConnected(undefined, connectionName);
2179
2249
  } catch (error) {
2180
- Logger.error('Worker persistence failed after registering runtime databases', error);
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 = {