@fluojs/terminus 1.0.4 → 1.0.5

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.ko.md CHANGED
@@ -14,6 +14,7 @@ fluo 애플리케이션을 위한 헬스 인디케이터(Health Indicator) 툴
14
14
  - [DI 기반 인디케이터](#di-기반-인디케이터)
15
15
  - [실행 가드레일](#실행-가드레일)
16
16
  - [실패 시맨틱](#실패-시맨틱)
17
+ - [NestJS 마이그레이션 경계](#nestjs-마이그레이션-경계)
17
18
  - [공개 API 개요](#공개-api-개요)
18
19
  - [관련 패키지](#관련-패키지)
19
20
  - [예제 소스](#예제-소스)
@@ -106,6 +107,8 @@ Provider factory는 반복 등록할 수 있습니다. 각 인스턴스가 서
106
107
 
107
108
  커스텀 인디케이터가 멈추거나 느린 하위 서비스에 의존할 수 있다면 `execution.indicatorTimeoutMs`를 사용하세요. probe가 설정된 시간을 넘기면 Terminus는 무기한 대기하지 않고 해당 인디케이터를 `down`으로 표시합니다.
108
109
 
110
+ Terminus는 같은 indicator instance에 대한 check도 직렬화합니다. Timeout된 probe나 느린 probe가 아직 실행 중일 때 다른 `/health` 또는 `/ready` 요청이 들어오면, Terminus는 같은 downstream에 겹치는 probe를 새로 시작하지 않고 해당 indicator를 새 요청에서 `down`으로 보고합니다. Built-in HTTP indicator는 자체 timeout이 만료되면 `fetch` 요청을 abort하지만, 다른 driver와 custom callback은 cancellation을 노출하지 않을 수 있으므로 원래 promise가 settle될 때까지 overlap을 막는 방식으로 보호합니다.
111
+
109
112
  ```typescript
110
113
  TerminusModule.forRoot({
111
114
  execution: {
@@ -121,7 +124,7 @@ TerminusModule.forRoot({
121
124
 
122
125
  ### 실패 시맨틱
123
126
 
124
- 인디케이터가 실패하면 `HealthCheckError`를 던집니다. `TerminusHealthService`는 이 실패들을 모아 보고서를 작성합니다.
127
+ 인디케이터가 `down` 결과를 반환하거나 `HealthCheckError`를 던지면, `TerminusHealthService`는 이 실패들을 모아 보고서를 작성합니다.
125
128
 
126
129
  - 하나 이상의 인디케이터가 실패하면 `/health`는 HTTP `503`을 반환합니다.
127
130
  - 등록된 indicator가 실패하거나, custom readiness check가 `false`를 반환하거나, runtime shutdown이 시작되었거나, platform readiness가 `ready`가 아닌 경우 `/ready`는 HTTP `503`을 반환합니다. Platform `critical` metadata는 diagnostics에 보존되지만 HTTP readiness endpoint 자체는 binary ready/unavailable gate이며 warning severity bucket을 노출하지 않습니다.
@@ -130,11 +133,19 @@ TerminusModule.forRoot({
130
133
  - 지원하지 않는 status, 빈 결과, 객체가 아닌 인디케이터 결과는 조용히 버려지지 않고 `down` 진단으로 보고됩니다.
131
134
  - Blank indicator result key는 healthy entry로 기여하지 않고 `down` 진단으로 보고됩니다.
132
135
  - 같은 실행에서 이미 보고된 key를 다른 인디케이터가 다시 사용하면, Terminus는 먼저 기록된 entry를 유지하고 데이터를 조용히 덮어쓰는 대신 결정적인 `*-duplicate-key-error` contributor를 추가합니다.
133
- - 플랫폼 health/readiness 실패는 `/health` 응답에서 결정적인 `fluo-platform-health`, `fluo-platform-readiness` contributor로 노출됩니다.
136
+ - 플랫폼 health/readiness 실패는 `/health` 응답에서 결정적인 `fluo-platform-health`, `fluo-platform-readiness` contributor로 노출됩니다. 이 key들은 platform diagnostic용으로 예약되어 있으며, platform failure 중 user indicator가 같은 key를 반환하면 Terminus는 platform payload를 예약 key 아래에 유지하고 runtime state를 떨어뜨리지 않도록 결정적인 `*-user-key-collision` diagnostic을 추가합니다.
134
137
  - Runtime diagnostics가 있으면 `/health` response에 platform health/readiness detail을 담은 `platform` block이 포함될 수 있습니다.
135
138
  - DI provider로 생성한 Drizzle indicator는 SQL probe보다 먼저 Drizzle lifecycle readiness/health state를 반영하므로, underlying driver가 raw ping을 아직 받을 수 있어도 종료 중이거나 중지된 통합은 `/health`와 `/ready`를 unavailable로 표시합니다.
136
139
  - Redis subpath로 생성한 Redis indicator는 `PING` 전에 `@fluojs/redis` client lifecycle state를 반영하므로, 종료 중이거나 연결이 끊긴 Redis client는 command 실행 전에도 `/health`와 `/ready`를 unavailable로 표시합니다.
137
140
 
141
+ ## NestJS 마이그레이션 경계
142
+
143
+ `@nestjs/terminus`에서 마이그레이션할 때는 `TerminusModule.forRoot(...)`를 fluo의 기본 API로 취급하세요. fluo는 `HealthCheckService.check([...])`를 호출하는 controller-level `@HealthCheck()` 메서드를 주요 애플리케이션 계약으로 모델링하지 않습니다. 테스트나 커스텀 애플리케이션 코드에서 `TerminusHealthService.check()`를 직접 호출할 수는 있지만, 프로덕션 엔드포인트 등록은 indicator와 readiness hook을 module option에 두어 runtime `/health`와 `/ready` 경로가 platform diagnostics를 일관되게 포함하도록 해야 합니다.
144
+
145
+ Terminus는 별도의 process-only liveness route도 기본으로 만들지 않습니다. 기본 route model은 집계 헬스를 위한 `GET /health`, readiness를 위한 `GET /ready`입니다. 배포 환경에서 좁은 의미의 process liveness probe가 필요하다면, Terminus가 NestJS-style 추가 route를 만들어 준다고 가정하지 말고 애플리케이션 또는 배포 계층에서 해당 probe를 정의하세요.
146
+
147
+ Runtime-specific indicator는 subpath별로 분리되어 있습니다. Node.js memory 및 disk check에는 `@fluojs/terminus/node`를 사용하고, Redis check에는 `@fluojs/terminus/redis`를 사용하세요. Root package는 Redis optional-peer import를 root entrypoint 밖에 두고 Node disk filesystem access도 lazy하게 유지하므로, 애플리케이션은 runtime-specific probe를 명시적으로 opt in합니다.
148
+
138
149
  ## 공개 API 개요
139
150
 
140
151
  ### `TerminusModule`
package/README.md CHANGED
@@ -14,6 +14,7 @@ Health indicator toolkit for fluo applications. `@fluojs/terminus` layers on top
14
14
  - [DI-Backed Indicators](#di-backed-indicators)
15
15
  - [Execution Guardrails](#execution-guardrails)
16
16
  - [Failure Semantics](#failure-semantics)
17
+ - [NestJS Migration Boundaries](#nestjs-migration-boundaries)
17
18
  - [Public API Overview](#public-api-overview)
18
19
  - [Related Packages](#related-packages)
19
20
  - [Example Sources](#example-sources)
@@ -106,6 +107,8 @@ Provider factories are repeatable. You may register multiple providers created b
106
107
 
107
108
  Use `execution.indicatorTimeoutMs` when custom indicators might hang or depend on slow downstreams. When a probe exceeds the configured timeout, Terminus marks that indicator as `down` instead of waiting forever.
108
109
 
110
+ Terminus also serializes checks per indicator instance. If a timed-out or otherwise slow probe is still running when another `/health` or `/ready` request arrives, Terminus reports that indicator as `down` for the new request instead of starting an overlapping probe against the same downstream. Built-in HTTP indicators abort their `fetch` request when their own timeout expires; other drivers and custom callbacks may not expose cancellation, so they are protected from overlap until the original promise settles.
111
+
109
112
  ```typescript
110
113
  TerminusModule.forRoot({
111
114
  execution: {
@@ -121,7 +124,7 @@ Use `path` to mount the health endpoints under a custom path, and `readinessChec
121
124
 
122
125
  ### Failure Semantics
123
126
 
124
- When an indicator fails, it throws a `HealthCheckError`. The `TerminusHealthService` aggregates these failures into a report:
127
+ When an indicator returns a `down` result or throws a `HealthCheckError`, the `TerminusHealthService` aggregates the failure into a report:
125
128
 
126
129
  - `/health` returns HTTP `503` if any indicator fails.
127
130
  - `/ready` returns HTTP `503` when registered indicators fail, a custom readiness check returns `false`, runtime shutdown has begun, or platform readiness is anything other than `ready`. Platform `critical` metadata is preserved in diagnostics, but the HTTP readiness endpoint itself is a binary ready/unavailable gate and does not expose warning severity buckets.
@@ -130,11 +133,19 @@ When an indicator fails, it throws a `HealthCheckError`. The `TerminusHealthServ
130
133
  - Unsupported, empty, or non-object indicator results are reported as `down` diagnostics instead of being silently discarded.
131
134
  - Blank indicator result keys are reported as `down` diagnostics instead of contributing healthy entries.
132
135
  - If an indicator reuses a key that was already reported earlier in the same run, Terminus keeps the first entry and adds a deterministic `*-duplicate-key-error` contributor instead of silently overwriting data.
133
- - Platform health/readiness failures are surfaced as deterministic `fluo-platform-health` and `fluo-platform-readiness` contributors in `/health` responses.
136
+ - Platform health/readiness failures are surfaced as deterministic `fluo-platform-health` and `fluo-platform-readiness` contributors in `/health` responses. These keys are reserved for platform diagnostics; if a user indicator returns one of them during a platform failure, Terminus keeps the platform payload under the reserved key and adds a deterministic `*-user-key-collision` diagnostic instead of dropping runtime state.
134
137
  - `/health` responses may include a `platform` block with platform health/readiness details when runtime diagnostics are available.
135
138
  - Drizzle indicators created through the DI provider map Drizzle lifecycle readiness/health state before SQL probing, so shutdown or stopped integrations mark `/health` and `/ready` as unavailable even if the underlying driver still accepts a raw ping.
136
139
  - Redis indicators created through the Redis subpath map `@fluojs/redis` client lifecycle state before `PING`, so shutdown or disconnected Redis clients mark `/health` and `/ready` as unavailable even before command execution.
137
140
 
141
+ ## NestJS Migration Boundaries
142
+
143
+ When migrating from `@nestjs/terminus`, treat `TerminusModule.forRoot(...)` as the primary fluo API. fluo does not model controller-level `@HealthCheck()` methods that call `HealthCheckService.check([...])` as the main application contract. You can still call `TerminusHealthService.check()` directly from tests or custom application code, but production endpoint registration should keep indicators and readiness hooks in module options so the runtime `/health` and `/ready` routes include platform diagnostics consistently.
144
+
145
+ Terminus also does not create a separate process-only liveness route by default. The default route model remains `GET /health` for aggregated health and `GET /ready` for readiness. If your deployment requires a narrow process liveness probe, define that probe at the application or deployment layer instead of assuming Terminus will add a NestJS-style extra route.
146
+
147
+ Runtime-specific indicators are split by subpath. Use `@fluojs/terminus/node` for Node.js memory and disk checks, and use `@fluojs/terminus/redis` for Redis checks. The root package keeps Redis optional-peer imports out of the root entrypoint and keeps Node disk filesystem access lazy so applications opt into runtime-specific probes explicitly.
148
+
138
149
  ## Public API Overview
139
150
 
140
151
  ### `TerminusModule`
@@ -1 +1 @@
1
- {"version":3,"file":"health-check.d.ts","sourceRoot":"","sources":["../src/health-check.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,2BAA2B,EAC3B,iBAAiB,EACjB,eAAe,EAGhB,MAAM,YAAY,CAAC;AAyQpB;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,UAAU,EAAE,SAAS,eAAe,EAAE,EACtC,gBAAgB,GAAE,2BAAgC,GACjD,OAAO,CAAC,iBAAiB,CAAC,CAoB5B;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,SAAyB,GAAG,iBAAiB,CAMhH;AAED,0FAA0F;AAC1F,qBAAa,qBAAqB;IAE9B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;gBADhB,UAAU,EAAE,SAAS,eAAe,EAAE,EACtC,gBAAgB,GAAE,2BAAgC;IAGrE;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAIzC;;;;OAIG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAGpC"}
1
+ {"version":3,"file":"health-check.d.ts","sourceRoot":"","sources":["../src/health-check.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,2BAA2B,EAC3B,iBAAiB,EACjB,eAAe,EAGhB,MAAM,YAAY,CAAC;AAqTpB;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,UAAU,EAAE,SAAS,eAAe,EAAE,EACtC,gBAAgB,GAAE,2BAAgC,GACjD,OAAO,CAAC,iBAAiB,CAAC,CAoB5B;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,SAAyB,GAAG,iBAAiB,CAMhH;AAED,0FAA0F;AAC1F,qBAAa,qBAAqB;IAE9B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;gBADhB,UAAU,EAAE,SAAS,eAAe,EAAE,EACtC,gBAAgB,GAAE,2BAAgC;IAGrE;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAIzC;;;;OAIG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAGpC"}
@@ -1,4 +1,5 @@
1
1
  import { HealthCheckError } from './errors.js';
2
+ const runningIndicatorChecks = new WeakMap();
2
3
  function normalizeIndicatorTimeoutMs(value) {
3
4
  if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
4
5
  return undefined;
@@ -8,6 +9,32 @@ function normalizeIndicatorTimeoutMs(value) {
8
9
  function createTimeoutMessage(timeoutMs) {
9
10
  return `Health indicator timed out after ${String(timeoutMs)}ms.`;
10
11
  }
12
+ function createInFlightResult(key) {
13
+ return {
14
+ [key]: {
15
+ message: 'A previous health indicator probe is still running; Terminus will not start an overlapping probe for the same indicator instance.',
16
+ status: 'down'
17
+ }
18
+ };
19
+ }
20
+ function startSerializedIndicatorCheck(indicator, key) {
21
+ const runningCheck = runningIndicatorChecks.get(indicator);
22
+ if (runningCheck) {
23
+ return undefined;
24
+ }
25
+ const check = Promise.resolve().then(() => indicator.check(key));
26
+ runningIndicatorChecks.set(indicator, check);
27
+ check.then(() => {
28
+ if (runningIndicatorChecks.get(indicator) === check) {
29
+ runningIndicatorChecks.delete(indicator);
30
+ }
31
+ }, () => {
32
+ if (runningIndicatorChecks.get(indicator) === check) {
33
+ runningIndicatorChecks.delete(indicator);
34
+ }
35
+ });
36
+ return check;
37
+ }
11
38
  async function withTimeout(promise, timeoutMs) {
12
39
  let timer;
13
40
  try {
@@ -137,8 +164,15 @@ function createDuplicateKeyFailure(indicatorKey, duplicateKeys, seenKeys) {
137
164
  async function runIndicator(indicator, index, executionOptions) {
138
165
  const key = inferIndicatorKey(indicator, index);
139
166
  const indicatorTimeoutMs = normalizeIndicatorTimeoutMs(executionOptions.indicatorTimeoutMs);
167
+ const runningCheck = startSerializedIndicatorCheck(indicator, key);
168
+ if (!runningCheck) {
169
+ return {
170
+ entries: Object.entries(createInFlightResult(key)),
171
+ indicatorKey: key
172
+ };
173
+ }
140
174
  try {
141
- const result = indicatorTimeoutMs === undefined ? await indicator.check(key) : await withTimeout(indicator.check(key), indicatorTimeoutMs);
175
+ const result = indicatorTimeoutMs === undefined ? await runningCheck : await withTimeout(runningCheck, indicatorTimeoutMs);
142
176
  return {
143
177
  entries: normalizeIndicatorResult(key, result),
144
178
  indicatorKey: key
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,UAAU,EAIhB,MAAM,iBAAiB,CAAC;AAIzB,OAAO,KAAK,EAA4D,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAyQlH,uFAAuF;AACvF,qBAAa,cAAc;IACzB;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,qBAA0B,GAAG,UAAU;CAGhE"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,UAAU,EAIhB,MAAM,iBAAiB,CAAC;AAIzB,OAAO,KAAK,EAA4D,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAiSlH,uFAAuF;AACvF,qBAAa,cAAc;IACzB;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,qBAA0B,GAAG,UAAU;CAGhE"}
package/dist/module.js CHANGED
@@ -83,7 +83,7 @@ function createPlatformReadinessDiagnostic(readiness) {
83
83
  };
84
84
  }
85
85
  function createPlatformDiagnosticCollisionKey(diagnosticKey, seenKeys) {
86
- const baseKey = `${diagnosticKey}-duplicate-key-error`;
86
+ const baseKey = `${diagnosticKey}-user-key-collision`;
87
87
  if (!seenKeys.has(baseKey)) {
88
88
  return baseKey;
89
89
  }
@@ -95,42 +95,51 @@ function createPlatformDiagnosticCollisionKey(diagnosticKey, seenKeys) {
95
95
  }
96
96
  return candidate;
97
97
  }
98
- function appendPlatformDiagnostic(entries, existingKeys, diagnosticKey, diagnostic) {
98
+ function appendPlatformDiagnostic(entries, collisionEntries, diagnosticKey, diagnostic) {
99
99
  if (diagnostic === undefined) {
100
100
  return;
101
101
  }
102
- if (!existingKeys.has(diagnosticKey) && !(diagnosticKey in entries)) {
103
- entries[diagnosticKey] = diagnostic;
104
- return;
102
+ if (diagnosticKey in collisionEntries) {
103
+ const collisionKey = createPlatformDiagnosticCollisionKey(diagnosticKey, new Set([...Object.keys(entries), ...Object.keys(collisionEntries)]));
104
+ entries[collisionKey] = {
105
+ message: `User health result key "${diagnosticKey}" is reserved for Terminus platform diagnostics; the platform diagnostic remains available under the reserved key.`,
106
+ status: 'down'
107
+ };
105
108
  }
106
- const collisionKey = createPlatformDiagnosticCollisionKey(diagnosticKey, new Set([...existingKeys, ...Object.keys(entries)]));
107
- entries[collisionKey] = {
108
- message: `Platform diagnostic key "${diagnosticKey}" collided with an existing health result key.`,
109
- status: 'down'
110
- };
109
+ entries[diagnosticKey] = diagnostic;
110
+ }
111
+ function withoutKeys(entries, keys) {
112
+ return Object.fromEntries(Object.entries(entries).filter(([key]) => !keys.has(key)));
113
+ }
114
+ function remapContributors(contributors, reservedKeys, platformDiagnosticKeys) {
115
+ return contributors.filter(key => !reservedKeys.has(key) && !platformDiagnosticKeys.includes(key));
111
116
  }
112
117
  function withPlatformDiagnostics(report, health, readiness) {
113
118
  const platformDiagnostics = {};
114
119
  const healthDiagnostic = createPlatformHealthDiagnostic(health);
115
120
  const readinessDiagnostic = createPlatformReadinessDiagnostic(readiness);
116
- const existingKeys = new Set(Object.keys(report.details));
117
- appendPlatformDiagnostic(platformDiagnostics, existingKeys, 'fluo-platform-health', healthDiagnostic);
118
- appendPlatformDiagnostic(platformDiagnostics, existingKeys, 'fluo-platform-readiness', readinessDiagnostic);
121
+ appendPlatformDiagnostic(platformDiagnostics, report.details, 'fluo-platform-health', healthDiagnostic);
122
+ appendPlatformDiagnostic(platformDiagnostics, report.details, 'fluo-platform-readiness', readinessDiagnostic);
119
123
  const platformDiagnosticKeys = Object.keys(platformDiagnostics);
124
+ const platformReservedKeys = new Set([...(healthDiagnostic ? ['fluo-platform-health'] : []), ...(readinessDiagnostic ? ['fluo-platform-readiness'] : [])]);
125
+ const reportDetails = withoutKeys(report.details, platformReservedKeys);
126
+ const reportInfo = withoutKeys(report.info, platformReservedKeys);
127
+ const reportError = withoutKeys(report.error, platformReservedKeys);
120
128
  return {
121
129
  ...report,
122
130
  contributors: {
123
- down: [...report.contributors.down, ...platformDiagnosticKeys],
124
- up: [...report.contributors.up]
131
+ down: [...remapContributors(report.contributors.down, platformReservedKeys, platformDiagnosticKeys), ...platformDiagnosticKeys],
132
+ up: remapContributors(report.contributors.up, platformReservedKeys, platformDiagnosticKeys)
125
133
  },
126
134
  details: {
127
- ...report.details,
135
+ ...reportDetails,
128
136
  ...platformDiagnostics
129
137
  },
130
138
  error: {
131
- ...report.error,
139
+ ...reportError,
132
140
  ...platformDiagnostics
133
141
  },
142
+ info: reportInfo,
134
143
  platform: {
135
144
  health,
136
145
  readiness
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "liveness",
10
10
  "health-check"
11
11
  ],
12
- "version": "1.0.4",
12
+ "version": "1.0.5",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -46,13 +46,13 @@
46
46
  "dependencies": {
47
47
  "@fluojs/core": "^1.0.3",
48
48
  "@fluojs/di": "^1.1.0",
49
- "@fluojs/http": "^1.1.0",
50
- "@fluojs/runtime": "^1.1.6"
49
+ "@fluojs/http": "^1.1.2",
50
+ "@fluojs/runtime": "^1.1.8"
51
51
  },
52
52
  "peerDependencies": {
53
- "@fluojs/drizzle": "^1.0.2",
54
- "@fluojs/prisma": "^1.0.2",
55
- "@fluojs/redis": "^1.0.1"
53
+ "@fluojs/drizzle": "^1.1.0",
54
+ "@fluojs/prisma": "^1.1.0",
55
+ "@fluojs/redis": "^1.0.2"
56
56
  },
57
57
  "peerDependenciesMeta": {
58
58
  "@fluojs/drizzle": {
@@ -67,7 +67,7 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "vitest": "^3.2.4",
70
- "@fluojs/testing": "^1.0.5"
70
+ "@fluojs/testing": "^1.0.6"
71
71
  },
72
72
  "scripts": {
73
73
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",