@lenne.tech/nest-server 11.31.2 → 11.32.0
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/.claude/rules/architecture.md +2 -0
- package/.claude/rules/configurable-features.md +2 -0
- package/.claude/rules/versioning.md +5 -8
- package/CLAUDE.md +31 -4
- package/FRAMEWORK-API.md +7 -2
- package/README.md +1 -0
- package/dist/config.env.d.ts +4 -0
- package/dist/config.env.js +32 -2
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/logging.helper.d.ts +1 -0
- package/dist/core/common/helpers/logging.helper.js +12 -0
- package/dist/core/common/helpers/logging.helper.js.map +1 -1
- package/dist/core/common/helpers/meta.helper.d.ts +1 -0
- package/dist/core/common/helpers/meta.helper.js +32 -1
- package/dist/core/common/helpers/meta.helper.js.map +1 -1
- package/dist/core/common/helpers/process-diagnostics.helper.d.ts +18 -0
- package/dist/core/common/helpers/process-diagnostics.helper.js +88 -0
- package/dist/core/common/helpers/process-diagnostics.helper.js.map +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +12 -0
- package/dist/core/common/services/brevo.service.d.ts +7 -1
- package/dist/core/common/services/brevo.service.js +37 -16
- package/dist/core/common/services/brevo.service.js.map +1 -1
- package/dist/core/common/services/email.service.d.ts +3 -1
- package/dist/core/common/services/email.service.js +33 -2
- package/dist/core/common/services/email.service.js.map +1 -1
- package/dist/core/common/services/template.service.js +9 -4
- package/dist/core/common/services/template.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +9 -4
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/hub/core-hub-actions.controller.d.ts +22 -0
- package/dist/core/modules/hub/core-hub-actions.controller.js +141 -0
- package/dist/core/modules/hub/core-hub-actions.controller.js.map +1 -0
- package/dist/core/modules/hub/core-hub-html.service.d.ts +8 -0
- package/dist/core/modules/hub/core-hub-html.service.js +123 -0
- package/dist/core/modules/hub/core-hub-html.service.js.map +1 -0
- package/dist/core/modules/hub/core-hub.controller.d.ts +56 -0
- package/dist/core/modules/hub/core-hub.controller.js +398 -0
- package/dist/core/modules/hub/core-hub.controller.js.map +1 -0
- package/dist/core/modules/hub/core-hub.module.d.ts +17 -0
- package/dist/core/modules/hub/core-hub.module.js +109 -0
- package/dist/core/modules/hub/core-hub.module.js.map +1 -0
- package/dist/core/modules/hub/core-hub.service.d.ts +18 -0
- package/dist/core/modules/hub/core-hub.service.js +153 -0
- package/dist/core/modules/hub/core-hub.service.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js +755 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.js +33 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.js +46 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.d.ts +10 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.js +35 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.d.ts +9 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.js +62 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.js.map +1 -0
- package/dist/core/modules/hub/hub-action-messages.d.ts +17 -0
- package/dist/core/modules/hub/hub-action-messages.js +21 -0
- package/dist/core/modules/hub/hub-action-messages.js.map +1 -0
- package/dist/core/modules/hub/hub-config.helper.d.ts +11 -0
- package/dist/core/modules/hub/hub-config.helper.js +164 -0
- package/dist/core/modules/hub/hub-config.helper.js.map +1 -0
- package/dist/core/modules/hub/hub-nav.d.ts +12 -0
- package/dist/core/modules/hub/hub-nav.js +43 -0
- package/dist/core/modules/hub/hub-nav.js.map +1 -0
- package/dist/core/modules/hub/hub-ring-buffer.d.ts +22 -0
- package/dist/core/modules/hub/hub-ring-buffer.js +59 -0
- package/dist/core/modules/hub/hub-ring-buffer.js.map +1 -0
- package/dist/core/modules/hub/hub.constants.d.ts +18 -0
- package/dist/core/modules/hub/hub.constants.js +22 -0
- package/dist/core/modules/hub/hub.constants.js.map +1 -0
- package/dist/core/modules/hub/index.d.ts +25 -0
- package/dist/core/modules/hub/index.js +42 -0
- package/dist/core/modules/hub/index.js.map +1 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.d.ts +115 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.js +3 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.js.map +1 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.d.ts +231 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.js +3 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.js.map +1 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.d.ts +8 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.js +50 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.d.ts +48 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.js +136 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-db.service.d.ts +19 -0
- package/dist/core/modules/hub/services/core-hub-db.service.js +180 -0
- package/dist/core/modules/hub/services/core-hub-db.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-email.service.d.ts +24 -0
- package/dist/core/modules/hub/services/core-hub-email.service.js +148 -0
- package/dist/core/modules/hub/services/core-hub-email.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +20 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js +103 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.d.ts +25 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.js +115 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.d.ts +28 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.js +187 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +24 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.js +210 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +38 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.js +235 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +18 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js +123 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -0
- package/dist/core.module.js +42 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +5 -2
- package/dist/main.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +2 -0
- package/docs/brevo-manual-test.md +166 -0
- package/docs/security-overrides.md +90 -0
- package/migration-guides/11.31.2-to-11.31.3.md +135 -0
- package/migration-guides/11.31.3-to-11.32.0.md +254 -0
- package/package.json +16 -14
- package/src/config.env.ts +75 -2
- package/src/core/common/helpers/logging.helper.spec.ts +61 -0
- package/src/core/common/helpers/logging.helper.ts +48 -0
- package/src/core/common/helpers/meta.helper.ts +46 -1
- package/src/core/common/helpers/process-diagnostics.helper.spec.ts +310 -0
- package/src/core/common/helpers/process-diagnostics.helper.ts +321 -0
- package/src/core/common/interfaces/server-options.interface.ts +78 -0
- package/src/core/common/services/brevo.service.spec.ts +266 -0
- package/src/core/common/services/brevo.service.ts +100 -17
- package/src/core/common/services/email.service.ts +33 -1
- package/src/core/common/services/template.service.ts +21 -16
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +14 -4
- package/src/core/modules/hub/INTEGRATION-CHECKLIST.md +64 -0
- package/src/core/modules/hub/README.md +159 -0
- package/src/core/modules/hub/core-hub-actions.controller.ts +137 -0
- package/src/core/modules/hub/core-hub-html.service.ts +135 -0
- package/src/core/modules/hub/core-hub.controller.ts +286 -0
- package/src/core/modules/hub/core-hub.module.spec.ts +108 -0
- package/src/core/modules/hub/core-hub.module.ts +159 -0
- package/src/core/modules/hub/core-hub.service.ts +169 -0
- package/src/core/modules/hub/helpers/hub-client-js.helper.ts +768 -0
- package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +48 -0
- package/src/core/modules/hub/helpers/hub-command-shape.helper.ts +47 -0
- package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +67 -0
- package/src/core/modules/hub/helpers/hub-mask.helper.ts +78 -0
- package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +54 -0
- package/src/core/modules/hub/helpers/hub-mermaid.helper.ts +62 -0
- package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +106 -0
- package/src/core/modules/hub/helpers/hub-shell.helper.ts +90 -0
- package/src/core/modules/hub/hub-action-messages.ts +47 -0
- package/src/core/modules/hub/hub-config.helper.spec.ts +108 -0
- package/src/core/modules/hub/hub-config.helper.ts +233 -0
- package/src/core/modules/hub/hub-nav.ts +66 -0
- package/src/core/modules/hub/hub-ring-buffer.spec.ts +95 -0
- package/src/core/modules/hub/hub-ring-buffer.ts +101 -0
- package/src/core/modules/hub/hub.constants.ts +84 -0
- package/src/core/modules/hub/index.ts +25 -0
- package/src/core/modules/hub/interfaces/hub-config.interface.ts +265 -0
- package/src/core/modules/hub/interfaces/hub-panels.interface.ts +186 -0
- package/src/core/modules/hub/middleware/hub-trace.middleware.ts +45 -0
- package/src/core/modules/hub/services/core-hub-actions.service.ts +133 -0
- package/src/core/modules/hub/services/core-hub-db.service.ts +185 -0
- package/src/core/modules/hub/services/core-hub-email.service.ts +158 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +116 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.ts +121 -0
- package/src/core/modules/hub/services/core-hub-migrations.service.ts +112 -0
- package/src/core/modules/hub/services/core-hub-sources.service.ts +194 -0
- package/src/core/modules/hub/services/hub-log-buffer.service.ts +252 -0
- package/src/core/modules/hub/services/hub-query-profiler.service.ts +274 -0
- package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +112 -0
- package/src/core/modules/hub/services/hub-trace-buffer.service.ts +134 -0
- package/src/core.module.ts +61 -1
- package/src/index.ts +7 -0
- package/src/main.ts +22 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { isHubEnabled, isHubQueriesEnabled, normalizeHubConfig } from './hub-config.helper';
|
|
4
|
+
|
|
5
|
+
describe('hub-config.helper', () => {
|
|
6
|
+
describe('isHubEnabled', () => {
|
|
7
|
+
it('is disabled by default (undefined)', () => {
|
|
8
|
+
expect(isHubEnabled(undefined)).toBe(false);
|
|
9
|
+
expect(isHubEnabled(false)).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('is enabled for true / {} / a config object', () => {
|
|
13
|
+
expect(isHubEnabled(true)).toBe(true);
|
|
14
|
+
expect(isHubEnabled({})).toBe(true);
|
|
15
|
+
expect(isHubEnabled({ path: 'x' })).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('respects explicit { enabled: false }', () => {
|
|
19
|
+
expect(isHubEnabled({ enabled: false, path: 'x' })).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('isHubQueriesEnabled', () => {
|
|
24
|
+
it('is off by default even when the hub is enabled', () => {
|
|
25
|
+
expect(isHubQueriesEnabled(true)).toBe(false);
|
|
26
|
+
expect(isHubQueriesEnabled({})).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('is on when queries collector is explicitly enabled', () => {
|
|
30
|
+
expect(isHubQueriesEnabled({ collectors: { queries: true } })).toBe(true);
|
|
31
|
+
expect(isHubQueriesEnabled({ collectors: { queries: { warnMs: 5 } } })).toBe(true);
|
|
32
|
+
expect(isHubQueriesEnabled({ collectors: { queries: { enabled: false } } })).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('is off when the hub itself is disabled', () => {
|
|
36
|
+
expect(isHubQueriesEnabled(false)).toBe(false);
|
|
37
|
+
expect(isHubQueriesEnabled({ collectors: { queries: true }, enabled: false })).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('normalizeHubConfig', () => {
|
|
42
|
+
it('applies defaults for the boolean-true form', () => {
|
|
43
|
+
const cfg = normalizeHubConfig(true, { env: 'local', version: '1.2.3' });
|
|
44
|
+
|
|
45
|
+
expect(cfg.path).toBe('hub');
|
|
46
|
+
expect(cfg.roles).toEqual(['admin']);
|
|
47
|
+
expect(cfg.actions).toBe(true);
|
|
48
|
+
expect(cfg.pollIntervalMs).toBe(5000);
|
|
49
|
+
expect(cfg.env).toBe('local');
|
|
50
|
+
expect(cfg.version).toBe('1.2.3');
|
|
51
|
+
// Default collectors: logs + traces on, queries off.
|
|
52
|
+
expect(cfg.collectors.logs).not.toBe(false);
|
|
53
|
+
expect(cfg.collectors.traces).not.toBe(false);
|
|
54
|
+
expect(cfg.collectors.queries).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('clamps the poll interval to the minimum', () => {
|
|
58
|
+
const cfg = normalizeHubConfig({ pollIntervalMs: 10 }, { env: 'local', version: '1' });
|
|
59
|
+
expect(cfg.pollIntervalMs).toBe(1000);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('normalizes a single role string to an array', () => {
|
|
63
|
+
const cfg = normalizeHubConfig({ roles: 'S_USER' }, { env: 'local', version: '1' });
|
|
64
|
+
expect(cfg.roles).toEqual(['S_USER']);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('keeps roles: false (opt-out of auth)', () => {
|
|
68
|
+
const cfg = normalizeHubConfig({ roles: false }, { env: 'local', version: '1' });
|
|
69
|
+
expect(cfg.roles).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('resolves the queries collector with thresholds when enabled', () => {
|
|
73
|
+
const cfg = normalizeHubConfig({ collectors: { queries: { warnMs: 5 } } }, { env: 'local', version: '1' });
|
|
74
|
+
expect(cfg.collectors.queries).not.toBe(false);
|
|
75
|
+
if (cfg.collectors.queries) {
|
|
76
|
+
expect(cfg.collectors.queries.warnMs).toBe(5);
|
|
77
|
+
expect(cfg.collectors.queries.criticalMs).toBe(200);
|
|
78
|
+
expect(cfg.collectors.queries.capacity).toBe(500);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('defaults the mailbox to disabled but enables it with capture mode when present', () => {
|
|
83
|
+
expect(normalizeHubConfig({}, { env: 'local', version: '1' }).mailbox).toBe(false);
|
|
84
|
+
|
|
85
|
+
const cfg = normalizeHubConfig({ mailbox: true }, { env: 'local', version: '1' });
|
|
86
|
+
expect(cfg.mailbox).not.toBe(false);
|
|
87
|
+
if (cfg.mailbox) {
|
|
88
|
+
expect(cfg.mailbox.mode).toBe('capture');
|
|
89
|
+
expect(cfg.mailbox.capacity).toBe(100);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('resolves default external links', () => {
|
|
94
|
+
const cfg = normalizeHubConfig(
|
|
95
|
+
{},
|
|
96
|
+
{ env: 'local', graphQlEnabled: true, permissionsPath: 'permissions', version: '1' },
|
|
97
|
+
);
|
|
98
|
+
expect(cfg.links.swagger).toBe('/swagger');
|
|
99
|
+
expect(cfg.links.graphql).toBe('/graphql');
|
|
100
|
+
expect(cfg.links.permissions).toBe('/permissions');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('omits the graphql link when GraphQL is disabled', () => {
|
|
104
|
+
const cfg = normalizeHubConfig({}, { env: 'local', graphQlEnabled: false, version: '1' });
|
|
105
|
+
expect(cfg.links.graphql).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { RoleEnum } from '../../common/enums/role.enum';
|
|
2
|
+
import { getVersion } from '../../common/helpers/meta.helper';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
HUB_DEFAULT_LOG_CAPACITY,
|
|
6
|
+
HUB_DEFAULT_LOG_MAX_MESSAGE_LENGTH,
|
|
7
|
+
HUB_DEFAULT_MAILBOX_CAPACITY,
|
|
8
|
+
HUB_DEFAULT_MAILBOX_MAX_MAIL_SIZE,
|
|
9
|
+
HUB_DEFAULT_MIGRATIONS_COLLECTION,
|
|
10
|
+
HUB_DEFAULT_MIGRATIONS_DIR,
|
|
11
|
+
HUB_DEFAULT_PATH,
|
|
12
|
+
HUB_DEFAULT_POLL_INTERVAL_MS,
|
|
13
|
+
HUB_DEFAULT_QUERY_CAPACITY,
|
|
14
|
+
HUB_DEFAULT_QUERY_CRITICAL_MS,
|
|
15
|
+
HUB_DEFAULT_QUERY_MAX_SHAPE_LENGTH,
|
|
16
|
+
HUB_DEFAULT_QUERY_WARN_MS,
|
|
17
|
+
HUB_DEFAULT_TRACE_CAPACITY,
|
|
18
|
+
HUB_DEFAULT_TRACE_SLOW_MS,
|
|
19
|
+
HUB_MIN_POLL_INTERVAL_MS,
|
|
20
|
+
} from './hub.constants';
|
|
21
|
+
import {
|
|
22
|
+
IHubConfig,
|
|
23
|
+
IHubLogsConfig,
|
|
24
|
+
IHubMailboxConfig,
|
|
25
|
+
IHubQueriesConfig,
|
|
26
|
+
IHubTracesConfig,
|
|
27
|
+
ResolvedHubConfig,
|
|
28
|
+
} from './interfaces/hub-config.interface';
|
|
29
|
+
|
|
30
|
+
/** Context needed to resolve environment-dependent defaults (env badge, links, mailbox guard). */
|
|
31
|
+
export interface HubConfigContext {
|
|
32
|
+
env: string;
|
|
33
|
+
graphQlEnabled?: boolean;
|
|
34
|
+
permissionsPath?: string;
|
|
35
|
+
version: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Environments treated as NON-reachable (local dev / test / CI). These are the ONLY envs where a
|
|
40
|
+
* public Hub (`roles: false`) or mailbox `capture` mode are allowed without an explicit acknowledgment.
|
|
41
|
+
* Everything else is treated as reachable — fail-safe by design, so a custom prod-like env name
|
|
42
|
+
* (`prod`, `live`, `preprod`, `production-eu`, `staging-2`, …) cannot bypass the startup guards.
|
|
43
|
+
*/
|
|
44
|
+
const NON_REACHABLE_ENVS = new Set(['ci', 'development', 'e2e', 'local', 'test']);
|
|
45
|
+
|
|
46
|
+
/** True when `env` is a reachable/deployed environment (anything not in the known non-reachable set). */
|
|
47
|
+
export function isReachableEnv(env: string | undefined): boolean {
|
|
48
|
+
return !NON_REACHABLE_ENVS.has((env ?? '').toLowerCase());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True when the Hub should be registered (presence implies enabled, unless `{ enabled: false }`). */
|
|
52
|
+
export function isHubEnabled(config: boolean | IHubConfig | undefined): boolean {
|
|
53
|
+
if (config === true) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (config === false || config === undefined || config === null) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return config.enabled !== false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** True when the query profiler is active — gates the `monitorCommands` opt-in in core.module.ts. */
|
|
63
|
+
export function isHubQueriesEnabled(config: boolean | IHubConfig | undefined): boolean {
|
|
64
|
+
if (!isHubEnabled(config) || typeof config !== 'object') {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return isCollectorEnabled(config.collectors?.queries);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve `boolean | IHubConfig` into a fully-defaulted {@link ResolvedHubConfig}.
|
|
72
|
+
* The result is bound to the `HUB_CONFIG` token and injected throughout the module.
|
|
73
|
+
*/
|
|
74
|
+
export function normalizeHubConfig(config: boolean | IHubConfig, ctx: HubConfigContext): ResolvedHubConfig {
|
|
75
|
+
const cfg: IHubConfig = typeof config === 'object' && config !== null ? config : {};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
actions: cfg.actions !== false,
|
|
79
|
+
allowPublicAccessInProduction: cfg.allowPublicAccessInProduction === true,
|
|
80
|
+
collectors: {
|
|
81
|
+
logs: resolveLogsCollector(cfg.collectors?.logs, true),
|
|
82
|
+
queries: resolveQueriesCollector(cfg.collectors?.queries, false),
|
|
83
|
+
traces: resolveTracesCollector(cfg.collectors?.traces, true),
|
|
84
|
+
},
|
|
85
|
+
db:
|
|
86
|
+
cfg.db === false
|
|
87
|
+
? false
|
|
88
|
+
: { includeIndexes: typeof cfg.db === 'object' ? cfg.db.includeIndexes === true : false },
|
|
89
|
+
emailPreview: cfg.emailPreview !== false,
|
|
90
|
+
env: ctx.env,
|
|
91
|
+
links: resolveLinks(cfg.links, ctx),
|
|
92
|
+
loginEndpoint: cfg.loginEndpoint ?? '/iam/sign-in/email',
|
|
93
|
+
logoutEndpoint: cfg.logoutEndpoint ?? '/iam/sign-out',
|
|
94
|
+
mailbox: resolveMailbox(cfg.mailbox),
|
|
95
|
+
migrations: resolveMigrations(cfg.migrations),
|
|
96
|
+
path: sanitizePath(cfg.path) || HUB_DEFAULT_PATH,
|
|
97
|
+
pollIntervalMs: Math.max(HUB_MIN_POLL_INTERVAL_MS, cfg.pollIntervalMs ?? HUB_DEFAULT_POLL_INTERVAL_MS),
|
|
98
|
+
roles: resolveRoles(cfg.roles),
|
|
99
|
+
// Prefer an explicit version; otherwise read it from package.json (same source as getBuildInfo).
|
|
100
|
+
version: ctx.version && ctx.version !== 'unknown' ? ctx.version : getVersion(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isCollectorEnabled(value: boolean | { enabled?: boolean } | undefined, defaultOn = false): boolean {
|
|
105
|
+
if (value === undefined) {
|
|
106
|
+
return defaultOn;
|
|
107
|
+
}
|
|
108
|
+
if (typeof value === 'boolean') {
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
return value.enabled !== false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveLinks(links: IHubConfig['links'], ctx: HubConfigContext): ResolvedHubConfig['links'] {
|
|
115
|
+
const out: ResolvedHubConfig['links'] = {};
|
|
116
|
+
|
|
117
|
+
const swagger = links?.swagger;
|
|
118
|
+
if (swagger !== false) {
|
|
119
|
+
out.swagger = typeof swagger === 'string' ? swagger : '/swagger';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const graphql = links?.graphql;
|
|
123
|
+
if (graphql === false) {
|
|
124
|
+
// explicitly hidden
|
|
125
|
+
} else if (typeof graphql === 'string') {
|
|
126
|
+
out.graphql = graphql;
|
|
127
|
+
} else if (ctx.graphQlEnabled !== false) {
|
|
128
|
+
out.graphql = '/graphql';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const permissions = links?.permissions;
|
|
132
|
+
if (permissions === false) {
|
|
133
|
+
// explicitly hidden
|
|
134
|
+
} else if (typeof permissions === 'string') {
|
|
135
|
+
out.permissions = permissions;
|
|
136
|
+
} else if (ctx.permissionsPath) {
|
|
137
|
+
out.permissions = '/' + ctx.permissionsPath.replace(/^\/+/, '');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (links?.mailpit) {
|
|
141
|
+
out.mailpit = links.mailpit;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveLogsCollector(
|
|
148
|
+
value: boolean | IHubLogsConfig | undefined,
|
|
149
|
+
defaultOn: boolean,
|
|
150
|
+
): ResolvedHubConfig['collectors']['logs'] {
|
|
151
|
+
if (!isCollectorEnabled(value, defaultOn)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const obj: IHubLogsConfig = typeof value === 'object' ? value : {};
|
|
155
|
+
return {
|
|
156
|
+
capacity: obj.capacity ?? HUB_DEFAULT_LOG_CAPACITY,
|
|
157
|
+
excludeContexts: obj.excludeContexts ?? [],
|
|
158
|
+
levels: obj.levels ?? ['log', 'warn', 'error', 'fatal'],
|
|
159
|
+
maxMessageLength: obj.maxMessageLength ?? HUB_DEFAULT_LOG_MAX_MESSAGE_LENGTH,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveMailbox(value: boolean | IHubMailboxConfig | undefined): ResolvedHubConfig['mailbox'] {
|
|
164
|
+
if (!isCollectorEnabled(value, false)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const obj: IHubMailboxConfig = typeof value === 'object' ? value : {};
|
|
168
|
+
return {
|
|
169
|
+
capacity: obj.capacity ?? HUB_DEFAULT_MAILBOX_CAPACITY,
|
|
170
|
+
maxMailSize: obj.maxMailSize ?? HUB_DEFAULT_MAILBOX_MAX_MAIL_SIZE,
|
|
171
|
+
mode: obj.mode === 'copy' ? 'copy' : 'capture',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function resolveMigrations(value: false | IHubConfig['migrations']): ResolvedHubConfig['migrations'] {
|
|
176
|
+
if (value === false) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const obj = typeof value === 'object' && value ? value : {};
|
|
180
|
+
return {
|
|
181
|
+
collectionName: obj.collectionName ?? HUB_DEFAULT_MIGRATIONS_COLLECTION,
|
|
182
|
+
dir: obj.dir ?? HUB_DEFAULT_MIGRATIONS_DIR,
|
|
183
|
+
lockCollectionName: obj.lockCollectionName,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolveQueriesCollector(
|
|
188
|
+
value: boolean | IHubQueriesConfig | undefined,
|
|
189
|
+
defaultOn: boolean,
|
|
190
|
+
): ResolvedHubConfig['collectors']['queries'] {
|
|
191
|
+
if (!isCollectorEnabled(value, defaultOn)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
const obj: IHubQueriesConfig = typeof value === 'object' ? value : {};
|
|
195
|
+
return {
|
|
196
|
+
capacity: obj.capacity ?? HUB_DEFAULT_QUERY_CAPACITY,
|
|
197
|
+
criticalMs: obj.criticalMs ?? HUB_DEFAULT_QUERY_CRITICAL_MS,
|
|
198
|
+
ignoreCommands: obj.ignoreCommands,
|
|
199
|
+
maxShapeLength: obj.maxShapeLength ?? HUB_DEFAULT_QUERY_MAX_SHAPE_LENGTH,
|
|
200
|
+
warnMs: obj.warnMs ?? HUB_DEFAULT_QUERY_WARN_MS,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resolveRoles(roles: IHubConfig['roles']): false | string[] {
|
|
205
|
+
if (roles === false) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (roles === undefined) {
|
|
209
|
+
return [RoleEnum.ADMIN];
|
|
210
|
+
}
|
|
211
|
+
return Array.isArray(roles) ? roles : [roles];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function resolveTracesCollector(
|
|
215
|
+
value: boolean | IHubTracesConfig | undefined,
|
|
216
|
+
defaultOn: boolean,
|
|
217
|
+
): ResolvedHubConfig['collectors']['traces'] {
|
|
218
|
+
if (!isCollectorEnabled(value, defaultOn)) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const obj: IHubTracesConfig = typeof value === 'object' ? value : {};
|
|
222
|
+
return {
|
|
223
|
+
capacity: obj.capacity ?? HUB_DEFAULT_TRACE_CAPACITY,
|
|
224
|
+
captureGraphQlOperation: obj.captureGraphQlOperation !== false,
|
|
225
|
+
excludePaths: obj.excludePaths ?? ['/hub'],
|
|
226
|
+
slowMs: obj.slowMs ?? HUB_DEFAULT_TRACE_SLOW_MS,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Strip leading/trailing slashes so the path composes cleanly with `Reflect.defineMetadata`. */
|
|
231
|
+
function sanitizePath(path: string | undefined): string {
|
|
232
|
+
return (path ?? '').replace(/^\/+|\/+$/g, '');
|
|
233
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the Hub's panels: id, route sub-path, title, sidebar group and the
|
|
3
|
+
* JSON sidecar the client polls. The HTML shell renders the sidebar from this list; the client
|
|
4
|
+
* runtime routes and renders from the same list (embedded into the shell).
|
|
5
|
+
*
|
|
6
|
+
* Pure data — no imports — so both the server shell builder and the client-JS string can share it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type HubPanelGroup = 'Overview' | 'Runtime' | 'Data' | 'API & Docs';
|
|
10
|
+
|
|
11
|
+
export interface HubPanel {
|
|
12
|
+
/** Sidebar group. */
|
|
13
|
+
readonly group: HubPanelGroup;
|
|
14
|
+
/** Stable id (also the client router key). */
|
|
15
|
+
readonly id: string;
|
|
16
|
+
/** Sidecar sub-path relative to the base path (e.g. 'dashboard.json'). Empty when the panel has none. */
|
|
17
|
+
readonly json: string;
|
|
18
|
+
/** Whether the panel depends on an optional source and may render an "unavailable" state. */
|
|
19
|
+
readonly optional?: boolean;
|
|
20
|
+
/** Route sub-path relative to the base path (''=dashboard root). */
|
|
21
|
+
readonly path: string;
|
|
22
|
+
/** Human-readable title. */
|
|
23
|
+
readonly title: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const HUB_PANELS: readonly HubPanel[] = [
|
|
27
|
+
{ group: 'Overview', id: 'dashboard', json: 'dashboard.json', path: '', title: 'Dashboard' },
|
|
28
|
+
{ group: 'Overview', id: 'diagnostics', json: 'diagnostics.json', path: 'diagnostics', title: 'Diagnostics' },
|
|
29
|
+
{ group: 'Runtime', id: 'logs', json: 'logs.json', path: 'logs', title: 'Logs' },
|
|
30
|
+
{ group: 'Runtime', id: 'traces', json: 'traces.json', path: 'traces', title: 'Request Traces' },
|
|
31
|
+
{ group: 'Runtime', id: 'queries', json: 'queries.json', path: 'queries', title: 'Query Performance' },
|
|
32
|
+
{ group: 'Runtime', id: 'cron', json: 'cron.json', optional: true, path: 'cron', title: 'Cron Jobs' },
|
|
33
|
+
{ group: 'Data', id: 'db', json: 'db.json', path: 'db', title: 'Database' },
|
|
34
|
+
{ group: 'Data', id: 'models', json: 'models.json', path: 'models', title: 'Models / ERD' },
|
|
35
|
+
{ group: 'Data', id: 'migrations', json: 'migrations.json', path: 'migrations', title: 'Migrations' },
|
|
36
|
+
{ group: 'Data', id: 'files', json: 'files.json', path: 'files', title: 'Files' },
|
|
37
|
+
{ group: 'Data', id: 'config', json: 'config.json', path: 'config', title: 'Config' },
|
|
38
|
+
{
|
|
39
|
+
group: 'Data',
|
|
40
|
+
id: 'auth-migration',
|
|
41
|
+
json: 'auth-migration.json',
|
|
42
|
+
optional: true,
|
|
43
|
+
path: 'auth-migration',
|
|
44
|
+
title: 'Auth Migration',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
group: 'API & Docs',
|
|
48
|
+
id: 'routes',
|
|
49
|
+
json: 'routes.json',
|
|
50
|
+
optional: true,
|
|
51
|
+
path: 'routes',
|
|
52
|
+
title: 'Routes / Permissions',
|
|
53
|
+
},
|
|
54
|
+
{ group: 'API & Docs', id: 'error-codes', json: 'error-codes.json', path: 'error-codes', title: 'Error Codes' },
|
|
55
|
+
{ group: 'API & Docs', id: 'emails', json: 'emails.json', path: 'emails', title: 'Email Preview' },
|
|
56
|
+
{ group: 'API & Docs', id: 'mailbox', json: 'mailbox.json', optional: true, path: 'mailbox', title: 'Mailbox' },
|
|
57
|
+
{ group: 'API & Docs', id: 'ai', json: 'ai.json', optional: true, path: 'ai', title: 'AI' },
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
export const HUB_PANEL_GROUPS: readonly HubPanelGroup[] = ['Overview', 'Runtime', 'Data', 'API & Docs'];
|
|
61
|
+
|
|
62
|
+
/** Find the panel that a given route sub-path renders. */
|
|
63
|
+
export function findHubPanelByPath(subPath: string): HubPanel | undefined {
|
|
64
|
+
const normalized = subPath.replace(/^\/+|\/+$/g, '');
|
|
65
|
+
return HUB_PANELS.find((panel) => panel.path === normalized);
|
|
66
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { HubRingBuffer } from './hub-ring-buffer';
|
|
4
|
+
|
|
5
|
+
interface TestEntry {
|
|
6
|
+
seq: number;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('HubRingBuffer', () => {
|
|
12
|
+
it('assigns monotonic sequence numbers and a timestamp', () => {
|
|
13
|
+
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
14
|
+
const a = buffer.add({ value: 'a' });
|
|
15
|
+
const b = buffer.add({ value: 'b' });
|
|
16
|
+
|
|
17
|
+
expect(a.seq).toBe(0);
|
|
18
|
+
expect(b.seq).toBe(1);
|
|
19
|
+
expect(typeof a.timestamp).toBe('number');
|
|
20
|
+
expect(b.seq).toBeGreaterThan(a.seq);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('honors an explicitly provided timestamp', () => {
|
|
24
|
+
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
25
|
+
const entry = buffer.add({ timestamp: 123456, value: 'a' });
|
|
26
|
+
|
|
27
|
+
expect(entry.timestamp).toBe(123456);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('evicts the oldest entries once capacity is exceeded (fixed memory)', () => {
|
|
31
|
+
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
32
|
+
for (const value of ['a', 'b', 'c', 'd', 'e']) {
|
|
33
|
+
buffer.add({ value });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const items = buffer.recent();
|
|
37
|
+
expect(items.map((i) => i.value)).toEqual(['c', 'd', 'e']);
|
|
38
|
+
expect(buffer.size).toBe(3);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('recent(limit) returns the newest N entries, oldest→newest', () => {
|
|
42
|
+
const buffer = new HubRingBuffer<TestEntry>(5);
|
|
43
|
+
for (const value of ['a', 'b', 'c', 'd']) {
|
|
44
|
+
buffer.add({ value });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
expect(buffer.recent(2).map((i) => i.value)).toEqual(['c', 'd']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('since(seq) returns only entries newer than the cursor', () => {
|
|
51
|
+
const buffer = new HubRingBuffer<TestEntry>(10);
|
|
52
|
+
buffer.add({ value: 'a' }); // seq 0
|
|
53
|
+
const b = buffer.add({ value: 'b' }); // seq 1
|
|
54
|
+
buffer.add({ value: 'c' }); // seq 2
|
|
55
|
+
|
|
56
|
+
expect(buffer.since(b.seq).map((i) => i.value)).toEqual(['c']);
|
|
57
|
+
expect(buffer.since(-1).map((i) => i.value)).toEqual(['a', 'b', 'c']);
|
|
58
|
+
expect(buffer.since(buffer.lastSeq)).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('since(seq) clamps to what is still retained after eviction', () => {
|
|
62
|
+
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
63
|
+
for (const value of ['a', 'b', 'c', 'd', 'e']) {
|
|
64
|
+
buffer.add({ value }); // seqs 0..4, only 2,3,4 retained
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Asking since an evicted cursor (seq 0) returns only what survives, no crash, no duplicates.
|
|
68
|
+
expect(buffer.since(0).map((i) => i.value)).toEqual(['c', 'd', 'e']);
|
|
69
|
+
expect(buffer.firstRetainedSeq).toBe(2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('exposes lastSeq (-1 when empty) and firstRetainedSeq', () => {
|
|
73
|
+
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
74
|
+
expect(buffer.lastSeq).toBe(-1);
|
|
75
|
+
expect(buffer.firstRetainedSeq).toBe(-1);
|
|
76
|
+
expect(buffer.size).toBe(0);
|
|
77
|
+
|
|
78
|
+
buffer.add({ value: 'a' });
|
|
79
|
+
expect(buffer.lastSeq).toBe(0);
|
|
80
|
+
expect(buffer.firstRetainedSeq).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('clear() empties the buffer but keeps the sequence counter monotonic', () => {
|
|
84
|
+
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
85
|
+
buffer.add({ value: 'a' });
|
|
86
|
+
buffer.add({ value: 'b' });
|
|
87
|
+
buffer.clear();
|
|
88
|
+
|
|
89
|
+
expect(buffer.size).toBe(0);
|
|
90
|
+
expect(buffer.recent()).toEqual([]);
|
|
91
|
+
// A new entry after clear must not reuse an old seq (cursors stay valid across a clear).
|
|
92
|
+
const c = buffer.add({ value: 'c' });
|
|
93
|
+
expect(c.seq).toBe(2);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The minimum shape every Hub ring-buffer entry carries.
|
|
3
|
+
*
|
|
4
|
+
* `seq` is a monotonic, per-buffer sequence number (never reused, survives eviction and `clear()`),
|
|
5
|
+
* so polling clients can pass the last seq they saw as a cursor and receive only newer entries.
|
|
6
|
+
*/
|
|
7
|
+
export interface HubBufferEntry {
|
|
8
|
+
/** Monotonic per-buffer sequence number (0-based, never reused). */
|
|
9
|
+
seq: number;
|
|
10
|
+
/** Epoch milliseconds. */
|
|
11
|
+
timestamp: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fixed-size ring buffer with O(1) append and cursor-based reads.
|
|
16
|
+
*
|
|
17
|
+
* - Append writes into `slots[seq % capacity]` — no `Array.shift()` (which would be O(n) and, on the
|
|
18
|
+
* long-lived buffers the Hub keeps, a steady GC drag). Memory is strictly `capacity × recordSize`.
|
|
19
|
+
* - No timers, no statics: one instance per collector per Nest app instance, so parallel e2e apps in
|
|
20
|
+
* one process never share buffer state.
|
|
21
|
+
*
|
|
22
|
+
* See CLAUDE.md "High-Frequency Path Design Rules" (buffers need a size cap + eviction).
|
|
23
|
+
*/
|
|
24
|
+
export class HubRingBuffer<T extends HubBufferEntry> {
|
|
25
|
+
/** Fixed capacity (entries retained). */
|
|
26
|
+
public readonly capacity: number;
|
|
27
|
+
|
|
28
|
+
/** Watermark raised by `clear()`: no seq below this is considered retained. */
|
|
29
|
+
private minValidSeq = 0;
|
|
30
|
+
private nextSeq = 0;
|
|
31
|
+
private readonly slots: (T | undefined)[];
|
|
32
|
+
|
|
33
|
+
constructor(capacity: number) {
|
|
34
|
+
this.capacity = Math.max(1, Math.floor(capacity));
|
|
35
|
+
this.slots = Array.from<T | undefined>({ length: this.capacity });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Seq of the oldest retained entry (`-1` when empty) — clients detect eviction gaps against this. */
|
|
39
|
+
get firstRetainedSeq(): number {
|
|
40
|
+
return this.size === 0 ? -1 : this.oldestSeq;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Highest assigned seq (`-1` when nothing was ever added) — the client's next cursor. */
|
|
44
|
+
get lastSeq(): number {
|
|
45
|
+
return this.nextSeq - 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Number of entries currently retained. */
|
|
49
|
+
get size(): number {
|
|
50
|
+
return this.nextSeq - this.oldestSeq;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Earliest seq that could still be retained (bounded by capacity and by the last `clear()`). */
|
|
54
|
+
private get oldestSeq(): number {
|
|
55
|
+
return Math.max(this.nextSeq - this.capacity, this.minValidSeq);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Append an entry. Assigns `seq` + `timestamp` (unless a timestamp is supplied) and overwrites the
|
|
60
|
+
* oldest slot once full. Returns the fully-formed entry.
|
|
61
|
+
*/
|
|
62
|
+
add(entry: Omit<T, 'seq' | 'timestamp'> & { timestamp?: number }): T {
|
|
63
|
+
const full = {
|
|
64
|
+
...entry,
|
|
65
|
+
seq: this.nextSeq,
|
|
66
|
+
timestamp: entry.timestamp ?? Date.now(),
|
|
67
|
+
} as T;
|
|
68
|
+
this.slots[this.nextSeq % this.capacity] = full;
|
|
69
|
+
this.nextSeq++;
|
|
70
|
+
return full;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Drop all retained entries. The seq counter keeps advancing so existing cursors stay valid. */
|
|
74
|
+
clear(): void {
|
|
75
|
+
this.slots.fill(undefined);
|
|
76
|
+
this.minValidSeq = this.nextSeq;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Newest up to `limit` entries, oldest→newest. */
|
|
80
|
+
recent(limit: number = this.capacity): T[] {
|
|
81
|
+
return this.collect(this.nextSeq - Math.max(0, limit));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Entries with `seq > sinceSeq`, clamped to what is still retained, oldest→newest. */
|
|
85
|
+
since(sinceSeq: number, limit: number = this.capacity): T[] {
|
|
86
|
+
const start = Math.max(sinceSeq + 1, this.nextSeq - Math.max(0, limit));
|
|
87
|
+
return this.collect(start);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private collect(startSeq: number): T[] {
|
|
91
|
+
const from = Math.max(startSeq, this.oldestSeq, 0);
|
|
92
|
+
const result: T[] = [];
|
|
93
|
+
for (let seq = from; seq < this.nextSeq; seq++) {
|
|
94
|
+
const entry = this.slots[seq % this.capacity];
|
|
95
|
+
if (entry !== undefined && entry.seq === seq) {
|
|
96
|
+
result.push(entry);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-injection tokens and numeric defaults of the Hub module.
|
|
3
|
+
*
|
|
4
|
+
* They live in a dedicated, import-free leaf file — never in `core-hub.module.ts` or a service — so
|
|
5
|
+
* that no file needing a token has to import the module (or vice versa) and close an import cycle.
|
|
6
|
+
*
|
|
7
|
+
* On a cycle, a token read at MODULE-EVALUATION time — inside an `@Inject()` decorator argument, in
|
|
8
|
+
* `design:paramtypes` metadata, or in a static field initializer — reads a `const` still in its
|
|
9
|
+
* temporal dead zone, and SWC-compiled builds (`nest start -b swc`) die at startup with:
|
|
10
|
+
*
|
|
11
|
+
* ReferenceError: Cannot access 'HUB_CONFIG' before initialization
|
|
12
|
+
*
|
|
13
|
+
* A file that imports nothing can never be mid-evaluation when someone imports it, in any module
|
|
14
|
+
* system, under any compiler. That is the whole point — keep it import-free.
|
|
15
|
+
*
|
|
16
|
+
* `tsc`, `pnpm test` and `oxlint` are all blind to a regression here; only `pnpm run check:swc-tdz`
|
|
17
|
+
* sees it. See .claude/rules/architecture.md → "DI Token Placement (SWC-Safe)".
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Token for injecting the resolved Hub configuration.
|
|
22
|
+
*
|
|
23
|
+
* Injected type: `ResolvedHubConfig` (see interfaces/hub-config.interface.ts).
|
|
24
|
+
*/
|
|
25
|
+
export const HUB_CONFIG = 'HUB_CONFIG';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Token for the optional outgoing-mail capture hook.
|
|
29
|
+
*
|
|
30
|
+
* `EmailService` injects it with `@Optional() @Inject(HUB_EMAIL_CAPTURE)`. When the Hub mailbox is
|
|
31
|
+
* enabled, `CoreHubModule` binds it to the mailbox service; otherwise it resolves to `undefined` and
|
|
32
|
+
* the mail path stays untouched (zero cost).
|
|
33
|
+
*
|
|
34
|
+
* Injected type: `IHubEmailCapture` (see interfaces/hub-config.interface.ts).
|
|
35
|
+
*/
|
|
36
|
+
export const HUB_EMAIL_CAPTURE = 'HUB_EMAIL_CAPTURE';
|
|
37
|
+
|
|
38
|
+
/** Default base path for all Hub routes. */
|
|
39
|
+
export const HUB_DEFAULT_PATH = 'hub';
|
|
40
|
+
|
|
41
|
+
/** Default client poll interval (ms) for JSON sidecars. */
|
|
42
|
+
export const HUB_DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
43
|
+
|
|
44
|
+
/** Lower clamp for the client poll interval (ms) — protects the server from hammering. */
|
|
45
|
+
export const HUB_MIN_POLL_INTERVAL_MS = 1000;
|
|
46
|
+
|
|
47
|
+
/** Default ring-buffer capacity for the log collector. */
|
|
48
|
+
export const HUB_DEFAULT_LOG_CAPACITY = 500;
|
|
49
|
+
|
|
50
|
+
/** Default per-record message cap (characters) for the log collector. */
|
|
51
|
+
export const HUB_DEFAULT_LOG_MAX_MESSAGE_LENGTH = 2048;
|
|
52
|
+
|
|
53
|
+
/** Default ring-buffer capacity for the request-trace collector. */
|
|
54
|
+
export const HUB_DEFAULT_TRACE_CAPACITY = 200;
|
|
55
|
+
|
|
56
|
+
/** Default duration (ms) above which a request trace is flagged slow. */
|
|
57
|
+
export const HUB_DEFAULT_TRACE_SLOW_MS = 1000;
|
|
58
|
+
|
|
59
|
+
/** Default ring-buffer capacity for the query profiler. */
|
|
60
|
+
export const HUB_DEFAULT_QUERY_CAPACITY = 500;
|
|
61
|
+
|
|
62
|
+
/** Default duration (ms) above which a query is classified "warn". */
|
|
63
|
+
export const HUB_DEFAULT_QUERY_WARN_MS = 50;
|
|
64
|
+
|
|
65
|
+
/** Default duration (ms) above which a query is classified "critical". */
|
|
66
|
+
export const HUB_DEFAULT_QUERY_CRITICAL_MS = 200;
|
|
67
|
+
|
|
68
|
+
/** Default command-summary length cap (characters) for the query profiler. */
|
|
69
|
+
export const HUB_DEFAULT_QUERY_MAX_SHAPE_LENGTH = 512;
|
|
70
|
+
|
|
71
|
+
/** Hard cap on the query profiler's in-flight (pending) command map before oldest-first eviction. */
|
|
72
|
+
export const HUB_QUERY_PENDING_LIMIT = 1000;
|
|
73
|
+
|
|
74
|
+
/** Default number of captured mails retained by the mailbox. */
|
|
75
|
+
export const HUB_DEFAULT_MAILBOX_CAPACITY = 100;
|
|
76
|
+
|
|
77
|
+
/** Default per-mail size cap (bytes, html + text) retained by the mailbox. */
|
|
78
|
+
export const HUB_DEFAULT_MAILBOX_MAX_MAIL_SIZE = 262144;
|
|
79
|
+
|
|
80
|
+
/** Default migrations directory (mirrors the migrate CLI default). */
|
|
81
|
+
export const HUB_DEFAULT_MIGRATIONS_DIR = './migrations';
|
|
82
|
+
|
|
83
|
+
/** Default MongoDB collection holding migration state. */
|
|
84
|
+
export const HUB_DEFAULT_MIGRATIONS_COLLECTION = 'migrations';
|