vona-core 5.1.47 → 5.1.48

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 (38) hide show
  1. package/dist/index.js +431 -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 +21 -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/framework/start.d.ts.map +1 -1
  25. package/package.json +2 -2
  26. package/src/lib/bean/beanBase.ts +4 -0
  27. package/src/lib/core/asyncLocalStorage.ts +84 -12
  28. package/src/lib/core/index.ts +14 -0
  29. package/src/lib/core/logger/appLogger.ts +11 -3
  30. package/src/lib/core/logger/diagnosticLogger.test.ts +146 -0
  31. package/src/lib/core/logger/diagnosticLogger.ts +138 -0
  32. package/src/lib/core/logger/index.ts +1 -0
  33. package/src/lib/core/logger/utils.ts +25 -5
  34. package/src/lib/core/requestBoundary.ts +89 -0
  35. package/src/lib/core/testFreshness.test.ts +379 -0
  36. package/src/lib/core/testFreshness.ts +238 -0
  37. package/src/lib/framework/start.test.ts +4 -3
  38. 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
 
@@ -2647,25 +2873,31 @@ class AppHmr extends BeanSimple {
2647
2873
  }
2648
2874
 
2649
2875
  const SymbolLoggerInstances = Symbol('SymbolLoggerInstances');
2876
+ const SymbolRawLoggerInstances = Symbol('SymbolRawLoggerInstances');
2650
2877
  class AppLogger extends BeanSimple {
2651
2878
  constructor(...args) {
2652
2879
  super(...args);
2653
2880
  this[SymbolLoggerInstances] = {};
2881
+ this[SymbolRawLoggerInstances] = {};
2654
2882
  }
2655
2883
  async dispose() {
2656
- for (const key in this[SymbolLoggerInstances]) {
2657
- const logger = this[SymbolLoggerInstances][key];
2884
+ for (const key in this[SymbolRawLoggerInstances]) {
2885
+ const logger = this[SymbolRawLoggerInstances][key];
2658
2886
  await _closeLogger(logger);
2659
2887
  }
2660
2888
  }
2661
2889
  get(clientName) {
2890
+ if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
2662
2891
  clientName = clientName || 'default';
2663
2892
  if (!this[SymbolLoggerInstances][clientName]) {
2664
- this[SymbolLoggerInstances][clientName] = this._createClient(clientName);
2893
+ const raw = this._createClient(clientName);
2894
+ this[SymbolRawLoggerInstances][clientName] = raw;
2895
+ this[SymbolLoggerInstances][clientName] = createDiagnosticLogger(raw);
2665
2896
  }
2666
2897
  return this[SymbolLoggerInstances][clientName];
2667
2898
  }
2668
2899
  child(childName, clientName) {
2900
+ if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
2669
2901
  const logger = this.get(clientName);
2670
2902
  if (!childName) return logger;
2671
2903
  return logger.child({
@@ -3081,10 +3313,16 @@ var localesDefault = {
3081
3313
  };
3082
3314
 
3083
3315
  const SymbolLoggerMessage = Symbol('SymbolLoggerMessage');
3084
- const formatLoggerAxiosError = Winston.format((einfo, {
3085
- stack,
3086
- cause
3087
- }) => {
3316
+ const formatLoggerBoundary = Winston.format(info => {
3317
+ if (getRequestBoundary()) return false;
3318
+ return info;
3319
+ });
3320
+ const formatLoggerAxiosError = Winston.format((einfo, opts) => {
3321
+ if (getRequestBoundary()) return false;
3322
+ const {
3323
+ stack,
3324
+ cause
3325
+ } = opts;
3088
3326
  if (einfo instanceof Error && einfo.constructor.name.includes('AxiosError') || einfo.name === 'AxiosError' || einfo.isAxiosError) {
3089
3327
  const info = Object.assign({}, einfo, {
3090
3328
  level: einfo.level,
@@ -3104,9 +3342,11 @@ const formatLoggerAxiosError = Winston.format((einfo, {
3104
3342
  return einfo;
3105
3343
  });
3106
3344
  const formatLoggerErrors = opts => {
3107
- return Winston.format.combine(formatLoggerAxiosError(opts), Winston.format.errors(opts));
3345
+ if (getRequestBoundary()) return formatLoggerBoundary();
3346
+ return Winston.format.combine(formatLoggerBoundary(), formatLoggerAxiosError(opts), Winston.format.errors(opts));
3108
3347
  };
3109
3348
  const formatLoggerCtx = Winston.format((info, _opts) => {
3349
+ if (getRequestBoundary()) return false;
3110
3350
  const app = useApp();
3111
3351
  if (!app.ctx) return info;
3112
3352
  if (app.ctx.method) info.method = app.ctx.method;
@@ -3123,11 +3363,13 @@ const formatLoggerCtx = Winston.format((info, _opts) => {
3123
3363
  return info;
3124
3364
  });
3125
3365
  const formatLoggerDummy = Winston.format(info => {
3366
+ if (getRequestBoundary()) return false;
3126
3367
  const app = useApp();
3127
3368
  if (app.meta.env.LOGGER_DUMMY === 'true') return false;
3128
3369
  return info;
3129
3370
  });
3130
3371
  const formatLoggerFilter = Winston.format((info, opts) => {
3372
+ if (getRequestBoundary()) return false;
3131
3373
  // check client
3132
3374
  const child = typeof opts.child === 'function' ? opts.child() : opts.child;
3133
3375
  if (child && !child.includes(info.name)) {
@@ -3147,16 +3389,18 @@ const formatLoggerFilter = Winston.format((info, opts) => {
3147
3389
  return false;
3148
3390
  });
3149
3391
  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
- }) => {
3392
+ return Winston.format.combine(formatLoggerBoundary(), Winston.format.printf(info => {
3393
+ if (getRequestBoundary()) return '';
3394
+ const {
3395
+ timestamp,
3396
+ level,
3397
+ stack,
3398
+ message,
3399
+ name,
3400
+ beanFullName,
3401
+ durationMs,
3402
+ ...meta
3403
+ } = info;
3160
3404
  const textClientName = clientInfo.clientName === 'default' ? '' : ` ${chalk.cyan(`[${clientInfo.clientName}]`)}`;
3161
3405
  const textName = name ? ` ${chalk.cyan(`[${name}]`)}` : '';
3162
3406
  const textBeanFullName = beanFullName ? ` ${chalk.gray(`[${beanFullName}]`)}` : '';
@@ -3165,9 +3409,10 @@ const formatLoggerConsole = clientInfo => {
3165
3409
  const textDurationMs = durationMs !== undefined ? ` ${chalk.cyan(`+${durationMs}ms`)}` : '';
3166
3410
  const textStack = stack ? `\n${stack}` : '';
3167
3411
  return `${timestamp} ${level}${textClientName}${textName}${textBeanFullName}${textMeta}${textMessage}${textDurationMs}${textStack}`;
3168
- });
3412
+ }));
3169
3413
  };
3170
3414
  function __formatLoggerFilterCheckInfo(info) {
3415
+ if (getRequestBoundary()) return false;
3171
3416
  if (typeof info.message === 'function') {
3172
3417
  if (info.message[SymbolLoggerMessage] === undefined) {
3173
3418
  info.message[SymbolLoggerMessage] = info.message();
@@ -4237,6 +4482,164 @@ function assertTestClaimRedisClient(app, clientName, options) {
4237
4482
  if (plan.kind === 'client' ? options.keyPrefix !== plan.prefix : options.keyPrefix !== undefined) throw new Error('Unregistered managed Redis key prefix');
4238
4483
  }
4239
4484
 
4485
+ const TEST_REQUIRE_FRESH_RESOURCES_ENV = 'VONA_TEST_REQUIRE_FRESH_RESOURCES';
4486
+ const freshApps = new WeakSet();
4487
+ function requiresFreshTestResources() {
4488
+ const value = process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV];
4489
+ if (value !== undefined && value !== 'true') {
4490
+ throw new Error('Invalid strict test resource freshness flag');
4491
+ }
4492
+ return value === 'true';
4493
+ }
4494
+ function assertFreshTestResourcesReady(app) {
4495
+ if (!requiresFreshTestResources()) return;
4496
+ if (!freshApps.has(app)) throw new Error('Strict test resource freshness was not established');
4497
+ }
4498
+ async function assertEmptyDirectory(directory) {
4499
+ assertIsolatedTestPath(directory, directory);
4500
+ try {
4501
+ if (!(await lstat(directory)).isDirectory() || (await readdir(directory)).length !== 0) {
4502
+ throw new Error('Strict test local resource is not fresh');
4503
+ }
4504
+ } catch (error) {
4505
+ if (error.code !== 'ENOENT') throw error;
4506
+ }
4507
+ }
4508
+
4509
+ // Runs after configuration, before module loading (including worker-alive Redis writes).
4510
+ // This is a freshness snapshot, not a lease against independent external writers.
4511
+ async function assertFreshTestResources(app, dependencies) {
4512
+ if (!requiresFreshTestResources()) return;
4513
+ if (!app.meta.env.VONA_TEST_RUN_ID || app.meta.appReady || freshApps.has(app)) {
4514
+ throw new Error('Strict test freshness requires a new isolated startup');
4515
+ }
4516
+ assertIsolatedTestConfig(app);
4517
+ const config = app.config;
4518
+ const paths = resolveVonaLocalPaths(app.projectPath, 'test', app.meta.env.VONA_TEST_RUN_ID);
4519
+ for (const directory of [paths.publicRoot, paths.runtimeRoot, paths.sqliteRoot]) {
4520
+ await assertEmptyDirectory(directory);
4521
+ }
4522
+ const inventory = buildTestClaimResourceInventory(app);
4523
+ const readers = dependencies ?? createFreshnessDependencies(app.projectPath);
4524
+ try {
4525
+ for (const plan of inventory.databases) {
4526
+ if (plan.endpoint.dialect === 'better-sqlite3') continue;
4527
+ const options = mergeTestClaimDatabaseOptions(config.database.base, undefined, config.database.clients[plan.clientName]);
4528
+ if (await readers.databaseExists(plan, options)) {
4529
+ throw new Error('Preexisting strict test database namespace');
4530
+ }
4531
+ }
4532
+ for (const resource of inventory.redis) {
4533
+ const options = mergeTestClaimRedisOptions(config.redis.base, config.redis.clients[resource.clientName]);
4534
+ if (await readers.redisExists(resource, options)) {
4535
+ throw new Error('Preexisting strict test Redis namespace');
4536
+ }
4537
+ }
4538
+ } catch {
4539
+ // Driver messages may embed credentials, database names or Redis keys.
4540
+ throw new Error('Strict test resource freshness check failed');
4541
+ }
4542
+ freshApps.add(app);
4543
+ }
4544
+ async function hasFreshnessDatabase(connection, plan) {
4545
+ try {
4546
+ const pg = plan.endpoint.dialect === 'pg';
4547
+ if (!pg && !['mysql', 'mysql2'].includes(plan.endpoint.dialect)) {
4548
+ throw new Error('Unsupported freshness database dialect');
4549
+ }
4550
+ 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]);
4551
+ const rows = pg ? result?.rows : result?.[0];
4552
+ if (!Array.isArray(rows)) throw new Error('Invalid freshness database response');
4553
+ return rows.length !== 0;
4554
+ } finally {
4555
+ await connection.destroy();
4556
+ }
4557
+ }
4558
+ async function hasFreshnessRedis(connection, prefix, maxScans = 10000) {
4559
+ if (!prefix || !Number.isSafeInteger(maxScans) || maxScans < 1) {
4560
+ throw new Error('Invalid Redis freshness bounds');
4561
+ }
4562
+ const pattern = `${prefix.replace(/[\\*?[\]]/g, '\\$&')}*`;
4563
+ let cursor = '0';
4564
+ for (let scans = 0; scans < maxScans; scans++) {
4565
+ const result = await connection.scan(cursor, 'MATCH', pattern, 'COUNT', 128);
4566
+ 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))) {
4567
+ throw new Error('Invalid Redis freshness response');
4568
+ }
4569
+ if (result[1].length !== 0) return true;
4570
+ cursor = result[0];
4571
+ if (cursor === '0') return false;
4572
+ }
4573
+ throw new Error('Redis freshness scan limit exceeded');
4574
+ }
4575
+ function createFreshnessDependencies(projectPath, load = createRequire(path.join(projectPath, 'package.json'))) {
4576
+ return {
4577
+ async databaseExists(plan, options) {
4578
+ const endpoint = plan.endpoint;
4579
+ const source = options.connection;
4580
+ const target = {
4581
+ user: source.user,
4582
+ password: source.password,
4583
+ ssl: source.ssl,
4584
+ database: endpoint.adminDatabase,
4585
+ connectionTimeoutMillis: 10000,
4586
+ connectTimeout: 10000
4587
+ };
4588
+ if (endpoint.transport === 'tcp') {
4589
+ target.host = endpoint.host;
4590
+ target.port = endpoint.port;
4591
+ } else if (endpoint.dialect === 'pg') {
4592
+ target.host = endpoint.socketPath;
4593
+ target.port = endpoint.port;
4594
+ } else {
4595
+ target.socketPath = endpoint.socketPath;
4596
+ }
4597
+ const knex = load('knex');
4598
+ return await hasFreshnessDatabase(knex({
4599
+ client: endpoint.dialect,
4600
+ connection: target,
4601
+ pool: {
4602
+ min: 0,
4603
+ max: 1
4604
+ },
4605
+ acquireConnectionTimeout: 10000
4606
+ }), plan);
4607
+ },
4608
+ async redisExists(resource, options) {
4609
+ const endpoint = resource.endpoint;
4610
+ const target = endpoint.transport === 'socket' ? {
4611
+ path: endpoint.socketPath
4612
+ } : {
4613
+ host: endpoint.host,
4614
+ port: endpoint.port
4615
+ };
4616
+ const Redis = load('ioredis').Redis;
4617
+ const connection = new Redis({
4618
+ ...target,
4619
+ username: options.username,
4620
+ password: options.password,
4621
+ tls: options.tls,
4622
+ db: endpoint.db,
4623
+ keyPrefix: undefined,
4624
+ lazyConnect: true,
4625
+ connectTimeout: 10000,
4626
+ commandTimeout: 10000,
4627
+ retryStrategy: () => null,
4628
+ maxRetriesPerRequest: 0,
4629
+ enableOfflineQueue: false
4630
+ });
4631
+ connection.on('error', () => {});
4632
+ try {
4633
+ await connection.connect();
4634
+ const prefix = resource.kind === 'queue' ? resource.prefix.slice(0, -'queue:'.length) : resource.prefix;
4635
+ return await hasFreshnessRedis(connection, prefix);
4636
+ } finally {
4637
+ connection.disconnect();
4638
+ }
4639
+ }
4640
+ };
4641
+ }
4642
+
4240
4643
  function jsxEnhance() {
4241
4644
  const createElementOriginal = React.createElement;
4242
4645
  React.createElement = function (...args) {
@@ -4456,6 +4859,7 @@ class Start {
4456
4859
  const app = this.app;
4457
4860
  try {
4458
4861
  await this._start_appConfig();
4862
+ await assertFreshTestResources(app);
4459
4863
  await this._start_appLoad();
4460
4864
  await this._start_appStart();
4461
4865
  await this._start_appReady();
@@ -4845,5 +5249,5 @@ async function retry(options, fn) {
4845
5249
 
4846
5250
  zodExtendOpenApi();
4847
5251
 
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 };
5252
+ 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
5253
  //# sourceMappingURL=index.js.map