cabloy 5.1.219 → 5.1.220
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/.cabloy-version +1 -1
- package/.github/workflows/create-cabloy.yml +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/repo-docs/fullstack/quickstart.md +3 -3
- package/repo-docs/reference/repo-scripts.md +12 -0
- package/scripts/generateOpenapiIsolated.test.ts +25 -0
- package/scripts/generateOpenapiIsolated.ts +28 -11
- package/scripts/runIsolatedTest.test.ts +157 -45
- package/scripts/runIsolatedTest.ts +30 -6
- package/scripts/strictGenerationStartup.test.ts +212 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/bean/beanBase.ts +4 -0
- package/vona/packages-vona/vona-core/src/lib/core/asyncLocalStorage.ts +84 -12
- package/vona/packages-vona/vona-core/src/lib/core/index.ts +14 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/appLogger.ts +11 -3
- package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.test.ts +146 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.ts +138 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/index.ts +1 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/utils.ts +25 -5
- package/vona/packages-vona/vona-core/src/lib/core/requestBoundary.ts +89 -0
- package/vona/packages-vona/vona-core/src/lib/core/testFreshness.test.ts +379 -0
- package/vona/packages-vona/vona-core/src/lib/core/testFreshness.ts +238 -0
- package/vona/packages-vona/vona-core/src/lib/framework/start.test.ts +4 -3
- package/vona/packages-vona/vona-core/src/lib/framework/start.ts +2 -0
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +154 -125
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +111 -60
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/service/executor.ts +6 -6
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/types/executor.ts +13 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/lib/jwtClientLogging.test.ts +182 -0
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +2 -8
- package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/aopMethod.log.ts +4 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/bean.logger.ts +9 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +27 -14
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactHttpLog.test.ts +207 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactHttpLog.ts +30 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +33 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +6 -0
- package/vona/src/suite-vendor/a-vona/modules/a-startup/test/devSeed.test.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/bean.telemetry.ts +8 -1
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/middlewareSystem.trace.ts +9 -14
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/lib/spanDiagnostics.ts +70 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/service/telemetry.ts +37 -23
- package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/database.ts +31 -4
- package/vona/src/suite-vendor/a-vona/package.json +2 -2
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/cli.bin.buildRest.ts +11 -101
- package/zova/packages-cli/cli-set-front/src/lib/common/restDependencies.ts +78 -0
- package/zova/packages-cli/cli-set-front/src/lib/common/restDependencyVersions.ts +231 -0
- package/zova/packages-cli/cli-set-front/test/restDependencies.test.ts +431 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { registerHooks } from 'node:module';
|
|
4
|
+
import { after, test } from 'node:test';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
|
|
7
|
+
const root = new URL('../vona/src/suite-vendor/a-vona/modules/', import.meta.url);
|
|
8
|
+
const databaseUrl = new URL('a-version/src/service/database.ts', root).href;
|
|
9
|
+
const startupUrl = new URL('a-startup/src/service/startup.ts', root).href;
|
|
10
|
+
const helperUrl = new URL('a-version/src/lib/databaseTestName.ts', root).href;
|
|
11
|
+
const isolationUrl = new URL(
|
|
12
|
+
'../vona/packages-vona/vona-core/src/lib/core/testIsolation.ts',
|
|
13
|
+
import.meta.url,
|
|
14
|
+
).href;
|
|
15
|
+
let strict = true;
|
|
16
|
+
let ready = true;
|
|
17
|
+
const mocks = {
|
|
18
|
+
'vona': `
|
|
19
|
+
export class BeanBase {}
|
|
20
|
+
export function assertFreshTestResourcesReady() { globalThis.__strictGenerationTest.ready(); }
|
|
21
|
+
export function requiresFreshTestResources() { return globalThis.__strictGenerationTest.strict(); }
|
|
22
|
+
export function assertIsolatedTestConfig() {}
|
|
23
|
+
export function assertIsolatedTestPath() {}
|
|
24
|
+
export function assertTestRunOwner() {}
|
|
25
|
+
export function resolveVonaLocalPaths() { return { dataRoot: '/synthetic' }; }
|
|
26
|
+
export async function withIsolatedTestDatabaseDiscovery(_app, _name, _client, fn) { return await fn(); }
|
|
27
|
+
export function cast(value) { return value; }
|
|
28
|
+
export function getRuntimePathPhysicalRoot() { throw new Error('must not get mutating runtime path'); }
|
|
29
|
+
export { createDatabaseTestName, getDatabaseTestPrefix } from ${JSON.stringify(helperUrl)};
|
|
30
|
+
export { assertDisposableDatabaseName } from ${JSON.stringify(isolationUrl)};
|
|
31
|
+
`,
|
|
32
|
+
'vona-module-a-bean': 'export function Service() { return value => value; }',
|
|
33
|
+
'fs-extra':
|
|
34
|
+
'export default { async remove() { throw new Error("must not remove directory"); } };',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Execute the real service bodies; replace only framework collaborators. No app or drivers.
|
|
38
|
+
const previous = Object.getOwnPropertyDescriptor(globalThis, '__strictGenerationTest');
|
|
39
|
+
Object.defineProperty(globalThis, '__strictGenerationTest', {
|
|
40
|
+
configurable: true,
|
|
41
|
+
value: {
|
|
42
|
+
strict: () => strict,
|
|
43
|
+
ready: () => {
|
|
44
|
+
if (!ready) throw new Error('freshness missing');
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const hooks = registerHooks({
|
|
49
|
+
resolve(specifier, context, nextResolve) {
|
|
50
|
+
if ([databaseUrl, startupUrl].includes(context.parentURL ?? '') && mocks[specifier]) {
|
|
51
|
+
return {
|
|
52
|
+
url: `data:text/javascript,${encodeURIComponent(mocks[specifier])}`,
|
|
53
|
+
shortCircuit: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (context.parentURL === helperUrl && specifier === 'vona') {
|
|
57
|
+
return { url: isolationUrl, shortCircuit: true };
|
|
58
|
+
}
|
|
59
|
+
return nextResolve(specifier, context);
|
|
60
|
+
},
|
|
61
|
+
load(url, context, nextLoad) {
|
|
62
|
+
if ([databaseUrl, startupUrl].includes(url)) {
|
|
63
|
+
const source = ts.transpileModule(readFileSync(new URL(url), 'utf8'), {
|
|
64
|
+
compilerOptions: {
|
|
65
|
+
experimentalDecorators: true,
|
|
66
|
+
target: ts.ScriptTarget.ESNext,
|
|
67
|
+
module: ts.ModuleKind.ESNext,
|
|
68
|
+
},
|
|
69
|
+
}).outputText;
|
|
70
|
+
return { format: 'module', source, shortCircuit: true };
|
|
71
|
+
}
|
|
72
|
+
return nextLoad(url, context);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
let ServiceDatabase: any;
|
|
76
|
+
let ServiceStartup: any;
|
|
77
|
+
try {
|
|
78
|
+
({ ServiceDatabase } = await import(databaseUrl));
|
|
79
|
+
({ ServiceStartup } = await import(startupUrl));
|
|
80
|
+
} finally {
|
|
81
|
+
hooks.deregister();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function databaseFixture(existing: string[] = []) {
|
|
85
|
+
let current = 'postgres';
|
|
86
|
+
const calls: string[] = [];
|
|
87
|
+
const app = { name: 'synthetic', meta: { isTest: true, isDev: false, isProd: false, env: {} } };
|
|
88
|
+
const client = {
|
|
89
|
+
clientName: 'main',
|
|
90
|
+
db: { dialectName: 'pg' },
|
|
91
|
+
getDatabaseName: () => current,
|
|
92
|
+
connection: {
|
|
93
|
+
schema: {
|
|
94
|
+
fetchDatabases: async () => {
|
|
95
|
+
calls.push('discover');
|
|
96
|
+
return existing.map(name => ({ name }));
|
|
97
|
+
},
|
|
98
|
+
createDatabase: async (name: string) => {
|
|
99
|
+
calls.push('create');
|
|
100
|
+
existing.push(name);
|
|
101
|
+
return name;
|
|
102
|
+
},
|
|
103
|
+
dropDatabase: async () => {
|
|
104
|
+
throw new Error('must not drop');
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
changeConfigConnectionAndReloadWorker: async (name: string) => {
|
|
109
|
+
calls.push('reload');
|
|
110
|
+
current = name;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
const service = Object.create(ServiceDatabase.prototype);
|
|
114
|
+
Object.defineProperties(service, {
|
|
115
|
+
app: { value: app },
|
|
116
|
+
bean: { value: { database: { current: { client } } } },
|
|
117
|
+
$logger: { value: { silly() {} } },
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
service,
|
|
121
|
+
calls,
|
|
122
|
+
setCurrent: (name: string) => {
|
|
123
|
+
current = name;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// This file owns the collaborator sentinel for its entire isolated node:test process.
|
|
129
|
+
// Restore it after every test has settled, including assertions that fail.
|
|
130
|
+
after(() => {
|
|
131
|
+
if (previous) Object.defineProperty(globalThis, '__strictGenerationTest', previous);
|
|
132
|
+
else Reflect.deleteProperty(globalThis, '__strictGenerationTest');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('strict database startup refuses preexisting matches and unowned selected targets without deletion', async () => {
|
|
136
|
+
strict = true;
|
|
137
|
+
ready = true;
|
|
138
|
+
const f = databaseFixture(['vona-test-synthetic-s-20261019-010203']);
|
|
139
|
+
await assert.rejects(f.service.__prepareDatabaseInner(), /no longer fresh/);
|
|
140
|
+
assert.deepEqual(f.calls, ['discover']);
|
|
141
|
+
f.setCurrent('vona-test-synthetic-s-20261019-010203');
|
|
142
|
+
await assert.rejects(f.service.__prepareDatabaseInner(), /unowned database/);
|
|
143
|
+
assert.deepEqual(f.calls, ['discover']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('strict database startup creates once and reuses only its successful own creation', async () => {
|
|
147
|
+
strict = true;
|
|
148
|
+
ready = true;
|
|
149
|
+
const f = databaseFixture();
|
|
150
|
+
await f.service.__prepareDatabaseInner();
|
|
151
|
+
await f.service.__prepareDatabaseInner();
|
|
152
|
+
assert.deepEqual(f.calls, ['discover', 'create', 'reload']);
|
|
153
|
+
f.setCurrent('vona-test-synthetic-s-20261019-010203');
|
|
154
|
+
await assert.rejects(f.service.__prepareDatabaseInner(), /unowned database/);
|
|
155
|
+
ready = false;
|
|
156
|
+
await assert.rejects(f.service.__prepareDatabaseInner(), /freshness missing/);
|
|
157
|
+
ready = true;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('strict startup bypasses clearing only with established proof and does not bypass later startups', async () => {
|
|
161
|
+
strict = true;
|
|
162
|
+
ready = true;
|
|
163
|
+
const calls: string[] = [];
|
|
164
|
+
const service = Object.create(ServiceStartup.prototype);
|
|
165
|
+
Object.defineProperties(service, {
|
|
166
|
+
app: { value: { meta: { isTest: true, isDev: false, env: {} } } },
|
|
167
|
+
$scope: {
|
|
168
|
+
value: {
|
|
169
|
+
redis: {
|
|
170
|
+
service: {
|
|
171
|
+
redis: {
|
|
172
|
+
clearAllData: async () => {
|
|
173
|
+
throw new Error('must not clear Redis');
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
bean: {
|
|
181
|
+
value: {
|
|
182
|
+
onion: {
|
|
183
|
+
startup: {
|
|
184
|
+
getOnionsEnabledCached: () => [{ name: 'synthetic', beanOptions: { options: {} } }],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
runStartup: {
|
|
190
|
+
value: async () => {
|
|
191
|
+
calls.push('startup');
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
await service.appStart();
|
|
196
|
+
assert.deepEqual(calls, ['startup']);
|
|
197
|
+
ready = false;
|
|
198
|
+
await assert.rejects(service.appStart(), /freshness missing/);
|
|
199
|
+
assert.deepEqual(calls, ['startup']);
|
|
200
|
+
ready = true;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('ordinary test database selected-target behavior remains unchanged without strict flag', async () => {
|
|
204
|
+
strict = false;
|
|
205
|
+
ready = false;
|
|
206
|
+
const f = databaseFixture();
|
|
207
|
+
f.setCurrent('vona-test-synthetic-s-20261019-010203');
|
|
208
|
+
await f.service.__prepareDatabaseInner();
|
|
209
|
+
assert.deepEqual(f.calls, []);
|
|
210
|
+
strict = true;
|
|
211
|
+
ready = true;
|
|
212
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vona",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.111",
|
|
4
4
|
"gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
|
|
5
5
|
"description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
|
|
6
6
|
"keywords": [
|
|
@@ -4,6 +4,8 @@ import type { ILoggerChildRecord, ILoggerClientRecord } from '../../types/interf
|
|
|
4
4
|
// import type { IModuleLocaleText } from './resource/locale/type.ts';
|
|
5
5
|
import type { IBeanScopeContainer } from './scope/beanScopeContainer.ts';
|
|
6
6
|
|
|
7
|
+
import { getSuppressedDiagnosticLogger } from '../core/logger/diagnosticLogger.ts';
|
|
8
|
+
import { getRequestBoundary } from '../core/requestBoundary.ts';
|
|
7
9
|
import { BeanBaseSimple, SymbolBeanInstanceKey, SymbolModuleBelong } from './beanBaseSimple.ts';
|
|
8
10
|
|
|
9
11
|
// const SymbolText = Symbol('SymbolText');
|
|
@@ -30,6 +32,7 @@ export class BeanBase extends BeanBaseSimple {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
protected $loggerClient(clientName: keyof ILoggerClientRecord = 'default') {
|
|
35
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
33
36
|
if (!this[SymbolLogger][clientName]) {
|
|
34
37
|
this[SymbolLogger][clientName] = this.app.meta.logger
|
|
35
38
|
.get(clientName)
|
|
@@ -42,6 +45,7 @@ export class BeanBase extends BeanBaseSimple {
|
|
|
42
45
|
childName: keyof ILoggerChildRecord,
|
|
43
46
|
clientName: keyof ILoggerClientRecord = 'default',
|
|
44
47
|
) {
|
|
48
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
45
49
|
if (!this[SymbolLoggerChildren][clientName])
|
|
46
50
|
this[SymbolLoggerChildren][clientName] = {} as never;
|
|
47
51
|
if (!this[SymbolLoggerChildren][clientName][childName]) {
|
|
@@ -2,9 +2,17 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
2
2
|
|
|
3
3
|
import type { VonaContext } from '../../types/context/index.ts';
|
|
4
4
|
import type { VonaApplication } from './application.ts';
|
|
5
|
+
import type {
|
|
6
|
+
RequestBoundaryCapture,
|
|
7
|
+
VonaScopeOutcome,
|
|
8
|
+
VonaScopeOutcomeObserver,
|
|
9
|
+
} from './requestBoundary.ts';
|
|
10
|
+
|
|
11
|
+
import { captureRequestBoundary } from './requestBoundary.ts';
|
|
5
12
|
|
|
6
13
|
export class VonaAsyncLocalStorage extends AsyncLocalStorage<VonaContext> {
|
|
7
14
|
app: VonaApplication;
|
|
15
|
+
private readonly observedRuns = new WeakMap<VonaContext, { settled: boolean }>();
|
|
8
16
|
|
|
9
17
|
constructor(app: VonaApplication) {
|
|
10
18
|
super();
|
|
@@ -20,18 +28,82 @@ export class VonaAsyncLocalStorage extends AsyncLocalStorage<VonaContext> {
|
|
|
20
28
|
if (store === this.app.currentContext) {
|
|
21
29
|
return await callback(...args);
|
|
22
30
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
if (this.observedRuns.has(store)) throw new Error('Context scope already owned');
|
|
32
|
+
return this._runOwned(store, () => callback(...args));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async runObserved<R>(
|
|
36
|
+
store: VonaContext,
|
|
37
|
+
callback: () => R,
|
|
38
|
+
observer: VonaScopeOutcomeObserver,
|
|
39
|
+
): Promise<R> {
|
|
40
|
+
if (store === this.app.currentContext || this.observedRuns.has(store)) {
|
|
41
|
+
throw new Error('Context scope already owned');
|
|
42
|
+
}
|
|
43
|
+
const frame = { settled: false };
|
|
44
|
+
this.observedRuns.set(store, frame);
|
|
45
|
+
try {
|
|
46
|
+
return await this._runOwned(store, callback, observer);
|
|
47
|
+
} finally {
|
|
48
|
+
frame.settled = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private _runOwned<R>(
|
|
53
|
+
store: VonaContext,
|
|
54
|
+
callback: () => R,
|
|
55
|
+
observer?: VonaScopeOutcomeObserver,
|
|
56
|
+
): Promise<R> {
|
|
57
|
+
const capture = captureRequestBoundary();
|
|
58
|
+
return super.run(store, async () => {
|
|
59
|
+
try {
|
|
60
|
+
this.app.meta.ctxCounter.increment();
|
|
61
|
+
return await callback();
|
|
62
|
+
} finally {
|
|
63
|
+
await this._disposeOwned(store, observer, capture);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private async _disposeOwned(
|
|
69
|
+
store: VonaContext,
|
|
70
|
+
observer: VonaScopeOutcomeObserver | undefined,
|
|
71
|
+
capture: RequestBoundaryCapture | undefined,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
await store.bean.dispose();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
this._notifyScopeOutcome(observer, 'dispose_failed', capture);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
this.app.meta.ctxCounter.decrement();
|
|
81
|
+
} catch (error) {
|
|
82
|
+
capture?.reportFailure('diagnostic_failed');
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
this._notifyScopeOutcome(observer, 'disposed', capture);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private _notifyScopeOutcome(
|
|
89
|
+
observer: VonaScopeOutcomeObserver | undefined,
|
|
90
|
+
outcome: VonaScopeOutcome,
|
|
91
|
+
capture: RequestBoundaryCapture | undefined,
|
|
92
|
+
): void {
|
|
93
|
+
if (!observer) return;
|
|
94
|
+
try {
|
|
95
|
+
const result: unknown = observer(outcome);
|
|
96
|
+
if (result && (typeof result === 'object' || typeof result === 'function')) {
|
|
97
|
+
const then = (result as PromiseLike<unknown>).then;
|
|
98
|
+
if (typeof then === 'function') {
|
|
99
|
+
capture?.reportFailure('observer_failed');
|
|
100
|
+
void new Promise<unknown>((resolve, reject) => {
|
|
101
|
+
then.call(result, resolve, reject);
|
|
102
|
+
}).catch(() => {});
|
|
32
103
|
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
capture?.reportFailure('observer_failed');
|
|
107
|
+
}
|
|
36
108
|
}
|
|
37
109
|
}
|
|
@@ -7,6 +7,20 @@ export * from './locales.ts';
|
|
|
7
7
|
export * from './logger/index.ts';
|
|
8
8
|
export * from './meta.ts';
|
|
9
9
|
export * from './metadata.ts';
|
|
10
|
+
export type {
|
|
11
|
+
RequestBoundary,
|
|
12
|
+
RequestBoundaryCapture,
|
|
13
|
+
RequestBoundaryFailure,
|
|
14
|
+
VonaScopeOutcome,
|
|
15
|
+
VonaScopeOutcomeObserver,
|
|
16
|
+
} from './requestBoundary.ts';
|
|
17
|
+
export {
|
|
18
|
+
captureRequestBoundary,
|
|
19
|
+
createRequestBoundary,
|
|
20
|
+
getRequestBoundary,
|
|
21
|
+
runWithRequestBoundary,
|
|
22
|
+
} from './requestBoundary.ts';
|
|
10
23
|
export * from './resource.ts';
|
|
11
24
|
export * from './testClaim.ts';
|
|
12
25
|
export * from './testIsolation.ts';
|
|
26
|
+
export * from './testFreshness.ts';
|
|
@@ -13,28 +13,36 @@ import type {
|
|
|
13
13
|
|
|
14
14
|
import { BeanSimple } from '../../bean/beanSimple.ts';
|
|
15
15
|
import { deepExtend } from '../../utils/util.ts';
|
|
16
|
+
import { getRequestBoundary } from '../requestBoundary.ts';
|
|
17
|
+
import { createDiagnosticLogger, getSuppressedDiagnosticLogger } from './diagnosticLogger.ts';
|
|
16
18
|
|
|
17
19
|
const SymbolLoggerInstances = Symbol('SymbolLoggerInstances');
|
|
20
|
+
const SymbolRawLoggerInstances = Symbol('SymbolRawLoggerInstances');
|
|
18
21
|
|
|
19
22
|
export class AppLogger extends BeanSimple {
|
|
20
23
|
private [SymbolLoggerInstances]: Record<keyof ILoggerClientRecord, Winston.Logger> = {} as any;
|
|
24
|
+
private [SymbolRawLoggerInstances]: Record<keyof ILoggerClientRecord, Winston.Logger> = {} as any;
|
|
21
25
|
|
|
22
26
|
public async dispose() {
|
|
23
|
-
for (const key in this[
|
|
24
|
-
const logger = this[
|
|
27
|
+
for (const key in this[SymbolRawLoggerInstances]) {
|
|
28
|
+
const logger = this[SymbolRawLoggerInstances][key];
|
|
25
29
|
await _closeLogger(logger);
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
get(clientName?: keyof ILoggerClientRecord) {
|
|
34
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
30
35
|
clientName = clientName || 'default';
|
|
31
36
|
if (!this[SymbolLoggerInstances][clientName]) {
|
|
32
|
-
|
|
37
|
+
const raw = this._createClient(clientName);
|
|
38
|
+
this[SymbolRawLoggerInstances][clientName] = raw;
|
|
39
|
+
this[SymbolLoggerInstances][clientName] = createDiagnosticLogger(raw);
|
|
33
40
|
}
|
|
34
41
|
return this[SymbolLoggerInstances][clientName];
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
child(childName?: keyof ILoggerChildRecord, clientName?: keyof ILoggerClientRecord) {
|
|
45
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
38
46
|
const logger = this.get(clientName);
|
|
39
47
|
if (!childName) return logger;
|
|
40
48
|
return logger.child({ name: childName });
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import * as Winston from 'winston';
|
|
4
|
+
import Transport from 'winston-transport';
|
|
5
|
+
|
|
6
|
+
import { createRequestBoundary, runWithRequestBoundary } from '../requestBoundary.ts';
|
|
7
|
+
import { AppLogger } from './appLogger.ts';
|
|
8
|
+
|
|
9
|
+
class MemoryTransport extends Transport {
|
|
10
|
+
count = 0;
|
|
11
|
+
|
|
12
|
+
override log(_info: unknown, callback: () => void) {
|
|
13
|
+
this.count++;
|
|
14
|
+
callback();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// No boundary formatter: only the facade can prevent Winston from reading input.
|
|
19
|
+
function fixture() {
|
|
20
|
+
const sink = new MemoryTransport();
|
|
21
|
+
const raw = Winston.createLogger({ transports: [sink] });
|
|
22
|
+
const factory = new AppLogger();
|
|
23
|
+
let creations = 0;
|
|
24
|
+
Object.defineProperty(factory, '_createClient', {
|
|
25
|
+
value() {
|
|
26
|
+
creations++;
|
|
27
|
+
return raw;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const logger = factory.get();
|
|
31
|
+
return {
|
|
32
|
+
raw,
|
|
33
|
+
logger,
|
|
34
|
+
sink,
|
|
35
|
+
factory,
|
|
36
|
+
creations: () => creations,
|
|
37
|
+
async close() {
|
|
38
|
+
try {
|
|
39
|
+
await factory.dispose();
|
|
40
|
+
} finally {
|
|
41
|
+
raw.close();
|
|
42
|
+
sink.close?.();
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function payloadProbe() {
|
|
49
|
+
let reads = 0;
|
|
50
|
+
return {
|
|
51
|
+
payload: {
|
|
52
|
+
get message() {
|
|
53
|
+
reads++;
|
|
54
|
+
return 'local';
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
reads: () => reads,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test('cached diagnostic handlers never expose raw Winston to a request boundary', async () => {
|
|
62
|
+
const local = fixture();
|
|
63
|
+
const { raw, logger, sink } = local;
|
|
64
|
+
const exceptions = logger.exceptions;
|
|
65
|
+
const rejections = logger.rejections;
|
|
66
|
+
const probe = payloadProbe();
|
|
67
|
+
try {
|
|
68
|
+
assert.equal(local.factory.get(), logger);
|
|
69
|
+
assert.equal(local.creations(), 1);
|
|
70
|
+
assert.equal(logger.exceptions, exceptions);
|
|
71
|
+
assert.equal(logger.rejections, rejections);
|
|
72
|
+
for (const handler of [exceptions, rejections]) {
|
|
73
|
+
assert.equal(handler.logger, logger);
|
|
74
|
+
assert.equal(Reflect.set(handler, 'logger', raw), false);
|
|
75
|
+
}
|
|
76
|
+
assert.equal(raw.exceptions.logger, raw);
|
|
77
|
+
assert.equal(raw.rejections.logger, raw);
|
|
78
|
+
runWithRequestBoundary(createRequestBoundary(), () => {
|
|
79
|
+
logger.error(probe.payload);
|
|
80
|
+
logger.exceptions.logger.error(probe.payload);
|
|
81
|
+
logger.rejections.logger.error(probe.payload);
|
|
82
|
+
exceptions.logger.error(probe.payload);
|
|
83
|
+
rejections.logger.error(probe.payload);
|
|
84
|
+
});
|
|
85
|
+
assert.equal(probe.reads(), 0);
|
|
86
|
+
assert.equal(sink.count, 0);
|
|
87
|
+
for (const handler of [exceptions, rejections]) {
|
|
88
|
+
const before = probe.reads();
|
|
89
|
+
handler.logger.error(probe.payload);
|
|
90
|
+
assert.ok(probe.reads() > before);
|
|
91
|
+
}
|
|
92
|
+
assert.equal(sink.count, 2);
|
|
93
|
+
const before = probe.reads();
|
|
94
|
+
raw.error(probe.payload);
|
|
95
|
+
assert.ok(probe.reads() > before);
|
|
96
|
+
assert.equal(sink.count, 3);
|
|
97
|
+
} finally {
|
|
98
|
+
await local.close();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('diagnostic handler methods keep their raw receiver and map fluent returns', async () => {
|
|
103
|
+
const local = fixture();
|
|
104
|
+
try {
|
|
105
|
+
for (const key of ['exceptions', 'rejections'] as const) {
|
|
106
|
+
const rawHandler = local.raw[key];
|
|
107
|
+
const handler = local.logger[key];
|
|
108
|
+
const marker = {};
|
|
109
|
+
const original = rawHandler.getAllInfo;
|
|
110
|
+
Object.defineProperty(rawHandler, 'getAllInfo', {
|
|
111
|
+
configurable: true,
|
|
112
|
+
value(this: typeof rawHandler, value: unknown) {
|
|
113
|
+
assert.equal(this, rawHandler);
|
|
114
|
+
assert.equal(value, marker);
|
|
115
|
+
return this;
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
assert.equal(handler.handlers, rawHandler.handlers);
|
|
120
|
+
assert.equal(handler.getAllInfo(marker as any), handler);
|
|
121
|
+
assert.equal(handler.logger, local.logger);
|
|
122
|
+
} finally {
|
|
123
|
+
Object.defineProperty(rawHandler, 'getAllInfo', {
|
|
124
|
+
configurable: true,
|
|
125
|
+
value: original,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const exceptions = local.logger.exceptions;
|
|
130
|
+
const rejections = local.logger.rejections;
|
|
131
|
+
local.logger.configure({ transports: [local.sink] });
|
|
132
|
+
assert.notEqual(local.logger.exceptions, exceptions);
|
|
133
|
+
assert.notEqual(local.logger.rejections, rejections);
|
|
134
|
+
assert.equal(local.logger.exceptions.logger, local.logger);
|
|
135
|
+
assert.equal(local.logger.rejections.logger, local.logger);
|
|
136
|
+
assert.equal(exceptions.logger, local.logger);
|
|
137
|
+
assert.equal(rejections.logger, local.logger);
|
|
138
|
+
runWithRequestBoundary(createRequestBoundary(), () => {
|
|
139
|
+
exceptions.logger.error('local');
|
|
140
|
+
rejections.logger.error('local');
|
|
141
|
+
});
|
|
142
|
+
assert.equal(local.sink.count, 0);
|
|
143
|
+
} finally {
|
|
144
|
+
await local.close();
|
|
145
|
+
}
|
|
146
|
+
});
|