@jintianxiayu/cache-decorator 0.1.2 → 1.0.0

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 (91) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +270 -133
  3. package/dist/adapters/ioredis-cache-client.d.ts +9 -0
  4. package/dist/adapters/ioredis-cache-client.d.ts.map +1 -0
  5. package/dist/adapters/ioredis-cache-client.js +73 -0
  6. package/dist/adapters/ioredis-cache-client.js.map +1 -0
  7. package/dist/adapters/node-redis-cache-client.d.ts +10 -0
  8. package/dist/adapters/node-redis-cache-client.d.ts.map +1 -0
  9. package/dist/adapters/node-redis-cache-client.js +81 -0
  10. package/dist/adapters/node-redis-cache-client.js.map +1 -0
  11. package/dist/adapters/redis-key-prefix.d.ts +22 -0
  12. package/dist/adapters/redis-key-prefix.d.ts.map +1 -0
  13. package/dist/adapters/redis-key-prefix.js +41 -0
  14. package/dist/adapters/redis-key-prefix.js.map +1 -0
  15. package/dist/core/cache-logger.d.ts +28 -0
  16. package/dist/core/cache-logger.d.ts.map +1 -0
  17. package/dist/core/cache-logger.js +70 -0
  18. package/dist/core/cache-logger.js.map +1 -0
  19. package/dist/core/cache-provider-registry.d.ts.map +1 -1
  20. package/dist/core/cache-provider-registry.js.map +1 -1
  21. package/dist/core/cache-provider.d.ts.map +1 -1
  22. package/dist/core/key-builder.d.ts.map +1 -1
  23. package/dist/core/key-builder.js +3 -2
  24. package/dist/core/key-builder.js.map +1 -1
  25. package/dist/core/native-cache.d.ts.map +1 -1
  26. package/dist/core/native-cache.js +2 -1
  27. package/dist/core/native-cache.js.map +1 -1
  28. package/dist/core/pending-cache.d.ts.map +1 -1
  29. package/dist/core/pending-cache.js +6 -3
  30. package/dist/core/pending-cache.js.map +1 -1
  31. package/dist/core/redis-cache-client.d.ts +156 -0
  32. package/dist/core/redis-cache-client.d.ts.map +1 -0
  33. package/dist/core/redis-cache-client.js +3 -0
  34. package/dist/core/redis-cache-client.js.map +1 -0
  35. package/dist/core/redis-cache.d.ts +39 -7
  36. package/dist/core/redis-cache.d.ts.map +1 -1
  37. package/dist/core/redis-cache.js +63 -22
  38. package/dist/core/redis-cache.js.map +1 -1
  39. package/dist/decorators/cache-evict.d.ts +1 -1
  40. package/dist/decorators/cache-evict.d.ts.map +1 -1
  41. package/dist/decorators/cache-evict.js +68 -14
  42. package/dist/decorators/cache-evict.js.map +1 -1
  43. package/dist/decorators/cache.d.ts +1 -1
  44. package/dist/decorators/cache.d.ts.map +1 -1
  45. package/dist/decorators/cache.js +85 -23
  46. package/dist/decorators/cache.js.map +1 -1
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +6 -0
  50. package/dist/index.js.map +1 -1
  51. package/jest.config.js +11 -14
  52. package/package.json +26 -29
  53. package/src/adapters/ioredis-cache-client.ts +89 -0
  54. package/src/adapters/node-redis-cache-client.ts +95 -0
  55. package/src/adapters/redis-key-prefix.ts +38 -0
  56. package/src/core/cache-logger.ts +105 -0
  57. package/src/core/cache-provider-registry.ts +39 -39
  58. package/src/core/cache-provider.ts +28 -28
  59. package/src/core/key-builder.ts +33 -31
  60. package/src/core/native-cache.ts +55 -53
  61. package/src/core/pending-cache.ts +29 -26
  62. package/src/core/redis-cache-client.ts +160 -0
  63. package/src/core/redis-cache.ts +104 -62
  64. package/src/decorators/cache-evict.ts +129 -73
  65. package/src/decorators/cache.ts +203 -129
  66. package/src/index.ts +11 -9
  67. package/test/cache-evict-logging.test.ts +362 -0
  68. package/test/cache-evict.test.ts +165 -165
  69. package/test/cache-logger.integration.test.ts +129 -0
  70. package/test/cache-logger.test.ts +153 -0
  71. package/test/cache-logging.test.ts +544 -0
  72. package/test/cache-provider-registry.test.ts +24 -24
  73. package/test/cache.test.ts +255 -256
  74. package/test/fixtures/cache-logger-child.mjs +108 -0
  75. package/test/helpers/legacy-redis-cache.ts +22 -0
  76. package/test/helpers/package-consumer.ts +231 -0
  77. package/test/helpers/redis-fixture.ts +142 -0
  78. package/test/ioredis-cache-client.test.ts +143 -0
  79. package/test/key-builder.test.ts +28 -28
  80. package/test/native-cache.test.ts +77 -71
  81. package/test/node-redis-cache-client.test.ts +149 -0
  82. package/test/pending-cache.test.ts +69 -56
  83. package/test/redis-cache-client-lifecycle.test.ts +112 -0
  84. package/test/redis-cache-client-types.test.ts +184 -0
  85. package/test/redis-cache-client.integration.test.ts +327 -0
  86. package/test/redis-cache-decorator.test.ts +201 -0
  87. package/test/redis-cache-package.integration.test.ts +328 -0
  88. package/test/redis-cache-provider.test.ts +269 -0
  89. package/test/type-contract/contract.ts +64 -0
  90. package/test/type-contract/tsconfig.json +12 -0
  91. package/tsconfig.json +8 -8
@@ -0,0 +1,201 @@
1
+ import { createIoredisCacheClient } from '../src/adapters/ioredis-cache-client';
2
+ import { CacheProviderRegistry } from '../src/core/cache-provider-registry';
3
+ import { IoredisCacheClientSource } from '../src/core/redis-cache-client';
4
+ import { KeyBuilder } from '../src/core/key-builder';
5
+ import { MemoryCacheProvider } from '../src/core/native-cache';
6
+ import { RedisCacheProvider } from '../src/core/redis-cache';
7
+ import { CacheEvict } from '../src/decorators/cache-evict';
8
+ import { Cache } from '../src/decorators/cache';
9
+
10
+ class FakeIoredisCacheClientSource implements IoredisCacheClientSource {
11
+ readonly options = Object.freeze({ keyPrefix: '' });
12
+ readonly writes: Array<{
13
+ readonly key: string;
14
+ readonly value: string;
15
+ readonly ttlSeconds: number | undefined;
16
+ }> = [];
17
+ private readonly values = new Map<string, string>();
18
+
19
+ constructor(private readonly events: string[]) {}
20
+
21
+ get(key: string): Promise<unknown> {
22
+ this.events.push(`get:${key}`);
23
+ return Promise.resolve(this.values.get(key) ?? null);
24
+ }
25
+
26
+ set(key: string, value: string): Promise<unknown> {
27
+ this.events.push(`set:${key}`);
28
+ this.writes.push({ key, value, ttlSeconds: undefined });
29
+ this.values.set(key, value);
30
+ return Promise.resolve('OK');
31
+ }
32
+
33
+ setex(key: string, ttlSeconds: number, value: string): Promise<unknown> {
34
+ this.events.push(`setex:${key}`);
35
+ this.writes.push({ key, value, ttlSeconds });
36
+ this.values.set(key, value);
37
+ return Promise.resolve('OK');
38
+ }
39
+
40
+ del(...keys: string[]): Promise<unknown> {
41
+ this.events.push(`del:${keys.join(',')}`);
42
+ let deleted = 0;
43
+ for (const key of keys) {
44
+ if (this.values.delete(key)) {
45
+ deleted += 1;
46
+ }
47
+ }
48
+ return Promise.resolve(deleted);
49
+ }
50
+
51
+ scan(..._args: Parameters<IoredisCacheClientSource['scan']>): Promise<unknown> {
52
+ return Promise.resolve(['0', []]);
53
+ }
54
+
55
+ flushdb(): Promise<unknown> {
56
+ this.values.clear();
57
+ return Promise.resolve('OK');
58
+ }
59
+
60
+ seed(key: string, value: string): void {
61
+ this.values.set(key, value);
62
+ }
63
+
64
+ raw(key: string): string | undefined {
65
+ return this.values.get(key);
66
+ }
67
+ }
68
+
69
+ beforeEach(() => CacheProviderRegistry.clear());
70
+ afterEach(() => CacheProviderRegistry.clear());
71
+
72
+ it('redis-cache-client/D01 默认 Redis Provider 保持装饰器调用方式', async () => {
73
+ const events: string[] = [];
74
+ const source = new FakeIoredisCacheClientSource(events);
75
+ const provider = new RedisCacheProvider(createIoredisCacheClient(source));
76
+ CacheProviderRegistry.register('redis', provider);
77
+ CacheProviderRegistry.setDefault('redis');
78
+ const cacheKey = KeyBuilder.build('d01-users', [1]);
79
+
80
+ class UserService {
81
+ @Cache('d01-users')
82
+ async getUser(id: number): Promise<{ id: number; version: number }> {
83
+ events.push(`business:get:${id}`);
84
+ return { id, version: events.filter((event) => event === `business:get:${id}`).length };
85
+ }
86
+
87
+ @CacheEvict('d01-users')
88
+ async updateUser(id: number): Promise<{ id: number; updated: boolean }> {
89
+ events.push(`business:update:${id}`);
90
+ return { id, updated: true };
91
+ }
92
+ }
93
+
94
+ const service = new UserService();
95
+ await expect(service.getUser(1)).resolves.toEqual({ id: 1, version: 1 });
96
+ await expect(service.getUser(1)).resolves.toEqual({ id: 1, version: 1 });
97
+ await expect(service.updateUser(1)).resolves.toEqual({ id: 1, updated: true });
98
+ await expect(service.getUser(1)).resolves.toEqual({ id: 1, version: 2 });
99
+
100
+ expect(events).toEqual([
101
+ `get:${cacheKey}`,
102
+ 'business:get:1',
103
+ `set:${cacheKey}`,
104
+ `get:${cacheKey}`,
105
+ 'business:update:1',
106
+ `del:${cacheKey}`,
107
+ `get:${cacheKey}`,
108
+ 'business:get:1',
109
+ `set:${cacheKey}`,
110
+ ]);
111
+ });
112
+
113
+ it('redis-cache-client/D02 指定 Provider 名称保持原行为', async () => {
114
+ const events: string[] = [];
115
+ const redisProvider = new RedisCacheProvider(createIoredisCacheClient(new FakeIoredisCacheClientSource(events)));
116
+ const memoryProvider = new MemoryCacheProvider();
117
+ CacheProviderRegistry.register('redis', redisProvider);
118
+ CacheProviderRegistry.register('memory', memoryProvider);
119
+ CacheProviderRegistry.setDefault('memory');
120
+ const redisKey = KeyBuilder.build('d02-orders', ['active']);
121
+
122
+ class OrderService {
123
+ redisCalls = 0;
124
+ memoryCalls = 0;
125
+
126
+ @Cache('d02-orders', { providerName: 'redis', key: 'active' })
127
+ async getRedisOrder(id: number): Promise<{ id: number; source: string }> {
128
+ this.redisCalls += 1;
129
+ return { id, source: 'redis' };
130
+ }
131
+
132
+ @Cache('d02-orders', { providerName: 'memory', key: 'active' })
133
+ async getMemoryOrder(id: number): Promise<{ id: number; source: string }> {
134
+ this.memoryCalls += 1;
135
+ return { id, source: 'memory' };
136
+ }
137
+
138
+ @CacheEvict('d02-orders', { providerName: 'redis', key: 'active' })
139
+ async evictRedisOrder(): Promise<string> {
140
+ return 'evicted';
141
+ }
142
+ }
143
+
144
+ const service = new OrderService();
145
+ await expect(service.getRedisOrder(1)).resolves.toEqual({ id: 1, source: 'redis' });
146
+ await expect(service.getRedisOrder(2)).resolves.toEqual({ id: 1, source: 'redis' });
147
+ await expect(service.getMemoryOrder(3)).resolves.toEqual({ id: 3, source: 'memory' });
148
+ await expect(service.getMemoryOrder(4)).resolves.toEqual({ id: 3, source: 'memory' });
149
+ await expect(service.evictRedisOrder()).resolves.toBe('evicted');
150
+ await expect(service.getRedisOrder(5)).resolves.toEqual({ id: 5, source: 'redis' });
151
+ await expect(service.getMemoryOrder(6)).resolves.toEqual({ id: 3, source: 'memory' });
152
+
153
+ expect(service.redisCalls).toBe(2);
154
+ expect(service.memoryCalls).toBe(1);
155
+ expect(events.filter((event) => event === `get:${redisKey}`)).toHaveLength(3);
156
+ expect(events).toContain(`del:${redisKey}`);
157
+ });
158
+
159
+ it('cache-operation-logging/A03 日志改造保持旧版 Redis key、entry、TTL 与淘汰 pattern', async () => {
160
+ const events: string[] = [];
161
+ const source = new FakeIoredisCacheClientSource(events);
162
+ const scan = jest.spyOn(source, 'scan');
163
+ const provider = new RedisCacheProvider(createIoredisCacheClient(source));
164
+ CacheProviderRegistry.register('compat-redis', provider);
165
+ const legacyKey = KeyBuilder.build('compat-users', ['legacy']);
166
+ const freshKey = KeyBuilder.build('compat-users', ['fresh']);
167
+ source.seed(legacyKey, '{"value":{"id":18,"source":"legacy-0.1.3"}}');
168
+ let legacyBusinessCalls = 0;
169
+
170
+ class UserService {
171
+ @Cache('compat-users', { providerName: 'compat-redis', key: 'legacy' })
172
+ async getLegacyUser(): Promise<{ id: number; source: string }> {
173
+ legacyBusinessCalls += 1;
174
+ return { id: 0, source: 'business' };
175
+ }
176
+
177
+ @Cache('compat-users', { providerName: 'compat-redis', key: 'fresh', ttl: 45 })
178
+ async getFreshUser(): Promise<{ id: number; source: string }> {
179
+ return { id: 19, source: 'current' };
180
+ }
181
+
182
+ @CacheEvict('compat-users', { providerName: 'compat-redis', allEntries: true })
183
+ async clearUsers(): Promise<boolean> {
184
+ return true;
185
+ }
186
+ }
187
+
188
+ const service = new UserService();
189
+ await expect(service.getLegacyUser()).resolves.toEqual({ id: 18, source: 'legacy-0.1.3' });
190
+ expect(legacyBusinessCalls).toBe(0);
191
+ await expect(service.getFreshUser()).resolves.toEqual({ id: 19, source: 'current' });
192
+ expect(source.raw(freshKey)).toBe('{"value":{"id":19,"source":"current"}}');
193
+ expect(source.writes).toContainEqual({
194
+ key: freshKey,
195
+ value: '{"value":{"id":19,"source":"current"}}',
196
+ ttlSeconds: 45,
197
+ });
198
+
199
+ await expect(service.clearUsers()).resolves.toBe(true);
200
+ expect(scan).toHaveBeenCalledWith('0', 'MATCH', 'compat-users*', 'COUNT', 100);
201
+ });
@@ -0,0 +1,328 @@
1
+ import { readFileSync, readdirSync, realpathSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { join } from 'node:path';
4
+ import {
5
+ createPackageTestRoot,
6
+ installConsumer,
7
+ PackageConsumer,
8
+ packCurrentPackage,
9
+ packLoggerPackage,
10
+ packageRoot,
11
+ readReadmeExample,
12
+ removePackageTestRoot,
13
+ runCommand,
14
+ } from './helpers/package-consumer';
15
+
16
+ jest.setTimeout(180000);
17
+
18
+ const sources = {
19
+ none: `
20
+ import assert from 'node:assert/strict';
21
+ import { LoggerFactory } from '@jintianxiayu/logger';
22
+ import {
23
+ Cache, CacheProviderRegistry, MemoryCacheProvider, RedisCacheProvider, createIoredisCacheClient,
24
+ createNodeRedisCacheClient, type CacheProvider, type RedisCacheClient,
25
+ type IoredisCacheClientSource, type NodeRedisCacheClientSource, type NodeRedisCacheClientOptions,
26
+ } from '@jintianxiayu/cache-decorator';
27
+ const values = new Map<string, string>();
28
+ const client: RedisCacheClient = {
29
+ get: async (key) => values.get(key) ?? null,
30
+ set: async ({ key, value }) => { values.set(key, value); },
31
+ deleteMany: async (keys) => { for (const key of keys) { values.delete(key); } },
32
+ scan: async () => ({ cursor: '0', keys: [...values.keys()] }),
33
+ flushDatabase: async () => { values.clear(); },
34
+ };
35
+ const custom: CacheProvider = {
36
+ get: async () => undefined, set: async () => undefined, delete: async () => undefined,
37
+ clear: async () => undefined, deleteByPattern: async () => undefined,
38
+ };
39
+ export type Sources = IoredisCacheClientSource | NodeRedisCacheClientSource;
40
+ export type Options = NodeRedisCacheClientOptions;
41
+ async function main(): Promise<void> {
42
+ LoggerFactory.init({
43
+ root: { console: { enabled: false }, file: { enabled: false } },
44
+ processErrors: { uncaughtException: false, unhandledRejection: false, exitOnError: false },
45
+ });
46
+ try {
47
+ assert.equal(typeof createIoredisCacheClient, 'function');
48
+ assert.equal(typeof createNodeRedisCacheClient, 'function');
49
+ const memory = new MemoryCacheProvider();
50
+ await memory.set('memory-key', { source: 'memory' });
51
+ assert.deepEqual(await memory.get('memory-key'), { source: 'memory' });
52
+ CacheProviderRegistry.register('memory', memory);
53
+ let calls = 0;
54
+ class Service {
55
+ @Cache('external-memory', { providerName: 'memory' })
56
+ getValue(id: number): { id: number } {
57
+ calls += 1;
58
+ return { id };
59
+ }
60
+ }
61
+ const service = new Service();
62
+ assert.deepEqual(await service.getValue(1), { id: 1 });
63
+ assert.deepEqual(await service.getValue(1), { id: 1 });
64
+ assert.equal(calls, 1);
65
+
66
+ const redis = new RedisCacheProvider(client);
67
+ await redis.set('redis-key', { source: 'custom-client' });
68
+ assert.deepEqual(await redis.get('redis-key'), { source: 'custom-client' });
69
+ CacheProviderRegistry.register('custom', custom);
70
+ CacheProviderRegistry.setDefault('custom');
71
+ assert.equal(await CacheProviderRegistry.get().get('key'), undefined);
72
+ } finally {
73
+ CacheProviderRegistry.clear();
74
+ await LoggerFactory.shutdown({ timeout: 2_000 });
75
+ }
76
+ }
77
+ main().catch((error: unknown) => { console.error(error); process.exitCode = 1; });
78
+ `,
79
+ redis: `
80
+ import assert from 'node:assert/strict';
81
+ import { randomUUID } from 'node:crypto';
82
+ import { createClient } from 'redis';
83
+ import { RedisCacheProvider, createNodeRedisCacheClient } from '@jintianxiayu/cache-decorator';
84
+ const redis = createClient();
85
+ const provider = new RedisCacheProvider(createNodeRedisCacheClient(redis));
86
+ assert.ok(provider instanceof RedisCacheProvider);
87
+ assert.equal(redis.isOpen, false);
88
+ async function runCache(): Promise<void> {
89
+ const url = process.env.CACHE_DECORATOR_TEST_REDIS_URL;
90
+ assert.ok(url);
91
+ const connection = createClient({
92
+ url, disableOfflineQueue: true, socket: { connectTimeout: 1000, reconnectStrategy: false },
93
+ });
94
+ const errors: Error[] = [];
95
+ connection.on('error', (error: Error) => errors.push(error));
96
+ const key = 'cache-consumer:' + randomUUID();
97
+ try {
98
+ await connection.connect();
99
+ const cache = new RedisCacheProvider(createNodeRedisCacheClient(connection));
100
+ await cache.set(key, { source: 'node-redis' }, 30);
101
+ assert.deepEqual(await cache.get(key), { source: 'node-redis' });
102
+ await cache.delete(key);
103
+ assert.equal(await cache.get(key), undefined);
104
+ assert.equal(await connection.ping(), 'PONG');
105
+ assert.deepEqual(errors, []);
106
+ } finally {
107
+ if (connection.isOpen) {
108
+ try { await connection.del(key); }
109
+ finally { connection.destroy(); }
110
+ }
111
+ }
112
+ }
113
+ if (process.argv[2] === 'cache') {
114
+ runCache().catch((error: unknown) => { console.error(error); process.exitCode = 1; });
115
+ }
116
+ `,
117
+ ioredis: `
118
+ import assert from 'node:assert/strict';
119
+ import { randomUUID } from 'node:crypto';
120
+ import Redis from 'ioredis';
121
+ import { RedisCacheProvider, createIoredisCacheClient } from '@jintianxiayu/cache-decorator';
122
+ const redis = new Redis({ lazyConnect: true });
123
+ const provider = new RedisCacheProvider(createIoredisCacheClient(redis));
124
+ assert.ok(provider instanceof RedisCacheProvider);
125
+ assert.equal(redis.status, 'wait');
126
+ async function runCache(): Promise<void> {
127
+ const url = process.env.CACHE_DECORATOR_TEST_REDIS_URL;
128
+ assert.ok(url);
129
+ const connection = new Redis(url, {
130
+ lazyConnect: true, enableOfflineQueue: false, connectTimeout: 1000, commandTimeout: 1000,
131
+ retryStrategy: () => null,
132
+ });
133
+ const errors: Error[] = [];
134
+ connection.on('error', (error: Error) => errors.push(error));
135
+ const key = 'cache-consumer:' + randomUUID();
136
+ try {
137
+ await connection.connect();
138
+ const cache = new RedisCacheProvider(createIoredisCacheClient(connection));
139
+ await cache.set(key, { source: 'ioredis' }, 30);
140
+ assert.deepEqual(await cache.get(key), { source: 'ioredis' });
141
+ await cache.delete(key);
142
+ assert.equal(await cache.get(key), undefined);
143
+ assert.equal(await connection.ping(), 'PONG');
144
+ assert.deepEqual(errors, []);
145
+ } finally {
146
+ try { if (connection.status === 'ready') { await connection.del(key); } }
147
+ finally { connection.disconnect(); }
148
+ }
149
+ }
150
+ if (process.argv[2] === 'cache') {
151
+ runCache().catch((error: unknown) => { console.error(error); process.exitCode = 1; });
152
+ }
153
+ `,
154
+ };
155
+
156
+ let root: string;
157
+ let consumers: Record<'none' | 'redis' | 'ioredis', PackageConsumer>;
158
+
159
+ /** 在干净 Node 子进程内检查依赖解析,避免 Jest 启动脚本的 NODE_PATH 干扰。 */
160
+ function clientResolution(consumer: PackageConsumer): Record<string, string | null> {
161
+ return JSON.parse(
162
+ runCommand({
163
+ executable: process.execPath,
164
+ cwd: consumer.directory,
165
+ args: [
166
+ '-e',
167
+ `
168
+ const results = {};
169
+ for (const name of ['redis', 'ioredis']) {
170
+ try { results[name] = require.resolve(name); }
171
+ catch (error) {
172
+ if (error.code !== 'MODULE_NOT_FOUND') { throw error; }
173
+ results[name] = null;
174
+ }
175
+ }
176
+ process.stdout.write(JSON.stringify(results));
177
+ `,
178
+ ],
179
+ })
180
+ );
181
+ }
182
+
183
+ beforeAll(() => {
184
+ root = createPackageTestRoot();
185
+ const archive = packCurrentPackage(root);
186
+ const loggerArchive = packLoggerPackage(root);
187
+ consumers = {
188
+ none: installConsumer({
189
+ root,
190
+ archive,
191
+ loggerArchive,
192
+ client: 'none',
193
+ source: sources.none,
194
+ readmeSource: readReadmeExample('自定义 RedisCacheClient'),
195
+ quickStartSource: readReadmeExample('快速开始', 2),
196
+ }),
197
+ redis: installConsumer({
198
+ root,
199
+ archive,
200
+ loggerArchive,
201
+ client: 'redis',
202
+ source: sources.redis,
203
+ readmeSource: readReadmeExample('node-redis'),
204
+ quickStartSource: readReadmeExample('快速开始', 2),
205
+ }),
206
+ ioredis: installConsumer({
207
+ root,
208
+ archive,
209
+ loggerArchive,
210
+ client: 'ioredis',
211
+ source: sources.ioredis,
212
+ readmeSource: readReadmeExample('ioredis'),
213
+ quickStartSource: readReadmeExample('快速开始', 2),
214
+ }),
215
+ };
216
+ });
217
+
218
+ it('cache-operation-logging/P01 外部严格消费项目显式提供并共享唯一 Logger peer', () => {
219
+ for (const consumer of Object.values(consumers)) {
220
+ const requireFromConsumer = createRequire(join(consumer.directory, 'package.json'));
221
+ const requireFromCache = createRequire(join(consumer.installedPackage, 'package.json'));
222
+ expect(realpathSync(requireFromCache.resolve('@jintianxiayu/logger'))).toBe(
223
+ realpathSync(requireFromConsumer.resolve('@jintianxiayu/logger'))
224
+ );
225
+ expect(consumer.dependencyTree).toContain('"@jintianxiayu/logger"');
226
+ expect(consumer.dependencyTree).toContain('"reflect-metadata"');
227
+ }
228
+ });
229
+
230
+ it('cache-operation-logging/P02 发布 manifest 将 Logger workspace peer 转为实际 semver', () => {
231
+ const manifest = JSON.parse(readFileSync(join(consumers.none.installedPackage, 'package.json'), 'utf8')) as {
232
+ readonly dependencies?: Record<string, string>;
233
+ readonly peerDependencies?: Record<string, string>;
234
+ };
235
+ const loggerManifest = JSON.parse(readFileSync(join(packageRoot, '../logger/package.json'), 'utf8')) as {
236
+ readonly version: string;
237
+ };
238
+ expect(manifest.peerDependencies?.['@jintianxiayu/logger']).toBe(`^${loggerManifest.version}`);
239
+ expect(manifest.peerDependencies?.['@jintianxiayu/logger']).not.toContain('workspace:');
240
+ expect(manifest.dependencies?.['@jintianxiayu/logger']).toBeUndefined();
241
+ });
242
+
243
+ afterAll(() => {
244
+ if (root) {
245
+ removePackageTestRoot(root);
246
+ }
247
+ });
248
+
249
+ it('redis-cache-client/P01 不安装 Redis 客户端也能使用非 Redis Provider', () => {
250
+ const consumer = consumers.none;
251
+ expect(clientResolution(consumer)).toEqual({ ioredis: null, redis: null });
252
+ expect(consumer.dependencyTree).not.toContain('"ioredis":');
253
+ expect(consumer.dependencyTree).not.toContain('"redis":');
254
+ });
255
+
256
+ it('redis-cache-client/P02 仅安装 node-redis 的消费项目', () => {
257
+ const consumer = consumers.redis;
258
+ const resolution = clientResolution(consumer);
259
+ expect(resolution.redis?.startsWith(consumer.directory)).toBe(true);
260
+ expect(resolution.ioredis).toBeNull();
261
+ expect(consumer.dependencyTree).toContain('"redis":');
262
+ expect(consumer.dependencyTree).not.toContain('"ioredis":');
263
+ });
264
+
265
+ it('redis-cache-client/P03 仅安装 ioredis 的消费项目', () => {
266
+ const consumer = consumers.ioredis;
267
+ const resolution = clientResolution(consumer);
268
+ expect(resolution.ioredis?.startsWith(consumer.directory)).toBe(true);
269
+ expect(resolution.redis).toBeNull();
270
+ expect(consumer.dependencyTree).toContain('"ioredis":');
271
+ expect(consumer.dependencyTree).not.toContain('"redis":');
272
+ });
273
+
274
+ const redisTest = process.env.CACHE_DECORATOR_TEST_REDIS_URL ? it : it.skip;
275
+
276
+ redisTest('redis-cache-client/P02 仅 node-redis 的消费项目执行真实缓存操作', () => {
277
+ const consumer = consumers.redis;
278
+ expect(
279
+ runCommand({
280
+ executable: process.execPath,
281
+ args: [join(consumer.directory, 'out/consumer.js'), 'cache'],
282
+ cwd: consumer.directory,
283
+ })
284
+ ).toBe('');
285
+ });
286
+
287
+ redisTest('redis-cache-client/P03 仅 ioredis 的消费项目执行真实缓存操作', () => {
288
+ const consumer = consumers.ioredis;
289
+ expect(
290
+ runCommand({
291
+ executable: process.execPath,
292
+ args: [join(consumer.directory, 'out/consumer.js'), 'cache'],
293
+ cwd: consumer.directory,
294
+ })
295
+ ).toBe('');
296
+ });
297
+
298
+ it('redis-cache-client/P04 公共入口与声明不泄漏第三方客户端', () => {
299
+ const consumer = consumers.none;
300
+ const requireFromConsumer = createRequire(join(consumer.directory, 'package.json'));
301
+ const api: Record<string, unknown> = requireFromConsumer('@jintianxiayu/cache-decorator');
302
+ expect(Object.keys(api).sort()).toEqual(
303
+ [
304
+ 'Cache',
305
+ 'CacheEvict',
306
+ 'CacheProviderRegistry',
307
+ 'KeyBuilder',
308
+ 'MemoryCacheProvider',
309
+ 'PendingCache',
310
+ 'RedisCacheProvider',
311
+ 'createIoredisCacheClient',
312
+ 'createNodeRedisCacheClient',
313
+ ].sort()
314
+ );
315
+ const manifest = JSON.parse(readFileSync(join(consumer.installedPackage, 'package.json'), 'utf8'));
316
+ for (const field of ['dependencies', 'peerDependencies', 'optionalDependencies']) {
317
+ expect(manifest[field]?.ioredis).toBeUndefined();
318
+ expect(manifest[field]?.redis).toBeUndefined();
319
+ }
320
+ const dist = join(consumer.installedPackage, 'dist');
321
+ const externalClient = /(?:from\s*|require\s*\(\s*|import\s*\(\s*)['"](?:ioredis|redis)(?:\/|['"])/;
322
+ for (const entry of readdirSync(dist, { recursive: true })) {
323
+ const name = String(entry);
324
+ if (name.endsWith('.js') || name.endsWith('.d.ts')) {
325
+ expect(readFileSync(join(dist, name), 'utf8')).not.toMatch(externalClient);
326
+ }
327
+ }
328
+ });