@zintrust/workers 0.4.48 → 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.
@@ -4,7 +4,7 @@
4
4
  * Sealed namespace for immutability
5
5
  */
6
6
  import * as ZintrustCoreModule from '@zintrust/core';
7
- import { Cloudflare, createRedisConnection, databaseConfig, Env, ErrorFactory, generateUuid, getBullMQSafeQueueName, isFunction, isNonEmptyString, isObject, JobStateTracker, Logger, NodeSingletons, queueConfig, registerDatabasesFromRuntimeConfig, useEnsureDbConnected, workersConfig, ZintrustLang, } 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';
8
8
  import { Worker } from 'bullmq';
9
9
  import { AutoScaler } from './AutoScaler.js';
10
10
  import { CanaryController } from './CanaryController.js';
@@ -63,7 +63,7 @@ const resolveRuntimeBridgeUrl = (specifier) => {
63
63
  const filePath = path.join(dir, `${safeName}.bridge.mjs`);
64
64
  const exportLines = Object.keys(bridgeModule)
65
65
  .filter((key) => key !== 'default' && isValidBridgeExportName(key))
66
- .sort()
66
+ .sort((a, b) => a.localeCompare(b))
67
67
  .map((key) => `export const ${key} = bridge[${JSON.stringify(key)}];`);
68
68
  const code = [
69
69
  'const bridgeMap = globalThis.__zintrustProcessorPackageBridges__;',
@@ -1544,19 +1544,25 @@ const resolvePersistenceConfig = (config) => {
1544
1544
  throw ErrorFactory.createConfigError('WORKER_PERSISTENCE_DRIVER must be one of memory, redis, or database');
1545
1545
  };
1546
1546
  const resolveDbClientFromEnv = async (connectionName = 'default') => {
1547
- const connect = async () => await useEnsureDbConnected(undefined, connectionName);
1548
- try {
1549
- return await connect();
1550
- }
1551
- catch (error) {
1552
- 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
+ }
1553
1560
  }
1554
1561
  try {
1555
- registerDatabasesFromRuntimeConfig(databaseConfig);
1556
- return await connect();
1562
+ return await useEnsureDbConnected(undefined, connectionName);
1557
1563
  }
1558
1564
  catch (error) {
1559
- Logger.error('Worker persistence failed after registering runtime databases', error);
1565
+ Logger.error('Worker persistence failed to resolve database connection', error);
1560
1566
  throw ErrorFactory.createConfigError(`Worker persistence requires a database client. Register connection '${connectionName}' or pass infrastructure.persistence.client.`);
1561
1567
  }
1562
1568
  };
@@ -0,0 +1,12 @@
1
+ type DurableObjectState = {
2
+ storage: {
3
+ get: (key: string) => Promise<unknown>;
4
+ put: (key: string, value: unknown) => Promise<void>;
5
+ };
6
+ };
7
+ export declare class ZinTrustWorkerShutdownDurableObject {
8
+ private readonly state;
9
+ constructor(state: DurableObjectState);
10
+ fetch(request: Request): Promise<Response>;
11
+ }
12
+ export {};
@@ -0,0 +1,41 @@
1
+ import { Logger } from '@zintrust/core';
2
+ const loadState = async (state) => {
3
+ const stored = (await state.storage.get('shutdown'));
4
+ return stored ?? { shuttingDown: false };
5
+ };
6
+ const saveState = async (state, value) => {
7
+ await state.storage.put('shutdown', value);
8
+ };
9
+ // eslint-disable-next-line no-restricted-syntax
10
+ export class ZinTrustWorkerShutdownDurableObject {
11
+ state;
12
+ constructor(state) {
13
+ this.state = state;
14
+ }
15
+ async fetch(request) {
16
+ const url = new URL(request.url);
17
+ const path = url.pathname;
18
+ if (request.method === 'GET' && path === '/status') {
19
+ const current = await loadState(this.state);
20
+ return new Response(JSON.stringify(current), {
21
+ status: 200,
22
+ headers: { 'content-type': 'application/json' },
23
+ });
24
+ }
25
+ if (request.method === 'POST' && path === '/shutdown') {
26
+ const payload = (await request.json().catch(() => ({})));
27
+ const next = {
28
+ shuttingDown: true,
29
+ startedAt: new Date().toISOString(),
30
+ reason: payload.reason ?? 'manual',
31
+ };
32
+ await saveState(this.state, next);
33
+ Logger.info('Worker shutdown requested via Durable Object', next);
34
+ return new Response(JSON.stringify({ ok: true }), {
35
+ status: 202,
36
+ headers: { 'content-type': 'application/json' },
37
+ });
38
+ }
39
+ return new Response('Not Found', { status: 404 });
40
+ }
41
+ }
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@zintrust/workers",
3
- "version": "0.4.48",
4
- "buildDate": "2026-04-03T10:00:24.111Z",
3
+ "version": "0.4.43",
4
+ "buildDate": "2026-04-01T18:18:47.850Z",
5
5
  "buildEnvironment": {
6
- "node": "v20.20.1",
7
- "platform": "linux",
8
- "arch": "x64"
6
+ "node": "v22.22.1",
7
+ "platform": "darwin",
8
+ "arch": "arm64"
9
9
  },
10
10
  "git": {
11
- "commit": "0ac23637",
12
- "branch": "master"
11
+ "commit": "57e4d1b5",
12
+ "branch": "release"
13
13
  },
14
14
  "package": {
15
15
  "engines": {
@@ -178,8 +178,8 @@
178
178
  "sha256": "3869f960c87260588e40941ff91bffcfa0757be7a04815fd28b57dd4840c51df"
179
179
  },
180
180
  "WorkerFactory.js": {
181
- "size": 104787,
182
- "sha256": "2ff4782577a8e15d28c5989f5bfe16af84ecdbdc4ecc860c716d0119dcc891e4"
181
+ "size": 102863,
182
+ "sha256": "1024756e603ca67461a955723dc004de9395267e50c7f59c9f8f0c13b2f0f7d8"
183
183
  },
184
184
  "WorkerInit.d.ts": {
185
185
  "size": 3284,
@@ -213,6 +213,14 @@
213
213
  "size": 6643,
214
214
  "sha256": "bef3a37ebc8292f4f548d5f66890ef7d6dd386e39c4c1cc61db058fd8815c927"
215
215
  },
216
+ "WorkerShutdownDurableObject.d.ts": {
217
+ "size": 354,
218
+ "sha256": "521df11172067bc036bc7b95ff6adf40b7c594afa0454742963317ddfe776d37"
219
+ },
220
+ "WorkerShutdownDurableObject.js": {
221
+ "size": 1547,
222
+ "sha256": "9c7298133dfc4073fda80d3fe9a37ac00d31cacf500ce10c1dd43a70c3a8ddd7"
223
+ },
216
224
  "WorkerVersioning.d.ts": {
217
225
  "size": 2881,
218
226
  "sha256": "a3f1f9e518e8a46201b181679bb5d7a484da7c021b4b7cfd5653cf98fe36e995"
@@ -221,6 +229,10 @@
221
229
  "size": 10953,
222
230
  "sha256": "8af20d462270e7044c6ea983821f5b6e6ce8a5caf39b6e8fefff07c9a0bf071e"
223
231
  },
232
+ "build-manifest.json": {
233
+ "size": 19594,
234
+ "sha256": "ddb01f1c22cddfc1201631a2774726d0134407e811359eed517bf36b508d829c"
235
+ },
224
236
  "config/workerConfig.d.ts": {
225
237
  "size": 132,
226
238
  "sha256": "577486dd9e0ef5b5c27d070e0f6a383337d9d68725fae0f0bad258254b828a3b"
@@ -403,7 +415,7 @@
403
415
  },
404
416
  "index.js": {
405
417
  "size": 2337,
406
- "sha256": "ec0e7e4ee51acd850c41f6120d54d4600cd1e462d0dd5c1696b16bc3517177e0"
418
+ "sha256": "698bb78d6613e898a60f390ca1766071324e3ec3a726184c35f174ea64cadb99"
407
419
  },
408
420
  "register.d.ts": {
409
421
  "size": 256,
@@ -524,6 +536,22 @@
524
536
  "ui/types/worker-ui.js": {
525
537
  "size": 94,
526
538
  "sha256": "7e9f752c2a8eb29dab365b0c836bac90fa7d3567aaf961537d3aa782e85c884e"
539
+ },
540
+ "ui/workers/index.html": {
541
+ "size": 7940,
542
+ "sha256": "a71e384de568f720ff23bf5c90dca60cc97d30c0bbb4ccf5c3b026a8086381b4"
543
+ },
544
+ "ui/workers/main.js": {
545
+ "size": 60723,
546
+ "sha256": "8437771f28963e1b039bee0b1721a8e83a8c65bed19dbd89af0e37d5a2882bb4"
547
+ },
548
+ "ui/workers/styles.css": {
549
+ "size": 22652,
550
+ "sha256": "6de91f206251b0b69ca1a41b71b454b00e3b1213a9762abd8fd8f0ff0381556b"
551
+ },
552
+ "ui/workers/zintrust.svg": {
553
+ "size": 1035,
554
+ "sha256": "9b791269aad740b4191b0ae9f005b08bb28a3407a79a8ed38c11d225fa443f69"
527
555
  }
528
556
  }
529
557
  }
@@ -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,
@@ -0,0 +1,202 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ZinTrust Workers Dashboard</title>
7
+ <link rel="stylesheet" href="workers/styles.css" />
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <div class="header">
12
+ <div class="header-top">
13
+ <div style="display: flex; align-items: center; gap: 16px">
14
+ <div class="logo-frame">
15
+ <img src="workers/zintrust.svg" alt="ZinTrust" class="logo-img" />
16
+ </div>
17
+ <h1>ZinTrust Workers</h1>
18
+ </div>
19
+ <div class="header-actions">
20
+ <button id="theme-toggle" class="theme-toggle">
21
+ <svg class="icon" viewBox="0 0 24 24">
22
+ <circle cx="12" cy="12" r="5" />
23
+ <path
24
+ d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
25
+ />
26
+ </svg>
27
+ Theme
28
+ </button>
29
+ <button id="auto-refresh-toggle" class="btn" onclick="toggleAutoRefresh()">
30
+ <svg id="auto-refresh-icon" class="icon" viewBox="0 0 24 24">
31
+ <polygon points="5 3 19 12 5 21 5 3" />
32
+ </svg>
33
+ <span id="auto-refresh-label">Auto Refresh</span>
34
+ </button>
35
+ <button class="btn" onclick="fetchData()">
36
+ <svg class="icon" viewBox="0 0 24 24">
37
+ <path d="M23 4v6h-6" />
38
+ <path d="M1 20v-6h6" />
39
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
40
+ </svg>
41
+ Refresh
42
+ </button>
43
+ <button class="btn btn-primary" onclick="showAddWorkerModal()">
44
+ <svg class="icon" viewBox="0 0 24 24">
45
+ <line x1="12" y1="5" x2="12" y2="19" />
46
+ <line x1="5" y1="12" x2="19" y2="12" />
47
+ </svg>
48
+ Add Worker
49
+ </button>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="nav-bar">
54
+ <nav class="nav-links">
55
+ <a href="/queue-monitor" class="nav-link">Queue Monitor</a>
56
+ <a href="/telemetry" class="nav-link">Telemetry</a>
57
+ <a href="/metrics" class="nav-link">Metrics</a>
58
+ </nav>
59
+ </div>
60
+
61
+ <div class="filters-bar">
62
+ <div class="filter-group">
63
+ <span>Status:</span>
64
+ <select id="status-filter">
65
+ <option value="">All Status</option>
66
+ <option value="running">Running</option>
67
+ <option value="stopped">Stopped</option>
68
+ <option value="error">Error</option>
69
+ <option value="paused">Paused</option>
70
+ </select>
71
+ </div>
72
+ <div class="filter-group">
73
+ <span>Driver:</span>
74
+ <select id="driver-filter">
75
+ <option value="">All Drivers</option>
76
+ </select>
77
+ </div>
78
+ <div class="filter-group">
79
+ <span>Sort:</span>
80
+ <select id="sort-select">
81
+ <option value="name">Sort by Name</option>
82
+ <option value="status" selected>Sort by Status</option>
83
+ <option value="driver">Sort by Driver</option>
84
+ <option value="health">Sort by Health</option>
85
+ <option value="version">Sort by Version</option>
86
+ <option value="processed">Sort by Performance</option>
87
+ </select>
88
+ </div>
89
+ <div style="flex-grow: 1"></div>
90
+ <div class="search-box">
91
+ <svg class="search-icon" viewBox="0 0 24 24">
92
+ <circle cx="11" cy="11" r="8"></circle>
93
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
94
+ </svg>
95
+ <input type="text" id="search-input" placeholder="Search workers..." />
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <div id="loading" style="text-align: center; padding: 40px; color: var(--muted)">
101
+ <div>Loading workers...</div>
102
+ </div>
103
+
104
+ <div
105
+ id="error"
106
+ style="display: none; text-align: center; padding: 40px; color: var(--danger)"
107
+ >
108
+ <div>Failed to load workers data</div>
109
+ <button class="btn" onclick="fetchData()" style="margin-top: 16px">Retry</button>
110
+ </div>
111
+
112
+ <div id="workers-content" style="display: none">
113
+ <div class="summary-bar" id="queue-summary">
114
+ <div class="summary-item">
115
+ <span class="summary-label">Queue Driver</span>
116
+ <span class="summary-value" id="queue-driver">-</span>
117
+ </div>
118
+ <div class="summary-item">
119
+ <span class="summary-label">Queues</span>
120
+ <span class="summary-value" id="queue-total">0</span>
121
+ </div>
122
+ <div class="summary-item">
123
+ <span class="summary-label">Jobs</span>
124
+ <span class="summary-value" id="queue-jobs">0</span>
125
+ </div>
126
+ <div class="summary-item">
127
+ <span class="summary-label">Processing</span>
128
+ <span class="summary-value" id="queue-processing">0</span>
129
+ </div>
130
+ <div class="summary-item">
131
+ <span class="summary-label">Failed</span>
132
+ <span class="summary-value" id="queue-failed">0</span>
133
+ </div>
134
+ <div class="summary-item">
135
+ <span class="summary-label">Drivers</span>
136
+ <div class="drivers-list" id="drivers-list"></div>
137
+ </div>
138
+ </div>
139
+ <div class="table-container">
140
+ <div class="table-wrapper">
141
+ <table>
142
+ <thead>
143
+ <tr>
144
+ <th style="width: 250px">Worker</th>
145
+ <th style="width: 120px">Status</th>
146
+ <th style="width: 120px">Health</th>
147
+ <th style="width: 100px">Driver</th>
148
+ <th style="width: 100px">Version</th>
149
+ <th style="width: 320px">Performance</th>
150
+ <th style="width: 180px">Actions</th>
151
+ </tr>
152
+ </thead>
153
+ <tbody id="workers-tbody">
154
+ <!-- Workers will be populated here -->
155
+ </tbody>
156
+ </table>
157
+ </div>
158
+
159
+ <div class="pagination">
160
+ <div class="pagination-info" id="pagination-info">Showing 0-0 of 0 workers</div>
161
+ <div class="pagination-controls">
162
+ <button class="page-btn" id="prev-btn" onclick="loadPage('prev')" disabled>
163
+ <svg
164
+ viewBox="0 0 24 24"
165
+ fill="none"
166
+ stroke="currentColor"
167
+ stroke-linecap="round"
168
+ stroke-linejoin="round"
169
+ >
170
+ <polyline points="15 18 9 12 15 6"></polyline>
171
+ </svg>
172
+ </button>
173
+ <div id="page-numbers" style="display: flex; gap: 8px"></div>
174
+ <button class="page-btn" id="next-btn" onclick="loadPage('next')" disabled>
175
+ <svg
176
+ viewBox="0 0 24 24"
177
+ fill="none"
178
+ stroke="currentColor"
179
+ stroke-linecap="round"
180
+ stroke-linejoin="round"
181
+ >
182
+ <polyline points="9 18 15 12 9 6"></polyline>
183
+ </svg>
184
+ </button>
185
+
186
+ <div class="page-size-selector">
187
+ <span>Show:</span>
188
+ <select id="limit-select" onchange="changeLimit(this.value)">
189
+ <option value="10">10</option>
190
+ <option value="25">25</option>
191
+ <option value="50">50</option>
192
+ <option value="100">100</option>
193
+ </select>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ <script src="workers/main.js"></script>
201
+ </body>
202
+ </html>