@onlineapps/service-wrapper 2.1.42 → 2.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.
- package/config/runtime-defaults.json +10 -0
- package/package.json +1 -1
- package/src/ConfigLoader.js +21 -3
- package/src/ServiceWrapper.js +91 -32
- package/src/index.js +2 -2
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
"enabled": true,
|
|
12
12
|
"heartbeatInterval": 30000
|
|
13
13
|
},
|
|
14
|
+
"infrastructureGate": {
|
|
15
|
+
"maxWaitMs": 300000,
|
|
16
|
+
"checkIntervalMs": 5000
|
|
17
|
+
},
|
|
18
|
+
"infrastructureVerify": {
|
|
19
|
+
"maxRetries": 12,
|
|
20
|
+
"baseDelayMs": 5000,
|
|
21
|
+
"maxDelayMs": 30000,
|
|
22
|
+
"queueCheckTimeoutMs": 5000
|
|
23
|
+
},
|
|
14
24
|
"monitoring": {
|
|
15
25
|
"enabled": true,
|
|
16
26
|
"metrics": ["requests", "errors", "duration"]
|
package/package.json
CHANGED
package/src/ConfigLoader.js
CHANGED
|
@@ -67,7 +67,14 @@ class ConfigLoader {
|
|
|
67
67
|
const varName = match[1];
|
|
68
68
|
const defaultValue = match[2];
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
const envValue = process.env[varName];
|
|
71
|
+
if (envValue !== undefined) {
|
|
72
|
+
return envValue;
|
|
73
|
+
}
|
|
74
|
+
if (defaultValue !== undefined) {
|
|
75
|
+
return defaultValue;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`[ConfigLoader] Missing environment variable - ${varName} is required (config placeholder ${match[0]})`);
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
/**
|
|
@@ -296,17 +303,28 @@ class ConfigLoader {
|
|
|
296
303
|
const operations = this.loadOperations(basePath);
|
|
297
304
|
const validationProof = this.loadValidationProof(basePath);
|
|
298
305
|
|
|
306
|
+
// Include any custom top-level sections from config.json (e.g., storage, smtp, etc.)
|
|
307
|
+
// Keep service-wrapper owned sections (service, wrapper) explicit and stable.
|
|
308
|
+
const customSections = {};
|
|
309
|
+
for (const [k, v] of Object.entries(serviceConfig || {})) {
|
|
310
|
+
if (k === 'service' || k === 'wrapper') {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
customSections[k] = v;
|
|
314
|
+
}
|
|
315
|
+
|
|
299
316
|
return {
|
|
300
317
|
service: {
|
|
301
318
|
name: serviceConfig.service.name,
|
|
302
319
|
version: serviceConfig.service.version,
|
|
303
320
|
port: parseInt(serviceConfig.service.port, 10),
|
|
304
321
|
url: serviceConfig.service.url,
|
|
305
|
-
env: process.env.NODE_ENV
|
|
322
|
+
env: process.env.NODE_ENV
|
|
306
323
|
},
|
|
307
324
|
wrapper: serviceConfig.wrapper || {},
|
|
308
325
|
operations,
|
|
309
|
-
validationProof
|
|
326
|
+
validationProof,
|
|
327
|
+
...customSections
|
|
310
328
|
};
|
|
311
329
|
}
|
|
312
330
|
|
package/src/ServiceWrapper.js
CHANGED
|
@@ -156,8 +156,15 @@ class ServiceWrapper {
|
|
|
156
156
|
const { waitForInfrastructureReady } = require('@onlineapps/service-common');
|
|
157
157
|
const { requireEnv } = require('@onlineapps/service-common');
|
|
158
158
|
const redisUrl = this.config.wrapper?.cache?.url || requireEnv('REDIS_URL', 'Redis connection URL');
|
|
159
|
-
const maxWait =
|
|
160
|
-
const checkInterval =
|
|
159
|
+
const maxWait = this.config.wrapper?.infrastructureGate?.maxWaitMs;
|
|
160
|
+
const checkInterval = this.config.wrapper?.infrastructureGate?.checkIntervalMs;
|
|
161
|
+
|
|
162
|
+
if (typeof maxWait !== 'number' || Number.isNaN(maxWait) || maxWait <= 0) {
|
|
163
|
+
throw new Error(`[InfrastructureGate] Invalid configuration - wrapper.infrastructureGate.maxWaitMs must be a positive number, got: ${maxWait}`);
|
|
164
|
+
}
|
|
165
|
+
if (typeof checkInterval !== 'number' || Number.isNaN(checkInterval) || checkInterval <= 0) {
|
|
166
|
+
throw new Error(`[InfrastructureGate] Invalid configuration - wrapper.infrastructureGate.checkIntervalMs must be a positive number, got: ${checkInterval}`);
|
|
167
|
+
}
|
|
161
168
|
|
|
162
169
|
const logWithPrefix = (level, message) => {
|
|
163
170
|
const prefixed = `[InfrastructureGate:${context}] ${message}`;
|
|
@@ -238,9 +245,8 @@ class ServiceWrapper {
|
|
|
238
245
|
return defaultValue;
|
|
239
246
|
}
|
|
240
247
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
return '';
|
|
248
|
+
// Fail-fast: placeholder without default requires ENV to be set
|
|
249
|
+
throw new Error(`[ServiceWrapper] Missing environment variable - ${varName} is required (config placeholder ${match})`);
|
|
244
250
|
});
|
|
245
251
|
}
|
|
246
252
|
return value;
|
|
@@ -794,9 +800,25 @@ class ServiceWrapper {
|
|
|
794
800
|
throw new Error('Registry URL is required when registry is enabled. Set wrapper.registry.url in config');
|
|
795
801
|
}
|
|
796
802
|
|
|
797
|
-
const serviceName = this.config.service?.name
|
|
798
|
-
|
|
799
|
-
|
|
803
|
+
const serviceName = this.config.service?.name;
|
|
804
|
+
if (!serviceName) {
|
|
805
|
+
throw new Error('[ServiceWrapper] Missing configuration - service.name is required');
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const servicePort = this.config.service?.port;
|
|
809
|
+
if (!servicePort) {
|
|
810
|
+
throw new Error('[ServiceWrapper] Missing configuration - service.port is required');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const serviceUrl = this.config.service?.url;
|
|
814
|
+
if (!serviceUrl) {
|
|
815
|
+
throw new Error('[ServiceWrapper] Missing configuration - service.url is required');
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const mqUrl = this.config.wrapper?.mq?.url;
|
|
819
|
+
if (!mqUrl) {
|
|
820
|
+
throw new Error('[ServiceWrapper] Missing configuration - wrapper.mq.url is required');
|
|
821
|
+
}
|
|
800
822
|
|
|
801
823
|
// Infrastructure health check was already done in Fáze 0.4
|
|
802
824
|
// FÁZE 0.5 a 0.6: Vytvoření front a spuštění konzumerů se provádí v registryClient.init()
|
|
@@ -805,7 +827,7 @@ class ServiceWrapper {
|
|
|
805
827
|
this.registryClient = new RegistryConnector.ServiceRegistryClient({
|
|
806
828
|
amqpUrl: mqUrl,
|
|
807
829
|
serviceName: serviceName,
|
|
808
|
-
version: this.config.service?.version
|
|
830
|
+
version: this.config.service?.version,
|
|
809
831
|
registryQueue: 'registry.register', // Use correct queue name
|
|
810
832
|
registryUrl: registryUrl,
|
|
811
833
|
logger: this.logger || console,
|
|
@@ -819,10 +841,10 @@ class ServiceWrapper {
|
|
|
819
841
|
// Register service
|
|
820
842
|
const serviceInfo = {
|
|
821
843
|
name: serviceName,
|
|
822
|
-
url:
|
|
844
|
+
url: serviceUrl,
|
|
823
845
|
operations: this.operations?.operations || {},
|
|
824
846
|
metadata: {
|
|
825
|
-
version: this.config.service?.version
|
|
847
|
+
version: this.config.service?.version,
|
|
826
848
|
description: this.config.service?.description || ''
|
|
827
849
|
}
|
|
828
850
|
};
|
|
@@ -888,10 +910,10 @@ class ServiceWrapper {
|
|
|
888
910
|
}
|
|
889
911
|
|
|
890
912
|
// Start heartbeat
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
913
|
+
const heartbeatInterval = this.config.wrapper?.registry?.heartbeatInterval;
|
|
914
|
+
if (typeof heartbeatInterval !== 'number' || Number.isNaN(heartbeatInterval) || heartbeatInterval <= 0) {
|
|
915
|
+
throw new Error(`[ServiceWrapper] Invalid configuration - wrapper.registry.heartbeatInterval must be a positive number, got: ${heartbeatInterval}`);
|
|
916
|
+
}
|
|
895
917
|
this.logger?.info(`[ServiceWrapper] Heartbeat interval set to ${heartbeatInterval}ms`);
|
|
896
918
|
|
|
897
919
|
this.heartbeatTimer = setInterval(async () => {
|
|
@@ -920,9 +942,23 @@ class ServiceWrapper {
|
|
|
920
942
|
* @private
|
|
921
943
|
*/
|
|
922
944
|
async _verifyInfrastructureReady(serviceName) {
|
|
923
|
-
const maxRetries =
|
|
924
|
-
const baseDelay =
|
|
925
|
-
const maxDelay =
|
|
945
|
+
const maxRetries = this.config.wrapper?.infrastructureVerify?.maxRetries;
|
|
946
|
+
const baseDelay = this.config.wrapper?.infrastructureVerify?.baseDelayMs;
|
|
947
|
+
const maxDelay = this.config.wrapper?.infrastructureVerify?.maxDelayMs;
|
|
948
|
+
const queueCheckTimeoutMs = this.config.wrapper?.infrastructureVerify?.queueCheckTimeoutMs;
|
|
949
|
+
|
|
950
|
+
if (typeof maxRetries !== 'number' || Number.isNaN(maxRetries) || maxRetries <= 0) {
|
|
951
|
+
throw new Error(`[InfrastructureVerify] Invalid configuration - wrapper.infrastructureVerify.maxRetries must be a positive number, got: ${maxRetries}`);
|
|
952
|
+
}
|
|
953
|
+
if (typeof baseDelay !== 'number' || Number.isNaN(baseDelay) || baseDelay <= 0) {
|
|
954
|
+
throw new Error(`[InfrastructureVerify] Invalid configuration - wrapper.infrastructureVerify.baseDelayMs must be a positive number, got: ${baseDelay}`);
|
|
955
|
+
}
|
|
956
|
+
if (typeof maxDelay !== 'number' || Number.isNaN(maxDelay) || maxDelay <= 0) {
|
|
957
|
+
throw new Error(`[InfrastructureVerify] Invalid configuration - wrapper.infrastructureVerify.maxDelayMs must be a positive number, got: ${maxDelay}`);
|
|
958
|
+
}
|
|
959
|
+
if (typeof queueCheckTimeoutMs !== 'number' || Number.isNaN(queueCheckTimeoutMs) || queueCheckTimeoutMs <= 0) {
|
|
960
|
+
throw new Error(`[InfrastructureVerify] Invalid configuration - wrapper.infrastructureVerify.queueCheckTimeoutMs must be a positive number, got: ${queueCheckTimeoutMs}`);
|
|
961
|
+
}
|
|
926
962
|
|
|
927
963
|
// Required infrastructure queues that must exist before business queues can be created
|
|
928
964
|
const requiredInfrastructureQueues = [
|
|
@@ -962,15 +998,14 @@ class ServiceWrapper {
|
|
|
962
998
|
}
|
|
963
999
|
|
|
964
1000
|
const missingQueues = [];
|
|
965
|
-
const QUEUE_CHECK_TIMEOUT = 5000; // 5 seconds per queue check
|
|
966
1001
|
for (const queueName of requiredInfrastructureQueues) {
|
|
967
1002
|
try {
|
|
968
1003
|
// Add timeout to prevent hanging on checkQueue
|
|
969
1004
|
const checkPromise = channel.checkQueue(queueName);
|
|
970
1005
|
const checkTimeoutPromise = new Promise((_, reject) => {
|
|
971
1006
|
setTimeout(() => {
|
|
972
|
-
reject(new Error(`checkQueue timeout after ${
|
|
973
|
-
},
|
|
1007
|
+
reject(new Error(`checkQueue timeout after ${queueCheckTimeoutMs}ms`));
|
|
1008
|
+
}, queueCheckTimeoutMs);
|
|
974
1009
|
});
|
|
975
1010
|
|
|
976
1011
|
await Promise.race([checkPromise, checkTimeoutPromise]);
|
|
@@ -1004,7 +1039,7 @@ class ServiceWrapper {
|
|
|
1004
1039
|
try {
|
|
1005
1040
|
const checkPromise = channel.checkQueue(queueName);
|
|
1006
1041
|
const checkTimeoutPromise = new Promise((_, reject) => {
|
|
1007
|
-
setTimeout(() => reject(new Error('timeout')),
|
|
1042
|
+
setTimeout(() => reject(new Error('timeout')), queueCheckTimeoutMs);
|
|
1008
1043
|
});
|
|
1009
1044
|
await Promise.race([checkPromise, checkTimeoutPromise]);
|
|
1010
1045
|
this.logger?.info(`[InfrastructureVerify] ✓ Optional queue exists: ${queueName}`);
|
|
@@ -1102,14 +1137,29 @@ class ServiceWrapper {
|
|
|
1102
1137
|
* @private
|
|
1103
1138
|
*/
|
|
1104
1139
|
async _initializeOrchestrator() {
|
|
1105
|
-
const
|
|
1106
|
-
const
|
|
1140
|
+
const serviceName = this.config.service?.name;
|
|
1141
|
+
const serviceVersion = this.config.service?.version;
|
|
1142
|
+
const environment = this.config.service?.env;
|
|
1143
|
+
const serviceUrl = this.config.service?.url;
|
|
1144
|
+
|
|
1145
|
+
if (!serviceName) {
|
|
1146
|
+
throw new Error('[ServiceWrapper] Missing configuration - service.name is required');
|
|
1147
|
+
}
|
|
1148
|
+
if (!serviceVersion) {
|
|
1149
|
+
throw new Error('[ServiceWrapper] Missing configuration - service.version is required');
|
|
1150
|
+
}
|
|
1151
|
+
if (!environment) {
|
|
1152
|
+
throw new Error('[ServiceWrapper] Missing configuration - service.env (NODE_ENV) is required');
|
|
1153
|
+
}
|
|
1154
|
+
if (!serviceUrl) {
|
|
1155
|
+
throw new Error('[ServiceWrapper] Missing configuration - service.url is required');
|
|
1156
|
+
}
|
|
1107
1157
|
|
|
1108
1158
|
// Create error handler with monitoring instance
|
|
1109
1159
|
const errorHandler = new ErrorHandlerConnector({
|
|
1110
|
-
serviceName
|
|
1111
|
-
serviceVersion
|
|
1112
|
-
environment
|
|
1160
|
+
serviceName,
|
|
1161
|
+
serviceVersion,
|
|
1162
|
+
environment,
|
|
1113
1163
|
monitoring: this.monitoring, // Use monitoring instance from conn-base-monitoring
|
|
1114
1164
|
handling: {
|
|
1115
1165
|
maxRetries: this.config.wrapper?.errorHandling?.maxRetries || 3,
|
|
@@ -1374,8 +1424,12 @@ class ServiceWrapper {
|
|
|
1374
1424
|
throw new Error(`Unknown operation: ${operationName}`);
|
|
1375
1425
|
}
|
|
1376
1426
|
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1427
|
+
const serviceUrl = this.config.service?.url;
|
|
1428
|
+
if (!serviceUrl) {
|
|
1429
|
+
throw new Error('[ServiceWrapper] Missing configuration - service.url is required');
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const url = new URL(operation.endpoint, serviceUrl).toString();
|
|
1379
1433
|
const method = operation.method || 'POST';
|
|
1380
1434
|
|
|
1381
1435
|
this.logger?.debug(`Executing operation ${operationName} via ${method} ${url}`);
|
|
@@ -1386,10 +1440,15 @@ class ServiceWrapper {
|
|
|
1386
1440
|
return new Promise((resolve, reject) => {
|
|
1387
1441
|
const postData = JSON.stringify(input);
|
|
1388
1442
|
|
|
1443
|
+
const target = new URL(url);
|
|
1444
|
+
const targetPort = target.port
|
|
1445
|
+
? parseInt(target.port, 10)
|
|
1446
|
+
: (target.protocol === 'https:' ? 443 : 80);
|
|
1447
|
+
|
|
1389
1448
|
const options = {
|
|
1390
|
-
hostname:
|
|
1391
|
-
port:
|
|
1392
|
-
path:
|
|
1449
|
+
hostname: target.hostname,
|
|
1450
|
+
port: targetPort,
|
|
1451
|
+
path: `${target.pathname}${target.search}`,
|
|
1393
1452
|
method: method,
|
|
1394
1453
|
headers: {
|
|
1395
1454
|
'Content-Type': 'application/json',
|
package/src/index.js
CHANGED
|
@@ -63,7 +63,7 @@ async function bootstrap(serviceRoot, options = {}) {
|
|
|
63
63
|
const PORT = config.service.port;
|
|
64
64
|
const server = app.listen(PORT, () => {
|
|
65
65
|
console.log(`✓ HTTP server listening on port ${PORT}`);
|
|
66
|
-
console.log(`✓ Health check:
|
|
66
|
+
console.log(`✓ Health check: /health`);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
// 2. Initialize Service Wrapper (MQ, Registry, Monitoring, etc.)
|
|
@@ -76,7 +76,7 @@ async function bootstrap(serviceRoot, options = {}) {
|
|
|
76
76
|
name: config.service.name,
|
|
77
77
|
version: config.service.version,
|
|
78
78
|
port: config.service.port,
|
|
79
|
-
url:
|
|
79
|
+
url: config.service.url
|
|
80
80
|
},
|
|
81
81
|
wrapper: config.wrapper
|
|
82
82
|
},
|