@zintrust/workers 0.1.27
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/README.md +861 -0
- package/dist/AnomalyDetection.d.ts +102 -0
- package/dist/AnomalyDetection.js +321 -0
- package/dist/AutoScaler.d.ts +127 -0
- package/dist/AutoScaler.js +425 -0
- package/dist/BroadcastWorker.d.ts +21 -0
- package/dist/BroadcastWorker.js +24 -0
- package/dist/CanaryController.d.ts +103 -0
- package/dist/CanaryController.js +380 -0
- package/dist/ChaosEngineering.d.ts +79 -0
- package/dist/ChaosEngineering.js +216 -0
- package/dist/CircuitBreaker.d.ts +106 -0
- package/dist/CircuitBreaker.js +374 -0
- package/dist/ClusterLock.d.ts +90 -0
- package/dist/ClusterLock.js +385 -0
- package/dist/ComplianceManager.d.ts +177 -0
- package/dist/ComplianceManager.js +556 -0
- package/dist/DatacenterOrchestrator.d.ts +133 -0
- package/dist/DatacenterOrchestrator.js +404 -0
- package/dist/DeadLetterQueue.d.ts +122 -0
- package/dist/DeadLetterQueue.js +539 -0
- package/dist/HealthMonitor.d.ts +42 -0
- package/dist/HealthMonitor.js +301 -0
- package/dist/MultiQueueWorker.d.ts +89 -0
- package/dist/MultiQueueWorker.js +277 -0
- package/dist/NotificationWorker.d.ts +21 -0
- package/dist/NotificationWorker.js +23 -0
- package/dist/Observability.d.ts +153 -0
- package/dist/Observability.js +530 -0
- package/dist/PluginManager.d.ts +123 -0
- package/dist/PluginManager.js +392 -0
- package/dist/PriorityQueue.d.ts +117 -0
- package/dist/PriorityQueue.js +244 -0
- package/dist/ResourceMonitor.d.ts +164 -0
- package/dist/ResourceMonitor.js +605 -0
- package/dist/SLAMonitor.d.ts +110 -0
- package/dist/SLAMonitor.js +274 -0
- package/dist/WorkerFactory.d.ts +193 -0
- package/dist/WorkerFactory.js +1507 -0
- package/dist/WorkerInit.d.ts +85 -0
- package/dist/WorkerInit.js +223 -0
- package/dist/WorkerMetrics.d.ts +114 -0
- package/dist/WorkerMetrics.js +509 -0
- package/dist/WorkerRegistry.d.ts +145 -0
- package/dist/WorkerRegistry.js +319 -0
- package/dist/WorkerShutdown.d.ts +61 -0
- package/dist/WorkerShutdown.js +159 -0
- package/dist/WorkerVersioning.d.ts +107 -0
- package/dist/WorkerVersioning.js +300 -0
- package/dist/build-manifest.json +462 -0
- package/dist/config/workerConfig.d.ts +3 -0
- package/dist/config/workerConfig.js +19 -0
- package/dist/createQueueWorker.d.ts +23 -0
- package/dist/createQueueWorker.js +113 -0
- package/dist/dashboard/index.d.ts +1 -0
- package/dist/dashboard/index.js +1 -0
- package/dist/dashboard/types.d.ts +117 -0
- package/dist/dashboard/types.js +1 -0
- package/dist/dashboard/workers-api.d.ts +4 -0
- package/dist/dashboard/workers-api.js +638 -0
- package/dist/dashboard/workers-dashboard-ui.d.ts +3 -0
- package/dist/dashboard/workers-dashboard-ui.js +1026 -0
- package/dist/dashboard/workers-dashboard.d.ts +4 -0
- package/dist/dashboard/workers-dashboard.js +904 -0
- package/dist/helper/index.d.ts +5 -0
- package/dist/helper/index.js +10 -0
- package/dist/http/WorkerApiController.d.ts +38 -0
- package/dist/http/WorkerApiController.js +312 -0
- package/dist/http/WorkerController.d.ts +374 -0
- package/dist/http/WorkerController.js +1351 -0
- package/dist/http/middleware/CustomValidation.d.ts +92 -0
- package/dist/http/middleware/CustomValidation.js +270 -0
- package/dist/http/middleware/DatacenterValidator.d.ts +3 -0
- package/dist/http/middleware/DatacenterValidator.js +94 -0
- package/dist/http/middleware/EditWorkerValidation.d.ts +7 -0
- package/dist/http/middleware/EditWorkerValidation.js +55 -0
- package/dist/http/middleware/FeaturesValidator.d.ts +3 -0
- package/dist/http/middleware/FeaturesValidator.js +60 -0
- package/dist/http/middleware/InfrastructureValidator.d.ts +31 -0
- package/dist/http/middleware/InfrastructureValidator.js +226 -0
- package/dist/http/middleware/OptionsValidator.d.ts +3 -0
- package/dist/http/middleware/OptionsValidator.js +112 -0
- package/dist/http/middleware/PayloadSanitizer.d.ts +7 -0
- package/dist/http/middleware/PayloadSanitizer.js +42 -0
- package/dist/http/middleware/ProcessorPathSanitizer.d.ts +3 -0
- package/dist/http/middleware/ProcessorPathSanitizer.js +74 -0
- package/dist/http/middleware/QueueNameSanitizer.d.ts +3 -0
- package/dist/http/middleware/QueueNameSanitizer.js +45 -0
- package/dist/http/middleware/ValidateDriver.d.ts +7 -0
- package/dist/http/middleware/ValidateDriver.js +20 -0
- package/dist/http/middleware/VersionSanitizer.d.ts +3 -0
- package/dist/http/middleware/VersionSanitizer.js +25 -0
- package/dist/http/middleware/WorkerNameSanitizer.d.ts +3 -0
- package/dist/http/middleware/WorkerNameSanitizer.js +46 -0
- package/dist/http/middleware/WorkerValidationChain.d.ts +27 -0
- package/dist/http/middleware/WorkerValidationChain.js +185 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +48 -0
- package/dist/routes/workers.d.ts +12 -0
- package/dist/routes/workers.js +81 -0
- package/dist/storage/WorkerStore.d.ts +45 -0
- package/dist/storage/WorkerStore.js +195 -0
- package/dist/type.d.ts +76 -0
- package/dist/type.js +1 -0
- package/dist/ui/router/ui.d.ts +3 -0
- package/dist/ui/router/ui.js +83 -0
- package/dist/ui/types/worker-ui.d.ts +229 -0
- package/dist/ui/types/worker-ui.js +5 -0
- package/package.json +53 -0
- package/src/AnomalyDetection.ts +434 -0
- package/src/AutoScaler.ts +654 -0
- package/src/BroadcastWorker.ts +34 -0
- package/src/CanaryController.ts +531 -0
- package/src/ChaosEngineering.ts +301 -0
- package/src/CircuitBreaker.ts +495 -0
- package/src/ClusterLock.ts +499 -0
- package/src/ComplianceManager.ts +815 -0
- package/src/DatacenterOrchestrator.ts +561 -0
- package/src/DeadLetterQueue.ts +733 -0
- package/src/HealthMonitor.ts +390 -0
- package/src/MultiQueueWorker.ts +431 -0
- package/src/NotificationWorker.ts +33 -0
- package/src/Observability.ts +696 -0
- package/src/PluginManager.ts +551 -0
- package/src/PriorityQueue.ts +351 -0
- package/src/ResourceMonitor.ts +769 -0
- package/src/SLAMonitor.ts +408 -0
- package/src/WorkerFactory.ts +2108 -0
- package/src/WorkerInit.ts +313 -0
- package/src/WorkerMetrics.ts +709 -0
- package/src/WorkerRegistry.ts +443 -0
- package/src/WorkerShutdown.ts +210 -0
- package/src/WorkerVersioning.ts +422 -0
- package/src/config/workerConfig.ts +25 -0
- package/src/createQueueWorker.ts +174 -0
- package/src/dashboard/index.ts +6 -0
- package/src/dashboard/types.ts +141 -0
- package/src/dashboard/workers-api.ts +785 -0
- package/src/dashboard/zintrust.svg +30 -0
- package/src/helper/index.ts +11 -0
- package/src/http/WorkerApiController.ts +369 -0
- package/src/http/WorkerController.ts +1512 -0
- package/src/http/middleware/CustomValidation.ts +360 -0
- package/src/http/middleware/DatacenterValidator.ts +124 -0
- package/src/http/middleware/EditWorkerValidation.ts +74 -0
- package/src/http/middleware/FeaturesValidator.ts +82 -0
- package/src/http/middleware/InfrastructureValidator.ts +295 -0
- package/src/http/middleware/OptionsValidator.ts +144 -0
- package/src/http/middleware/PayloadSanitizer.ts +52 -0
- package/src/http/middleware/ProcessorPathSanitizer.ts +86 -0
- package/src/http/middleware/QueueNameSanitizer.ts +55 -0
- package/src/http/middleware/ValidateDriver.ts +29 -0
- package/src/http/middleware/VersionSanitizer.ts +30 -0
- package/src/http/middleware/WorkerNameSanitizer.ts +56 -0
- package/src/http/middleware/WorkerValidationChain.ts +230 -0
- package/src/index.ts +98 -0
- package/src/routes/workers.ts +154 -0
- package/src/storage/WorkerStore.ts +240 -0
- package/src/type.ts +89 -0
- package/src/types/queue-monitor.d.ts +38 -0
- package/src/types/queue-redis.d.ts +38 -0
- package/src/ui/README.md +13 -0
- package/src/ui/components/JsonEditor.js +670 -0
- package/src/ui/components/JsonViewer.js +387 -0
- package/src/ui/components/WorkerCard.js +178 -0
- package/src/ui/components/WorkerExpandPanel.js +257 -0
- package/src/ui/components/fetcher.js +42 -0
- package/src/ui/components/sla-scorecard.js +32 -0
- package/src/ui/components/styles.css +30 -0
- package/src/ui/components/table-expander.js +34 -0
- package/src/ui/integration/worker-ui-integration.js +565 -0
- package/src/ui/router/ui.ts +99 -0
- package/src/ui/services/workerApi.js +240 -0
- package/src/ui/types/worker-ui.ts +283 -0
- package/src/ui/utils/jsonValidator.js +444 -0
- package/src/ui/workers/index.html +202 -0
- package/src/ui/workers/main.js +1781 -0
- package/src/ui/workers/styles.css +1350 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Versioning System
|
|
3
|
+
* Semantic versioning support for workers with backward compatibility
|
|
4
|
+
* Sealed namespace for immutability
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ErrorFactory, Logger } from '@zintrust/core';
|
|
8
|
+
|
|
9
|
+
export type SemanticVersion = {
|
|
10
|
+
major: number;
|
|
11
|
+
minor: number;
|
|
12
|
+
patch: number;
|
|
13
|
+
prerelease?: string;
|
|
14
|
+
build?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type WorkerVersion = {
|
|
18
|
+
workerName: string;
|
|
19
|
+
version: SemanticVersion;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
deprecatedAt?: Date;
|
|
22
|
+
eolDate?: Date; // End of life date
|
|
23
|
+
isActive: boolean;
|
|
24
|
+
isDeprecated: boolean;
|
|
25
|
+
migrationPath?: string; // Version to migrate to
|
|
26
|
+
changelog?: string;
|
|
27
|
+
breakingChanges?: string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type VersionCompatibility = {
|
|
31
|
+
sourceVersion: SemanticVersion;
|
|
32
|
+
targetVersion: SemanticVersion;
|
|
33
|
+
compatible: boolean;
|
|
34
|
+
requiresMigration: boolean;
|
|
35
|
+
breakingChanges: string[];
|
|
36
|
+
recommendations: string[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Internal state
|
|
40
|
+
const workerVersions = new Map<string, WorkerVersion[]>();
|
|
41
|
+
const versionAliases = new Map<string, string>(); // 'latest', 'stable', etc. -> version string
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Helper: Parse version string
|
|
45
|
+
*/
|
|
46
|
+
const parseVersion = (versionStr: string): SemanticVersion => {
|
|
47
|
+
const match = new RegExp(
|
|
48
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+))?(?:\+([a-zA-Z0-9.-]+))?$/
|
|
49
|
+
).exec(versionStr);
|
|
50
|
+
|
|
51
|
+
if (!match) {
|
|
52
|
+
throw ErrorFactory.createConfigError(`Invalid version format: ${versionStr}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
major: Number.parseInt(match[1], 10),
|
|
57
|
+
minor: Number.parseInt(match[2], 10),
|
|
58
|
+
patch: Number.parseInt(match[3], 10),
|
|
59
|
+
prerelease: match[4],
|
|
60
|
+
build: match[5],
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Helper: Convert version to string
|
|
66
|
+
*/
|
|
67
|
+
const versionToString = (version: SemanticVersion): string => {
|
|
68
|
+
let str = `${version.major}.${version.minor}.${version.patch}`;
|
|
69
|
+
if (typeof version.prerelease === 'string' && version.prerelease.length > 0) {
|
|
70
|
+
str += `-${version.prerelease}`;
|
|
71
|
+
}
|
|
72
|
+
if (typeof version.build === 'string' && version.build.length > 0) {
|
|
73
|
+
str += `+${version.build}`;
|
|
74
|
+
}
|
|
75
|
+
return str;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Helper: Compare versions
|
|
80
|
+
* Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
|
|
81
|
+
*/
|
|
82
|
+
const compareVersions = (v1: SemanticVersion, v2: SemanticVersion): number => {
|
|
83
|
+
if (v1.major !== v2.major) return v1.major - v2.major;
|
|
84
|
+
if (v1.minor !== v2.minor) return v1.minor - v2.minor;
|
|
85
|
+
if (v1.patch !== v2.patch) return v1.patch - v2.patch;
|
|
86
|
+
|
|
87
|
+
// Prerelease versions have lower precedence
|
|
88
|
+
if (v1.prerelease === undefined && v2.prerelease !== undefined) return 1;
|
|
89
|
+
if (v1.prerelease !== undefined && v2.prerelease === undefined) return -1;
|
|
90
|
+
if (v1.prerelease !== undefined && v2.prerelease !== undefined) {
|
|
91
|
+
return v1.prerelease.localeCompare(v2.prerelease);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return 0;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Helper: Check if versions are compatible (no breaking changes)
|
|
99
|
+
*/
|
|
100
|
+
const areVersionsCompatible = (v1: SemanticVersion, v2: SemanticVersion): boolean => {
|
|
101
|
+
// Same major version = compatible (semver rules)
|
|
102
|
+
return v1.major === v2.major;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Worker Versioning - Sealed namespace
|
|
107
|
+
*/
|
|
108
|
+
export const WorkerVersioning = Object.freeze({
|
|
109
|
+
/**
|
|
110
|
+
* Register a worker version
|
|
111
|
+
*/
|
|
112
|
+
register(workerVersion: Omit<WorkerVersion, 'createdAt' | 'isActive' | 'isDeprecated'>): void {
|
|
113
|
+
const { workerName, version } = workerVersion;
|
|
114
|
+
const versionStr = versionToString(version);
|
|
115
|
+
|
|
116
|
+
let versions = workerVersions.get(workerName);
|
|
117
|
+
|
|
118
|
+
if (!versions) {
|
|
119
|
+
versions = [];
|
|
120
|
+
workerVersions.set(workerName, versions);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check if version already exists
|
|
124
|
+
const existing = versions.find((v) => versionToString(v.version) === versionStr);
|
|
125
|
+
|
|
126
|
+
if (existing) {
|
|
127
|
+
ErrorFactory.createConfigError(
|
|
128
|
+
`Version ${versionStr} already registered for worker "${workerName}"`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const fullVersion: WorkerVersion = {
|
|
133
|
+
...workerVersion,
|
|
134
|
+
createdAt: new Date(),
|
|
135
|
+
isActive: true,
|
|
136
|
+
isDeprecated: false,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
versions.push(fullVersion);
|
|
140
|
+
|
|
141
|
+
// Sort by version (descending)
|
|
142
|
+
versions.sort((a, b) => -compareVersions(a.version, b.version));
|
|
143
|
+
|
|
144
|
+
// Update 'latest' alias if this is the newest version
|
|
145
|
+
if (versions[0] === fullVersion) {
|
|
146
|
+
const aliasKey = `${workerName}:latest`;
|
|
147
|
+
versionAliases.set(aliasKey, versionStr);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Logger.info(`Worker version registered: ${workerName}@${versionStr}`, {
|
|
151
|
+
isActive: fullVersion.isActive,
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get worker version
|
|
157
|
+
*/
|
|
158
|
+
getVersion(workerName: string, versionStr: string): WorkerVersion | null {
|
|
159
|
+
// Check if it's an alias
|
|
160
|
+
let newVersionStr = versionStr;
|
|
161
|
+
const aliasKey = `${workerName}:${newVersionStr}`;
|
|
162
|
+
const aliasedVersion = versionAliases.get(aliasKey);
|
|
163
|
+
|
|
164
|
+
if (aliasedVersion !== undefined) {
|
|
165
|
+
newVersionStr = aliasedVersion;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const versions = workerVersions.get(workerName);
|
|
169
|
+
|
|
170
|
+
if (!versions) return null;
|
|
171
|
+
|
|
172
|
+
const version = parseVersion(newVersionStr);
|
|
173
|
+
const found = versions.find((v) => compareVersions(v.version, version) === 0);
|
|
174
|
+
|
|
175
|
+
return found ? { ...found } : null;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get all versions for a worker
|
|
180
|
+
*/
|
|
181
|
+
getVersions(workerName: string, includeDeprecated = false): ReadonlyArray<WorkerVersion> {
|
|
182
|
+
const versions = workerVersions.get(workerName) ?? [];
|
|
183
|
+
|
|
184
|
+
if (!includeDeprecated) {
|
|
185
|
+
return versions.filter((v) => !v.isDeprecated);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return versions.map((v) => ({ ...v }));
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get latest version
|
|
193
|
+
*/
|
|
194
|
+
getLatest(workerName: string): WorkerVersion | null {
|
|
195
|
+
const versions = workerVersions.get(workerName);
|
|
196
|
+
|
|
197
|
+
if (!versions || versions.length === 0) return null;
|
|
198
|
+
|
|
199
|
+
// Already sorted by version (descending)
|
|
200
|
+
const latest = versions.find((v) => v.isActive && !v.isDeprecated);
|
|
201
|
+
|
|
202
|
+
return latest ? { ...latest } : null;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Deprecate a version
|
|
207
|
+
*/
|
|
208
|
+
deprecate(workerName: string, versionStr: string, migrationPath?: string, eolDate?: Date): void {
|
|
209
|
+
const version = WorkerVersioning.getVersion(workerName, versionStr);
|
|
210
|
+
|
|
211
|
+
if (!version) {
|
|
212
|
+
throw ErrorFactory.createNotFoundError(
|
|
213
|
+
`Version ${versionStr} not found for worker "${workerName}"`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const versions = workerVersions.get(workerName) ?? [];
|
|
218
|
+
const index = versions.findIndex((v) => versionToString(v.version) === versionStr);
|
|
219
|
+
|
|
220
|
+
versions[index].isDeprecated = true;
|
|
221
|
+
versions[index].deprecatedAt = new Date();
|
|
222
|
+
versions[index].migrationPath = migrationPath;
|
|
223
|
+
versions[index].eolDate = eolDate;
|
|
224
|
+
|
|
225
|
+
Logger.warn(`Worker version deprecated: ${workerName}@${versionStr}`, {
|
|
226
|
+
migrationPath,
|
|
227
|
+
eolDate,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Deactivate a version (stop accepting new jobs)
|
|
233
|
+
*/
|
|
234
|
+
deactivate(workerName: string, versionStr: string): void {
|
|
235
|
+
const version = WorkerVersioning.getVersion(workerName, versionStr);
|
|
236
|
+
|
|
237
|
+
if (!version) {
|
|
238
|
+
throw ErrorFactory.createNotFoundError(
|
|
239
|
+
`Version ${versionStr} not found for worker "${workerName}"`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const versions = workerVersions.get(workerName) ?? [];
|
|
244
|
+
const index = versions.findIndex((v) => versionToString(v.version) === versionStr);
|
|
245
|
+
|
|
246
|
+
versions[index].isActive = false;
|
|
247
|
+
|
|
248
|
+
Logger.info(`Worker version deactivated: ${workerName}@${versionStr}`);
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Activate a version
|
|
253
|
+
*/
|
|
254
|
+
activate(workerName: string, versionStr: string): void {
|
|
255
|
+
const version = WorkerVersioning.getVersion(workerName, versionStr);
|
|
256
|
+
|
|
257
|
+
if (!version) {
|
|
258
|
+
throw ErrorFactory.createNotFoundError(
|
|
259
|
+
`Version ${versionStr} not found for worker "${workerName}"`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const getWorkerVersions = workerVersions.get(workerName);
|
|
264
|
+
const versions = getWorkerVersions ?? [];
|
|
265
|
+
const index = versions.findIndex((v) => versionToString(v.version) === versionStr);
|
|
266
|
+
|
|
267
|
+
versions[index].isActive = true;
|
|
268
|
+
|
|
269
|
+
Logger.info(`Worker version activated: ${workerName}@${versionStr}`);
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Check compatibility between versions
|
|
274
|
+
*/
|
|
275
|
+
checkCompatibility(
|
|
276
|
+
workerName: string,
|
|
277
|
+
sourceVersionStr: string,
|
|
278
|
+
targetVersionStr: string
|
|
279
|
+
): VersionCompatibility {
|
|
280
|
+
const sourceVersion = parseVersion(sourceVersionStr);
|
|
281
|
+
const targetVersion = parseVersion(targetVersionStr);
|
|
282
|
+
|
|
283
|
+
const source = WorkerVersioning.getVersion(workerName, sourceVersionStr);
|
|
284
|
+
const target = WorkerVersioning.getVersion(workerName, targetVersionStr);
|
|
285
|
+
|
|
286
|
+
const compatible = areVersionsCompatible(sourceVersion, targetVersion);
|
|
287
|
+
const requiresMigration = !compatible || sourceVersion.major < targetVersion.major;
|
|
288
|
+
|
|
289
|
+
const breakingChanges = target?.breakingChanges ?? [];
|
|
290
|
+
const recommendations: string[] = [];
|
|
291
|
+
|
|
292
|
+
if (target?.isDeprecated === true) {
|
|
293
|
+
recommendations.push(
|
|
294
|
+
`Target version is deprecated. Consider migrating to ${(target.migrationPath ?? '') || 'latest'}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!compatible) {
|
|
299
|
+
recommendations.push('Major version change detected. Review breaking changes carefully.');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (source?.eolDate && source.eolDate < new Date()) {
|
|
303
|
+
recommendations.push('Source version has reached end of life. Migration is required.');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
sourceVersion,
|
|
308
|
+
targetVersion,
|
|
309
|
+
compatible,
|
|
310
|
+
requiresMigration,
|
|
311
|
+
breakingChanges,
|
|
312
|
+
recommendations,
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Set version alias
|
|
318
|
+
*/
|
|
319
|
+
setAlias(workerName: string, alias: string, versionStr: string): void {
|
|
320
|
+
const version = WorkerVersioning.getVersion(workerName, versionStr);
|
|
321
|
+
|
|
322
|
+
if (!version) {
|
|
323
|
+
throw ErrorFactory.createNotFoundError(
|
|
324
|
+
`Version ${versionStr} not found for worker "${workerName}"`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const aliasKey = `${workerName}:${alias}`;
|
|
329
|
+
versionAliases.set(aliasKey, versionStr);
|
|
330
|
+
|
|
331
|
+
Logger.info(`Version alias set: ${alias} -> ${workerName}@${versionStr}`);
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get version by alias
|
|
336
|
+
*/
|
|
337
|
+
resolveAlias(workerName: string, alias: string): string | null {
|
|
338
|
+
const aliasKey = `${workerName}:${alias}`;
|
|
339
|
+
return versionAliases.get(aliasKey) ?? null;
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Parse version string
|
|
344
|
+
*/
|
|
345
|
+
parse(versionStr: string): SemanticVersion {
|
|
346
|
+
return parseVersion(versionStr);
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Convert version to string
|
|
351
|
+
*/
|
|
352
|
+
stringify(version: SemanticVersion): string {
|
|
353
|
+
return versionToString(version);
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Compare two versions
|
|
358
|
+
*/
|
|
359
|
+
compare(v1Str: string, v2Str: string): number {
|
|
360
|
+
const v1 = parseVersion(v1Str);
|
|
361
|
+
const v2 = parseVersion(v2Str);
|
|
362
|
+
return compareVersions(v1, v2);
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get version summary
|
|
367
|
+
*/
|
|
368
|
+
getSummary(workerName: string): {
|
|
369
|
+
totalVersions: number;
|
|
370
|
+
activeVersions: number;
|
|
371
|
+
deprecatedVersions: number;
|
|
372
|
+
latest: string | null;
|
|
373
|
+
stable: string | null;
|
|
374
|
+
} {
|
|
375
|
+
const versions = workerVersions.get(workerName) ?? [];
|
|
376
|
+
|
|
377
|
+
const summary = {
|
|
378
|
+
totalVersions: versions.length,
|
|
379
|
+
activeVersions: versions.filter((v) => v.isActive).length,
|
|
380
|
+
deprecatedVersions: versions.filter((v) => v.isDeprecated).length,
|
|
381
|
+
latest: WorkerVersioning.resolveAlias(workerName, 'latest'),
|
|
382
|
+
stable: WorkerVersioning.resolveAlias(workerName, 'stable'),
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
return summary;
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Clear all versions for a worker
|
|
390
|
+
*/
|
|
391
|
+
clear(workerName: string): void {
|
|
392
|
+
workerVersions.delete(workerName);
|
|
393
|
+
|
|
394
|
+
// Remove aliases
|
|
395
|
+
const keysToDelete: string[] = [];
|
|
396
|
+
for (const [key] of versionAliases.entries()) {
|
|
397
|
+
if (key.startsWith(`${workerName}:`)) {
|
|
398
|
+
keysToDelete.push(key);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const key of keysToDelete) {
|
|
403
|
+
versionAliases.delete(key);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
Logger.info(`All versions cleared for worker: ${workerName}`);
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Shutdown
|
|
411
|
+
*/
|
|
412
|
+
shutdown(): void {
|
|
413
|
+
Logger.info('WorkerVersioning shutting down...');
|
|
414
|
+
|
|
415
|
+
workerVersions.clear();
|
|
416
|
+
versionAliases.clear();
|
|
417
|
+
|
|
418
|
+
Logger.info('WorkerVersioning shutdown complete');
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Graceful shutdown handled by WorkerShutdown
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Env } from '@zintrust/core';
|
|
2
|
+
|
|
3
|
+
const normalizeBaseUrl = (value: string): string => {
|
|
4
|
+
let end = value.length;
|
|
5
|
+
while (end > 0 && value.charAt(end - 1) === '/') {
|
|
6
|
+
end--;
|
|
7
|
+
}
|
|
8
|
+
return value.slice(0, end);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const withHttpScheme = (value: string): string =>
|
|
12
|
+
value.startsWith('http://') || value.startsWith('https://') ? value : `http://${value}`;
|
|
13
|
+
|
|
14
|
+
const resolveWorkerApiUrl = (): string => {
|
|
15
|
+
const workerApiUrl = Env.get('WORKER_API_URL');
|
|
16
|
+
if (workerApiUrl) {
|
|
17
|
+
return normalizeBaseUrl(withHttpScheme(workerApiUrl));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return '';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const WorkerConfig = Object.freeze({
|
|
24
|
+
getWorkerBaseUrl: resolveWorkerApiUrl,
|
|
25
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { QueueMessage } from '@zintrust/core';
|
|
2
|
+
import { Logger, Queue } from '@zintrust/core';
|
|
3
|
+
|
|
4
|
+
type QueueWorker = {
|
|
5
|
+
processOne: (queueName?: string, driverName?: string) => Promise<boolean>;
|
|
6
|
+
processAll: (queueName?: string, driverName?: string) => Promise<number>;
|
|
7
|
+
runOnce: (opts?: {
|
|
8
|
+
queueName?: string;
|
|
9
|
+
driverName?: string;
|
|
10
|
+
maxItems?: number;
|
|
11
|
+
}) => Promise<number>;
|
|
12
|
+
startWorker: (opts?: {
|
|
13
|
+
queueName?: string;
|
|
14
|
+
driverName?: string;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}) => Promise<number>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type CreateQueueWorkerOptions<TPayload> = {
|
|
20
|
+
kindLabel: string;
|
|
21
|
+
defaultQueueName: string;
|
|
22
|
+
maxAttempts: number;
|
|
23
|
+
getLogFields: (payload: TPayload) => Record<string, unknown>;
|
|
24
|
+
handle: (payload: TPayload) => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const buildBaseLogFields = <TPayload>(
|
|
28
|
+
message: QueueMessage<TPayload>,
|
|
29
|
+
getLogFields: (payload: TPayload) => Record<string, unknown>
|
|
30
|
+
): Record<string, unknown> => {
|
|
31
|
+
return {
|
|
32
|
+
messageId: message.id,
|
|
33
|
+
...getLogFields(message.payload),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const createProcessOne = <TPayload>(
|
|
38
|
+
options: CreateQueueWorkerOptions<TPayload>
|
|
39
|
+
): ((queueName?: string, driverName?: string) => Promise<boolean>) => {
|
|
40
|
+
return async (queueName = options.defaultQueueName, driverName?: string): Promise<boolean> => {
|
|
41
|
+
const message = await Queue.dequeue<TPayload>(queueName, driverName);
|
|
42
|
+
if (!message) return false;
|
|
43
|
+
|
|
44
|
+
const baseLogFields = buildBaseLogFields(message, options.getLogFields);
|
|
45
|
+
|
|
46
|
+
// Check for delayed execution
|
|
47
|
+
const payload = message.payload as Record<string, unknown> & { timestamp?: number };
|
|
48
|
+
const rawTimestamp = 'timestamp' in payload ? payload['timestamp'] : 0;
|
|
49
|
+
const timestamp = typeof rawTimestamp === 'number' ? rawTimestamp : 0;
|
|
50
|
+
|
|
51
|
+
if (timestamp > Date.now()) {
|
|
52
|
+
Logger.info(`${options.kindLabel} not due yet, re-queueing`, {
|
|
53
|
+
...baseLogFields,
|
|
54
|
+
dueAt: new Date(timestamp).toISOString(),
|
|
55
|
+
});
|
|
56
|
+
// Re-queue original payload
|
|
57
|
+
await Queue.enqueue(queueName, message.payload, driverName);
|
|
58
|
+
await Queue.ack(queueName, message.id, driverName);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
Logger.info(`Processing queued ${options.kindLabel}`, baseLogFields);
|
|
64
|
+
await options.handle(message.payload);
|
|
65
|
+
await Queue.ack(queueName, message.id, driverName);
|
|
66
|
+
Logger.info(`${options.kindLabel} processed successfully`, baseLogFields);
|
|
67
|
+
return true;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const attempts = (message as QueueMessage<TPayload> & { attempts?: number }).attempts ?? 0;
|
|
70
|
+
|
|
71
|
+
Logger.error(`Failed to process ${options.kindLabel}`, {
|
|
72
|
+
...baseLogFields,
|
|
73
|
+
error,
|
|
74
|
+
attempts,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (attempts < options.maxAttempts) {
|
|
78
|
+
await Queue.enqueue(queueName, message.payload, driverName);
|
|
79
|
+
Logger.info(`${options.kindLabel} re-queued for retry`, {
|
|
80
|
+
...baseLogFields,
|
|
81
|
+
attempts: attempts + 1,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await Queue.ack(queueName, message.id, driverName);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const createProcessAll = (
|
|
92
|
+
defaultQueueName: string,
|
|
93
|
+
processOne: (queueName?: string, driverName?: string) => Promise<boolean>
|
|
94
|
+
): ((queueName?: string, driverName?: string) => Promise<number>) => {
|
|
95
|
+
return async (queueName = defaultQueueName, driverName?: string): Promise<number> => {
|
|
96
|
+
let processed = 0;
|
|
97
|
+
let hasMore = true;
|
|
98
|
+
|
|
99
|
+
while (hasMore) {
|
|
100
|
+
// eslint-disable-next-line no-await-in-loop
|
|
101
|
+
hasMore = await processOne(queueName, driverName);
|
|
102
|
+
if (hasMore) processed++;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return processed;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const createRunOnce = (
|
|
110
|
+
defaultQueueName: string,
|
|
111
|
+
processOne: (queueName?: string, driverName?: string) => Promise<boolean>
|
|
112
|
+
): ((opts?: { queueName?: string; driverName?: string; maxItems?: number }) => Promise<number>) => {
|
|
113
|
+
return async (opts = {}): Promise<number> => {
|
|
114
|
+
const { queueName = defaultQueueName, driverName, maxItems } = opts;
|
|
115
|
+
let processed = 0;
|
|
116
|
+
|
|
117
|
+
if (maxItems === undefined) {
|
|
118
|
+
while (true) {
|
|
119
|
+
// eslint-disable-next-line no-await-in-loop
|
|
120
|
+
const didProcess = await processOne(queueName, driverName);
|
|
121
|
+
if (!didProcess) break;
|
|
122
|
+
processed++;
|
|
123
|
+
}
|
|
124
|
+
return processed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < maxItems; i++) {
|
|
128
|
+
// eslint-disable-next-line no-await-in-loop
|
|
129
|
+
const didProcess = await processOne(queueName, driverName);
|
|
130
|
+
if (!didProcess) break;
|
|
131
|
+
processed++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return processed;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const createStartWorker = (
|
|
139
|
+
kindLabel: string,
|
|
140
|
+
defaultQueueName: string,
|
|
141
|
+
processOne: (queueName?: string, driverName?: string) => Promise<boolean>
|
|
142
|
+
): ((opts?: {
|
|
143
|
+
queueName?: string;
|
|
144
|
+
driverName?: string;
|
|
145
|
+
signal?: AbortSignal;
|
|
146
|
+
}) => Promise<number>) => {
|
|
147
|
+
return async (opts = {}): Promise<number> => {
|
|
148
|
+
const { queueName = defaultQueueName, driverName, signal } = opts;
|
|
149
|
+
|
|
150
|
+
Logger.info(`Starting ${kindLabel} worker (drain-until-empty)`, { queueName });
|
|
151
|
+
|
|
152
|
+
let processedCount = 0;
|
|
153
|
+
while (signal?.aborted !== true) {
|
|
154
|
+
// eslint-disable-next-line no-await-in-loop
|
|
155
|
+
const didProcess = await processOne(queueName, driverName);
|
|
156
|
+
if (!didProcess) break;
|
|
157
|
+
processedCount++;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Logger.info(`${kindLabel} worker finished (queue drained)`, { queueName, processedCount });
|
|
161
|
+
return processedCount;
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export function createQueueWorker<TPayload>(
|
|
166
|
+
options: CreateQueueWorkerOptions<TPayload>
|
|
167
|
+
): QueueWorker {
|
|
168
|
+
const processOne = createProcessOne(options);
|
|
169
|
+
const processAll = createProcessAll(options.defaultQueueName, processOne);
|
|
170
|
+
const runOnce = createRunOnce(options.defaultQueueName, processOne);
|
|
171
|
+
const startWorker = createStartWorker(options.kindLabel, options.defaultQueueName, processOne);
|
|
172
|
+
|
|
173
|
+
return Object.freeze({ processOne, processAll, runOnce, startWorker });
|
|
174
|
+
}
|