@jintianxiayu/cache-decorator 1.0.1 → 1.0.2

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.
@@ -37,6 +37,7 @@ interface ChildResult {
37
37
  const packageRoot = resolve(__dirname, '..');
38
38
  const loggerRoot = resolve(packageRoot, '../logger');
39
39
  const fixture = join(__dirname, 'fixtures', 'cache-logger-child.mjs');
40
+ const providerFailureFixture = join(__dirname, 'fixtures', 'cache-provider-failure-child.mjs');
40
41
 
41
42
  /**
42
43
  * 在隔离进程运行真实 Logger fixture,确保关闭流程完整刷新结构化输出。
@@ -62,6 +63,29 @@ function runFixture(profileLevel: 'debug' | 'error'): Promise<ChildResult> {
62
63
  });
63
64
  }
64
65
 
66
+ /**
67
+ * 在严格未处理 rejection 模式运行 Provider 异步失败 fixture。
68
+ * @returns 子进程退出码和完整标准流。
69
+ * @throws 子进程无法启动时拒绝。
70
+ */
71
+ function runProviderFailureFixture(): Promise<ChildResult> {
72
+ return new Promise((resolveResult, reject) => {
73
+ const child = spawn(process.execPath, ['--unhandled-rejections=strict', providerFailureFixture], {
74
+ cwd: packageRoot,
75
+ stdio: ['ignore', 'pipe', 'pipe'],
76
+ windowsHide: true,
77
+ });
78
+ let stdout = '';
79
+ let stderr = '';
80
+ child.stdout.setEncoding('utf8');
81
+ child.stderr.setEncoding('utf8');
82
+ child.stdout.on('data', (chunk: string) => (stdout += chunk));
83
+ child.stderr.on('data', (chunk: string) => (stderr += chunk));
84
+ child.once('error', reject);
85
+ child.once('exit', (code) => resolveResult({ code, stdout, stderr }));
86
+ });
87
+ }
88
+
65
89
  function parseEvents(output: string): JsonLogEvent[] {
66
90
  return output
67
91
  .split(/\r?\n/)
@@ -121,6 +145,12 @@ it('cache-operation-logging/Logger 配置筛选输出且复用应用提供的唯
121
145
  });
122
146
  });
123
147
 
148
+ it('异步缓存写入和单 key 删除拒绝不会触发未处理 rejection', async () => {
149
+ const result = await runProviderFailureFixture();
150
+ expect(result.code).toBe(0);
151
+ expect(result.stderr).toBe('');
152
+ });
153
+
124
154
  it('cache decorator 源码不读取或写入 LoggerContext', () => {
125
155
  const sources = ['src/core/cache-logger.ts', 'src/decorators/cache.ts', 'src/decorators/cache-evict.ts']
126
156
  .map((file) => readFileSync(join(packageRoot, file), 'utf8'))
@@ -307,13 +307,12 @@ it('cache-operation-logging/C06 回填后的重复调用命中缓存且不重复
307
307
  expect(events(mockLogger.debug)).toEqual(['cache.miss', 'cache.write_dispatched', 'cache.hit']);
308
308
  });
309
309
 
310
- it('cache-operation-logging/F04 异步 set 拒绝不会被等待或消费', async () => {
310
+ it('cache-operation-logging/F04 异步 set 拒绝被观察但不改变业务结果', async () => {
311
311
  let rejectWrite: ((reason?: unknown) => void) | undefined;
312
312
  const writeError = new Error('async write failed');
313
313
  const writePromise = new Promise<void>((_resolve, reject) => {
314
314
  rejectWrite = reject;
315
315
  });
316
- const observedWriteFailure = writePromise.catch((error: unknown) => error);
317
316
  const thenSpy = jest.spyOn(writePromise, 'then');
318
317
  const provider = createProvider(undefined);
319
318
  provider.set.mockReturnValue(writePromise);
@@ -328,7 +327,7 @@ it('cache-operation-logging/F04 异步 set 拒绝不会被等待或消费', asyn
328
327
 
329
328
  await expect(new UserService().getUser()).resolves.toBe(10);
330
329
  expect(provider.set).toHaveBeenCalledTimes(1);
331
- expect(thenSpy).not.toHaveBeenCalled();
330
+ expect(thenSpy).toHaveBeenCalledTimes(1);
332
331
  expect(events(mockLogger.debug)).toEqual(['cache.miss', 'cache.write_dispatched']);
333
332
  expect(events(mockLogger.debug)).not.toContain('cache.write_completed');
334
333
 
@@ -336,15 +335,66 @@ it('cache-operation-logging/F04 异步 set 拒绝不会被等待或消费', asyn
336
335
  throw new Error('Write rejecter was not initialized');
337
336
  }
338
337
  rejectWrite(writeError);
339
- await expect(observedWriteFailure).resolves.toBe(writeError);
338
+ await Promise.resolve();
339
+ expect(loggedMetadata(mockLogger.error)).toContainEqual({
340
+ event: 'cache.operation_failed',
341
+ cacheName: 'async-write-users',
342
+ methodName: 'getUser',
343
+ providerName: 'async-write-provider',
344
+ operation: 'write',
345
+ error: writeError,
346
+ });
340
347
  });
341
348
 
342
- it('cache-operation-logging/F01 Provider 解析失败记录原始注册表异常且不执行业务', async () => {
349
+ it('cache-operation-logging/F07 异常条目异步 set 拒绝被消费且保留原业务异常', async () => {
350
+ let rejectWrite: ((reason?: unknown) => void) | undefined;
351
+ const writeError = new Error('async error entry write failed');
352
+ const writePromise = new Promise<void>((_resolve, reject) => {
353
+ rejectWrite = reject;
354
+ });
355
+ const provider = createProvider(undefined);
356
+ provider.set.mockReturnValue(writePromise);
357
+ CacheProviderRegistry.register('async-error-write-provider', provider.provider);
358
+ const businessError = new Error('original business error');
359
+
360
+ class UserService {
361
+ @Cache('async-error-write-users', {
362
+ providerName: 'async-error-write-provider',
363
+ errorCache: { ttl: 5 },
364
+ })
365
+ getUser(): never {
366
+ throw businessError;
367
+ }
368
+ }
369
+
370
+ await expect(new UserService().getUser()).rejects.toBe(businessError);
371
+ expect(events(mockLogger.debug)).toEqual(['cache.miss', 'cache.write_dispatched']);
372
+ expect(mockLogger.error).not.toHaveBeenCalled();
373
+
374
+ if (!rejectWrite) {
375
+ throw new Error('Write rejecter was not initialized');
376
+ }
377
+ rejectWrite(writeError);
378
+ await Promise.resolve();
379
+ expect(loggedMetadata(mockLogger.error)).toEqual([
380
+ {
381
+ event: 'cache.operation_failed',
382
+ cacheName: 'async-error-write-users',
383
+ methodName: 'getUser',
384
+ providerName: 'async-error-write-provider',
385
+ operation: 'write',
386
+ error: writeError,
387
+ },
388
+ ]);
389
+ });
390
+
391
+ it('cache-operation-logging/F01 Provider 解析失败记录原始注册表异常并旁路执行业务', async () => {
343
392
  const registryError = new Error('provider resolution failed');
344
393
  jest.spyOn(CacheProviderRegistry, 'get').mockImplementation(() => {
345
394
  throw registryError;
346
395
  });
347
- const businessMethod = jest.fn();
396
+ const businessResult = { id: 20 };
397
+ const businessMethod = jest.fn(() => businessResult);
348
398
 
349
399
  class UserService {
350
400
  @Cache('missing-provider-users', { providerName: 'missing-provider' })
@@ -353,8 +403,8 @@ it('cache-operation-logging/F01 Provider 解析失败记录原始注册表异常
353
403
  }
354
404
  }
355
405
 
356
- await expect(new UserService().getUser()).rejects.toBe(registryError);
357
- expect(businessMethod).not.toHaveBeenCalled();
406
+ await expect(new UserService().getUser()).resolves.toBe(businessResult);
407
+ expect(businessMethod).toHaveBeenCalledTimes(1);
358
408
  expect(events(mockLogger.debug)).toEqual([]);
359
409
  expect(loggedMetadata(mockLogger.error)).toEqual([
360
410
  {
@@ -368,11 +418,12 @@ it('cache-operation-logging/F01 Provider 解析失败记录原始注册表异常
368
418
  ]);
369
419
  });
370
420
 
371
- it('cache-operation-logging/F02 cache-operation-logging/M03 读取失败记录同一错误且不转为 miss', async () => {
421
+ it('cache-operation-logging/F02 cache-operation-logging/M03 读取失败记录同一错误并旁路执行业务', async () => {
372
422
  const readError = new Error('redis unavailable');
373
423
  const provider = createProvider(Promise.reject(readError));
374
424
  CacheProviderRegistry.register('unavailable-provider', provider.provider);
375
- const businessMethod = jest.fn();
425
+ const businessResult = { id: 21 };
426
+ const businessMethod = jest.fn(() => businessResult);
376
427
 
377
428
  class UserService {
378
429
  @Cache('unavailable-users', { providerName: 'unavailable-provider' })
@@ -381,10 +432,10 @@ it('cache-operation-logging/F02 cache-operation-logging/M03 读取失败记录
381
432
  }
382
433
  }
383
434
 
384
- await expect(new UserService().getUser()).rejects.toBe(readError);
435
+ await expect(new UserService().getUser()).resolves.toBe(businessResult);
385
436
  expect(provider.get).toHaveBeenCalledTimes(1);
386
437
  expect(provider.set).not.toHaveBeenCalled();
387
- expect(businessMethod).not.toHaveBeenCalled();
438
+ expect(businessMethod).toHaveBeenCalledTimes(1);
388
439
  expect(events(mockLogger.debug)).not.toContain('cache.miss');
389
440
  expect(loggedMetadata(mockLogger.error)).toEqual([
390
441
  {
@@ -511,14 +562,14 @@ it('cache-operation-logging/L03 Logger debug 失败不影响 cache hit', async (
511
562
  expect(businessMethod).not.toHaveBeenCalled();
512
563
  });
513
564
 
514
- it('cache-operation-logging/L04 Logger error 失败不遮蔽原始读取异常', async () => {
565
+ it('cache-operation-logging/L04 Logger error 失败不影响读取失败后的业务旁路', async () => {
515
566
  const readError = new Error('provider read failed');
516
567
  const provider = createProvider(Promise.reject(readError));
517
568
  CacheProviderRegistry.register('logger-error-provider', provider.provider);
518
569
  mockLogger.error.mockImplementationOnce(() => {
519
570
  throw new Error('logger error failed');
520
571
  });
521
- const businessMethod = jest.fn();
572
+ const businessMethod = jest.fn(() => 'business-result');
522
573
 
523
574
  class UserService {
524
575
  @Cache('logger-error-users', { providerName: 'logger-error-provider' })
@@ -527,12 +578,38 @@ it('cache-operation-logging/L04 Logger error 失败不遮蔽原始读取异常',
527
578
  }
528
579
  }
529
580
 
530
- await expect(new UserService().getUser()).rejects.toBe(readError);
531
- expect(businessMethod).not.toHaveBeenCalled();
581
+ await expect(new UserService().getUser()).resolves.toBe('business-result');
582
+ expect(businessMethod).toHaveBeenCalledTimes(1);
583
+ expect(events(mockLogger.debug)).not.toContain('cache.miss');
584
+ });
585
+
586
+ it('cache-operation-logging/L04 Provider 与 Logger 同时失败时保留原业务异常', async () => {
587
+ const providerError = new Error('provider read failed');
588
+ const provider = createProvider(Promise.reject(providerError));
589
+ CacheProviderRegistry.register('logger-business-error-provider', provider.provider);
590
+ mockLogger.error.mockImplementationOnce(() => {
591
+ throw new Error('logger error failed');
592
+ });
593
+ const businessError = new Error('original business failure');
594
+ const shouldCache = jest.fn((_error: unknown): boolean => true);
595
+
596
+ class UserService {
597
+ @Cache('logger-business-error-users', {
598
+ providerName: 'logger-business-error-provider',
599
+ errorCache: { ttl: 5, shouldCache },
600
+ })
601
+ getUser(): never {
602
+ throw businessError;
603
+ }
604
+ }
605
+
606
+ await expect(new UserService().getUser()).rejects.toBe(businessError);
607
+ expect(shouldCache).not.toHaveBeenCalled();
608
+ expect(provider.set).not.toHaveBeenCalled();
532
609
  expect(events(mockLogger.debug)).not.toContain('cache.miss');
533
610
  });
534
611
 
535
- it('正常结果同步 set 失败只记录一次 write operation 且不进入异常策略', async () => {
612
+ it('正常结果同步 set 失败只记录一次 write operation 且保留业务结果', async () => {
536
613
  const writeError = new Error('synchronous write failed');
537
614
  const provider = createProvider(undefined);
538
615
  const shouldCache = jest.fn((_error: unknown): boolean => true);
@@ -551,7 +628,7 @@ it('正常结果同步 set 失败只记录一次 write operation 且不进入异
551
628
  }
552
629
  }
553
630
 
554
- await expect(new UserService().getUser()).rejects.toBe(writeError);
631
+ await expect(new UserService().getUser()).resolves.toBe(12);
555
632
  expect(provider.set).toHaveBeenCalledTimes(1);
556
633
  expect(shouldCache).not.toHaveBeenCalled();
557
634
  expect(loggedMetadata(mockLogger.error)).toContainEqual({
@@ -350,7 +350,7 @@ describe('异常策略配置边界', () => {
350
350
  });
351
351
 
352
352
  describe('异常策略编排', () => {
353
- it('Provider 读取失败不调用异常筛选器、codec 或业务方法', async () => {
353
+ it('Provider 同步读取失败时旁路缓存并执行业务方法', async () => {
354
354
  const provider = createProvider();
355
355
  const readError = new Error('redis unavailable');
356
356
  provider.get.mockImplementation(() => {
@@ -360,7 +360,8 @@ describe('异常策略编排', () => {
360
360
  const shouldCache = jest.fn(() => true);
361
361
  const encode = jest.fn((error: unknown): unknown => error);
362
362
  const decode = jest.fn((payload: unknown): unknown => payload);
363
- const businessMethod = jest.fn();
363
+ const businessResult = { id: 1 };
364
+ const businessMethod = jest.fn(() => businessResult);
364
365
 
365
366
  class UserService {
366
367
  @Cache('failed-read-users', {
@@ -372,14 +373,91 @@ describe('异常策略编排', () => {
372
373
  }
373
374
  }
374
375
 
375
- await expect(new UserService().getUser()).rejects.toBe(readError);
376
- expect(businessMethod).not.toHaveBeenCalled();
376
+ await expect(new UserService().getUser()).resolves.toBe(businessResult);
377
+ expect(businessMethod).toHaveBeenCalledTimes(1);
377
378
  expect(shouldCache).not.toHaveBeenCalled();
378
379
  expect(encode).not.toHaveBeenCalled();
379
380
  expect(decode).not.toHaveBeenCalled();
380
381
  expect(provider.set).not.toHaveBeenCalled();
381
382
  });
383
+ });
384
+
385
+ describe('Provider 故障旁路', () => {
386
+ it.each(['missing', 'sync-read', 'async-read'] as const)(
387
+ 'Provider %s 故障旁路时传播原始业务异常且不执行异常策略',
388
+ async (failureMode) => {
389
+ const provider = createProvider();
390
+ const providerError = new Error(`provider ${failureMode}`);
391
+ if (failureMode === 'sync-read') {
392
+ provider.get.mockImplementation(() => {
393
+ throw providerError;
394
+ });
395
+ CacheProviderRegistry.register('failed-provider', provider.provider);
396
+ } else if (failureMode === 'async-read') {
397
+ provider.get.mockRejectedValue(providerError);
398
+ CacheProviderRegistry.register('failed-provider', provider.provider);
399
+ }
400
+
401
+ const businessError = new Error(`business ${failureMode}`);
402
+ const shouldCache = jest.fn((_error: unknown): boolean => true);
403
+ const encode = jest.fn((error: unknown): unknown => error);
404
+ const decode = jest.fn((payload: unknown): unknown => payload);
405
+ const businessMethod = jest.fn(() => {
406
+ throw businessError;
407
+ });
408
+
409
+ class UserService {
410
+ @Cache('failed-provider-users', {
411
+ providerName: 'failed-provider',
412
+ errorCache: { ttl: 5, shouldCache, codec: { encode, decode } },
413
+ })
414
+ getUser(): never {
415
+ return businessMethod();
416
+ }
417
+ }
418
+
419
+ await expect(new UserService().getUser()).rejects.toBe(businessError);
420
+ expect(businessMethod).toHaveBeenCalledTimes(1);
421
+ expect(provider.get).toHaveBeenCalledTimes(failureMode === 'missing' ? 0 : 1);
422
+ expect(provider.set).not.toHaveBeenCalled();
423
+ expect(shouldCache).not.toHaveBeenCalled();
424
+ expect(encode).not.toHaveBeenCalled();
425
+ expect(decode).not.toHaveBeenCalled();
426
+ }
427
+ );
428
+
429
+ it('Provider 异步读取失败时并发调用复用 pending Promise 且 settled 后清理', async () => {
430
+ const provider = createProvider();
431
+ const readFailure = createRejectionDeferred();
432
+ provider.get.mockReturnValue(readFailure.promise);
433
+ CacheProviderRegistry.register('pending-failed-read', provider.provider);
434
+ const businessMethod = jest.fn(() => 23);
382
435
 
436
+ class UserService {
437
+ @Cache('pending-failed-read-users', { providerName: 'pending-failed-read' })
438
+ getUser(): number {
439
+ return businessMethod();
440
+ }
441
+ }
442
+
443
+ const service = new UserService();
444
+ const first = service.getUser();
445
+ const second = service.getUser();
446
+ expect(second).toBe(first);
447
+ expect(provider.get).toHaveBeenCalledTimes(1);
448
+
449
+ readFailure.reject(new Error('redis read failed'));
450
+ await expect(Promise.all([first, second])).resolves.toEqual([23, 23]);
451
+ expect(businessMethod).toHaveBeenCalledTimes(1);
452
+ expect(provider.set).not.toHaveBeenCalled();
453
+
454
+ await expect(service.getUser()).resolves.toBe(23);
455
+ expect(provider.get).toHaveBeenCalledTimes(2);
456
+ expect(businessMethod).toHaveBeenCalledTimes(2);
457
+ });
458
+ });
459
+
460
+ describe('异常策略编排', () => {
383
461
  it('key resolver 失败后的业务成功只写入正常结果', async () => {
384
462
  const provider = createProvider();
385
463
  CacheProviderRegistry.register('key-fallback', provider.provider);
@@ -601,7 +679,7 @@ describe('异常策略编排', () => {
601
679
  expect(provider.set).toHaveBeenCalledTimes(1);
602
680
  });
603
681
 
604
- it('正常写入同步失败不进入异常缓存策略', async () => {
682
+ it('正常写入同步失败不进入异常缓存策略且保留业务结果', async () => {
605
683
  const provider = createProvider();
606
684
  const writeError = new Error('normal write failed');
607
685
  const shouldCache = jest.fn((_error: unknown): boolean => true);
@@ -620,7 +698,7 @@ describe('异常策略编排', () => {
620
698
  }
621
699
  }
622
700
 
623
- await expect(new UserService().getUser()).rejects.toBe(writeError);
701
+ await expect(new UserService().getUser()).resolves.toBe('value');
624
702
  expect(provider.set).toHaveBeenCalledTimes(1);
625
703
  expect(shouldCache).not.toHaveBeenCalled();
626
704
  });
@@ -1,108 +1,108 @@
1
- import assert from 'node:assert/strict';
2
- import { realpathSync } from 'node:fs';
3
- import { createRequire } from 'node:module';
4
- import { dirname, join, resolve } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import cachePackage from '../../dist/index.js';
7
- import loggerPackage from '../../../logger/dist/index.js';
8
-
9
- const { Cache, CacheProviderRegistry, MemoryCacheProvider } = cachePackage;
10
- const { LoggerContext, LoggerFactory } = loggerPackage;
11
- const CACHE_LOGGER_NAME = '@jintianxiayu/cache-decorator';
12
- const profileLevel = process.argv[2];
13
- const fixtureDirectory = dirname(fileURLToPath(import.meta.url));
14
- const fixtureRequire = createRequire(import.meta.url);
15
- if (profileLevel !== 'debug' && profileLevel !== 'error') {
16
- throw new Error('Expected cache Logger profile level: debug or error');
17
- }
18
-
19
- /** 将 legacy method decorator 应用到 JavaScript fixture 的指定方法。 */
20
- function decorateMethod(target, methodName, decorator) {
21
- const descriptor = Object.getOwnPropertyDescriptor(target, methodName);
22
- assert.ok(descriptor);
23
- decorator(target, methodName, descriptor);
24
- Object.defineProperty(target, methodName, descriptor);
25
- }
26
-
27
- /** 在应用初始化 Logger 后执行正常缓存和 Provider 失败调用。 */
28
- async function main() {
29
- LoggerFactory.init({
30
- root: {
31
- level: 'error',
32
- console: { enabled: false, colors: false, format: 'json' },
33
- file: { enabled: false },
34
- },
35
- loggers: {
36
- [CACHE_LOGGER_NAME]: {
37
- level: profileLevel,
38
- console: { enabled: true, colors: false, format: 'json' },
39
- file: { enabled: false },
40
- },
41
- },
42
- masking: { enabled: true },
43
- processErrors: {
44
- uncaughtException: false,
45
- unhandledRejection: false,
46
- exitOnError: false,
47
- },
48
- });
49
-
50
- const cachePackageRoot = resolve(fixtureDirectory, '../..');
51
- const requireFromCache = createRequire(join(cachePackageRoot, 'package.json'));
52
- const directLoggerEntry = fixtureRequire.resolve('@jintianxiayu/logger');
53
- const cacheLoggerEntry = requireFromCache.resolve('@jintianxiayu/logger');
54
- assert.equal(realpathSync(directLoggerEntry), realpathSync(cacheLoggerEntry));
55
- const namedLogger = LoggerFactory.getLogger(CACHE_LOGGER_NAME);
56
-
57
- const memoryProvider = new MemoryCacheProvider();
58
- const providerError = Object.assign(new Error('redis unavailable'), {
59
- password: 'provider-password-secret',
60
- email: 'provider@example.com',
61
- });
62
- const failingProvider = {
63
- get: async () => {
64
- throw providerError;
65
- },
66
- set: () => undefined,
67
- delete: () => undefined,
68
- clear: () => undefined,
69
- deleteByPattern: () => undefined,
70
- };
71
-
72
- class SuccessService {
73
- getUser() {
74
- return { id: 17, password: 'business-value-secret' };
75
- }
76
- }
77
- decorateMethod(SuccessService.prototype, 'getUser', Cache('fixture-users', { providerName: 'memory' }));
78
-
79
- class FailureService {
80
- getUser() {
81
- throw new Error('business method must not run');
82
- }
83
- }
84
- decorateMethod(FailureService.prototype, 'getUser', Cache('fixture-failure-users', { providerName: 'failing' }));
85
-
86
- CacheProviderRegistry.register('memory', memoryProvider);
87
- CacheProviderRegistry.register('failing', failingProvider);
88
- try {
89
- const result = await LoggerContext.withContext({ traceId: 'cache-fixture-trace' }, () =>
90
- new SuccessService().getUser()
91
- );
92
- assert.deepEqual(result, { id: 17, password: 'business-value-secret' });
93
-
94
- const failure = LoggerContext.withContext({ traceId: 'cache-fixture-trace' }, () =>
95
- new FailureService().getUser()
96
- );
97
- await assert.rejects(failure, (error) => error === providerError);
98
- assert.strictEqual(LoggerFactory.getLogger(CACHE_LOGGER_NAME), namedLogger);
99
- } finally {
100
- CacheProviderRegistry.clear();
101
- await LoggerFactory.shutdown({ timeout: 2_000 });
102
- }
103
- }
104
-
105
- main().catch((error) => {
106
- process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
107
- process.exitCode = 1;
108
- });
1
+ import assert from 'node:assert/strict';
2
+ import { realpathSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import cachePackage from '../../dist/index.js';
7
+ import loggerPackage from '../../../logger/dist/index.js';
8
+
9
+ const { Cache, CacheProviderRegistry, MemoryCacheProvider } = cachePackage;
10
+ const { LoggerContext, LoggerFactory } = loggerPackage;
11
+ const CACHE_LOGGER_NAME = '@jintianxiayu/cache-decorator';
12
+ const profileLevel = process.argv[2];
13
+ const fixtureDirectory = dirname(fileURLToPath(import.meta.url));
14
+ const fixtureRequire = createRequire(import.meta.url);
15
+ if (profileLevel !== 'debug' && profileLevel !== 'error') {
16
+ throw new Error('Expected cache Logger profile level: debug or error');
17
+ }
18
+
19
+ /** 将 legacy method decorator 应用到 JavaScript fixture 的指定方法。 */
20
+ function decorateMethod(target, methodName, decorator) {
21
+ const descriptor = Object.getOwnPropertyDescriptor(target, methodName);
22
+ assert.ok(descriptor);
23
+ decorator(target, methodName, descriptor);
24
+ Object.defineProperty(target, methodName, descriptor);
25
+ }
26
+
27
+ /** 在应用初始化 Logger 后执行正常缓存和 Provider 失败调用。 */
28
+ async function main() {
29
+ LoggerFactory.init({
30
+ root: {
31
+ level: 'error',
32
+ console: { enabled: false, colors: false, format: 'json' },
33
+ file: { enabled: false },
34
+ },
35
+ loggers: {
36
+ [CACHE_LOGGER_NAME]: {
37
+ level: profileLevel,
38
+ console: { enabled: true, colors: false, format: 'json' },
39
+ file: { enabled: false },
40
+ },
41
+ },
42
+ masking: { enabled: true },
43
+ processErrors: {
44
+ uncaughtException: false,
45
+ unhandledRejection: false,
46
+ exitOnError: false,
47
+ },
48
+ });
49
+
50
+ const cachePackageRoot = resolve(fixtureDirectory, '../..');
51
+ const requireFromCache = createRequire(join(cachePackageRoot, 'package.json'));
52
+ const directLoggerEntry = fixtureRequire.resolve('@jintianxiayu/logger');
53
+ const cacheLoggerEntry = requireFromCache.resolve('@jintianxiayu/logger');
54
+ assert.equal(realpathSync(directLoggerEntry), realpathSync(cacheLoggerEntry));
55
+ const namedLogger = LoggerFactory.getLogger(CACHE_LOGGER_NAME);
56
+
57
+ const memoryProvider = new MemoryCacheProvider();
58
+ const providerError = Object.assign(new Error('redis unavailable'), {
59
+ password: 'provider-password-secret',
60
+ email: 'provider@example.com',
61
+ });
62
+ const failingProvider = {
63
+ get: async () => {
64
+ throw providerError;
65
+ },
66
+ set: () => undefined,
67
+ delete: () => undefined,
68
+ clear: () => undefined,
69
+ deleteByPattern: () => undefined,
70
+ };
71
+
72
+ class SuccessService {
73
+ getUser() {
74
+ return { id: 17, password: 'business-value-secret' };
75
+ }
76
+ }
77
+ decorateMethod(SuccessService.prototype, 'getUser', Cache('fixture-users', { providerName: 'memory' }));
78
+
79
+ class FailureService {
80
+ getUser() {
81
+ return { id: 18, source: 'business-bypass' };
82
+ }
83
+ }
84
+ decorateMethod(FailureService.prototype, 'getUser', Cache('fixture-failure-users', { providerName: 'failing' }));
85
+
86
+ CacheProviderRegistry.register('memory', memoryProvider);
87
+ CacheProviderRegistry.register('failing', failingProvider);
88
+ try {
89
+ const result = await LoggerContext.withContext({ traceId: 'cache-fixture-trace' }, () =>
90
+ new SuccessService().getUser()
91
+ );
92
+ assert.deepEqual(result, { id: 17, password: 'business-value-secret' });
93
+
94
+ const bypassResult = await LoggerContext.withContext({ traceId: 'cache-fixture-trace' }, () =>
95
+ new FailureService().getUser()
96
+ );
97
+ assert.deepEqual(bypassResult, { id: 18, source: 'business-bypass' });
98
+ assert.strictEqual(LoggerFactory.getLogger(CACHE_LOGGER_NAME), namedLogger);
99
+ } finally {
100
+ CacheProviderRegistry.clear();
101
+ await LoggerFactory.shutdown({ timeout: 2_000 });
102
+ }
103
+ }
104
+
105
+ main().catch((error) => {
106
+ process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
107
+ process.exitCode = 1;
108
+ });
@@ -0,0 +1,70 @@
1
+ import assert from 'node:assert/strict';
2
+ import { join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import cachePackage from '../../dist/index.js';
5
+
6
+ const { Cache, CacheEvict, CacheProviderRegistry } = cachePackage;
7
+
8
+ /** 将 legacy method decorator 应用到 JavaScript fixture 的指定方法。 */
9
+ function decorateMethod(target, methodName, decorator) {
10
+ const descriptor = Object.getOwnPropertyDescriptor(target, methodName);
11
+ assert.ok(descriptor);
12
+ decorator(target, methodName, descriptor);
13
+ Object.defineProperty(target, methodName, descriptor);
14
+ }
15
+
16
+ /** 执行异步写入和删除拒绝场景,进程必须在 strict rejection 模式下正常退出。 */
17
+ async function main() {
18
+ const writeError = new Error('async cache write failed');
19
+ const deleteError = new Error('async cache delete failed');
20
+ const writeProvider = {
21
+ get: () => undefined,
22
+ set: () => Promise.reject(writeError),
23
+ delete: () => undefined,
24
+ clear: () => undefined,
25
+ deleteByPattern: () => undefined,
26
+ };
27
+ const deleteProvider = {
28
+ get: () => undefined,
29
+ set: () => undefined,
30
+ delete: () => Promise.reject(deleteError),
31
+ clear: () => undefined,
32
+ deleteByPattern: () => undefined,
33
+ };
34
+
35
+ class UserService {
36
+ getUser() {
37
+ return 'business-value';
38
+ }
39
+
40
+ updateUser() {
41
+ return 'business-update';
42
+ }
43
+ }
44
+
45
+ decorateMethod(UserService.prototype, 'getUser', Cache('failure-fixture-users', { providerName: 'write' }));
46
+ decorateMethod(
47
+ UserService.prototype,
48
+ 'updateUser',
49
+ CacheEvict('failure-fixture-users', { providerName: 'delete' })
50
+ );
51
+ CacheProviderRegistry.register('write', writeProvider);
52
+ CacheProviderRegistry.register('delete', deleteProvider);
53
+
54
+ try {
55
+ const service = new UserService();
56
+ assert.equal(await service.getUser(), 'business-value');
57
+ assert.equal(await service.updateUser(), 'business-update');
58
+ await new Promise((resolve) => setImmediate(resolve));
59
+ } finally {
60
+ CacheProviderRegistry.clear();
61
+ }
62
+ }
63
+
64
+ const currentFile = fileURLToPath(import.meta.url);
65
+ if (currentFile === join(process.cwd(), 'test', 'fixtures', 'cache-provider-failure-child.mjs')) {
66
+ main().catch((error) => {
67
+ process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
68
+ process.exitCode = 1;
69
+ });
70
+ }