@onlineapps/service-wrapper 2.2.7 → 2.2.9

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/jest.config.js CHANGED
@@ -27,7 +27,8 @@ module.exports = {
27
27
  '@onlineapps/conn-base-logger': '<rootDir>/tests/mocks/connectors.js',
28
28
  '@onlineapps/conn-orch-orchestrator': '<rootDir>/tests/mocks/connectors.js',
29
29
  '@onlineapps/conn-orch-api-mapper': '<rootDir>/tests/mocks/connectors.js',
30
- '@onlineapps/conn-orch-cookbook': '<rootDir>/tests/mocks/connectors.js'
30
+ '@onlineapps/conn-orch-cookbook': '<rootDir>/tests/mocks/connectors.js',
31
+ '@onlineapps/conn-base-state': '<rootDir>/tests/mocks/connectors.js'
31
32
  },
32
33
  setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
33
34
  verbose: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -25,6 +25,7 @@
25
25
  "license": "MIT",
26
26
  "dependencies": {
27
27
  "@onlineapps/conn-base-cache": "1.0.9",
28
+ "@onlineapps/conn-base-state": "1.0.1",
28
29
  "@onlineapps/conn-base-monitoring": "1.0.10",
29
30
  "@onlineapps/conn-infra-error-handler": "1.0.9",
30
31
  "@onlineapps/conn-infra-mq": "1.1.69",
@@ -20,6 +20,7 @@ const OrchestratorConnector = require('@onlineapps/conn-orch-orchestrator');
20
20
  const ApiMapperConnector = require('@onlineapps/conn-orch-api-mapper');
21
21
  const CookbookConnector = require('@onlineapps/conn-orch-cookbook');
22
22
  const CacheConnector = require('@onlineapps/conn-base-cache');
23
+ const StateConnector = require('@onlineapps/conn-base-state');
23
24
  const ErrorHandlerConnector = require('@onlineapps/conn-infra-error-handler');
24
25
  const { ValidationOrchestrator } = require('@onlineapps/conn-orch-validator');
25
26
  const runtimeCfg = require('./config');
@@ -415,13 +416,22 @@ class ServiceWrapper {
415
416
  }
416
417
  }
417
418
 
418
- // 4. Close Redis connection (if used)
419
+ // 4. Close Redis connections (cache + state)
419
420
  if (this.cacheConnector) {
420
421
  try {
421
422
  await this.cacheConnector.disconnect();
422
- console.log(`[CLEANUP] ✓ Closed Redis connection`);
423
+ console.log(`[CLEANUP] ✓ Closed Redis cache connection`);
423
424
  } catch (err) {
424
- console.warn(`[CLEANUP] Failed to close Redis connection:`, err.message);
425
+ console.warn(`[CLEANUP] Failed to close Redis cache connection:`, err.message);
426
+ }
427
+ }
428
+
429
+ if (this.stateConnector) {
430
+ try {
431
+ await this.stateConnector.disconnect();
432
+ console.log(`[CLEANUP] ✓ Closed Redis state connection`);
433
+ } catch (err) {
434
+ console.warn(`[CLEANUP] Failed to close Redis state connection:`, err.message);
425
435
  }
426
436
  }
427
437
 
@@ -574,6 +584,11 @@ class ServiceWrapper {
574
584
  }
575
585
  }
576
586
 
587
+ // Initialize state connector if configured (critical — let it throw)
588
+ if (this.config.wrapper?.state?.enabled === true) {
589
+ await this._initializeState();
590
+ }
591
+
577
592
  // Setup health checks
578
593
  if (this.config.wrapper?.health?.enabled !== false) {
579
594
  this._setupHealthChecks();
@@ -1089,6 +1104,37 @@ class ServiceWrapper {
1089
1104
  this.logger?.info('Cache connector initialized');
1090
1105
  }
1091
1106
 
1107
+ /**
1108
+ * Initialize state connector (persistent Redis state, no TTL)
1109
+ * @private
1110
+ */
1111
+ async _initializeState() {
1112
+ const stateConfig = this.config.wrapper?.state;
1113
+ const serviceName = stateConfig?.serviceName || this.config.service?.name;
1114
+ if (!serviceName) {
1115
+ throw new Error('[ServiceWrapper] state.serviceName or service.name is required when state is enabled');
1116
+ }
1117
+
1118
+ const cacheUrl = this.config.wrapper?.cache?.url;
1119
+ let host, port;
1120
+ if (cacheUrl?.startsWith('redis://')) {
1121
+ const parsed = new URL(cacheUrl);
1122
+ host = parsed.hostname;
1123
+ port = parseInt(parsed.port, 10);
1124
+ }
1125
+
1126
+ this.stateConnector = new StateConnector({
1127
+ host: stateConfig?.host || host,
1128
+ port: stateConfig?.port || port,
1129
+ password: stateConfig?.password,
1130
+ db: stateConfig?.db,
1131
+ serviceName,
1132
+ });
1133
+
1134
+ await this.stateConnector.connect();
1135
+ this.logger?.info('State connector initialized');
1136
+ }
1137
+
1092
1138
  /**
1093
1139
  * Setup health check endpoint
1094
1140
  * @private
@@ -1105,7 +1151,8 @@ class ServiceWrapper {
1105
1151
  http: 'healthy',
1106
1152
  mq: 'disabled',
1107
1153
  registry: this.registryClient ? 'healthy' : 'disabled',
1108
- cache: this.cacheConnector ? 'healthy' : 'disabled'
1154
+ cache: this.cacheConnector ? 'healthy' : 'disabled',
1155
+ state: this.stateConnector ? 'healthy' : 'disabled'
1109
1156
  }
1110
1157
  };
1111
1158
 
@@ -2061,6 +2108,11 @@ class ServiceWrapper {
2061
2108
  await this.cacheConnector.disconnect();
2062
2109
  }
2063
2110
 
2111
+ // Disconnect from state
2112
+ if (this.stateConnector) {
2113
+ await this.stateConnector.disconnect();
2114
+ }
2115
+
2064
2116
  // Disconnect from MQ
2065
2117
  if (this.mqClient) {
2066
2118
  await this.mqClient.disconnect();
@@ -54,7 +54,10 @@ function createTenantContextMiddleware(options = {}) {
54
54
 
55
55
  function sendJson(res, statusCode, payload) {
56
56
  if (!res || typeof res.status !== 'function' || typeof res.json !== 'function') {
57
- throw new Error('[service-wrapper][TenantContext] Invalid response object - Expected Express res with res.status().json()');
57
+ throw new Error(
58
+ '[service-wrapper][TenantContext] res.status/res.json unavailable - ' +
59
+ 'middleware is likely running before expressInit (check bootstrap stack order)'
60
+ );
58
61
  }
59
62
  return res.status(statusCode).json(payload);
60
63
  }
package/src/index.js CHANGED
@@ -93,7 +93,16 @@ async function bootstrap(serviceRoot, options = {}) {
93
93
  }
94
94
 
95
95
  stack.pop();
96
- stack.unshift(lastLayer);
96
+ // Insert AFTER expressInit (which sets up res.status/res.json via setPrototypeOf).
97
+ // Position 0 = query, 1 = expressInit. Tenant context must run after both.
98
+ const expressInitIdx = stack.findIndex(l => l.name === 'expressInit');
99
+ if (expressInitIdx === -1) {
100
+ throw new Error(
101
+ '[service-wrapper][TenantContext] Express stack missing expressInit layer - ' +
102
+ 'cannot guarantee tenant context runs after Express initialization'
103
+ );
104
+ }
105
+ stack.splice(expressInitIdx + 1, 0, lastLayer);
97
106
 
98
107
  // Register /health route BEFORE service's 404 handler.
99
108
  // ServiceWrapper._setupHealthChecks() will later set _healthImpl with actual MQ checks.
@@ -111,7 +120,7 @@ async function bootstrap(serviceRoot, options = {}) {
111
120
  const healthLayer = stack.pop();
112
121
  if (healthLayer) {
113
122
  const catchAllIdx = stack.findIndex((layer, i) =>
114
- i > 0 && !layer.route && !['query', 'expressInit', 'jsonParser', 'urlencodedParser'].includes(layer.name)
123
+ i > 0 && !layer.route && !['query', 'expressInit', 'tenantContextMiddleware', 'jsonParser', 'urlencodedParser'].includes(layer.name)
115
124
  );
116
125
  if (catchAllIdx > 0) {
117
126
  stack.splice(catchAllIdx, 0, healthLayer);