@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.
- package/CHANGELOG.md +8 -0
- package/README.md +11 -5
- package/dist/decorators/cache-evict.d.ts.map +1 -1
- package/dist/decorators/cache-evict.js +15 -10
- package/dist/decorators/cache-evict.js.map +1 -1
- package/dist/decorators/cache.d.ts.map +1 -1
- package/dist/decorators/cache.js +23 -29
- package/dist/decorators/cache.js.map +1 -1
- package/package.json +1 -1
- package/src/decorators/cache-evict.ts +19 -11
- package/src/decorators/cache.ts +34 -30
- package/test/cache-evict-logging.test.ts +47 -11
- package/test/cache-logger.integration.test.ts +30 -0
- package/test/cache-logging.test.ts +95 -18
- package/test/cache.test.ts +84 -6
- package/test/fixtures/cache-logger-child.mjs +108 -108
- package/test/fixtures/cache-provider-failure-child.mjs +70 -0
- package/test/redis-cache-decorator.test.ts +65 -4
|
@@ -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
|
|
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).
|
|
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
|
|
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/
|
|
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
|
|
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()).
|
|
357
|
-
expect(businessMethod).
|
|
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
|
|
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
|
|
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()).
|
|
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).
|
|
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
|
|
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()).
|
|
531
|
-
expect(businessMethod).
|
|
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
|
|
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()).
|
|
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({
|
package/test/cache.test.ts
CHANGED
|
@@ -350,7 +350,7 @@ describe('异常策略配置边界', () => {
|
|
|
350
350
|
});
|
|
351
351
|
|
|
352
352
|
describe('异常策略编排', () => {
|
|
353
|
-
it('Provider
|
|
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
|
|
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()).
|
|
376
|
-
expect(businessMethod).
|
|
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('
|
|
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()).
|
|
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
|
-
|
|
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
|
|
95
|
-
new FailureService().getUser()
|
|
96
|
-
);
|
|
97
|
-
|
|
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
|
+
}
|