@zintrust/workers 0.1.31 → 0.1.43

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 (53) 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 +384 -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 +99 -83
  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/index.d.ts +1 -2
  20. package/dist/index.js +0 -1
  21. package/dist/routes/workers.js +10 -7
  22. package/dist/storage/WorkerStore.d.ts +6 -3
  23. package/dist/storage/WorkerStore.js +16 -0
  24. package/dist/telemetry/api/TelemetryMonitoringService.js +29 -2
  25. package/dist/ui/router/ui.js +58 -29
  26. package/dist/ui/workers/index.html +202 -0
  27. package/dist/ui/workers/main.js +1952 -0
  28. package/dist/ui/workers/styles.css +1350 -0
  29. package/dist/ui/workers/zintrust.svg +30 -0
  30. package/package.json +5 -5
  31. package/src/ClusterLock.ts +13 -7
  32. package/src/ComplianceManager.ts +3 -2
  33. package/src/DeadLetterQueue.ts +6 -4
  34. package/src/HealthMonitor.ts +33 -17
  35. package/src/Observability.ts +11 -0
  36. package/src/WorkerFactory.ts +446 -43
  37. package/src/WorkerInit.ts +167 -48
  38. package/src/WorkerMetrics.ts +14 -8
  39. package/src/WorkerRegistry.ts +11 -0
  40. package/src/WorkerShutdown.ts +1 -69
  41. package/src/config/workerConfig.ts +9 -1
  42. package/src/createQueueWorker.ts +428 -43
  43. package/src/dashboard/workers-api.ts +8 -1
  44. package/src/http/WorkerController.ts +111 -36
  45. package/src/http/WorkerMonitoringService.ts +35 -2
  46. package/src/index.ts +2 -3
  47. package/src/routes/workers.ts +10 -8
  48. package/src/storage/WorkerStore.ts +21 -3
  49. package/src/telemetry/api/TelemetryMonitoringService.ts +35 -2
  50. package/src/types/queue-monitor.d.ts +2 -1
  51. package/src/ui/router/EmbeddedAssets.ts +3 -0
  52. package/src/ui/router/ui.ts +57 -39
  53. package/src/WorkerShutdownDurableObject.ts +0 -64
@@ -113,6 +113,9 @@ export const InMemoryWorkerStore = Object.freeze({
113
113
  async remove(name) {
114
114
  store.delete(name);
115
115
  },
116
+ async close() {
117
+ // No-op for memory store
118
+ },
116
119
  };
117
120
  },
118
121
  });
@@ -176,6 +179,15 @@ export const RedisWorkerStore = Object.freeze({
176
179
  return;
177
180
  await client.hset(key, ...updates);
178
181
  },
182
+ async close() {
183
+ try {
184
+ // Force disconnect in Cloudflare env to avoid hanging on quit()
185
+ client.disconnect();
186
+ }
187
+ catch {
188
+ // Ignore connection errors during cleanup
189
+ }
190
+ },
179
191
  async remove(name) {
180
192
  await client.hdel(key, name);
181
193
  },
@@ -238,6 +250,10 @@ export const DbWorkerStore = Object.freeze({
238
250
  async remove(name) {
239
251
  await db.table(table).where('name', '=', name).delete();
240
252
  },
253
+ async close() {
254
+ // Database clients often managed by pool, but if needed:
255
+ // await db.destroy?.();
256
+ },
241
257
  };
242
258
  },
243
259
  });
@@ -1,7 +1,34 @@
1
1
  import { Logger, NodeSingletons } from '@zintrust/core';
2
+ const createFallbackEmitter = () => {
3
+ const listeners = new Map();
4
+ return {
5
+ on(event, listener) {
6
+ const set = listeners.get(event) ?? new Set();
7
+ set.add(listener);
8
+ listeners.set(event, set);
9
+ },
10
+ off(event, listener) {
11
+ const set = listeners.get(event);
12
+ if (!set)
13
+ return;
14
+ set.delete(listener);
15
+ if (set.size === 0)
16
+ listeners.delete(event);
17
+ },
18
+ emit(event, payload) {
19
+ const set = listeners.get(event);
20
+ if (!set)
21
+ return false;
22
+ for (const listener of set)
23
+ listener(payload);
24
+ return true;
25
+ },
26
+ };
27
+ };
2
28
  // Internal state for singleton service
3
- const emitter = new NodeSingletons.EventEmitter();
4
- emitter.setMaxListeners(Infinity);
29
+ const EventEmitterCtor = NodeSingletons?.EventEmitter;
30
+ const emitter = typeof EventEmitterCtor === 'function' ? new EventEmitterCtor() : createFallbackEmitter();
31
+ emitter.setMaxListeners?.(Infinity);
5
32
  let interval = null;
6
33
  let subscribers = 0;
7
34
  let currentSettings = null;
@@ -1,6 +1,13 @@
1
- import { Cloudflare, Logger, MIME_TYPES, NodeSingletons, Router, detectRuntime, } from '@zintrust/core';
1
+ import { Cloudflare, Logger, MIME_TYPES, NodeSingletons, Router } from '@zintrust/core';
2
2
  import { INDEX_HTML, MAIN_JS, STYLES_CSS, ZINTRUST_SVG } from './EmbeddedAssets';
3
- const isCloudflare = detectRuntime().isCloudflare;
3
+ const isCloudflare = (() => {
4
+ try {
5
+ return Cloudflare.getWorkersEnv() !== null;
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ })();
4
11
  const safeFileUrlToPath = (url) => {
5
12
  if (typeof url !== 'string' || url.trim() === '')
6
13
  return '';
@@ -44,6 +51,26 @@ const fetchAssetBytes = async (assetPath) => {
44
51
  const buffer = await response.arrayBuffer();
45
52
  return new Uint8Array(buffer);
46
53
  };
54
+ const resolveEmbeddedAssetText = (assetPath) => {
55
+ const normalizedPath = assetPath.replace(/^\//, '');
56
+ if (normalizedPath === 'workers/index.html') {
57
+ return Buffer.from(INDEX_HTML, 'base64').toString('utf-8');
58
+ }
59
+ return null;
60
+ };
61
+ const resolveEmbeddedAssetBytes = (assetPath) => {
62
+ const normalizedPath = assetPath.replace(/^\//, '');
63
+ if (normalizedPath === 'workers/styles.css') {
64
+ return Buffer.from(STYLES_CSS, 'base64');
65
+ }
66
+ if (normalizedPath === 'workers/main.js') {
67
+ return Buffer.from(MAIN_JS, 'base64');
68
+ }
69
+ if (normalizedPath === 'workers/zintrust.svg') {
70
+ return Buffer.from(ZINTRUST_SVG, 'base64');
71
+ }
72
+ return null;
73
+ };
47
74
  export const uiResolver = async (uiBasePath) => {
48
75
  // Resolve base path for UI assets
49
76
  // const __filename = NodeSingletons.url.fileURLToPath(import.meta.url);
@@ -51,12 +78,16 @@ export const uiResolver = async (uiBasePath) => {
51
78
  const assetHtml = await fetchAssetText('/workers/index.html');
52
79
  if (assetHtml !== '')
53
80
  return assetHtml;
54
- if (isCloudflare) {
55
- return Buffer.from(INDEX_HTML, 'base64').toString('utf-8');
56
- }
57
81
  const uiPath = NodeSingletons.path.resolve(uiBasePath, 'workers/index.html');
58
- const html = await NodeSingletons.fs.readFile(uiPath, 'utf8');
59
- return html;
82
+ try {
83
+ return await NodeSingletons.fs.readFile(uiPath, 'utf8');
84
+ }
85
+ catch {
86
+ const embedded = resolveEmbeddedAssetText('/workers/index.html');
87
+ if (embedded !== null)
88
+ return embedded;
89
+ throw Error('workers index.html is unavailable');
90
+ }
60
91
  };
61
92
  // MIME type mapping for static files
62
93
  const getMimeType = (filePath) => {
@@ -96,6 +127,16 @@ const getUiBase = () => {
96
127
  return uiBasePath;
97
128
  };
98
129
  const serveStaticFile = async (req, res) => {
130
+ const tryServeEmbedded = (assetPath) => {
131
+ const bytes = resolveEmbeddedAssetBytes(assetPath);
132
+ if (bytes === null)
133
+ return false;
134
+ const mimeType = getMimeType(assetPath);
135
+ res.setHeader('Content-Type', mimeType);
136
+ res.setHeader('Cache-Control', 'public, max-age=3600');
137
+ res.send(Buffer.from(bytes));
138
+ return true;
139
+ };
99
140
  try {
100
141
  const filePath = req.getPath();
101
142
  const assetBytes = await fetchAssetBytes(filePath);
@@ -107,28 +148,8 @@ const serveStaticFile = async (req, res) => {
107
148
  return;
108
149
  }
109
150
  if (isCloudflare) {
110
- const normalizedPath = filePath.replace(/^\//, '');
111
- if (normalizedPath === 'workers/styles.css') {
112
- const mimeType = MIME_TYPES.CSS;
113
- res.setHeader('Content-Type', mimeType);
114
- res.setHeader('Cache-Control', 'public, max-age=3600');
115
- res.send(Buffer.from(STYLES_CSS, 'base64'));
151
+ if (tryServeEmbedded(filePath))
116
152
  return;
117
- }
118
- if (normalizedPath === 'workers/main.js') {
119
- const mimeType = MIME_TYPES.JS;
120
- res.setHeader('Content-Type', mimeType);
121
- res.setHeader('Cache-Control', 'public, max-age=3600');
122
- res.send(Buffer.from(MAIN_JS, 'base64'));
123
- return;
124
- }
125
- if (normalizedPath === 'workers/zintrust.svg') {
126
- const mimeType = MIME_TYPES.SVG;
127
- res.setHeader('Content-Type', mimeType);
128
- res.setHeader('Cache-Control', 'public, max-age=3600');
129
- res.send(Buffer.from(ZINTRUST_SVG, 'base64'));
130
- return;
131
- }
132
153
  res.setStatus(404);
133
154
  res.send(Buffer.from('Not Found'));
134
155
  return;
@@ -140,7 +161,15 @@ const serveStaticFile = async (req, res) => {
140
161
  res.send(Buffer.from('Forbidden'));
141
162
  return;
142
163
  }
143
- const content = await NodeSingletons.fs.readFile(fullPath);
164
+ let content;
165
+ try {
166
+ content = await NodeSingletons.fs.readFile(fullPath);
167
+ }
168
+ catch {
169
+ if (tryServeEmbedded(filePath))
170
+ return;
171
+ throw Error(`Missing static asset: ${filePath}`);
172
+ }
144
173
  const mimeType = getMimeType(filePath);
145
174
  res.setHeader('Content-Type', mimeType);
146
175
  res.setHeader('Cache-Control', 'public, max-age=3600'); // 1 hour cache
@@ -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>