vona-core 5.1.47 → 5.1.49

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.
Files changed (41) hide show
  1. package/dist/index.js +439 -27
  2. package/dist/index.js.map +1 -1
  3. package/dist/lib/bean/beanBase.d.ts.map +1 -1
  4. package/dist/lib/core/asyncLocalStorage.d.ts +6 -0
  5. package/dist/lib/core/asyncLocalStorage.d.ts.map +1 -1
  6. package/dist/lib/core/index.d.ts +3 -0
  7. package/dist/lib/core/index.d.ts.map +1 -1
  8. package/dist/lib/core/logger/appLogger.d.ts +2 -0
  9. package/dist/lib/core/logger/appLogger.d.ts.map +1 -1
  10. package/dist/lib/core/logger/diagnosticLogger.d.ts +4 -0
  11. package/dist/lib/core/logger/diagnosticLogger.d.ts.map +1 -0
  12. package/dist/lib/core/logger/diagnosticLogger.test.d.ts +2 -0
  13. package/dist/lib/core/logger/diagnosticLogger.test.d.ts.map +1 -0
  14. package/dist/lib/core/logger/index.d.ts +1 -0
  15. package/dist/lib/core/logger/index.d.ts.map +1 -1
  16. package/dist/lib/core/logger/utils.d.ts +1 -0
  17. package/dist/lib/core/logger/utils.d.ts.map +1 -1
  18. package/dist/lib/core/requestBoundary.d.ts +22 -0
  19. package/dist/lib/core/requestBoundary.d.ts.map +1 -0
  20. package/dist/lib/core/testFreshness.d.ts +20 -0
  21. package/dist/lib/core/testFreshness.d.ts.map +1 -0
  22. package/dist/lib/core/testFreshness.test.d.ts +2 -0
  23. package/dist/lib/core/testFreshness.test.d.ts.map +1 -0
  24. package/dist/lib/core/testStrictMode.d.ts +3 -0
  25. package/dist/lib/core/testStrictMode.d.ts.map +1 -0
  26. package/dist/lib/framework/start.d.ts.map +1 -1
  27. package/package.json +2 -2
  28. package/src/lib/bean/beanBase.ts +4 -0
  29. package/src/lib/core/asyncLocalStorage.ts +84 -12
  30. package/src/lib/core/index.ts +14 -0
  31. package/src/lib/core/logger/appLogger.ts +19 -3
  32. package/src/lib/core/logger/diagnosticLogger.test.ts +346 -0
  33. package/src/lib/core/logger/diagnosticLogger.ts +138 -0
  34. package/src/lib/core/logger/index.ts +1 -0
  35. package/src/lib/core/logger/utils.ts +25 -5
  36. package/src/lib/core/requestBoundary.ts +89 -0
  37. package/src/lib/core/testFreshness.test.ts +379 -0
  38. package/src/lib/core/testFreshness.ts +232 -0
  39. package/src/lib/core/testStrictMode.ts +9 -0
  40. package/src/lib/framework/start.test.ts +4 -3
  41. package/src/lib/framework/start.ts +2 -0
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import '@cabloy/json5';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
3
  import { combineApiPathControllerAndActionRaw, combineApiPathControllerAndAction, combineApiPath, combineResourceName, forEach, forEachSync, sleep, isUndefined, isClass, isNilOrEmptyString, evaluateExpressions, catchErrorSync, isEmptyObject, catchError, hashkey } from '@cabloy/utils';
3
4
  import { toLowerCaseFirstChar, splitWords } from '@cabloy/word-utils';
4
5
  import path from 'node:path';
@@ -12,7 +13,6 @@ import { pathToFileURL, fileURLToPath } from 'node:url';
12
13
  import * as uuid from 'uuid';
13
14
  import { getPlatformArch as getPlatformArch$1 } from 'vona';
14
15
  import os from 'node:os';
15
- import { AsyncLocalStorage } from 'node:async_hooks';
16
16
  import { isDeepStrictEqual } from 'node:util';
17
17
  import { setLocaleErrors, setLocaleAdapter, translateError } from '@cabloy/zod-errors-custom';
18
18
  import { ZodMetadata } from '@cabloy/zod-openapi';
@@ -27,7 +27,7 @@ import * as Winston from 'winston';
27
27
  import DailyRotateFile from 'winston-daily-rotate-file';
28
28
  import { MESSAGE, LEVEL } from 'triple-beam';
29
29
  import { createServer } from 'node:http';
30
- import { mkdir, open, rename, unlink, lstat } from 'node:fs/promises';
30
+ import { mkdir, open, rename, unlink, lstat, readdir } from 'node:fs/promises';
31
31
  import { execFileSync, execFile } from 'node:child_process';
32
32
  import cluster from 'node:cluster';
33
33
  import React from 'react';
@@ -36,6 +36,179 @@ import { z } from 'zod';
36
36
  import * as Retry from 'retry';
37
37
  export { Retry };
38
38
 
39
+ const boundaries = new WeakMap();
40
+ const boundaryStorage = new AsyncLocalStorage();
41
+ function createRequestBoundary(options) {
42
+ const onFailure = options?.onFailure;
43
+ const boundary = Object.freeze({});
44
+ let portFault = false;
45
+ let reporting = false;
46
+ const reported = new Set();
47
+ const capture = Object.freeze({
48
+ diagnostics: 'suppress',
49
+ reportFailure(failure) {
50
+ if (failure !== 'observer_failed' && failure !== 'diagnostic_failed' && failure !== 'late_callback_failed') {
51
+ portFault = true;
52
+ return;
53
+ }
54
+ portFault = true;
55
+ if (reported.has(failure)) return;
56
+ reported.add(failure);
57
+ if (reporting || !onFailure) return;
58
+ reporting = true;
59
+ try {
60
+ const result = onFailure(failure);
61
+ if (result && (typeof result === 'object' || typeof result === 'function')) {
62
+ const then = result.then;
63
+ if (typeof then === 'function') {
64
+ portFault = true;
65
+ void new Promise((resolve, reject) => {
66
+ then.call(result, resolve, reject);
67
+ }).catch(() => {});
68
+ }
69
+ }
70
+ } catch {
71
+ portFault = true;
72
+ } finally {
73
+ reporting = false;
74
+ }
75
+ },
76
+ snapshot() {
77
+ return Object.freeze({
78
+ portFault
79
+ });
80
+ }
81
+ });
82
+ boundaries.set(boundary, capture);
83
+ return boundary;
84
+ }
85
+ function runWithRequestBoundary(boundary, continuation) {
86
+ if (!boundaries.has(boundary)) throw new Error('Invalid request boundary');
87
+ const current = boundaryStorage.getStore();
88
+ if (current && current !== boundary) throw new Error('Request boundary cannot be replaced');
89
+ if (current === boundary) return continuation();
90
+ return boundaryStorage.run(boundary, continuation);
91
+ }
92
+ function getRequestBoundary() {
93
+ return boundaryStorage.getStore();
94
+ }
95
+ function captureRequestBoundary() {
96
+ const boundary = boundaryStorage.getStore();
97
+ return boundary ? boundaries.get(boundary) : undefined;
98
+ }
99
+
100
+ const loggingMethods = new Set(['log', 'error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly']);
101
+ function completeWrite(args) {
102
+ const callback = typeof args[1] === 'function' ? args[1] : args[2];
103
+ if (typeof callback === 'function') {
104
+ try {
105
+ callback();
106
+ } catch {
107
+ captureRequestBoundary()?.reportFailure('diagnostic_failed');
108
+ }
109
+ }
110
+ return true;
111
+ }
112
+ const suppressedLogger = createFacade();
113
+ function getSuppressedDiagnosticLogger() {
114
+ return suppressedLogger;
115
+ }
116
+ function createDiagnosticLogger(raw) {
117
+ if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
118
+ return createFacade(raw);
119
+ }
120
+ function createFacade(raw) {
121
+ const methods = new Map();
122
+ const handlers = new WeakMap();
123
+ const inertProfiler = Object.freeze({
124
+ get logger() {
125
+ return getSuppressedDiagnosticLogger();
126
+ },
127
+ done: () => getSuppressedDiagnosticLogger()
128
+ });
129
+ const facade = new Proxy({}, {
130
+ get(_target, key) {
131
+ if (loggingMethods.has(key) || ['write', 'child', 'startTimer', 'profile'].includes(key)) {
132
+ if (!methods.has(key)) {
133
+ methods.set(key, (...args) => {
134
+ if (!raw || getRequestBoundary()) {
135
+ if (key === 'write') return completeWrite(args);
136
+ if (key === 'child') return getSuppressedDiagnosticLogger();
137
+ if (key === 'startTimer') return inertProfiler;
138
+ return facade;
139
+ }
140
+ const result = Reflect.apply(Reflect.get(raw, key, raw), raw, args);
141
+ if (key === 'child') return createFacade(result);
142
+ if (key === 'startTimer') return wrapProfiler(result, facade);
143
+ return result === raw ? facade : result;
144
+ });
145
+ }
146
+ return methods.get(key);
147
+ }
148
+ if (!raw) return undefined;
149
+ const value = Reflect.get(raw, key, raw);
150
+ if ((key === 'exceptions' || key === 'rejections') && value) {
151
+ if (!handlers.has(value)) handlers.set(value, wrapHandler(value, raw, facade));
152
+ return handlers.get(value);
153
+ }
154
+ if (typeof value !== 'function') return value;
155
+ if (!methods.has(key)) {
156
+ methods.set(key, (...args) => {
157
+ const result = Reflect.apply(value, raw, args);
158
+ return result === raw ? facade : result;
159
+ });
160
+ }
161
+ return methods.get(key);
162
+ },
163
+ set(_target, key, value) {
164
+ return raw ? Reflect.set(raw, key, value, raw) : true;
165
+ }
166
+ });
167
+ return facade;
168
+ }
169
+ function wrapHandler(raw, rawLogger, logger) {
170
+ const methods = new Map();
171
+ const handler = new Proxy({}, {
172
+ get(_target, key) {
173
+ if (key === 'logger') return logger;
174
+ const value = Reflect.get(raw, key, raw);
175
+ if (typeof value !== 'function') return value;
176
+ if (!methods.has(key)) {
177
+ methods.set(key, (...args) => {
178
+ const result = Reflect.apply(value, raw, args);
179
+ if (result === raw) return handler;
180
+ return result === rawLogger ? logger : result;
181
+ });
182
+ }
183
+ return methods.get(key);
184
+ },
185
+ set(_target, key, value) {
186
+ if (key === 'logger') return false;
187
+ return Reflect.set(raw, key, value, raw);
188
+ }
189
+ });
190
+ return handler;
191
+ }
192
+ function wrapProfiler(raw, logger) {
193
+ return new Proxy({}, {
194
+ get(_target, key) {
195
+ if (key === 'logger') return logger;
196
+ if (key === 'done') {
197
+ return (...args) => {
198
+ if (getRequestBoundary()) return logger;
199
+ const result = Reflect.apply(raw.done, raw, args);
200
+ return result === raw.logger ? logger : result;
201
+ };
202
+ }
203
+ return Reflect.get(raw, key, raw);
204
+ },
205
+ set(_target, key, value) {
206
+ if (key === 'logger') return false;
207
+ return Reflect.set(raw, key, value, raw);
208
+ }
209
+ });
210
+ }
211
+
39
212
  function cast(source) {
40
213
  return source;
41
214
  }
@@ -1185,6 +1358,7 @@ class BeanBase extends BeanBaseSimple {
1185
1358
  return this.$loggerClient('default');
1186
1359
  }
1187
1360
  $loggerClient(clientName = 'default') {
1361
+ if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
1188
1362
  if (!this[SymbolLogger][clientName]) {
1189
1363
  this[SymbolLogger][clientName] = this.app.meta.logger.get(clientName).child({
1190
1364
  beanFullName: this.$beanFullName
@@ -1193,6 +1367,7 @@ class BeanBase extends BeanBaseSimple {
1193
1367
  return this[SymbolLogger][clientName];
1194
1368
  }
1195
1369
  $loggerChild(childName, clientName = 'default') {
1370
+ if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
1196
1371
  if (!this[SymbolLoggerChildren][clientName]) this[SymbolLoggerChildren][clientName] = {};
1197
1372
  if (!this[SymbolLoggerChildren][clientName][childName]) {
1198
1373
  this[SymbolLoggerChildren][clientName][childName] = this.app.meta.logger.get(clientName).child({
@@ -2456,21 +2631,72 @@ class VonaAsyncLocalStorage extends AsyncLocalStorage {
2456
2631
  constructor(app) {
2457
2632
  super();
2458
2633
  this.app = void 0;
2634
+ this.observedRuns = new WeakMap();
2459
2635
  this.app = app;
2460
2636
  }
2461
2637
  async run(store, callback, ...args) {
2462
2638
  if (store === this.app.currentContext) {
2463
2639
  return await callback(...args);
2464
2640
  }
2465
- return super.run(store, async (...args) => {
2641
+ if (this.observedRuns.has(store)) throw new Error('Context scope already owned');
2642
+ return this._runOwned(store, () => callback(...args));
2643
+ }
2644
+ async runObserved(store, callback, observer) {
2645
+ if (store === this.app.currentContext || this.observedRuns.has(store)) {
2646
+ throw new Error('Context scope already owned');
2647
+ }
2648
+ const frame = {
2649
+ settled: false
2650
+ };
2651
+ this.observedRuns.set(store, frame);
2652
+ try {
2653
+ return await this._runOwned(store, callback, observer);
2654
+ } finally {
2655
+ frame.settled = true;
2656
+ }
2657
+ }
2658
+ _runOwned(store, callback, observer) {
2659
+ const capture = captureRequestBoundary();
2660
+ return super.run(store, async () => {
2466
2661
  try {
2467
2662
  this.app.meta.ctxCounter.increment();
2468
- return await callback(...args);
2663
+ return await callback();
2469
2664
  } finally {
2470
- await store.bean.dispose();
2471
- this.app.meta.ctxCounter.decrement();
2665
+ await this._disposeOwned(store, observer, capture);
2472
2666
  }
2473
- }, ...args);
2667
+ });
2668
+ }
2669
+ async _disposeOwned(store, observer, capture) {
2670
+ try {
2671
+ await store.bean.dispose();
2672
+ } catch (error) {
2673
+ this._notifyScopeOutcome(observer, 'dispose_failed', capture);
2674
+ throw error;
2675
+ }
2676
+ try {
2677
+ this.app.meta.ctxCounter.decrement();
2678
+ } catch (error) {
2679
+ capture?.reportFailure('diagnostic_failed');
2680
+ throw error;
2681
+ }
2682
+ this._notifyScopeOutcome(observer, 'disposed', capture);
2683
+ }
2684
+ _notifyScopeOutcome(observer, outcome, capture) {
2685
+ if (!observer) return;
2686
+ try {
2687
+ const result = observer(outcome);
2688
+ if (result && (typeof result === 'object' || typeof result === 'function')) {
2689
+ const then = result.then;
2690
+ if (typeof then === 'function') {
2691
+ capture?.reportFailure('observer_failed');
2692
+ void new Promise((resolve, reject) => {
2693
+ then.call(result, resolve, reject);
2694
+ }).catch(() => {});
2695
+ }
2696
+ }
2697
+ } catch {
2698
+ capture?.reportFailure('observer_failed');
2699
+ }
2474
2700
  }
2475
2701
  }
2476
2702
 
@@ -2646,26 +2872,45 @@ class AppHmr extends BeanSimple {
2646
2872
  }
2647
2873
  }
2648
2874
 
2875
+ const TEST_REQUIRE_FRESH_RESOURCES_ENV = 'VONA_TEST_REQUIRE_FRESH_RESOURCES';
2876
+ function requiresFreshTestResources(env = process.env) {
2877
+ const value = env[TEST_REQUIRE_FRESH_RESOURCES_ENV];
2878
+ if (value !== undefined && value !== 'true') {
2879
+ throw new Error('Invalid strict test resource freshness flag');
2880
+ }
2881
+ return value === 'true';
2882
+ }
2883
+
2649
2884
  const SymbolLoggerInstances = Symbol('SymbolLoggerInstances');
2885
+ const SymbolRawLoggerInstances = Symbol('SymbolRawLoggerInstances');
2650
2886
  class AppLogger extends BeanSimple {
2651
2887
  constructor(...args) {
2652
2888
  super(...args);
2653
2889
  this[SymbolLoggerInstances] = {};
2890
+ this[SymbolRawLoggerInstances] = {};
2654
2891
  }
2655
2892
  async dispose() {
2656
- for (const key in this[SymbolLoggerInstances]) {
2657
- const logger = this[SymbolLoggerInstances][key];
2893
+ for (const key in this[SymbolRawLoggerInstances]) {
2894
+ const logger = this[SymbolRawLoggerInstances][key];
2658
2895
  await _closeLogger(logger);
2659
2896
  }
2660
2897
  }
2661
2898
  get(clientName) {
2899
+ if (requiresFreshTestResources() || getRequestBoundary()) {
2900
+ return getSuppressedDiagnosticLogger();
2901
+ }
2662
2902
  clientName = clientName || 'default';
2663
2903
  if (!this[SymbolLoggerInstances][clientName]) {
2664
- this[SymbolLoggerInstances][clientName] = this._createClient(clientName);
2904
+ const raw = this._createClient(clientName);
2905
+ this[SymbolRawLoggerInstances][clientName] = raw;
2906
+ this[SymbolLoggerInstances][clientName] = createDiagnosticLogger(raw);
2665
2907
  }
2666
2908
  return this[SymbolLoggerInstances][clientName];
2667
2909
  }
2668
2910
  child(childName, clientName) {
2911
+ if (requiresFreshTestResources() || getRequestBoundary()) {
2912
+ return getSuppressedDiagnosticLogger();
2913
+ }
2669
2914
  const logger = this.get(clientName);
2670
2915
  if (!childName) return logger;
2671
2916
  return logger.child({
@@ -2721,6 +2966,9 @@ class AppLogger extends BeanSimple {
2721
2966
  }, Winston);
2722
2967
  }
2723
2968
  createTransportFile(fileName, clientInfo, options) {
2969
+ if (requiresFreshTestResources()) {
2970
+ throw new Error('Strict test file logger transport is disabled');
2971
+ }
2724
2972
  const dirname = this.app.config.logger.baseDir;
2725
2973
  if (!fse.existsSync(dirname)) {
2726
2974
  const [_, err] = catchErrorSync(() => {
@@ -3081,10 +3329,16 @@ var localesDefault = {
3081
3329
  };
3082
3330
 
3083
3331
  const SymbolLoggerMessage = Symbol('SymbolLoggerMessage');
3084
- const formatLoggerAxiosError = Winston.format((einfo, {
3085
- stack,
3086
- cause
3087
- }) => {
3332
+ const formatLoggerBoundary = Winston.format(info => {
3333
+ if (getRequestBoundary()) return false;
3334
+ return info;
3335
+ });
3336
+ const formatLoggerAxiosError = Winston.format((einfo, opts) => {
3337
+ if (getRequestBoundary()) return false;
3338
+ const {
3339
+ stack,
3340
+ cause
3341
+ } = opts;
3088
3342
  if (einfo instanceof Error && einfo.constructor.name.includes('AxiosError') || einfo.name === 'AxiosError' || einfo.isAxiosError) {
3089
3343
  const info = Object.assign({}, einfo, {
3090
3344
  level: einfo.level,
@@ -3104,9 +3358,11 @@ const formatLoggerAxiosError = Winston.format((einfo, {
3104
3358
  return einfo;
3105
3359
  });
3106
3360
  const formatLoggerErrors = opts => {
3107
- return Winston.format.combine(formatLoggerAxiosError(opts), Winston.format.errors(opts));
3361
+ if (getRequestBoundary()) return formatLoggerBoundary();
3362
+ return Winston.format.combine(formatLoggerBoundary(), formatLoggerAxiosError(opts), Winston.format.errors(opts));
3108
3363
  };
3109
3364
  const formatLoggerCtx = Winston.format((info, _opts) => {
3365
+ if (getRequestBoundary()) return false;
3110
3366
  const app = useApp();
3111
3367
  if (!app.ctx) return info;
3112
3368
  if (app.ctx.method) info.method = app.ctx.method;
@@ -3123,11 +3379,13 @@ const formatLoggerCtx = Winston.format((info, _opts) => {
3123
3379
  return info;
3124
3380
  });
3125
3381
  const formatLoggerDummy = Winston.format(info => {
3382
+ if (getRequestBoundary()) return false;
3126
3383
  const app = useApp();
3127
3384
  if (app.meta.env.LOGGER_DUMMY === 'true') return false;
3128
3385
  return info;
3129
3386
  });
3130
3387
  const formatLoggerFilter = Winston.format((info, opts) => {
3388
+ if (getRequestBoundary()) return false;
3131
3389
  // check client
3132
3390
  const child = typeof opts.child === 'function' ? opts.child() : opts.child;
3133
3391
  if (child && !child.includes(info.name)) {
@@ -3147,16 +3405,18 @@ const formatLoggerFilter = Winston.format((info, opts) => {
3147
3405
  return false;
3148
3406
  });
3149
3407
  const formatLoggerConsole = clientInfo => {
3150
- return Winston.format.printf(({
3151
- timestamp,
3152
- level,
3153
- stack,
3154
- message,
3155
- name,
3156
- beanFullName,
3157
- durationMs,
3158
- ...meta
3159
- }) => {
3408
+ return Winston.format.combine(formatLoggerBoundary(), Winston.format.printf(info => {
3409
+ if (getRequestBoundary()) return '';
3410
+ const {
3411
+ timestamp,
3412
+ level,
3413
+ stack,
3414
+ message,
3415
+ name,
3416
+ beanFullName,
3417
+ durationMs,
3418
+ ...meta
3419
+ } = info;
3160
3420
  const textClientName = clientInfo.clientName === 'default' ? '' : ` ${chalk.cyan(`[${clientInfo.clientName}]`)}`;
3161
3421
  const textName = name ? ` ${chalk.cyan(`[${name}]`)}` : '';
3162
3422
  const textBeanFullName = beanFullName ? ` ${chalk.gray(`[${beanFullName}]`)}` : '';
@@ -3165,9 +3425,10 @@ const formatLoggerConsole = clientInfo => {
3165
3425
  const textDurationMs = durationMs !== undefined ? ` ${chalk.cyan(`+${durationMs}ms`)}` : '';
3166
3426
  const textStack = stack ? `\n${stack}` : '';
3167
3427
  return `${timestamp} ${level}${textClientName}${textName}${textBeanFullName}${textMeta}${textMessage}${textDurationMs}${textStack}`;
3168
- });
3428
+ }));
3169
3429
  };
3170
3430
  function __formatLoggerFilterCheckInfo(info) {
3431
+ if (getRequestBoundary()) return false;
3171
3432
  if (typeof info.message === 'function') {
3172
3433
  if (info.message[SymbolLoggerMessage] === undefined) {
3173
3434
  info.message[SymbolLoggerMessage] = info.message();
@@ -4237,6 +4498,156 @@ function assertTestClaimRedisClient(app, clientName, options) {
4237
4498
  if (plan.kind === 'client' ? options.keyPrefix !== plan.prefix : options.keyPrefix !== undefined) throw new Error('Unregistered managed Redis key prefix');
4238
4499
  }
4239
4500
 
4501
+ const freshApps = new WeakSet();
4502
+ function assertFreshTestResourcesReady(app) {
4503
+ if (!requiresFreshTestResources()) return;
4504
+ if (!freshApps.has(app)) throw new Error('Strict test resource freshness was not established');
4505
+ }
4506
+ async function assertEmptyDirectory(directory) {
4507
+ assertIsolatedTestPath(directory, directory);
4508
+ try {
4509
+ if (!(await lstat(directory)).isDirectory() || (await readdir(directory)).length !== 0) {
4510
+ throw new Error('Strict test local resource is not fresh');
4511
+ }
4512
+ } catch (error) {
4513
+ if (error.code !== 'ENOENT') throw error;
4514
+ }
4515
+ }
4516
+
4517
+ // Runs after configuration, before module loading (including worker-alive Redis writes).
4518
+ // This is a freshness snapshot, not a lease against independent external writers.
4519
+ async function assertFreshTestResources(app, dependencies) {
4520
+ if (!requiresFreshTestResources()) return;
4521
+ if (!app.meta.env.VONA_TEST_RUN_ID || app.meta.appReady || freshApps.has(app)) {
4522
+ throw new Error('Strict test freshness requires a new isolated startup');
4523
+ }
4524
+ assertIsolatedTestConfig(app);
4525
+ const config = app.config;
4526
+ const paths = resolveVonaLocalPaths(app.projectPath, 'test', app.meta.env.VONA_TEST_RUN_ID);
4527
+ for (const directory of [paths.publicRoot, paths.runtimeRoot, paths.sqliteRoot]) {
4528
+ await assertEmptyDirectory(directory);
4529
+ }
4530
+ const inventory = buildTestClaimResourceInventory(app);
4531
+ const readers = dependencies ?? createFreshnessDependencies(app.projectPath);
4532
+ try {
4533
+ for (const plan of inventory.databases) {
4534
+ if (plan.endpoint.dialect === 'better-sqlite3') continue;
4535
+ const options = mergeTestClaimDatabaseOptions(config.database.base, undefined, config.database.clients[plan.clientName]);
4536
+ if (await readers.databaseExists(plan, options)) {
4537
+ throw new Error('Preexisting strict test database namespace');
4538
+ }
4539
+ }
4540
+ for (const resource of inventory.redis) {
4541
+ const options = mergeTestClaimRedisOptions(config.redis.base, config.redis.clients[resource.clientName]);
4542
+ if (await readers.redisExists(resource, options)) {
4543
+ throw new Error('Preexisting strict test Redis namespace');
4544
+ }
4545
+ }
4546
+ } catch {
4547
+ // Driver messages may embed credentials, database names or Redis keys.
4548
+ throw new Error('Strict test resource freshness check failed');
4549
+ }
4550
+ freshApps.add(app);
4551
+ }
4552
+ async function hasFreshnessDatabase(connection, plan) {
4553
+ try {
4554
+ const pg = plan.endpoint.dialect === 'pg';
4555
+ if (!pg && !['mysql', 'mysql2'].includes(plan.endpoint.dialect)) {
4556
+ throw new Error('Unsupported freshness database dialect');
4557
+ }
4558
+ const result = await connection.raw(pg ? 'select datname from pg_database where left(datname, ?) = ? limit 1' : 'select schema_name from information_schema.schemata where left(schema_name, ?) = ? limit 1', [plan.prefix.length, plan.prefix]);
4559
+ const rows = pg ? result?.rows : result?.[0];
4560
+ if (!Array.isArray(rows)) throw new Error('Invalid freshness database response');
4561
+ return rows.length !== 0;
4562
+ } finally {
4563
+ await connection.destroy();
4564
+ }
4565
+ }
4566
+ async function hasFreshnessRedis(connection, prefix, maxScans = 10000) {
4567
+ if (!prefix || !Number.isSafeInteger(maxScans) || maxScans < 1) {
4568
+ throw new Error('Invalid Redis freshness bounds');
4569
+ }
4570
+ const pattern = `${prefix.replace(/[\\*?[\]]/g, '\\$&')}*`;
4571
+ let cursor = '0';
4572
+ for (let scans = 0; scans < maxScans; scans++) {
4573
+ const result = await connection.scan(cursor, 'MATCH', pattern, 'COUNT', 128);
4574
+ if (!Array.isArray(result) || result.length !== 2 || typeof result[0] !== 'string' || !/^\d+$/.test(result[0]) || !Array.isArray(result[1]) || result[1].some(key => typeof key !== 'string' || !key.startsWith(prefix))) {
4575
+ throw new Error('Invalid Redis freshness response');
4576
+ }
4577
+ if (result[1].length !== 0) return true;
4578
+ cursor = result[0];
4579
+ if (cursor === '0') return false;
4580
+ }
4581
+ throw new Error('Redis freshness scan limit exceeded');
4582
+ }
4583
+ function createFreshnessDependencies(projectPath, load = createRequire(path.join(projectPath, 'package.json'))) {
4584
+ return {
4585
+ async databaseExists(plan, options) {
4586
+ const endpoint = plan.endpoint;
4587
+ const source = options.connection;
4588
+ const target = {
4589
+ user: source.user,
4590
+ password: source.password,
4591
+ ssl: source.ssl,
4592
+ database: endpoint.adminDatabase,
4593
+ connectionTimeoutMillis: 10000,
4594
+ connectTimeout: 10000
4595
+ };
4596
+ if (endpoint.transport === 'tcp') {
4597
+ target.host = endpoint.host;
4598
+ target.port = endpoint.port;
4599
+ } else if (endpoint.dialect === 'pg') {
4600
+ target.host = endpoint.socketPath;
4601
+ target.port = endpoint.port;
4602
+ } else {
4603
+ target.socketPath = endpoint.socketPath;
4604
+ }
4605
+ const knex = load('knex');
4606
+ return await hasFreshnessDatabase(knex({
4607
+ client: endpoint.dialect,
4608
+ connection: target,
4609
+ pool: {
4610
+ min: 0,
4611
+ max: 1
4612
+ },
4613
+ acquireConnectionTimeout: 10000
4614
+ }), plan);
4615
+ },
4616
+ async redisExists(resource, options) {
4617
+ const endpoint = resource.endpoint;
4618
+ const target = endpoint.transport === 'socket' ? {
4619
+ path: endpoint.socketPath
4620
+ } : {
4621
+ host: endpoint.host,
4622
+ port: endpoint.port
4623
+ };
4624
+ const Redis = load('ioredis').Redis;
4625
+ const connection = new Redis({
4626
+ ...target,
4627
+ username: options.username,
4628
+ password: options.password,
4629
+ tls: options.tls,
4630
+ db: endpoint.db,
4631
+ keyPrefix: undefined,
4632
+ lazyConnect: true,
4633
+ connectTimeout: 10000,
4634
+ commandTimeout: 10000,
4635
+ retryStrategy: () => null,
4636
+ maxRetriesPerRequest: 0,
4637
+ enableOfflineQueue: false
4638
+ });
4639
+ connection.on('error', () => {});
4640
+ try {
4641
+ await connection.connect();
4642
+ const prefix = resource.kind === 'queue' ? resource.prefix.slice(0, -'queue:'.length) : resource.prefix;
4643
+ return await hasFreshnessRedis(connection, prefix);
4644
+ } finally {
4645
+ connection.disconnect();
4646
+ }
4647
+ }
4648
+ };
4649
+ }
4650
+
4240
4651
  function jsxEnhance() {
4241
4652
  const createElementOriginal = React.createElement;
4242
4653
  React.createElement = function (...args) {
@@ -4456,6 +4867,7 @@ class Start {
4456
4867
  const app = this.app;
4457
4868
  try {
4458
4869
  await this._start_appConfig();
4870
+ await assertFreshTestResources(app);
4459
4871
  await this._start_appLoad();
4460
4872
  await this._start_appStart();
4461
4873
  await this._start_appReady();
@@ -4845,5 +5257,5 @@ async function retry(options, fn) {
4845
5257
 
4846
5258
  zodExtendOpenApi();
4847
5259
 
4848
- export { $Class, $localeScope, $makeLocaleMagic, $protocolKey, AppHmr, AppHmrDeps, AppLocale, AppLogger, AppMeta, AppMetadata, AppResource, AppUtil, BeanAopBase, BeanAopMethodBase, BeanBase, BeanBaseSimple, BeanContainer, BeanInfo, BeanScopeBase, BeanScopeContainer, BeanScopeError, BeanScopeErrorImpl, BeanScopeLocale, BeanScopeScene, BeanScopeUtil, BeanSimple, EnumAppEvent, ErrorClass, Global, LocaleModuleNameSeparator, PickClassInner, ProxyDisable, SymbolBeanContainerInstances, SymbolBeanFullName, SymbolBeanInstanceKey, SymbolBeanInstancePropsLazy, SymbolCacheAopChains, SymbolCacheAopChainsKey, SymbolDecoratorBeanFullName, SymbolDecoratorBeanInfo, SymbolDecoratorGlobal, SymbolDecoratorProxyDisable, SymbolDecoratorUse, SymbolDecoratorVirtual, SymbolHmrStateLoad, SymbolHmrStateSave, SymbolMappedClassMetadataKeys, SymbolModuleBelong, SymbolModuleName, TEST_CLAIM_DIRECTORY, TEST_CLAIM_TOKEN_ENV, TEST_CLAIM_URL_ENV, TEST_CLAIM_WINDOWS_GUARDIAN_ENV, TEST_RUN_ID_ENV, TEST_RUN_OWNER_FILE, Use, Virtual, VonaApplication, __prepareInjectSelectorInfo, appHmrDeps, appMetadata, appResource, assertDisposableDatabaseName, assertIsolatedTestConfig, assertIsolatedTestDatabaseClient, assertIsolatedTestPath, assertTestAppIdentity, assertTestClaimDatabaseClient, assertTestClaimRedisClient, assertTestClaimSafePath, assertTestClaimWindowsJob, assertTestRunOwner, beanFullNameFromOnionName, bootstrap, buildTestClaimResourceInventory, cast, closeApp, closeProcessWorker, combineConfigDefault, combineFilePathSafe, compose, copyAddon, copyMetadataOfClasses, copyProperties, copyPropertiesOfClasses, createApp, createAppMaster, createBeanDecorator, createGeneralApp, createHash, createTestClaim, deepExtend, disposeInstance, errorsInternal, filterHeaders, formatLoggerAxiosError, formatLoggerConsole, formatLoggerCtx, formatLoggerDummy, formatLoggerErrors, formatLoggerFilter, functionNoop, getDatabaseTestPrefix, getHomeVonaAppDir, getLoggerPathPhysicalRoot, getMappedClassMetadataKeys, getPlatformArch, getPublicPathPhysicalRoot, getRuntimePathPhysicalRoot, getSqlite3DatabaseNameDefault, getTestAppName, getTestClaimCreatedAt, getTestClaimPaths, getTestClaimRedisPrefix, getTestRunIdFromEnv, hasTestClaimTransport, instanceDesp, isLocaleMagic, loadJSONFile, loadTestClaim, mergeTestClaimDatabaseOptions, mergeTestClaimRedisOptions, normalizeTestClaimDatabaseEndpoint, normalizeTestClaimRedisEndpoint, onionNameFromBeanFullName, pathToHref, polyfillDispose, prepareEnv, registerMappedClassMetadataKey, registerTestClaimDatabaseCreation, registerTestClaimResourceInventory, registerTestClaimWindowsJobVerifier, requireAddon, requireDynamic, resolveAddon, resolveTestRunId, resolveVonaLocalPaths, resolverSharp, resolverSharpLibvips, resolverSqlite3, retry, saveJSONFile, sendTestClaimEvent, setMappedClassMetadataKeys, startTestClaimBroker, text$1 as text, useApp, usePrepareArg, usePrepareArgs, uuidv4, validateTestClaim, withIsolatedTestDatabaseDiscovery, writeTestClaim };
5260
+ export { $Class, $localeScope, $makeLocaleMagic, $protocolKey, AppHmr, AppHmrDeps, AppLocale, AppLogger, AppMeta, AppMetadata, AppResource, AppUtil, BeanAopBase, BeanAopMethodBase, BeanBase, BeanBaseSimple, BeanContainer, BeanInfo, BeanScopeBase, BeanScopeContainer, BeanScopeError, BeanScopeErrorImpl, BeanScopeLocale, BeanScopeScene, BeanScopeUtil, BeanSimple, EnumAppEvent, ErrorClass, Global, LocaleModuleNameSeparator, PickClassInner, ProxyDisable, SymbolBeanContainerInstances, SymbolBeanFullName, SymbolBeanInstanceKey, SymbolBeanInstancePropsLazy, SymbolCacheAopChains, SymbolCacheAopChainsKey, SymbolDecoratorBeanFullName, SymbolDecoratorBeanInfo, SymbolDecoratorGlobal, SymbolDecoratorProxyDisable, SymbolDecoratorUse, SymbolDecoratorVirtual, SymbolHmrStateLoad, SymbolHmrStateSave, SymbolMappedClassMetadataKeys, SymbolModuleBelong, SymbolModuleName, TEST_CLAIM_DIRECTORY, TEST_CLAIM_TOKEN_ENV, TEST_CLAIM_URL_ENV, TEST_CLAIM_WINDOWS_GUARDIAN_ENV, TEST_REQUIRE_FRESH_RESOURCES_ENV, TEST_RUN_ID_ENV, TEST_RUN_OWNER_FILE, Use, Virtual, VonaApplication, __prepareInjectSelectorInfo, appHmrDeps, appMetadata, appResource, assertDisposableDatabaseName, assertFreshTestResources, assertFreshTestResourcesReady, assertIsolatedTestConfig, assertIsolatedTestDatabaseClient, assertIsolatedTestPath, assertTestAppIdentity, assertTestClaimDatabaseClient, assertTestClaimRedisClient, assertTestClaimSafePath, assertTestClaimWindowsJob, assertTestRunOwner, beanFullNameFromOnionName, bootstrap, buildTestClaimResourceInventory, captureRequestBoundary, cast, closeApp, closeProcessWorker, combineConfigDefault, combineFilePathSafe, compose, copyAddon, copyMetadataOfClasses, copyProperties, copyPropertiesOfClasses, createApp, createAppMaster, createBeanDecorator, createDiagnosticLogger, createFreshnessDependencies, createGeneralApp, createHash, createRequestBoundary, createTestClaim, deepExtend, disposeInstance, errorsInternal, filterHeaders, formatLoggerAxiosError, formatLoggerBoundary, formatLoggerConsole, formatLoggerCtx, formatLoggerDummy, formatLoggerErrors, formatLoggerFilter, functionNoop, getDatabaseTestPrefix, getHomeVonaAppDir, getLoggerPathPhysicalRoot, getMappedClassMetadataKeys, getPlatformArch, getPublicPathPhysicalRoot, getRequestBoundary, getRuntimePathPhysicalRoot, getSqlite3DatabaseNameDefault, getSuppressedDiagnosticLogger, getTestAppName, getTestClaimCreatedAt, getTestClaimPaths, getTestClaimRedisPrefix, getTestRunIdFromEnv, hasFreshnessDatabase, hasFreshnessRedis, hasTestClaimTransport, instanceDesp, isLocaleMagic, loadJSONFile, loadTestClaim, mergeTestClaimDatabaseOptions, mergeTestClaimRedisOptions, normalizeTestClaimDatabaseEndpoint, normalizeTestClaimRedisEndpoint, onionNameFromBeanFullName, pathToHref, polyfillDispose, prepareEnv, registerMappedClassMetadataKey, registerTestClaimDatabaseCreation, registerTestClaimResourceInventory, registerTestClaimWindowsJobVerifier, requireAddon, requireDynamic, requiresFreshTestResources, resolveAddon, resolveTestRunId, resolveVonaLocalPaths, resolverSharp, resolverSharpLibvips, resolverSqlite3, retry, runWithRequestBoundary, saveJSONFile, sendTestClaimEvent, setMappedClassMetadataKeys, startTestClaimBroker, text$1 as text, useApp, usePrepareArg, usePrepareArgs, uuidv4, validateTestClaim, withIsolatedTestDatabaseDiscovery, writeTestClaim };
4849
5261
  //# sourceMappingURL=index.js.map