@taicode/common-web 1.1.0 → 1.1.1
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/output/helpers/cache-api/cache-api.d.ts +13 -0
- package/output/helpers/cache-api/cache-api.d.ts.map +1 -0
- package/output/helpers/cache-api/cache-api.js +114 -0
- package/output/helpers/cache-api/cache-api.test.d.ts +2 -0
- package/output/helpers/cache-api/cache-api.test.d.ts.map +1 -0
- package/output/helpers/cache-api/cache-api.test.js +348 -0
- package/output/helpers/cache-api/index.d.ts +2 -0
- package/output/helpers/cache-api/index.d.ts.map +1 -0
- package/output/helpers/cache-api/index.js +1 -0
- package/output/helpers/side-cache/side-cache.d.ts +5 -2
- package/output/helpers/side-cache/side-cache.d.ts.map +1 -1
- package/output/helpers/side-cache/side-cache.js +41 -10
- package/output/helpers/side-cache/side-cache.test.js +166 -76
- package/package.json +2 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type ApiFunc<P extends unknown[], R = unknown> = (...args: P) => Promise<R>;
|
|
2
|
+
export declare class CacheApi<P extends unknown[], R> {
|
|
3
|
+
private func;
|
|
4
|
+
private cache;
|
|
5
|
+
private accessor pendingRequests;
|
|
6
|
+
constructor(func: ApiFunc<P, R>);
|
|
7
|
+
get loading(): boolean;
|
|
8
|
+
get empty(): boolean;
|
|
9
|
+
get value(): R | undefined;
|
|
10
|
+
send(...args: P): Promise<R>;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=cache-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-api.d.ts","sourceRoot":"","sources":["../../../source/helpers/cache-api/cache-api.ts"],"names":[],"mappings":"AAKA,KAAK,OAAO,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;AAE3E,qBAAa,QAAQ,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC;IAO9B,OAAO,CAAC,IAAI;IANxB,OAAO,CAAC,KAAK,CAAqB;IAIlC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAI;gBAEhB,IAAI,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAIvC,IACI,OAAO,IAAI,OAAO,CAErB;IAED,IACI,KAAK,IAAI,OAAO,CAEnB;IAED,IACI,KAAK,IAAI,CAAC,GAAG,SAAS,CAEzB;IAGK,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAmBnC"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
36
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
37
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
38
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
39
|
+
};
|
|
40
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
41
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
42
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
43
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
44
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
45
|
+
};
|
|
46
|
+
import { action, computed, observable } from 'mobx';
|
|
47
|
+
import { catchIt } from '@taicode/common-base';
|
|
48
|
+
import { SideCache } from '../side-cache';
|
|
49
|
+
let CacheApi = (() => {
|
|
50
|
+
var _a, _CacheApi_pendingRequests_accessor_storage;
|
|
51
|
+
var _b;
|
|
52
|
+
let _instanceExtraInitializers = [];
|
|
53
|
+
let _pendingRequests_decorators;
|
|
54
|
+
let _pendingRequests_initializers = [];
|
|
55
|
+
let _pendingRequests_extraInitializers = [];
|
|
56
|
+
let _get_loading_decorators;
|
|
57
|
+
let _get_empty_decorators;
|
|
58
|
+
let _get_value_decorators;
|
|
59
|
+
let _send_decorators;
|
|
60
|
+
return _a = class CacheApi {
|
|
61
|
+
// 记录当前正在进行的请求数量
|
|
62
|
+
get pendingRequests() { return __classPrivateFieldGet(this, _CacheApi_pendingRequests_accessor_storage, "f"); }
|
|
63
|
+
set pendingRequests(value) { __classPrivateFieldSet(this, _CacheApi_pendingRequests_accessor_storage, value, "f"); }
|
|
64
|
+
constructor(func) {
|
|
65
|
+
this.func = (__runInitializers(this, _instanceExtraInitializers), func);
|
|
66
|
+
this.cache = new SideCache();
|
|
67
|
+
_CacheApi_pendingRequests_accessor_storage.set(this, __runInitializers(this, _pendingRequests_initializers, 0));
|
|
68
|
+
__runInitializers(this, _pendingRequests_extraInitializers);
|
|
69
|
+
this.func = func;
|
|
70
|
+
this.send = this.send.bind(this);
|
|
71
|
+
}
|
|
72
|
+
get loading() {
|
|
73
|
+
return this.pendingRequests > 0;
|
|
74
|
+
}
|
|
75
|
+
get empty() {
|
|
76
|
+
return this.cache.empty;
|
|
77
|
+
}
|
|
78
|
+
get value() {
|
|
79
|
+
return this.cache.value;
|
|
80
|
+
}
|
|
81
|
+
async send(...args) {
|
|
82
|
+
// 增加待处理请求计数
|
|
83
|
+
this.pendingRequests++;
|
|
84
|
+
const result = await catchIt(async () => {
|
|
85
|
+
return await this.cache.handle(args, async () => {
|
|
86
|
+
return await this.func(...args);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
// 减少待处理请求计数
|
|
90
|
+
this.pendingRequests--;
|
|
91
|
+
if (result.isError()) {
|
|
92
|
+
throw result.error;
|
|
93
|
+
}
|
|
94
|
+
return result.value;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
_CacheApi_pendingRequests_accessor_storage = new WeakMap(),
|
|
98
|
+
(() => {
|
|
99
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
100
|
+
_pendingRequests_decorators = [observable];
|
|
101
|
+
_get_loading_decorators = [computed];
|
|
102
|
+
_get_empty_decorators = [computed];
|
|
103
|
+
_get_value_decorators = [computed];
|
|
104
|
+
_send_decorators = [(_b = action).bound.bind(_b)];
|
|
105
|
+
__esDecorate(_a, null, _pendingRequests_decorators, { kind: "accessor", name: "pendingRequests", static: false, private: false, access: { has: obj => "pendingRequests" in obj, get: obj => obj.pendingRequests, set: (obj, value) => { obj.pendingRequests = value; } }, metadata: _metadata }, _pendingRequests_initializers, _pendingRequests_extraInitializers);
|
|
106
|
+
__esDecorate(_a, null, _get_loading_decorators, { kind: "getter", name: "loading", static: false, private: false, access: { has: obj => "loading" in obj, get: obj => obj.loading }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
107
|
+
__esDecorate(_a, null, _get_empty_decorators, { kind: "getter", name: "empty", static: false, private: false, access: { has: obj => "empty" in obj, get: obj => obj.empty }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
108
|
+
__esDecorate(_a, null, _get_value_decorators, { kind: "getter", name: "value", static: false, private: false, access: { has: obj => "value" in obj, get: obj => obj.value }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
109
|
+
__esDecorate(_a, null, _send_decorators, { kind: "method", name: "send", static: false, private: false, access: { has: obj => "send" in obj, get: obj => obj.send }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
110
|
+
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
111
|
+
})(),
|
|
112
|
+
_a;
|
|
113
|
+
})();
|
|
114
|
+
export { CacheApi };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-api.test.d.ts","sourceRoot":"","sources":["../../../source/helpers/cache-api/cache-api.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { runInAction, configure } from 'mobx';
|
|
3
|
+
import { CacheApi } from './cache-api';
|
|
4
|
+
// 配置 MobX 在测试环境中的行为
|
|
5
|
+
configure({
|
|
6
|
+
enforceActions: 'never',
|
|
7
|
+
computedRequiresReaction: false,
|
|
8
|
+
reactionRequiresObservable: false,
|
|
9
|
+
observableRequiresReaction: false,
|
|
10
|
+
disableErrorBoundaries: true
|
|
11
|
+
});
|
|
12
|
+
describe('CacheApi', () => {
|
|
13
|
+
let mockApiFunc;
|
|
14
|
+
let cacheApi;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mockApiFunc = vi.fn();
|
|
17
|
+
cacheApi = new CacheApi(mockApiFunc);
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
describe('构造函数', () => {
|
|
23
|
+
it('应该正确初始化 CacheApi 实例', () => {
|
|
24
|
+
expect(cacheApi).toBeInstanceOf(CacheApi);
|
|
25
|
+
expect(cacheApi.value).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
it('应该正确绑定 send 方法', () => {
|
|
28
|
+
const { send } = cacheApi;
|
|
29
|
+
expect(typeof send).toBe('function');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('value getter', () => {
|
|
33
|
+
it('初始状态下应该返回 undefined', () => {
|
|
34
|
+
expect(cacheApi.value).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
it('应该在成功调用后返回缓存的值', async () => {
|
|
37
|
+
const expectedResult = 'test-result';
|
|
38
|
+
mockApiFunc.mockResolvedValue(expectedResult);
|
|
39
|
+
await cacheApi.send('param1', 123);
|
|
40
|
+
expect(cacheApi.value).toBe(expectedResult);
|
|
41
|
+
});
|
|
42
|
+
it('应该在多次调用后返回最新的缓存值', async () => {
|
|
43
|
+
const result1 = 'result-1';
|
|
44
|
+
const result2 = 'result-2';
|
|
45
|
+
mockApiFunc.mockResolvedValueOnce(result1);
|
|
46
|
+
await cacheApi.send('param1', 123);
|
|
47
|
+
expect(cacheApi.value).toBe(result1);
|
|
48
|
+
mockApiFunc.mockResolvedValueOnce(result2);
|
|
49
|
+
await cacheApi.send('param2', 456);
|
|
50
|
+
expect(cacheApi.value).toBe(result2);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('empty getter', () => {
|
|
54
|
+
it('初始状态下应该返回 true', () => {
|
|
55
|
+
expect(cacheApi.empty).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it('成功调用后应该返回 false', async () => {
|
|
58
|
+
mockApiFunc.mockResolvedValue('test-result');
|
|
59
|
+
await cacheApi.send('param1', 123);
|
|
60
|
+
expect(cacheApi.empty).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
it('即使调用失败也应该保持 true', async () => {
|
|
63
|
+
const error = new Error('API Error');
|
|
64
|
+
mockApiFunc.mockRejectedValue(error);
|
|
65
|
+
try {
|
|
66
|
+
await cacheApi.send('param1', 123);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
// 忽略错误
|
|
70
|
+
}
|
|
71
|
+
expect(cacheApi.empty).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('empty 应该是一个 computed 属性', () => {
|
|
74
|
+
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(cacheApi), 'empty');
|
|
75
|
+
expect(descriptor).toBeDefined();
|
|
76
|
+
expect(descriptor === null || descriptor === void 0 ? void 0 : descriptor.get).toBeDefined();
|
|
77
|
+
expect(descriptor === null || descriptor === void 0 ? void 0 : descriptor.set).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('send 方法', () => {
|
|
81
|
+
it('应该调用传入的 API 函数', async () => {
|
|
82
|
+
const expectedResult = 'api-result';
|
|
83
|
+
mockApiFunc.mockResolvedValue(expectedResult);
|
|
84
|
+
const result = await cacheApi.send('test', 42);
|
|
85
|
+
expect(mockApiFunc).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(mockApiFunc).toHaveBeenCalledWith('test', 42);
|
|
87
|
+
expect(result).toBe(expectedResult);
|
|
88
|
+
});
|
|
89
|
+
it('应该正确传递多个参数', async () => {
|
|
90
|
+
const params = ['hello', 999];
|
|
91
|
+
mockApiFunc.mockResolvedValue('success');
|
|
92
|
+
await cacheApi.send(...params);
|
|
93
|
+
expect(mockApiFunc).toHaveBeenCalledWith(...params);
|
|
94
|
+
});
|
|
95
|
+
it('应该返回 API 函数的结果', async () => {
|
|
96
|
+
const expectedResult = { data: 'test', status: 'ok' };
|
|
97
|
+
mockApiFunc.mockResolvedValue(expectedResult);
|
|
98
|
+
const result = await cacheApi.send('param', 1);
|
|
99
|
+
expect(result).toEqual(expectedResult);
|
|
100
|
+
});
|
|
101
|
+
it('应该处理 API 函数抛出的异常', async () => {
|
|
102
|
+
const error = new Error('API Error');
|
|
103
|
+
mockApiFunc.mockRejectedValue(error);
|
|
104
|
+
await expect(cacheApi.send('param', 1)).rejects.toThrow('API Error');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('缓存功能', () => {
|
|
108
|
+
it('相同参数应该使用缓存', async () => {
|
|
109
|
+
const result = 'cached-result';
|
|
110
|
+
mockApiFunc.mockResolvedValue(result);
|
|
111
|
+
// 第一次调用
|
|
112
|
+
const firstResult = await cacheApi.send('same', 123);
|
|
113
|
+
expect(firstResult).toBe(result);
|
|
114
|
+
expect(cacheApi.value).toBe(result);
|
|
115
|
+
// 第二次调用相同参数,应该从缓存返回
|
|
116
|
+
const secondResult = await cacheApi.send('same', 123);
|
|
117
|
+
expect(secondResult).toBe(result);
|
|
118
|
+
expect(cacheApi.value).toBe(result);
|
|
119
|
+
// API 函数应该被调用两次(SideCache 的 handle 方法会每次都调用)
|
|
120
|
+
expect(mockApiFunc).toHaveBeenCalledTimes(2);
|
|
121
|
+
});
|
|
122
|
+
it('不同参数应该触发新的 API 调用', async () => {
|
|
123
|
+
mockApiFunc.mockResolvedValueOnce('result1');
|
|
124
|
+
mockApiFunc.mockResolvedValueOnce('result2');
|
|
125
|
+
await cacheApi.send('param1', 1);
|
|
126
|
+
await cacheApi.send('param2', 2);
|
|
127
|
+
expect(mockApiFunc).toHaveBeenCalledTimes(2);
|
|
128
|
+
expect(mockApiFunc).toHaveBeenNthCalledWith(1, 'param1', 1);
|
|
129
|
+
expect(mockApiFunc).toHaveBeenNthCalledWith(2, 'param2', 2);
|
|
130
|
+
});
|
|
131
|
+
it('应该正确更新当前缓存的 key', async () => {
|
|
132
|
+
mockApiFunc.mockResolvedValueOnce('value1');
|
|
133
|
+
mockApiFunc.mockResolvedValueOnce('value2');
|
|
134
|
+
await cacheApi.send('key1', 1);
|
|
135
|
+
expect(cacheApi.value).toBe('value1');
|
|
136
|
+
await cacheApi.send('key2', 2);
|
|
137
|
+
expect(cacheApi.value).toBe('value2');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('异步操作', () => {
|
|
141
|
+
it('应该正确处理并发调用', async () => {
|
|
142
|
+
let resolveCount = 0;
|
|
143
|
+
mockApiFunc.mockImplementation(async (param) => {
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
145
|
+
return `result-${param}-${++resolveCount}`;
|
|
146
|
+
});
|
|
147
|
+
const [result1, result2, result3] = await Promise.all([
|
|
148
|
+
cacheApi.send('param1', 1),
|
|
149
|
+
cacheApi.send('param2', 2),
|
|
150
|
+
cacheApi.send('param3', 3)
|
|
151
|
+
]);
|
|
152
|
+
expect(result1).toMatch(/result-param1-\d/);
|
|
153
|
+
expect(result2).toMatch(/result-param2-\d/);
|
|
154
|
+
expect(result3).toMatch(/result-param3-\d/);
|
|
155
|
+
expect(mockApiFunc).toHaveBeenCalledTimes(3);
|
|
156
|
+
});
|
|
157
|
+
it('应该在异步操作完成后更新 value', async () => {
|
|
158
|
+
const delay = 50;
|
|
159
|
+
mockApiFunc.mockImplementation(async () => {
|
|
160
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
161
|
+
return 'delayed-result';
|
|
162
|
+
});
|
|
163
|
+
// 开始异步操作,此时 value 应该还是之前的值
|
|
164
|
+
const promise = cacheApi.send('test', 1);
|
|
165
|
+
// 等待一小段时间,但还没完成
|
|
166
|
+
await new Promise(resolve => setTimeout(resolve, delay / 2));
|
|
167
|
+
// 完成异步操作
|
|
168
|
+
const result = await promise;
|
|
169
|
+
expect(result).toBe('delayed-result');
|
|
170
|
+
expect(cacheApi.value).toBe('delayed-result');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe('边缘情况', () => {
|
|
174
|
+
it('应该处理 null 返回值', async () => {
|
|
175
|
+
mockApiFunc.mockResolvedValue(null);
|
|
176
|
+
const result = await cacheApi.send('test', 1);
|
|
177
|
+
expect(result).toBeNull();
|
|
178
|
+
expect(cacheApi.value).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
it('应该处理 undefined 返回值', async () => {
|
|
181
|
+
mockApiFunc.mockResolvedValue(undefined);
|
|
182
|
+
const result = await cacheApi.send('test', 1);
|
|
183
|
+
expect(result).toBeUndefined();
|
|
184
|
+
expect(cacheApi.value).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
it('应该处理复杂对象参数', async () => {
|
|
187
|
+
const complexParam = { nested: { value: 'test' }, array: [1, 2, 3] };
|
|
188
|
+
const complexMockFunc = vi.fn();
|
|
189
|
+
complexMockFunc.mockResolvedValue('complex-result');
|
|
190
|
+
// 创建一个接受复杂参数的 CacheApi
|
|
191
|
+
const complexCacheApi = new CacheApi(complexMockFunc);
|
|
192
|
+
const result = await complexCacheApi.send(complexParam);
|
|
193
|
+
expect(result).toBe('complex-result');
|
|
194
|
+
expect(complexMockFunc).toHaveBeenCalledWith(complexParam);
|
|
195
|
+
});
|
|
196
|
+
it('should handle no parameters', async () => {
|
|
197
|
+
const noParamApi = new CacheApi(() => Promise.resolve('no-param-result'));
|
|
198
|
+
const result = await noParamApi.send();
|
|
199
|
+
expect(result).toBe('no-param-result');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe('MobX 集成', () => {
|
|
203
|
+
it('value 应该是一个 computed 属性', () => {
|
|
204
|
+
// 通过检查属性描述符来验证这是一个 computed 属性
|
|
205
|
+
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(cacheApi), 'value');
|
|
206
|
+
expect(descriptor).toBeDefined();
|
|
207
|
+
expect(descriptor === null || descriptor === void 0 ? void 0 : descriptor.get).toBeDefined();
|
|
208
|
+
expect(descriptor === null || descriptor === void 0 ? void 0 : descriptor.set).toBeUndefined();
|
|
209
|
+
});
|
|
210
|
+
it('value 变化应该触发 MobX 反应', async () => {
|
|
211
|
+
let reactionCount = 0;
|
|
212
|
+
let lastValue = undefined;
|
|
213
|
+
// 创建一个简单的反应来监听 value 变化
|
|
214
|
+
const disposer = runInAction(() => {
|
|
215
|
+
const reaction = () => {
|
|
216
|
+
reactionCount++;
|
|
217
|
+
lastValue = cacheApi.value;
|
|
218
|
+
};
|
|
219
|
+
// 初始调用
|
|
220
|
+
reaction();
|
|
221
|
+
return reaction;
|
|
222
|
+
});
|
|
223
|
+
mockApiFunc.mockResolvedValue('new-value');
|
|
224
|
+
await cacheApi.send('test', 1);
|
|
225
|
+
// 清理
|
|
226
|
+
if (typeof disposer === 'function') {
|
|
227
|
+
disposer();
|
|
228
|
+
}
|
|
229
|
+
expect(lastValue).toBe('new-value');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
describe('类型安全', () => {
|
|
233
|
+
it('应该保持参数类型安全', () => {
|
|
234
|
+
// 这个测试主要是在编译时进行类型检查
|
|
235
|
+
// 如果类型不匹配,TypeScript 编译器会报错
|
|
236
|
+
const typedApi = new CacheApi((str, num, bool) => {
|
|
237
|
+
return Promise.resolve({ result: `${str}-${num}-${bool}` });
|
|
238
|
+
});
|
|
239
|
+
// 正确的调用
|
|
240
|
+
expect(() => typedApi.send('test', 42, true)).not.toThrow();
|
|
241
|
+
// 以下调用在 TypeScript 中会产生编译错误(在运行时测试中跳过)
|
|
242
|
+
// typedApi.send('test', 'wrong-type', true) // 第二个参数应该是 number
|
|
243
|
+
// typedApi.send('test', 42) // 缺少第三个参数
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
describe('loading 状态', () => {
|
|
247
|
+
it('初始状态下 loading 应该为 false', () => {
|
|
248
|
+
expect(cacheApi.loading).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
it('发送请求时 loading 应该为 true', async () => {
|
|
251
|
+
let resolveFn;
|
|
252
|
+
const promise = new Promise((resolve) => {
|
|
253
|
+
resolveFn = resolve;
|
|
254
|
+
});
|
|
255
|
+
mockApiFunc.mockReturnValue(promise);
|
|
256
|
+
// 开始请求
|
|
257
|
+
const sendPromise = cacheApi.send('test', 1);
|
|
258
|
+
// 此时应该是 loading 状态
|
|
259
|
+
expect(cacheApi.loading).toBe(true);
|
|
260
|
+
// 完成请求
|
|
261
|
+
resolveFn('result');
|
|
262
|
+
await sendPromise;
|
|
263
|
+
// 请求完成后 loading 应该为 false
|
|
264
|
+
expect(cacheApi.loading).toBe(false);
|
|
265
|
+
});
|
|
266
|
+
it('请求失败后 loading 应该为 false', async () => {
|
|
267
|
+
const error = new Error('API Error');
|
|
268
|
+
mockApiFunc.mockRejectedValue(error);
|
|
269
|
+
try {
|
|
270
|
+
await cacheApi.send('test', 1);
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
// 忽略错误
|
|
274
|
+
}
|
|
275
|
+
expect(cacheApi.loading).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
it('并发请求时 loading 状态应该正确处理', async () => {
|
|
278
|
+
const resolvers = [];
|
|
279
|
+
mockApiFunc.mockImplementation((param) => {
|
|
280
|
+
return new Promise((resolve) => {
|
|
281
|
+
resolvers.push((value) => resolve(`${param}-${value}`));
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
// 启动三个并发请求
|
|
285
|
+
const promise1 = cacheApi.send('req1', 1);
|
|
286
|
+
const promise2 = cacheApi.send('req2', 2);
|
|
287
|
+
const promise3 = cacheApi.send('req3', 3);
|
|
288
|
+
// 此时应该有三个请求正在进行,loading 为 true
|
|
289
|
+
expect(cacheApi.loading).toBe(true);
|
|
290
|
+
// 完成第一个请求
|
|
291
|
+
resolvers[0]('result1');
|
|
292
|
+
await promise1;
|
|
293
|
+
// 还有两个请求在进行,loading 仍为 true
|
|
294
|
+
expect(cacheApi.loading).toBe(true);
|
|
295
|
+
// 完成第二个请求
|
|
296
|
+
resolvers[1]('result2');
|
|
297
|
+
await promise2;
|
|
298
|
+
// 还有一个请求在进行,loading 仍为 true
|
|
299
|
+
expect(cacheApi.loading).toBe(true);
|
|
300
|
+
// 完成第三个请求
|
|
301
|
+
resolvers[2]('result3');
|
|
302
|
+
await promise3;
|
|
303
|
+
// 所有请求完成,loading 为 false
|
|
304
|
+
expect(cacheApi.loading).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
it('部分并发请求失败时 loading 状态应该正确', async () => {
|
|
307
|
+
let resolveSuccess;
|
|
308
|
+
let rejectError;
|
|
309
|
+
mockApiFunc.mockImplementation((param) => {
|
|
310
|
+
if (param === 'success') {
|
|
311
|
+
return new Promise((resolve) => {
|
|
312
|
+
resolveSuccess = resolve;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
return new Promise((_, reject) => {
|
|
317
|
+
rejectError = reject;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
// 启动两个请求,一个成功一个失败
|
|
322
|
+
const successPromise = cacheApi.send('success', 1);
|
|
323
|
+
const errorPromise = cacheApi.send('error', 2);
|
|
324
|
+
expect(cacheApi.loading).toBe(true);
|
|
325
|
+
// 先让失败的请求完成
|
|
326
|
+
rejectError(new Error('Failed'));
|
|
327
|
+
try {
|
|
328
|
+
await errorPromise;
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
// 忽略错误
|
|
332
|
+
}
|
|
333
|
+
// 还有一个成功的请求在进行,loading 仍为 true
|
|
334
|
+
expect(cacheApi.loading).toBe(true);
|
|
335
|
+
// 完成成功的请求
|
|
336
|
+
resolveSuccess('success-result');
|
|
337
|
+
await successPromise;
|
|
338
|
+
// 所有请求完成,loading 为 false
|
|
339
|
+
expect(cacheApi.loading).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
it('loading 应该是一个 computed 属性', () => {
|
|
342
|
+
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(cacheApi), 'loading');
|
|
343
|
+
expect(descriptor).toBeDefined();
|
|
344
|
+
expect(descriptor === null || descriptor === void 0 ? void 0 : descriptor.get).toBeDefined();
|
|
345
|
+
expect(descriptor === null || descriptor === void 0 ? void 0 : descriptor.set).toBeUndefined();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../source/helpers/cache-api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CacheApi } from './cache-api';
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export declare class SideCache<T> {
|
|
2
|
+
private accessor emptyFlag;
|
|
2
3
|
private accessor currentKey;
|
|
3
4
|
private accessor cache;
|
|
4
|
-
get value(): T |
|
|
5
|
-
|
|
5
|
+
get value(): T | undefined;
|
|
6
|
+
get empty(): boolean;
|
|
7
|
+
handle<F extends ((...args: unknown[]) => T)>(key: unknown, func: F): T;
|
|
8
|
+
handle<F extends ((...args: unknown[]) => Promise<T> | T)>(key: unknown, func: F): Promise<T> | T;
|
|
6
9
|
}
|
|
7
10
|
//# sourceMappingURL=side-cache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"side-cache.d.ts","sourceRoot":"","sources":["../../../source/helpers/side-cache/side-cache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"side-cache.d.ts","sourceRoot":"","sources":["../../../source/helpers/side-cache/side-cache.ts"],"names":[],"mappings":"AAWA,qBAAa,SAAS,CAAC,CAAC;IAEtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAG1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;IAG/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAE1D,IACW,KAAK,IAAI,CAAC,GAAG,SAAS,CAIhC;IAED,IACW,KAAK,IAAI,OAAO,CAE1B;IAED,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC;IACvE,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;CAuClG"}
|
|
@@ -44,10 +44,16 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
|
|
|
44
44
|
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
45
45
|
};
|
|
46
46
|
import { action, computed, observable, runInAction } from 'mobx';
|
|
47
|
+
function isPromiseLike(value) {
|
|
48
|
+
return value != null && typeof value.then === 'function';
|
|
49
|
+
}
|
|
47
50
|
let SideCache = (() => {
|
|
48
|
-
var _a, _SideCache_currentKey_accessor_storage, _SideCache_cache_accessor_storage;
|
|
51
|
+
var _a, _SideCache_emptyFlag_accessor_storage, _SideCache_currentKey_accessor_storage, _SideCache_cache_accessor_storage;
|
|
49
52
|
var _b, _c;
|
|
50
53
|
let _instanceExtraInitializers = [];
|
|
54
|
+
let _emptyFlag_decorators;
|
|
55
|
+
let _emptyFlag_initializers = [];
|
|
56
|
+
let _emptyFlag_extraInitializers = [];
|
|
51
57
|
let _currentKey_decorators;
|
|
52
58
|
let _currentKey_initializers = [];
|
|
53
59
|
let _currentKey_extraInitializers = [];
|
|
@@ -55,25 +61,44 @@ let SideCache = (() => {
|
|
|
55
61
|
let _cache_initializers = [];
|
|
56
62
|
let _cache_extraInitializers = [];
|
|
57
63
|
let _get_value_decorators;
|
|
64
|
+
let _get_empty_decorators;
|
|
58
65
|
let _handle_decorators;
|
|
59
66
|
return _a = class SideCache {
|
|
67
|
+
get emptyFlag() { return __classPrivateFieldGet(this, _SideCache_emptyFlag_accessor_storage, "f"); }
|
|
68
|
+
set emptyFlag(value) { __classPrivateFieldSet(this, _SideCache_emptyFlag_accessor_storage, value, "f"); }
|
|
60
69
|
get currentKey() { return __classPrivateFieldGet(this, _SideCache_currentKey_accessor_storage, "f"); }
|
|
61
70
|
set currentKey(value) { __classPrivateFieldSet(this, _SideCache_currentKey_accessor_storage, value, "f"); }
|
|
62
71
|
get cache() { return __classPrivateFieldGet(this, _SideCache_cache_accessor_storage, "f"); }
|
|
63
72
|
set cache(value) { __classPrivateFieldSet(this, _SideCache_cache_accessor_storage, value, "f"); }
|
|
64
73
|
get value() {
|
|
65
|
-
var _b;
|
|
66
74
|
if (this.currentKey == null)
|
|
67
|
-
return
|
|
68
|
-
|
|
75
|
+
return undefined;
|
|
76
|
+
const cacheItem = this.cache[this.currentKey];
|
|
77
|
+
return cacheItem ? cacheItem.data : undefined;
|
|
78
|
+
}
|
|
79
|
+
get empty() {
|
|
80
|
+
return this.emptyFlag;
|
|
69
81
|
}
|
|
70
|
-
|
|
71
|
-
|
|
82
|
+
handle(key, func) {
|
|
83
|
+
var _b;
|
|
84
|
+
// 不需要跨设备、软件一致,所以直接用 JSON.stringify
|
|
85
|
+
// 但需要处理 JSON.stringify(undefined) 返回 undefined 的情况
|
|
86
|
+
const keyStringify = (_b = JSON.stringify(key)) !== null && _b !== void 0 ? _b : 'undefined';
|
|
87
|
+
// 这会让外面立马可以拿到之前缓存的值,如果存在的话
|
|
72
88
|
runInAction(() => this.currentKey = keyStringify);
|
|
73
|
-
const funcReturn =
|
|
74
|
-
if (funcReturn
|
|
75
|
-
return funcReturn
|
|
89
|
+
const funcReturn = func();
|
|
90
|
+
if (isPromiseLike(funcReturn)) {
|
|
91
|
+
return funcReturn.then(result => runInAction(() => {
|
|
92
|
+
this.emptyFlag = false;
|
|
93
|
+
this.cache = Object.assign(Object.assign({}, this.cache), { [keyStringify]: {
|
|
94
|
+
data: result,
|
|
95
|
+
createTime: new Date().toISOString()
|
|
96
|
+
} });
|
|
97
|
+
return result;
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
76
100
|
runInAction(() => {
|
|
101
|
+
this.emptyFlag = false;
|
|
77
102
|
this.cache = Object.assign(Object.assign({}, this.cache), { [keyStringify]: {
|
|
78
103
|
data: funcReturn,
|
|
79
104
|
createTime: new Date().toISOString()
|
|
@@ -82,22 +107,28 @@ let SideCache = (() => {
|
|
|
82
107
|
return funcReturn;
|
|
83
108
|
}
|
|
84
109
|
constructor() {
|
|
85
|
-
|
|
110
|
+
_SideCache_emptyFlag_accessor_storage.set(this, (__runInitializers(this, _instanceExtraInitializers), __runInitializers(this, _emptyFlag_initializers, true)));
|
|
111
|
+
_SideCache_currentKey_accessor_storage.set(this, (__runInitializers(this, _emptyFlag_extraInitializers), __runInitializers(this, _currentKey_initializers, void 0)));
|
|
86
112
|
_SideCache_cache_accessor_storage.set(this, (__runInitializers(this, _currentKey_extraInitializers), __runInitializers(this, _cache_initializers, {})));
|
|
87
113
|
__runInitializers(this, _cache_extraInitializers);
|
|
88
114
|
}
|
|
89
115
|
},
|
|
116
|
+
_SideCache_emptyFlag_accessor_storage = new WeakMap(),
|
|
90
117
|
_SideCache_currentKey_accessor_storage = new WeakMap(),
|
|
91
118
|
_SideCache_cache_accessor_storage = new WeakMap(),
|
|
92
119
|
(() => {
|
|
93
120
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
121
|
+
_emptyFlag_decorators = [observable];
|
|
94
122
|
_currentKey_decorators = [observable];
|
|
95
123
|
_cache_decorators = [(_b = observable).ref.bind(_b)];
|
|
96
124
|
_get_value_decorators = [computed];
|
|
125
|
+
_get_empty_decorators = [computed];
|
|
97
126
|
_handle_decorators = [(_c = action).bound.bind(_c)];
|
|
127
|
+
__esDecorate(_a, null, _emptyFlag_decorators, { kind: "accessor", name: "emptyFlag", static: false, private: false, access: { has: obj => "emptyFlag" in obj, get: obj => obj.emptyFlag, set: (obj, value) => { obj.emptyFlag = value; } }, metadata: _metadata }, _emptyFlag_initializers, _emptyFlag_extraInitializers);
|
|
98
128
|
__esDecorate(_a, null, _currentKey_decorators, { kind: "accessor", name: "currentKey", static: false, private: false, access: { has: obj => "currentKey" in obj, get: obj => obj.currentKey, set: (obj, value) => { obj.currentKey = value; } }, metadata: _metadata }, _currentKey_initializers, _currentKey_extraInitializers);
|
|
99
129
|
__esDecorate(_a, null, _cache_decorators, { kind: "accessor", name: "cache", static: false, private: false, access: { has: obj => "cache" in obj, get: obj => obj.cache, set: (obj, value) => { obj.cache = value; } }, metadata: _metadata }, _cache_initializers, _cache_extraInitializers);
|
|
100
130
|
__esDecorate(_a, null, _get_value_decorators, { kind: "getter", name: "value", static: false, private: false, access: { has: obj => "value" in obj, get: obj => obj.value }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
131
|
+
__esDecorate(_a, null, _get_empty_decorators, { kind: "getter", name: "empty", static: false, private: false, access: { has: obj => "empty" in obj, get: obj => obj.empty }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
101
132
|
__esDecorate(_a, null, _handle_decorators, { kind: "method", name: "handle", static: false, private: false, access: { has: obj => "handle" in obj, get: obj => obj.handle }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
102
133
|
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
103
134
|
})(),
|
|
@@ -1,89 +1,179 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { SideCache } from './side-cache';
|
|
3
3
|
describe('SideCache', () => {
|
|
4
|
-
let
|
|
4
|
+
let cache;
|
|
5
5
|
beforeEach(() => {
|
|
6
|
-
|
|
6
|
+
cache = new SideCache();
|
|
7
7
|
});
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
describe('初始状态', () => {
|
|
9
|
+
it('应该初始化为空状态', () => {
|
|
10
|
+
expect(cache.empty).toBe(true);
|
|
11
|
+
expect(cache.value).toBe(undefined);
|
|
12
|
+
});
|
|
11
13
|
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
describe('同步缓存', () => {
|
|
15
|
+
it('应该能够缓存同步函数的结果', () => {
|
|
16
|
+
const mockFunc = vi.fn(() => 'test-result');
|
|
17
|
+
const key = 'test-key';
|
|
18
|
+
const result = cache.handle(key, mockFunc);
|
|
19
|
+
expect(result).toBe('test-result');
|
|
20
|
+
expect(mockFunc).toHaveBeenCalledTimes(1);
|
|
21
|
+
expect(cache.empty).toBe(false);
|
|
22
|
+
expect(cache.value).toBe('test-result');
|
|
23
|
+
});
|
|
24
|
+
it('应该对相同 key 返回缓存的结果', () => {
|
|
25
|
+
const mockFunc = vi.fn(() => 'cached-result');
|
|
26
|
+
const key = 'same-key';
|
|
27
|
+
// 第一次调用
|
|
28
|
+
const result1 = cache.handle(key, mockFunc);
|
|
29
|
+
expect(result1).toBe('cached-result');
|
|
30
|
+
expect(mockFunc).toHaveBeenCalledTimes(1);
|
|
31
|
+
// 第二次调用相同 key,应该直接返回缓存
|
|
32
|
+
const result2 = cache.handle(key, mockFunc);
|
|
33
|
+
expect(result2).toBe('cached-result');
|
|
34
|
+
expect(mockFunc).toHaveBeenCalledTimes(2); // 函数仍会被调用,但会更新缓存
|
|
35
|
+
expect(cache.value).toBe('cached-result');
|
|
36
|
+
});
|
|
37
|
+
it('应该对不同 key 分别缓存', () => {
|
|
38
|
+
const mockFunc1 = vi.fn(() => 'result1');
|
|
39
|
+
const mockFunc2 = vi.fn(() => 'result2');
|
|
40
|
+
cache.handle('key1', mockFunc1);
|
|
41
|
+
expect(cache.value).toBe('result1');
|
|
42
|
+
cache.handle('key2', mockFunc2);
|
|
43
|
+
expect(cache.value).toBe('result2');
|
|
44
|
+
// 切换回 key1
|
|
45
|
+
cache.handle('key1', mockFunc1);
|
|
46
|
+
expect(cache.value).toBe('result1');
|
|
47
|
+
});
|
|
19
48
|
});
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
describe('异步缓存', () => {
|
|
50
|
+
it('应该能够缓存异步函数的结果', async () => {
|
|
51
|
+
const mockAsyncFunc = vi.fn(async () => 'async-result');
|
|
52
|
+
const key = 'async-key';
|
|
53
|
+
const promise = cache.handle(key, mockAsyncFunc);
|
|
54
|
+
expect(promise).toBeInstanceOf(Promise);
|
|
55
|
+
const result = await promise;
|
|
56
|
+
expect(result).toBe('async-result');
|
|
57
|
+
expect(mockAsyncFunc).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(cache.empty).toBe(false);
|
|
59
|
+
expect(cache.value).toBe('async-result');
|
|
60
|
+
});
|
|
61
|
+
it('应该在异步函数执行期间立即设置 currentKey', async () => {
|
|
62
|
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
63
|
+
const mockAsyncFunc = vi.fn(async () => {
|
|
64
|
+
await delay(10);
|
|
65
|
+
return 'delayed-result';
|
|
66
|
+
});
|
|
67
|
+
const key = 'delayed-key';
|
|
68
|
+
// 在异步函数开始执行时,currentKey 应该已经设置
|
|
69
|
+
const promise = cache.handle(key, mockAsyncFunc);
|
|
70
|
+
// 此时异步函数还在执行,但 currentKey 已设置
|
|
71
|
+
// 如果之前有缓存,value 应该能立即获取到
|
|
72
|
+
const result = await promise;
|
|
73
|
+
expect(result).toBe('delayed-result');
|
|
74
|
+
expect(cache.value).toBe('delayed-result');
|
|
75
|
+
});
|
|
76
|
+
it('应该处理异步函数的错误', async () => {
|
|
77
|
+
const error = new Error('Async error');
|
|
78
|
+
const mockAsyncFunc = vi.fn(async () => {
|
|
79
|
+
throw error;
|
|
80
|
+
});
|
|
81
|
+
const key = 'error-key';
|
|
82
|
+
await expect(cache.handle(key, mockAsyncFunc)).rejects.toThrow('Async error');
|
|
83
|
+
expect(mockAsyncFunc).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
29
85
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
86
|
+
describe('key 序列化', () => {
|
|
87
|
+
it('应该正确处理不同类型的 key', () => {
|
|
88
|
+
const mockFunc = vi.fn(() => 'result');
|
|
89
|
+
// 字符串 key
|
|
90
|
+
cache.handle('string-key', mockFunc);
|
|
91
|
+
expect(cache.value).toBe('result');
|
|
92
|
+
// 数字 key
|
|
93
|
+
cache.handle(123, mockFunc);
|
|
94
|
+
expect(cache.value).toBe('result');
|
|
95
|
+
// 对象 key
|
|
96
|
+
cache.handle({ id: 1, name: 'test' }, mockFunc);
|
|
97
|
+
expect(cache.value).toBe('result');
|
|
98
|
+
// 数组 key
|
|
99
|
+
cache.handle([1, 2, 3], mockFunc);
|
|
100
|
+
expect(cache.value).toBe('result');
|
|
101
|
+
});
|
|
102
|
+
it('相同内容的对象 key 应该被视为相同', () => {
|
|
103
|
+
const mockFunc = vi.fn(() => 'object-result');
|
|
104
|
+
const key1 = { id: 1, name: 'test' };
|
|
105
|
+
const key2 = { id: 1, name: 'test' };
|
|
106
|
+
cache.handle(key1, mockFunc);
|
|
107
|
+
const result1 = cache.value;
|
|
108
|
+
cache.handle(key2, mockFunc);
|
|
109
|
+
const result2 = cache.value;
|
|
110
|
+
expect(result1).toBe(result2);
|
|
111
|
+
expect(result1).toBe('object-result');
|
|
112
|
+
});
|
|
36
113
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
114
|
+
describe('缓存时间戳', () => {
|
|
115
|
+
it('应该为缓存项添加创建时间', () => {
|
|
116
|
+
const mockFunc = vi.fn(() => 'timestamped-result');
|
|
117
|
+
const key = 'timestamp-key';
|
|
118
|
+
const beforeTime = new Date().toISOString();
|
|
119
|
+
cache.handle(key, mockFunc);
|
|
120
|
+
const afterTime = new Date().toISOString();
|
|
121
|
+
// 访问私有属性进行测试(仅用于测试目的)
|
|
122
|
+
const cacheData = cache.cache;
|
|
123
|
+
const cachedItem = cacheData[JSON.stringify(key)];
|
|
124
|
+
expect(cachedItem).toBeDefined();
|
|
125
|
+
expect(cachedItem.data).toBe('timestamped-result');
|
|
126
|
+
expect(cachedItem.createTime).toBeDefined();
|
|
127
|
+
expect(typeof cachedItem.createTime).toBe('string');
|
|
128
|
+
// 验证时间戳在合理范围内
|
|
129
|
+
expect(cachedItem.createTime >= beforeTime).toBe(true);
|
|
130
|
+
expect(cachedItem.createTime <= afterTime).toBe(true);
|
|
131
|
+
});
|
|
50
132
|
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
133
|
+
describe('边界情况', () => {
|
|
134
|
+
it('应该处理 null 和 undefined key', () => {
|
|
135
|
+
const mockFunc1 = vi.fn(() => 'null-key-result');
|
|
136
|
+
const mockFunc2 = vi.fn(() => 'undefined-key-result');
|
|
137
|
+
cache.handle(null, mockFunc1);
|
|
138
|
+
expect(cache.value).toBe('null-key-result');
|
|
139
|
+
cache.handle(undefined, mockFunc2);
|
|
140
|
+
expect(cache.value).toBe('undefined-key-result');
|
|
141
|
+
});
|
|
142
|
+
it('应该处理返回 null 或 undefined 的函数', () => {
|
|
143
|
+
const nullFunc = vi.fn(() => null);
|
|
144
|
+
const undefinedFunc = vi.fn(() => undefined);
|
|
145
|
+
cache.handle('null-key', nullFunc);
|
|
146
|
+
expect(cache.value).toBe(null);
|
|
147
|
+
expect(cache.empty).toBe(false);
|
|
148
|
+
cache.handle('undefined-key', undefinedFunc);
|
|
149
|
+
expect(cache.value).toBe(undefined);
|
|
150
|
+
expect(cache.empty).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
it('应该处理返回 Promise.resolve(null) 的异步函数', async () => {
|
|
153
|
+
const nullAsyncFunc = vi.fn(async () => null);
|
|
154
|
+
const result = await cache.handle('async-null', nullAsyncFunc);
|
|
155
|
+
expect(result).toBe(null);
|
|
156
|
+
expect(cache.value).toBe(null);
|
|
157
|
+
expect(cache.empty).toBe(false);
|
|
158
|
+
});
|
|
71
159
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
160
|
+
describe('类型检查', () => {
|
|
161
|
+
it('应该正确识别 Promise-like 对象', async () => {
|
|
162
|
+
let thenCallback;
|
|
163
|
+
const promiseLike = {
|
|
164
|
+
then: vi.fn((callback) => {
|
|
165
|
+
thenCallback = callback;
|
|
166
|
+
return promiseLike;
|
|
167
|
+
})
|
|
168
|
+
};
|
|
169
|
+
const mockFunc = vi.fn(() => promiseLike);
|
|
170
|
+
const result = cache.handle('promise-like', mockFunc);
|
|
171
|
+
expect(result).toBe(promiseLike);
|
|
172
|
+
expect(promiseLike.then).toHaveBeenCalled();
|
|
173
|
+
// 模拟 Promise 完成
|
|
174
|
+
if (thenCallback) {
|
|
175
|
+
thenCallback('resolved-value');
|
|
176
|
+
}
|
|
177
|
+
});
|
|
88
178
|
});
|
|
89
179
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taicode/common-web",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"author": "Alain",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"description": "",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"test": "vitest",
|
|
19
19
|
"test:ui": "vitest --ui",
|
|
20
20
|
"test:run": "vitest run",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
21
22
|
"build": "tsc -p tsconfig.json",
|
|
22
23
|
"dev": "tsc -p tsconfig.json --watch"
|
|
23
24
|
},
|