@jintianxiayu/cache-decorator 1.0.0 → 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 +25 -11
- package/README.md +364 -270
- package/dist/core/cache-error.d.ts +75 -0
- package/dist/core/cache-error.d.ts.map +1 -0
- package/dist/core/cache-error.js +168 -0
- package/dist/core/cache-error.js.map +1 -0
- package/dist/core/cache-logger.d.ts +3 -2
- package/dist/core/cache-logger.d.ts.map +1 -1
- package/dist/core/cache-logger.js +2 -0
- package/dist/core/cache-logger.js.map +1 -1
- 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 +6 -0
- package/dist/decorators/cache.d.ts.map +1 -1
- package/dist/decorators/cache.js +113 -36
- package/dist/decorators/cache.js.map +1 -1
- package/jest.config.js +11 -11
- package/package.json +1 -1
- package/src/adapters/ioredis-cache-client.ts +89 -89
- package/src/adapters/node-redis-cache-client.ts +95 -95
- package/src/adapters/redis-key-prefix.ts +38 -38
- package/src/core/cache-error.ts +233 -0
- package/src/core/cache-logger.ts +116 -105
- package/src/core/key-builder.ts +33 -33
- package/src/core/native-cache.ts +55 -55
- package/src/core/pending-cache.ts +29 -29
- package/src/core/redis-cache-client.ts +160 -160
- package/src/core/redis-cache.ts +104 -104
- package/src/decorators/cache-evict.ts +137 -129
- package/src/decorators/cache.ts +323 -203
- package/src/index.ts +11 -11
- package/test/cache-evict-logging.test.ts +398 -362
- package/test/cache-logger.integration.test.ts +159 -129
- package/test/cache-logger.test.ts +156 -153
- package/test/cache-logging.test.ts +929 -544
- package/test/cache.test.ts +1017 -255
- package/test/fixtures/cache-logger-child.mjs +108 -108
- package/test/fixtures/cache-provider-failure-child.mjs +70 -0
- package/test/helpers/legacy-redis-cache.ts +41 -22
- package/test/helpers/package-consumer.ts +231 -231
- package/test/helpers/redis-fixture.ts +142 -142
- package/test/ioredis-cache-client.test.ts +143 -143
- package/test/legacy-redis-cache.test.ts +51 -0
- package/test/native-cache.test.ts +77 -77
- package/test/node-redis-cache-client.test.ts +149 -149
- package/test/pending-cache.test.ts +69 -69
- package/test/redis-cache-client-lifecycle.test.ts +112 -112
- package/test/redis-cache-client-types.test.ts +184 -184
- package/test/redis-cache-client.integration.test.ts +355 -327
- package/test/redis-cache-decorator.test.ts +601 -201
- package/test/redis-cache-provider.test.ts +269 -269
- package/test/type-contract/contract.ts +119 -64
- package/test/type-contract/tsconfig.json +12 -12
- package/tsconfig.json +8 -8
|
@@ -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
|
+
}
|
|
@@ -1,22 +1,41 @@
|
|
|
1
|
-
import Redis from 'ioredis';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 使用 0.1.3 的字符串/JSON 与秒级 TTL 协议写入兼容性数据。
|
|
5
|
-
* @param client 由测试创建并持有的 ioredis 连接。
|
|
6
|
-
* @param key 旧版本收到的原始缓存 key。
|
|
7
|
-
* @param value 旧版本支持的字符串或 JSON 可序列化值。
|
|
8
|
-
* @param ttl 可选的秒级 TTL;0 与 undefined 使用普通 SET。
|
|
9
|
-
* @returns 旧版写入命令成功后完成。
|
|
10
|
-
* @throws 无法序列化或 Redis 命令失败时传播错误。
|
|
11
|
-
*/
|
|
12
|
-
export async function writeLegacyRedisCache(client: Redis, key: string, value: unknown, ttl?: number): Promise<void> {
|
|
13
|
-
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
14
|
-
if (typeof serialized !== 'string') {
|
|
15
|
-
throw new TypeError('Legacy cache fixture value must serialize to a string');
|
|
16
|
-
}
|
|
17
|
-
if (ttl) {
|
|
18
|
-
await client.setex(key, ttl, serialized);
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
await client.set(key, serialized);
|
|
22
|
-
}
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 使用 0.1.3 的字符串/JSON 与秒级 TTL 协议写入兼容性数据。
|
|
5
|
+
* @param client 由测试创建并持有的 ioredis 连接。
|
|
6
|
+
* @param key 旧版本收到的原始缓存 key。
|
|
7
|
+
* @param value 旧版本支持的字符串或 JSON 可序列化值。
|
|
8
|
+
* @param ttl 可选的秒级 TTL;0 与 undefined 使用普通 SET。
|
|
9
|
+
* @returns 旧版写入命令成功后完成。
|
|
10
|
+
* @throws 无法序列化或 Redis 命令失败时传播错误。
|
|
11
|
+
*/
|
|
12
|
+
export async function writeLegacyRedisCache(client: Redis, key: string, value: unknown, ttl?: number): Promise<void> {
|
|
13
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
14
|
+
if (typeof serialized !== 'string') {
|
|
15
|
+
throw new TypeError('Legacy cache fixture value must serialize to a string');
|
|
16
|
+
}
|
|
17
|
+
if (ttl) {
|
|
18
|
+
await client.setex(key, ttl, serialized);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await client.set(key, serialized);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 使用旧版普通 JSON 读取规则恢复缓存值,不理解当前版本的异常 envelope。
|
|
26
|
+
* @param client 由测试创建并持有的 ioredis 连接。
|
|
27
|
+
* @param key 旧版本收到的原始缓存 key。
|
|
28
|
+
* @returns miss 时返回 undefined;合法 JSON 返回解析值,其余字符串保持原值。
|
|
29
|
+
* @throws Redis GET 命令失败时传播原始错误。
|
|
30
|
+
*/
|
|
31
|
+
export async function readLegacyRedisCache(client: Redis, key: string): Promise<unknown> {
|
|
32
|
+
const value = await client.get(key);
|
|
33
|
+
if (value === null) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(value) as unknown;
|
|
38
|
+
} catch {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
}
|