@zintrust/workers 0.1.31 → 0.1.52

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.
Files changed (56) hide show
  1. package/dist/ClusterLock.js +3 -2
  2. package/dist/DeadLetterQueue.js +3 -2
  3. package/dist/HealthMonitor.js +24 -13
  4. package/dist/Observability.js +8 -0
  5. package/dist/WorkerFactory.d.ts +4 -0
  6. package/dist/WorkerFactory.js +409 -42
  7. package/dist/WorkerInit.js +122 -43
  8. package/dist/WorkerMetrics.js +5 -1
  9. package/dist/WorkerRegistry.js +8 -0
  10. package/dist/WorkerShutdown.d.ts +0 -13
  11. package/dist/WorkerShutdown.js +1 -44
  12. package/dist/build-manifest.json +101 -85
  13. package/dist/config/workerConfig.d.ts +1 -0
  14. package/dist/config/workerConfig.js +7 -1
  15. package/dist/createQueueWorker.js +281 -42
  16. package/dist/dashboard/workers-api.js +8 -1
  17. package/dist/http/WorkerController.js +90 -35
  18. package/dist/http/WorkerMonitoringService.js +29 -2
  19. package/dist/http/middleware/FeaturesValidator.js +5 -4
  20. package/dist/index.d.ts +1 -2
  21. package/dist/index.js +0 -1
  22. package/dist/routes/workers.js +10 -7
  23. package/dist/storage/WorkerStore.d.ts +6 -3
  24. package/dist/storage/WorkerStore.js +16 -0
  25. package/dist/telemetry/api/TelemetryMonitoringService.js +29 -2
  26. package/dist/ui/router/ui.js +58 -29
  27. package/dist/ui/workers/index.html +202 -0
  28. package/dist/ui/workers/main.js +1952 -0
  29. package/dist/ui/workers/styles.css +1350 -0
  30. package/dist/ui/workers/zintrust.svg +30 -0
  31. package/package.json +5 -5
  32. package/src/ClusterLock.ts +13 -7
  33. package/src/ComplianceManager.ts +3 -2
  34. package/src/DeadLetterQueue.ts +6 -4
  35. package/src/HealthMonitor.ts +33 -17
  36. package/src/Observability.ts +11 -0
  37. package/src/WorkerFactory.ts +480 -43
  38. package/src/WorkerInit.ts +167 -48
  39. package/src/WorkerMetrics.ts +14 -8
  40. package/src/WorkerRegistry.ts +11 -0
  41. package/src/WorkerShutdown.ts +1 -69
  42. package/src/config/workerConfig.ts +9 -1
  43. package/src/createQueueWorker.ts +428 -43
  44. package/src/dashboard/workers-api.ts +8 -1
  45. package/src/http/WorkerController.ts +111 -36
  46. package/src/http/WorkerMonitoringService.ts +35 -2
  47. package/src/http/middleware/FeaturesValidator.ts +8 -19
  48. package/src/index.ts +2 -3
  49. package/src/routes/workers.ts +10 -8
  50. package/src/storage/WorkerStore.ts +21 -3
  51. package/src/telemetry/api/TelemetryMonitoringService.ts +35 -2
  52. package/src/types/queue-monitor.d.ts +2 -1
  53. package/src/ui/components/WorkerExpandPanel.js +0 -8
  54. package/src/ui/router/EmbeddedAssets.ts +3 -0
  55. package/src/ui/router/ui.ts +57 -39
  56. package/src/WorkerShutdownDurableObject.ts +0 -64
package/src/WorkerInit.ts CHANGED
@@ -8,10 +8,12 @@
8
8
  * - Ensures graceful startup and shutdown
9
9
  */
10
10
 
11
- import { Env, Logger, workersConfig } from '@zintrust/core';
11
+ import { Env, Logger } from '@zintrust/core';
12
12
  import { ResourceMonitor } from './ResourceMonitor';
13
+ import type { WorkerPersistenceConfig } from './WorkerFactory';
13
14
  import { WorkerFactory } from './WorkerFactory';
14
15
  import { WorkerShutdown } from './WorkerShutdown';
16
+ import { keyPrefix } from './config/workerConfig';
15
17
 
16
18
  // ============================================================================
17
19
  // Types
@@ -105,6 +107,22 @@ function initializeResourceMonitoring(
105
107
  return false;
106
108
  }
107
109
 
110
+ const getPersistenceOverride = (driver: string): WorkerPersistenceConfig => {
111
+ if (driver === 'redis') {
112
+ return { driver: 'redis', keyPrefix: keyPrefix() };
113
+ }
114
+
115
+ if (driver === 'memory') {
116
+ return { driver: 'memory' };
117
+ }
118
+
119
+ return {
120
+ driver: 'database',
121
+ connection: Env.get('WORKER_PERSISTENCE_DB_CONNECTION', 'default') ?? 'default',
122
+ table: Env.get('WORKER_PERSISTENCE_TABLE', 'zintrust_workers') ?? 'zintrust_workers',
123
+ };
124
+ };
125
+
108
126
  /**
109
127
  * Check if any workers have resource monitoring enabled
110
128
  */
@@ -120,6 +138,146 @@ function shouldStartResourceMonitoring(): boolean {
120
138
  }
121
139
  }
122
140
 
141
+ type AutoStartCandidate = {
142
+ name: string;
143
+ autoStart: boolean;
144
+ activeStatus?: boolean;
145
+ };
146
+
147
+ type PersistenceOverride = WorkerPersistenceConfig;
148
+
149
+ type AutoStartTask = AutoStartCandidate & {
150
+ persistenceOverride: PersistenceOverride;
151
+ source: 'database' | 'redis' | 'memory';
152
+ };
153
+
154
+ const resolveAutoStartCandidates = (records: AutoStartCandidate[]): AutoStartCandidate[] => {
155
+ return records.filter((record) => record.activeStatus !== false && record.autoStart === true);
156
+ };
157
+
158
+ const resolvePersistenceTargets = (): Array<{
159
+ source: 'database' | 'redis' | 'memory';
160
+ persistenceOverride: PersistenceOverride;
161
+ }> => {
162
+ const configuredDriver = (Env.get('WORKER_PERSISTENCE_DRIVER', 'memory') || '')
163
+ .toLowerCase()
164
+ .trim();
165
+ const targets: Array<{
166
+ source: 'database' | 'redis' | 'memory';
167
+ persistenceOverride: PersistenceOverride;
168
+ }> =
169
+ configuredDriver === 'database'
170
+ ? [
171
+ { source: 'database', persistenceOverride: getPersistenceOverride('database') },
172
+ { source: 'redis', persistenceOverride: getPersistenceOverride('redis') },
173
+ { source: 'memory', persistenceOverride: { driver: 'memory' } },
174
+ ]
175
+ : [
176
+ { source: 'redis', persistenceOverride: getPersistenceOverride('redis') },
177
+ { source: 'memory', persistenceOverride: { driver: 'memory' } },
178
+ ];
179
+
180
+ // Sort so the configured driver comes first (priority)
181
+ return targets.sort((a, b) => {
182
+ const aIsConfigured = a.persistenceOverride.driver === configuredDriver;
183
+ const bIsConfigured = b.persistenceOverride.driver === configuredDriver;
184
+
185
+ if (aIsConfigured && !bIsConfigured) return -1;
186
+ if (!aIsConfigured && bIsConfigured) return 1;
187
+ return 0;
188
+ });
189
+ };
190
+
191
+ const collectAutoStartTasks = async (): Promise<AutoStartTask[]> => {
192
+ const targets = resolvePersistenceTargets();
193
+ const tasks: AutoStartTask[] = [];
194
+ const seenWorkerNames = new Set<string>();
195
+
196
+ for (const target of targets) {
197
+ try {
198
+ // eslint-disable-next-line no-await-in-loop
199
+ const records = await WorkerFactory.listPersistedRecords(target.persistenceOverride);
200
+ const candidates = resolveAutoStartCandidates(records);
201
+
202
+ Logger.debug('Auto-start discovery', {
203
+ source: target.source,
204
+ totalRecords: records.length,
205
+ candidateCount: candidates.length,
206
+ });
207
+
208
+ for (const record of candidates) {
209
+ if (seenWorkerNames.has(record.name)) {
210
+ Logger.warn(
211
+ `Worker ${record.name} appears in multiple persistence stores; keeping first discovered source and skipping duplicate from ${target.source}.`
212
+ );
213
+ continue;
214
+ }
215
+
216
+ seenWorkerNames.add(record.name);
217
+ tasks.push({
218
+ ...record,
219
+ persistenceOverride: target.persistenceOverride,
220
+ source: target.source,
221
+ });
222
+ }
223
+ } catch (error) {
224
+ const message = error instanceof Error ? error.message : String(error);
225
+ Logger.warn(`Auto-start discovery failed for ${target.source} persistence: ${message}`);
226
+ }
227
+ }
228
+
229
+ return tasks;
230
+ };
231
+
232
+ const isWorkerTrulyRunning = async (name: string): Promise<boolean> => {
233
+ const existing = WorkerFactory.get(name);
234
+ if (!existing) return false;
235
+
236
+ const workerLike = existing.worker as {
237
+ isRunning?: () => boolean | Promise<boolean>;
238
+ isPaused?: () => boolean;
239
+ };
240
+
241
+ const isRunning =
242
+ typeof workerLike.isRunning === 'function'
243
+ ? await Promise.resolve(workerLike.isRunning())
244
+ : false;
245
+ const isPaused = typeof workerLike.isPaused === 'function' ? workerLike.isPaused() : false;
246
+ return isRunning && !isPaused;
247
+ };
248
+
249
+ const autoStartOneWorker = async (
250
+ record: AutoStartTask
251
+ ): Promise<{ name: string; started: boolean; skipped: boolean }> => {
252
+ const existing = WorkerFactory.get(record.name);
253
+ if (existing) {
254
+ try {
255
+ if (await isWorkerTrulyRunning(record.name)) {
256
+ return { name: record.name, started: false, skipped: true };
257
+ }
258
+
259
+ Logger.warn(
260
+ `Worker ${record.name} was registered but not truly running. Restarting to recover from stale state.`
261
+ );
262
+ await WorkerFactory.restart(record.name, record.persistenceOverride);
263
+ return { name: record.name, started: true, skipped: false };
264
+ } catch (error) {
265
+ const message = error instanceof Error ? error.message : String(error);
266
+ Logger.warn(`Auto-start recovery failed for worker ${record.name}: ${message}`);
267
+ return { name: record.name, started: false, skipped: false };
268
+ }
269
+ }
270
+
271
+ try {
272
+ await WorkerFactory.startFromPersisted(record.name, record.persistenceOverride);
273
+ return { name: record.name, started: true, skipped: false };
274
+ } catch (error) {
275
+ const message = error instanceof Error ? error.message : String(error);
276
+ Logger.warn(`Auto-start failed for worker ${record.name}: ${message}`);
277
+ return { name: record.name, started: false, skipped: false };
278
+ }
279
+ };
280
+
123
281
  /**
124
282
  * Initialize the worker management system
125
283
  */
@@ -185,62 +343,23 @@ async function initialize(options: IWorkerInitOptions = {}): Promise<void> {
185
343
  }
186
344
 
187
345
  async function autoStartPersistedWorkers(): Promise<void> {
188
- // Check if auto-start is enabled globally via environment variable
346
+ const envAutoStart = Env.getBool('WORKER_AUTO_START', false);
347
+ const shouldAutoStart = envAutoStart;
348
+
189
349
  Logger.debug('Auto-start check', {
190
- envAutoStart: Env.getBool('WORKER_AUTO_START', false),
191
- configAutoStart: workersConfig.defaultWorker?.autoStart,
350
+ envAutoStart,
351
+ shouldAutoStart,
192
352
  });
193
353
 
194
- if (workersConfig.defaultWorker?.autoStart !== true) {
354
+ if (!shouldAutoStart) {
195
355
  Logger.debug('Auto-start disabled - WORKER_AUTO_START is not true');
196
356
  return;
197
357
  }
198
358
 
199
359
  try {
200
- const records = await WorkerFactory.listPersistedRecords();
201
- Logger.debug('Found persisted records', {
202
- count: records.length,
203
- records: records.map((r) => ({ name: r.name, autoStart: r.autoStart })),
204
- });
205
-
206
- const candidates = records.filter((record) => {
207
- if (record.activeStatus === false) {
208
- return false;
209
- }
210
- // If autoStart is explicitly true, always include
211
- if (record.autoStart === true) {
212
- return true;
213
- }
214
- // If autoStart is null or undefined and global auto-start is enabled, include
215
- if (
216
- (record.autoStart === null || record.autoStart === undefined) &&
217
- workersConfig.defaultWorker?.autoStart === true
218
- ) {
219
- return true;
220
- }
360
+ const candidates = await collectAutoStartTasks();
221
361
 
222
- return false;
223
- });
224
-
225
- Logger.debug('Auto-start candidates', {
226
- count: candidates.length,
227
- candidates: candidates.map((c) => c.name),
228
- });
229
- const results = await Promise.all(
230
- candidates.map(async (record) => {
231
- if (WorkerFactory.get(record.name)) {
232
- return { name: record.name, started: false, skipped: true };
233
- }
234
- try {
235
- await WorkerFactory.startFromPersisted(record.name);
236
- return { name: record.name, started: true, skipped: false };
237
- } catch (error) {
238
- const message = error instanceof Error ? error.message : String(error);
239
- Logger.warn(`Auto-start failed for worker ${record.name}: ${message}`);
240
- return { name: record.name, started: false, skipped: false };
241
- }
242
- })
243
- );
362
+ const results = await Promise.all(candidates.map(async (record) => autoStartOneWorker(record)));
244
363
 
245
364
  const startedCount = results.filter((item) => item.started).length;
246
365
  const skippedCount = results.filter((item) => item.skipped).length;
@@ -11,8 +11,9 @@ import {
11
11
  createRedisConnection,
12
12
  type RedisConfig,
13
13
  } from '@zintrust/core';
14
- import type IORedis from 'ioredis';
15
- import type { ChainableCommander } from 'ioredis';
14
+
15
+ type RedisConnection = ReturnType<typeof createRedisConnection>;
16
+ type RedisPipeline = ReturnType<RedisConnection['pipeline']>;
16
17
 
17
18
  export type MetricType =
18
19
  | 'processed'
@@ -96,14 +97,14 @@ const runInBatches = async <T>(
96
97
  };
97
98
 
98
99
  // Internal state
99
- let redisClient: IORedis | null = null;
100
+ let redisClient: RedisConnection | null = null;
100
101
  let cachedConfig: RedisConfig | null = null;
101
102
  let keepLoggin = 0;
102
103
 
103
104
  /**
104
105
  * Helper: Get valid Redis client
105
106
  */
106
- const getValidClient = async (): Promise<IORedis> => {
107
+ const getValidClient = async (): Promise<RedisConnection> => {
107
108
  if (!cachedConfig) {
108
109
  throw ErrorFactory.createWorkerError('WorkerMetrics not initialized. Call initialize() first.');
109
110
  }
@@ -113,7 +114,12 @@ const getValidClient = async (): Promise<IORedis> => {
113
114
  redisClient = createRedisConnection(cachedConfig);
114
115
  }
115
116
 
116
- return redisClient;
117
+ const client = redisClient;
118
+ if (!client) {
119
+ throw ErrorFactory.createConnectionError('Failed to initialize Redis client');
120
+ }
121
+
122
+ return client;
117
123
  };
118
124
 
119
125
  /**
@@ -166,7 +172,7 @@ const roundTimestamp = (date: Date, granularity: MetricGranularity): Date => {
166
172
  * Helper: Clean up old metrics based on retention policy
167
173
  */
168
174
  const cleanupOldMetrics = async (
169
- client: IORedis,
175
+ client: RedisConnection,
170
176
  key: string,
171
177
  granularity: MetricGranularity
172
178
  ): Promise<void> => {
@@ -280,9 +286,9 @@ const handleUninitializedMetrics = (optionsList: MetricQueryOptions[]): Aggregat
280
286
  * Helper: Build Redis pipeline for batch metrics query
281
287
  */
282
288
  const buildMetricsPipeline = (
283
- client: IORedis,
289
+ client: RedisConnection,
284
290
  optionsList: MetricQueryOptions[]
285
- ): ChainableCommander => {
291
+ ): RedisPipeline => {
286
292
  const pipeline = client.pipeline();
287
293
 
288
294
  for (const options of optionsList) {
@@ -82,6 +82,13 @@ const registrations = new Map<string, RegisterWorkerOptions>();
82
82
  const STOPPED_WORKER_CLEANUP_DELAY = 5 * 60 * 1000; // 5 minutes
83
83
  const cleanupTimers = new Map<string, NodeJS.Timeout>();
84
84
 
85
+ type UnrefableTimer = { unref: () => void };
86
+
87
+ const isUnrefableTimer = (value: unknown): value is UnrefableTimer => {
88
+ if (typeof value !== 'object' || value === null) return false;
89
+ return 'unref' in value && typeof (value as UnrefableTimer).unref === 'function';
90
+ };
91
+
85
92
  /**
86
93
  * Helper: Schedule cleanup of stopped worker
87
94
  */
@@ -109,6 +116,10 @@ const scheduleStoppedWorkerCleanup = (name: string): void => {
109
116
  }
110
117
  }, STOPPED_WORKER_CLEANUP_DELAY);
111
118
 
119
+ if (isUnrefableTimer(timer)) {
120
+ timer.unref();
121
+ }
122
+
112
123
  cleanupTimers.set(name, timer);
113
124
  };
114
125
 
@@ -5,7 +5,7 @@
5
5
  * Coordinates orderly shutdown of all worker modules and the WorkerFactory.
6
6
  */
7
7
 
8
- import { Cloudflare, Logger } from '@zintrust/core';
8
+ import { Logger } from '@zintrust/core';
9
9
  import { WorkerFactory } from './WorkerFactory';
10
10
 
11
11
  // ============================================================================
@@ -36,12 +36,6 @@ interface IShutdownState {
36
36
  reason: string | null;
37
37
  }
38
38
 
39
- type DurableShutdownState = {
40
- shuttingDown: boolean;
41
- startedAt?: string;
42
- reason?: string;
43
- };
44
-
45
39
  // ============================================================================
46
40
  // Implementation
47
41
  // ============================================================================
@@ -53,58 +47,6 @@ const state: IShutdownState = {
53
47
  reason: null,
54
48
  };
55
49
 
56
- const getDurableShutdownStub = (): {
57
- fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
58
- } | null => {
59
- const env = Cloudflare.getWorkersEnv();
60
- if (env === null) return null;
61
-
62
- const namespace = env['WORKER_SHUTDOWN'] as
63
- | {
64
- idFromName?: (name: string) => unknown;
65
- get?: (id: unknown) => {
66
- fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
67
- };
68
- }
69
- | undefined;
70
-
71
- if (
72
- !namespace ||
73
- typeof namespace.idFromName !== 'function' ||
74
- typeof namespace.get !== 'function'
75
- ) {
76
- return null;
77
- }
78
-
79
- const id = namespace.idFromName('zintrust-shutdown');
80
- return namespace.get(id) ?? null;
81
- };
82
-
83
- const requestDurableShutdown = async (reason = 'manual'): Promise<boolean> => {
84
- const stub = getDurableShutdownStub();
85
- if (!stub) {
86
- Logger.warn('Worker shutdown Durable Object binding not configured');
87
- return false;
88
- }
89
-
90
- const res = await stub.fetch('https://worker-shutdown/shutdown', {
91
- method: 'POST',
92
- headers: { 'content-type': 'application/json' },
93
- body: JSON.stringify({ reason }),
94
- });
95
-
96
- return res.ok;
97
- };
98
-
99
- const getDurableShutdownState = async (): Promise<DurableShutdownState | null> => {
100
- const stub = getDurableShutdownStub();
101
- if (!stub) return null;
102
-
103
- const res = await stub.fetch('https://worker-shutdown/status');
104
- if (!res.ok) return null;
105
- return (await res.json()) as DurableShutdownState;
106
- };
107
-
108
50
  let shutdownHandlersRegistered = false;
109
51
 
110
52
  const signalHandlers: {
@@ -300,14 +242,4 @@ export const WorkerShutdown = Object.freeze({
300
242
  * Get current shutdown state
301
243
  */
302
244
  getShutdownState,
303
-
304
- /**
305
- * Request shutdown via Durable Object (Workers)
306
- */
307
- requestDurableShutdown,
308
-
309
- /**
310
- * Read shutdown state from Durable Object (Workers)
311
- */
312
- getDurableShutdownState,
313
245
  });
@@ -1,4 +1,4 @@
1
- import { Env } from '@zintrust/core';
1
+ import { Env, appConfig } from '@zintrust/core';
2
2
 
3
3
  const normalizeBaseUrl = (value: string): string => {
4
4
  let end = value.length;
@@ -23,3 +23,11 @@ const resolveWorkerApiUrl = (): string => {
23
23
  export const WorkerConfig = Object.freeze({
24
24
  getWorkerBaseUrl: resolveWorkerApiUrl,
25
25
  });
26
+
27
+ export const keyPrefix = (): string => {
28
+ const redisKeyPrefix = (Env.get('WORKER_PERSISTENCE_REDIS_KEY_PREFIX', '') ?? '').trim();
29
+
30
+ return redisKeyPrefix
31
+ ? `${redisKeyPrefix}_worker_${appConfig.prefix}`
32
+ : `worker_${appConfig.prefix}`;
33
+ };