@objectstack/core 4.0.3 → 4.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.md +95 -10
- package/dist/index.cjs +169 -507
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -223
- package/dist/index.d.ts +24 -223
- package/dist/index.js +175 -505
- package/dist/index.js.map +1 -1
- package/dist/logger.cjs +177 -0
- package/dist/logger.cjs.map +1 -0
- package/dist/logger.d.cts +26 -0
- package/dist/logger.d.ts +26 -0
- package/dist/logger.js +158 -0
- package/dist/logger.js.map +1 -0
- package/package.json +36 -15
- package/.turbo/turbo-build.log +0 -22
- package/ADVANCED_FEATURES.md +0 -380
- package/API_REGISTRY.md +0 -392
- package/CHANGELOG.md +0 -465
- package/PHASE2_IMPLEMENTATION.md +0 -388
- package/REFACTORING_SUMMARY.md +0 -40
- package/examples/api-registry-example.ts +0 -559
- package/examples/kernel-features-example.ts +0 -311
- package/examples/phase2-integration.ts +0 -357
- package/src/api-registry-plugin.test.ts +0 -393
- package/src/api-registry-plugin.ts +0 -89
- package/src/api-registry.test.ts +0 -1089
- package/src/api-registry.ts +0 -739
- package/src/contracts/data-engine.ts +0 -57
- package/src/contracts/http-server.ts +0 -151
- package/src/contracts/logger.ts +0 -72
- package/src/dependency-resolver.test.ts +0 -287
- package/src/dependency-resolver.ts +0 -390
- package/src/fallbacks/fallbacks.test.ts +0 -281
- package/src/fallbacks/index.ts +0 -26
- package/src/fallbacks/memory-cache.ts +0 -34
- package/src/fallbacks/memory-i18n.ts +0 -112
- package/src/fallbacks/memory-job.ts +0 -23
- package/src/fallbacks/memory-metadata.ts +0 -50
- package/src/fallbacks/memory-queue.ts +0 -28
- package/src/health-monitor.test.ts +0 -81
- package/src/health-monitor.ts +0 -318
- package/src/hot-reload.ts +0 -382
- package/src/index.ts +0 -50
- package/src/kernel-base.ts +0 -273
- package/src/kernel.test.ts +0 -624
- package/src/kernel.ts +0 -631
- package/src/lite-kernel.test.ts +0 -248
- package/src/lite-kernel.ts +0 -137
- package/src/logger.test.ts +0 -116
- package/src/logger.ts +0 -355
- package/src/namespace-resolver.test.ts +0 -130
- package/src/namespace-resolver.ts +0 -188
- package/src/package-manager.test.ts +0 -225
- package/src/package-manager.ts +0 -428
- package/src/plugin-loader.test.ts +0 -421
- package/src/plugin-loader.ts +0 -484
- package/src/qa/adapter.ts +0 -16
- package/src/qa/http-adapter.ts +0 -116
- package/src/qa/index.ts +0 -5
- package/src/qa/runner.ts +0 -189
- package/src/security/index.ts +0 -50
- package/src/security/permission-manager.test.ts +0 -256
- package/src/security/permission-manager.ts +0 -338
- package/src/security/plugin-config-validator.test.ts +0 -276
- package/src/security/plugin-config-validator.ts +0 -193
- package/src/security/plugin-permission-enforcer.test.ts +0 -251
- package/src/security/plugin-permission-enforcer.ts +0 -436
- package/src/security/plugin-signature-verifier.ts +0 -403
- package/src/security/sandbox-runtime.ts +0 -462
- package/src/security/security-scanner.ts +0 -367
- package/src/types.ts +0 -120
- package/src/utils/env.test.ts +0 -62
- package/src/utils/env.ts +0 -53
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -10
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* In-memory Map-backed cache fallback.
|
|
5
|
-
*
|
|
6
|
-
* Implements the ICacheService contract with basic get/set/delete/has/clear
|
|
7
|
-
* and TTL expiry. Used by ObjectKernel as an automatic fallback when no
|
|
8
|
-
* real cache plugin (e.g. Redis) is registered.
|
|
9
|
-
*/
|
|
10
|
-
export function createMemoryCache() {
|
|
11
|
-
const store = new Map<string, { value: unknown; expires?: number }>();
|
|
12
|
-
let hits = 0;
|
|
13
|
-
let misses = 0;
|
|
14
|
-
return {
|
|
15
|
-
_fallback: true, _serviceName: 'cache',
|
|
16
|
-
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
17
|
-
const entry = store.get(key);
|
|
18
|
-
if (!entry || (entry.expires && Date.now() > entry.expires)) {
|
|
19
|
-
store.delete(key);
|
|
20
|
-
misses++;
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
hits++;
|
|
24
|
-
return entry.value as T;
|
|
25
|
-
},
|
|
26
|
-
async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {
|
|
27
|
-
store.set(key, { value, expires: ttl ? Date.now() + ttl * 1000 : undefined });
|
|
28
|
-
},
|
|
29
|
-
async delete(key: string): Promise<boolean> { return store.delete(key); },
|
|
30
|
-
async has(key: string): Promise<boolean> { return store.has(key); },
|
|
31
|
-
async clear(): Promise<void> { store.clear(); },
|
|
32
|
-
async stats() { return { hits, misses, keyCount: store.size }; },
|
|
33
|
-
};
|
|
34
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Resolve a locale code against available locales with fallback.
|
|
5
|
-
*
|
|
6
|
-
* Fallback chain:
|
|
7
|
-
* 1. Exact match (e.g. `zh-CN` → `zh-CN`)
|
|
8
|
-
* 2. Case-insensitive match (e.g. `zh-cn` → `zh-CN`)
|
|
9
|
-
* 3. Base language match (e.g. `zh-CN` → `zh`)
|
|
10
|
-
* 4. Variant expansion (e.g. `zh` → `zh-CN`)
|
|
11
|
-
*
|
|
12
|
-
* Returns the matched locale code, or `undefined` when no match is found.
|
|
13
|
-
*/
|
|
14
|
-
export function resolveLocale(requestedLocale: string, availableLocales: string[]): string | undefined {
|
|
15
|
-
if (availableLocales.length === 0) return undefined;
|
|
16
|
-
|
|
17
|
-
// 1. Exact match
|
|
18
|
-
if (availableLocales.includes(requestedLocale)) return requestedLocale;
|
|
19
|
-
|
|
20
|
-
// 2. Case-insensitive match
|
|
21
|
-
const lower = requestedLocale.toLowerCase();
|
|
22
|
-
const caseMatch = availableLocales.find(l => l.toLowerCase() === lower);
|
|
23
|
-
if (caseMatch) return caseMatch;
|
|
24
|
-
|
|
25
|
-
// 3. Base language match (zh-CN → zh)
|
|
26
|
-
const baseLang = requestedLocale.split('-')[0].toLowerCase();
|
|
27
|
-
const baseMatch = availableLocales.find(l => l.toLowerCase() === baseLang);
|
|
28
|
-
if (baseMatch) return baseMatch;
|
|
29
|
-
|
|
30
|
-
// 4. Variant expansion (zh → zh-CN, zh-TW, etc. — first match wins)
|
|
31
|
-
const variantMatch = availableLocales.find(l => l.split('-')[0].toLowerCase() === baseLang);
|
|
32
|
-
if (variantMatch) return variantMatch;
|
|
33
|
-
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* In-memory i18n service fallback.
|
|
39
|
-
*
|
|
40
|
-
* Implements the II18nService contract with basic translate/load/getLocales
|
|
41
|
-
* operations. Used by ObjectKernel as an automatic fallback when no real
|
|
42
|
-
* i18n plugin (e.g. I18nServicePlugin) is registered.
|
|
43
|
-
*
|
|
44
|
-
* Supports runtime translation loading, locale management, and
|
|
45
|
-
* locale code fallback (e.g. `zh` → `zh-CN`).
|
|
46
|
-
* Does not load files from disk — operates purely in-memory.
|
|
47
|
-
*/
|
|
48
|
-
export function createMemoryI18n() {
|
|
49
|
-
const translations = new Map<string, Record<string, unknown>>();
|
|
50
|
-
let defaultLocale = 'en';
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Resolve a dot-notation key from a nested object.
|
|
54
|
-
*/
|
|
55
|
-
function resolveKey(data: Record<string, unknown>, key: string): string | undefined {
|
|
56
|
-
const parts = key.split('.');
|
|
57
|
-
let current: unknown = data;
|
|
58
|
-
for (const part of parts) {
|
|
59
|
-
if (current == null || typeof current !== 'object') return undefined;
|
|
60
|
-
current = (current as Record<string, unknown>)[part];
|
|
61
|
-
}
|
|
62
|
-
return typeof current === 'string' ? current : undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Find translation data for a locale, with fallback resolution.
|
|
67
|
-
*/
|
|
68
|
-
function resolveTranslations(locale: string): Record<string, unknown> | undefined {
|
|
69
|
-
// Exact match
|
|
70
|
-
if (translations.has(locale)) return translations.get(locale);
|
|
71
|
-
|
|
72
|
-
// Locale fallback (zh → zh-CN, en-us → en-US, etc.)
|
|
73
|
-
const resolved = resolveLocale(locale, [...translations.keys()]);
|
|
74
|
-
if (resolved) return translations.get(resolved);
|
|
75
|
-
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
_fallback: true, _serviceName: 'i18n',
|
|
81
|
-
|
|
82
|
-
t(key: string, locale: string, params?: Record<string, unknown>): string {
|
|
83
|
-
const data = resolveTranslations(locale) ?? translations.get(defaultLocale);
|
|
84
|
-
const value = data ? resolveKey(data, key) : undefined;
|
|
85
|
-
if (value == null) return key;
|
|
86
|
-
if (!params) return value;
|
|
87
|
-
// Interpolation format: {{paramName}} — matches FileI18nAdapter convention
|
|
88
|
-
return value.replace(/\{\{(\w+)\}\}/g, (_, name) => String(params[name] ?? `{{${name}}}`));
|
|
89
|
-
},
|
|
90
|
-
|
|
91
|
-
getTranslations(locale: string): Record<string, unknown> {
|
|
92
|
-
return resolveTranslations(locale) ?? {};
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
loadTranslations(locale: string, data: Record<string, unknown>): void {
|
|
96
|
-
const existing = translations.get(locale) ?? {};
|
|
97
|
-
translations.set(locale, { ...existing, ...data });
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
getLocales(): string[] {
|
|
101
|
-
return [...translations.keys()];
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
getDefaultLocale(): string {
|
|
105
|
-
return defaultLocale;
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
setDefaultLocale(locale: string): void {
|
|
109
|
-
defaultLocale = locale;
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* In-memory job scheduler fallback.
|
|
5
|
-
*
|
|
6
|
-
* Implements the IJobService contract with basic schedule/cancel/trigger
|
|
7
|
-
* operations. Used by ObjectKernel as an automatic fallback when no real
|
|
8
|
-
* job plugin (e.g. Agenda / BullMQ) is registered.
|
|
9
|
-
*/
|
|
10
|
-
export function createMemoryJob() {
|
|
11
|
-
const jobs = new Map<string, any>();
|
|
12
|
-
return {
|
|
13
|
-
_fallback: true, _serviceName: 'job',
|
|
14
|
-
async schedule(name: string, schedule: any, handler: any): Promise<void> { jobs.set(name, { schedule, handler }); },
|
|
15
|
-
async cancel(name: string): Promise<void> { jobs.delete(name); },
|
|
16
|
-
async trigger(name: string, data?: unknown): Promise<void> {
|
|
17
|
-
const job = jobs.get(name);
|
|
18
|
-
if (job?.handler) await job.handler({ jobId: name, data });
|
|
19
|
-
},
|
|
20
|
-
async getExecutions(): Promise<any[]> { return []; },
|
|
21
|
-
async listJobs(): Promise<string[]> { return [...jobs.keys()]; },
|
|
22
|
-
};
|
|
23
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* In-memory metadata service fallback.
|
|
5
|
-
*
|
|
6
|
-
* Implements the IMetadataService contract with a simple Map-of-Maps store.
|
|
7
|
-
* Used by ObjectKernel as an automatic fallback when no real metadata plugin
|
|
8
|
-
* (e.g. MetadataPlugin with file-system persistence) is registered.
|
|
9
|
-
*/
|
|
10
|
-
export function createMemoryMetadata() {
|
|
11
|
-
// type -> name -> data
|
|
12
|
-
const store = new Map<string, Map<string, any>>();
|
|
13
|
-
|
|
14
|
-
function getTypeMap(type: string): Map<string, any> {
|
|
15
|
-
let map = store.get(type);
|
|
16
|
-
if (!map) {
|
|
17
|
-
map = new Map();
|
|
18
|
-
store.set(type, map);
|
|
19
|
-
}
|
|
20
|
-
return map;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
_fallback: true, _serviceName: 'metadata',
|
|
25
|
-
async register(type: string, name: string, data: any): Promise<void> {
|
|
26
|
-
getTypeMap(type).set(name, data);
|
|
27
|
-
},
|
|
28
|
-
async get(type: string, name: string): Promise<any> {
|
|
29
|
-
return getTypeMap(type).get(name);
|
|
30
|
-
},
|
|
31
|
-
async list(type: string): Promise<any[]> {
|
|
32
|
-
return Array.from(getTypeMap(type).values());
|
|
33
|
-
},
|
|
34
|
-
async unregister(type: string, name: string): Promise<void> {
|
|
35
|
-
getTypeMap(type).delete(name);
|
|
36
|
-
},
|
|
37
|
-
async exists(type: string, name: string): Promise<boolean> {
|
|
38
|
-
return getTypeMap(type).has(name);
|
|
39
|
-
},
|
|
40
|
-
async listNames(type: string): Promise<string[]> {
|
|
41
|
-
return Array.from(getTypeMap(type).keys());
|
|
42
|
-
},
|
|
43
|
-
async getObject(name: string): Promise<any> {
|
|
44
|
-
return getTypeMap('object').get(name);
|
|
45
|
-
},
|
|
46
|
-
async listObjects(): Promise<any[]> {
|
|
47
|
-
return Array.from(getTypeMap('object').values());
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* In-memory publish/subscribe queue fallback.
|
|
5
|
-
*
|
|
6
|
-
* Implements the IQueueService contract with synchronous in-process delivery.
|
|
7
|
-
* Used by ObjectKernel as an automatic fallback when no real queue plugin
|
|
8
|
-
* (e.g. BullMQ / RabbitMQ) is registered.
|
|
9
|
-
*/
|
|
10
|
-
export function createMemoryQueue() {
|
|
11
|
-
const handlers = new Map<string, Function[]>();
|
|
12
|
-
let msgId = 0;
|
|
13
|
-
return {
|
|
14
|
-
_fallback: true, _serviceName: 'queue',
|
|
15
|
-
async publish<T = unknown>(queue: string, data: T): Promise<string> {
|
|
16
|
-
const id = `fallback-msg-${++msgId}`;
|
|
17
|
-
const fns = handlers.get(queue) ?? [];
|
|
18
|
-
for (const fn of fns) fn({ id, data, attempts: 1, timestamp: Date.now() });
|
|
19
|
-
return id;
|
|
20
|
-
},
|
|
21
|
-
async subscribe(queue: string, handler: (msg: any) => Promise<void>): Promise<void> {
|
|
22
|
-
handlers.set(queue, [...(handlers.get(queue) ?? []), handler]);
|
|
23
|
-
},
|
|
24
|
-
async unsubscribe(queue: string): Promise<void> { handlers.delete(queue); },
|
|
25
|
-
async getQueueSize(): Promise<number> { return 0; },
|
|
26
|
-
async purge(queue: string): Promise<void> { handlers.delete(queue); },
|
|
27
|
-
};
|
|
28
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { PluginHealthMonitor } from './health-monitor.js';
|
|
3
|
-
import { createLogger } from './logger.js';
|
|
4
|
-
import type { PluginHealthCheck } from '@objectstack/spec/kernel';
|
|
5
|
-
|
|
6
|
-
describe('PluginHealthMonitor', () => {
|
|
7
|
-
let monitor: PluginHealthMonitor;
|
|
8
|
-
let logger: ReturnType<typeof createLogger>;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
logger = createLogger({ level: 'silent' });
|
|
12
|
-
monitor = new PluginHealthMonitor(logger);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should register plugin for health monitoring', () => {
|
|
16
|
-
const config: PluginHealthCheck = {
|
|
17
|
-
interval: 5000,
|
|
18
|
-
timeout: 1000,
|
|
19
|
-
failureThreshold: 3,
|
|
20
|
-
successThreshold: 1,
|
|
21
|
-
autoRestart: false,
|
|
22
|
-
maxRestartAttempts: 3,
|
|
23
|
-
restartBackoff: 'exponential',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
monitor.registerPlugin('test-plugin', config);
|
|
27
|
-
expect(monitor.getHealthStatus('test-plugin')).toBe('unknown');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('should report healthy status initially', () => {
|
|
31
|
-
const config: PluginHealthCheck = {
|
|
32
|
-
interval: 5000,
|
|
33
|
-
timeout: 1000,
|
|
34
|
-
failureThreshold: 3,
|
|
35
|
-
successThreshold: 1,
|
|
36
|
-
autoRestart: false,
|
|
37
|
-
maxRestartAttempts: 3,
|
|
38
|
-
restartBackoff: 'fixed',
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
monitor.registerPlugin('test-plugin', config);
|
|
42
|
-
expect(monitor.getHealthStatus('test-plugin')).toBe('unknown');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should get all health statuses', () => {
|
|
46
|
-
const config: PluginHealthCheck = {
|
|
47
|
-
interval: 5000,
|
|
48
|
-
timeout: 1000,
|
|
49
|
-
failureThreshold: 3,
|
|
50
|
-
successThreshold: 1,
|
|
51
|
-
autoRestart: false,
|
|
52
|
-
maxRestartAttempts: 3,
|
|
53
|
-
restartBackoff: 'linear',
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
monitor.registerPlugin('plugin1', config);
|
|
57
|
-
monitor.registerPlugin('plugin2', config);
|
|
58
|
-
|
|
59
|
-
const statuses = monitor.getAllHealthStatuses();
|
|
60
|
-
expect(statuses.size).toBe(2);
|
|
61
|
-
expect(statuses.has('plugin1')).toBe(true);
|
|
62
|
-
expect(statuses.has('plugin2')).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should shutdown cleanly', () => {
|
|
66
|
-
const config: PluginHealthCheck = {
|
|
67
|
-
interval: 5000,
|
|
68
|
-
timeout: 1000,
|
|
69
|
-
failureThreshold: 3,
|
|
70
|
-
successThreshold: 1,
|
|
71
|
-
autoRestart: false,
|
|
72
|
-
maxRestartAttempts: 3,
|
|
73
|
-
restartBackoff: 'exponential',
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
monitor.registerPlugin('test-plugin', config);
|
|
77
|
-
monitor.shutdown();
|
|
78
|
-
|
|
79
|
-
expect(monitor.getAllHealthStatuses().size).toBe(0);
|
|
80
|
-
});
|
|
81
|
-
});
|
package/src/health-monitor.ts
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
PluginHealthStatus,
|
|
5
|
-
PluginHealthCheck,
|
|
6
|
-
PluginHealthReport
|
|
7
|
-
} from '@objectstack/spec/kernel';
|
|
8
|
-
import type { ObjectLogger } from './logger.js';
|
|
9
|
-
import type { Plugin } from './types.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Plugin Health Monitor
|
|
13
|
-
*
|
|
14
|
-
* Monitors plugin health status and performs automatic recovery actions.
|
|
15
|
-
* Implements the advanced lifecycle health monitoring protocol.
|
|
16
|
-
*/
|
|
17
|
-
export class PluginHealthMonitor {
|
|
18
|
-
private logger: ObjectLogger;
|
|
19
|
-
private healthChecks = new Map<string, PluginHealthCheck>();
|
|
20
|
-
private healthStatus = new Map<string, PluginHealthStatus>();
|
|
21
|
-
private healthReports = new Map<string, PluginHealthReport>();
|
|
22
|
-
private checkIntervals = new Map<string, NodeJS.Timeout>();
|
|
23
|
-
private failureCounters = new Map<string, number>();
|
|
24
|
-
private successCounters = new Map<string, number>();
|
|
25
|
-
private restartAttempts = new Map<string, number>();
|
|
26
|
-
|
|
27
|
-
constructor(logger: ObjectLogger) {
|
|
28
|
-
this.logger = logger.child({ component: 'HealthMonitor' });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Register a plugin for health monitoring
|
|
33
|
-
*/
|
|
34
|
-
registerPlugin(pluginName: string, config: PluginHealthCheck): void {
|
|
35
|
-
this.healthChecks.set(pluginName, config);
|
|
36
|
-
this.healthStatus.set(pluginName, 'unknown');
|
|
37
|
-
this.failureCounters.set(pluginName, 0);
|
|
38
|
-
this.successCounters.set(pluginName, 0);
|
|
39
|
-
this.restartAttempts.set(pluginName, 0);
|
|
40
|
-
|
|
41
|
-
this.logger.info('Plugin registered for health monitoring', {
|
|
42
|
-
plugin: pluginName,
|
|
43
|
-
interval: config.interval
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Start monitoring a plugin
|
|
49
|
-
*/
|
|
50
|
-
startMonitoring(pluginName: string, plugin: Plugin): void {
|
|
51
|
-
const config = this.healthChecks.get(pluginName);
|
|
52
|
-
if (!config) {
|
|
53
|
-
this.logger.warn('Cannot start monitoring - plugin not registered', { plugin: pluginName });
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Clear any existing interval
|
|
58
|
-
this.stopMonitoring(pluginName);
|
|
59
|
-
|
|
60
|
-
// Set up periodic health checks
|
|
61
|
-
const interval = setInterval(() => {
|
|
62
|
-
this.performHealthCheck(pluginName, plugin, config).catch(error => {
|
|
63
|
-
this.logger.error('Health check failed with error', {
|
|
64
|
-
plugin: pluginName,
|
|
65
|
-
error
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
}, config.interval);
|
|
69
|
-
|
|
70
|
-
this.checkIntervals.set(pluginName, interval);
|
|
71
|
-
this.logger.info('Health monitoring started', { plugin: pluginName });
|
|
72
|
-
|
|
73
|
-
// Perform initial health check
|
|
74
|
-
this.performHealthCheck(pluginName, plugin, config).catch(error => {
|
|
75
|
-
this.logger.error('Initial health check failed', {
|
|
76
|
-
plugin: pluginName,
|
|
77
|
-
error
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Stop monitoring a plugin
|
|
84
|
-
*/
|
|
85
|
-
stopMonitoring(pluginName: string): void {
|
|
86
|
-
const interval = this.checkIntervals.get(pluginName);
|
|
87
|
-
if (interval) {
|
|
88
|
-
clearInterval(interval);
|
|
89
|
-
this.checkIntervals.delete(pluginName);
|
|
90
|
-
this.logger.info('Health monitoring stopped', { plugin: pluginName });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Perform a health check on a plugin
|
|
96
|
-
*/
|
|
97
|
-
private async performHealthCheck(
|
|
98
|
-
pluginName: string,
|
|
99
|
-
plugin: Plugin,
|
|
100
|
-
config: PluginHealthCheck
|
|
101
|
-
): Promise<void> {
|
|
102
|
-
const startTime = Date.now();
|
|
103
|
-
let status: PluginHealthStatus = 'healthy';
|
|
104
|
-
let message: string | undefined;
|
|
105
|
-
const checks: Array<{ name: string; status: 'passed' | 'failed' | 'warning'; message?: string }> = [];
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
// Check if plugin has a custom health check method
|
|
109
|
-
if (config.checkMethod && typeof (plugin as any)[config.checkMethod] === 'function') {
|
|
110
|
-
const checkResult = await Promise.race([
|
|
111
|
-
(plugin as any)[config.checkMethod](),
|
|
112
|
-
this.timeout(config.timeout, `Health check timeout after ${config.timeout}ms`)
|
|
113
|
-
]);
|
|
114
|
-
|
|
115
|
-
if (checkResult === false || (checkResult && checkResult.status === 'unhealthy')) {
|
|
116
|
-
status = 'unhealthy';
|
|
117
|
-
message = checkResult?.message || 'Custom health check failed';
|
|
118
|
-
checks.push({ name: config.checkMethod, status: 'failed', message });
|
|
119
|
-
} else {
|
|
120
|
-
checks.push({ name: config.checkMethod, status: 'passed' });
|
|
121
|
-
}
|
|
122
|
-
} else {
|
|
123
|
-
// Default health check - just verify plugin is loaded
|
|
124
|
-
checks.push({ name: 'plugin-loaded', status: 'passed' });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Update counters based on result
|
|
128
|
-
if (status === 'healthy') {
|
|
129
|
-
this.successCounters.set(pluginName, (this.successCounters.get(pluginName) || 0) + 1);
|
|
130
|
-
this.failureCounters.set(pluginName, 0);
|
|
131
|
-
|
|
132
|
-
// Recover from unhealthy state if we have enough successes
|
|
133
|
-
const currentStatus = this.healthStatus.get(pluginName);
|
|
134
|
-
if (currentStatus === 'unhealthy' || currentStatus === 'degraded') {
|
|
135
|
-
const successCount = this.successCounters.get(pluginName) || 0;
|
|
136
|
-
if (successCount >= config.successThreshold) {
|
|
137
|
-
this.healthStatus.set(pluginName, 'healthy');
|
|
138
|
-
this.logger.info('Plugin recovered to healthy state', { plugin: pluginName });
|
|
139
|
-
} else {
|
|
140
|
-
this.healthStatus.set(pluginName, 'recovering');
|
|
141
|
-
}
|
|
142
|
-
} else {
|
|
143
|
-
this.healthStatus.set(pluginName, 'healthy');
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
this.failureCounters.set(pluginName, (this.failureCounters.get(pluginName) || 0) + 1);
|
|
147
|
-
this.successCounters.set(pluginName, 0);
|
|
148
|
-
|
|
149
|
-
const failureCount = this.failureCounters.get(pluginName) || 0;
|
|
150
|
-
if (failureCount >= config.failureThreshold) {
|
|
151
|
-
this.healthStatus.set(pluginName, 'unhealthy');
|
|
152
|
-
this.logger.warn('Plugin marked as unhealthy', {
|
|
153
|
-
plugin: pluginName,
|
|
154
|
-
failures: failureCount
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Attempt auto-restart if configured
|
|
158
|
-
if (config.autoRestart) {
|
|
159
|
-
await this.attemptRestart(pluginName, plugin, config);
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
this.healthStatus.set(pluginName, 'degraded');
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
} catch (error) {
|
|
166
|
-
status = 'failed';
|
|
167
|
-
message = error instanceof Error ? error.message : 'Unknown error';
|
|
168
|
-
this.failureCounters.set(pluginName, (this.failureCounters.get(pluginName) || 0) + 1);
|
|
169
|
-
this.healthStatus.set(pluginName, 'failed');
|
|
170
|
-
|
|
171
|
-
checks.push({
|
|
172
|
-
name: 'health-check',
|
|
173
|
-
status: 'failed',
|
|
174
|
-
message: message
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
this.logger.error('Health check exception', {
|
|
178
|
-
plugin: pluginName,
|
|
179
|
-
error
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Create health report
|
|
184
|
-
const report: PluginHealthReport = {
|
|
185
|
-
status: this.healthStatus.get(pluginName) || 'unknown',
|
|
186
|
-
timestamp: new Date().toISOString(),
|
|
187
|
-
message,
|
|
188
|
-
metrics: {
|
|
189
|
-
uptime: Date.now() - startTime,
|
|
190
|
-
},
|
|
191
|
-
checks: checks.length > 0 ? checks : undefined,
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
this.healthReports.set(pluginName, report);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Attempt to restart a plugin
|
|
199
|
-
*/
|
|
200
|
-
private async attemptRestart(
|
|
201
|
-
pluginName: string,
|
|
202
|
-
plugin: Plugin,
|
|
203
|
-
config: PluginHealthCheck
|
|
204
|
-
): Promise<void> {
|
|
205
|
-
const attempts = this.restartAttempts.get(pluginName) || 0;
|
|
206
|
-
|
|
207
|
-
if (attempts >= config.maxRestartAttempts) {
|
|
208
|
-
this.logger.error('Max restart attempts reached, giving up', {
|
|
209
|
-
plugin: pluginName,
|
|
210
|
-
attempts
|
|
211
|
-
});
|
|
212
|
-
this.healthStatus.set(pluginName, 'failed');
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
this.restartAttempts.set(pluginName, attempts + 1);
|
|
217
|
-
|
|
218
|
-
// Calculate backoff delay
|
|
219
|
-
const delay = this.calculateBackoff(attempts, config.restartBackoff);
|
|
220
|
-
|
|
221
|
-
this.logger.info('Scheduling plugin restart', {
|
|
222
|
-
plugin: pluginName,
|
|
223
|
-
attempt: attempts + 1,
|
|
224
|
-
delay
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
// Call destroy and init to restart
|
|
231
|
-
if (plugin.destroy) {
|
|
232
|
-
await plugin.destroy();
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Note: Full restart would require kernel context
|
|
236
|
-
// This is a simplified version - actual implementation would need kernel integration
|
|
237
|
-
this.logger.info('Plugin restarted', { plugin: pluginName });
|
|
238
|
-
|
|
239
|
-
// Reset counters on successful restart
|
|
240
|
-
this.failureCounters.set(pluginName, 0);
|
|
241
|
-
this.successCounters.set(pluginName, 0);
|
|
242
|
-
this.healthStatus.set(pluginName, 'recovering');
|
|
243
|
-
} catch (error) {
|
|
244
|
-
this.logger.error('Plugin restart failed', {
|
|
245
|
-
plugin: pluginName,
|
|
246
|
-
error
|
|
247
|
-
});
|
|
248
|
-
this.healthStatus.set(pluginName, 'failed');
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Calculate backoff delay for restarts
|
|
254
|
-
*/
|
|
255
|
-
private calculateBackoff(attempt: number, strategy: 'fixed' | 'linear' | 'exponential'): number {
|
|
256
|
-
const baseDelay = 1000; // 1 second base
|
|
257
|
-
|
|
258
|
-
switch (strategy) {
|
|
259
|
-
case 'fixed':
|
|
260
|
-
return baseDelay;
|
|
261
|
-
case 'linear':
|
|
262
|
-
return baseDelay * (attempt + 1);
|
|
263
|
-
case 'exponential':
|
|
264
|
-
return baseDelay * Math.pow(2, attempt);
|
|
265
|
-
default:
|
|
266
|
-
return baseDelay;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Get current health status of a plugin
|
|
272
|
-
*/
|
|
273
|
-
getHealthStatus(pluginName: string): PluginHealthStatus | undefined {
|
|
274
|
-
return this.healthStatus.get(pluginName);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Get latest health report for a plugin
|
|
279
|
-
*/
|
|
280
|
-
getHealthReport(pluginName: string): PluginHealthReport | undefined {
|
|
281
|
-
return this.healthReports.get(pluginName);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Get all health statuses
|
|
286
|
-
*/
|
|
287
|
-
getAllHealthStatuses(): Map<string, PluginHealthStatus> {
|
|
288
|
-
return new Map(this.healthStatus);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Shutdown health monitor
|
|
293
|
-
*/
|
|
294
|
-
shutdown(): void {
|
|
295
|
-
// Stop all monitoring intervals
|
|
296
|
-
for (const pluginName of this.checkIntervals.keys()) {
|
|
297
|
-
this.stopMonitoring(pluginName);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
this.healthChecks.clear();
|
|
301
|
-
this.healthStatus.clear();
|
|
302
|
-
this.healthReports.clear();
|
|
303
|
-
this.failureCounters.clear();
|
|
304
|
-
this.successCounters.clear();
|
|
305
|
-
this.restartAttempts.clear();
|
|
306
|
-
|
|
307
|
-
this.logger.info('Health monitor shutdown complete');
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Timeout helper
|
|
312
|
-
*/
|
|
313
|
-
private timeout<T>(ms: number, message: string): Promise<T> {
|
|
314
|
-
return new Promise((_, reject) => {
|
|
315
|
-
setTimeout(() => reject(new Error(message)), ms);
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
}
|