@shihengtech/utils 0.0.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/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @shihengtech/utils
2
+
3
+ 一个轻量级的 JavaScript/TypeScript 工具函数库。
4
+
5
+ ## ✨ 特性
6
+
7
+ - 🚀 **轻量高效** - 精简的代码实现,最小化包体积
8
+ - 📦 **双格式支持** - 同时支持 ESM 和 CommonJS
9
+ - 🔒 **类型安全** - 使用 TypeScript 编写,提供完整的类型定义
10
+ - 🌲 **Tree Shaking** - 支持按需引入,减少打包体积
11
+ - ✅ **完善测试** - 高覆盖率的单元测试
12
+
13
+ ## 📦 安装
14
+
15
+ ```bash
16
+ # npm
17
+ npm install @shihengtech/utils
18
+
19
+ # pnpm
20
+ pnpm add @shihengtech/utils
21
+
22
+ # yarn
23
+ yarn add @shihengtech/utils
24
+ ```
25
+
26
+ ## 🔨 使用
27
+
28
+ ```typescript
29
+ import { isEmpty, isString } from '@shihengtech/utils'
30
+
31
+ // 类型判断
32
+ isString('hello') // true
33
+ isString(123) // false
34
+
35
+ // 空值检查
36
+ isEmpty('') // true
37
+ isEmpty([]) // true
38
+ isEmpty({}) // true
39
+ ```
40
+
41
+ ## 📖 文档
42
+
43
+ 查看完整文档:[文档站点](./docs)
44
+
45
+ ## 🛠️ 开发
46
+
47
+ ```bash
48
+ # 安装依赖
49
+ pnpm install
50
+
51
+ # 开发模式(监听文件变化)
52
+ pnpm dev
53
+
54
+ # 构建
55
+ pnpm build
56
+
57
+ # 运行测试
58
+ pnpm test
59
+
60
+ # 运行测试(单次)
61
+ pnpm test:run
62
+
63
+ # 运行测试(覆盖率)
64
+ pnpm test:coverage
65
+
66
+ # 启动文档开发服务器
67
+ pnpm docs:dev
68
+
69
+ # 构建文档
70
+ pnpm docs:build
71
+ ```
72
+
73
+ ## 📁 项目结构
74
+
75
+ ```
76
+ @shihengtech/utils/
77
+ ├── src/ # 源代码
78
+ │ ├── index.ts # 入口文件
79
+ │ ├── is.ts # 类型判断工具
80
+ │ └── is.test.ts # 测试文件
81
+ ├── docs/ # VitePress 文档
82
+ │ ├── .vitepress/ # VitePress 配置
83
+ │ ├── guide/ # 使用指南
84
+ │ └── api/ # API 文档
85
+ ├── dist/ # 构建输出(git ignored)
86
+ ├── tsconfig.json # TypeScript 配置
87
+ ├── tsup.config.ts # 构建配置
88
+ ├── vitest.config.ts # 测试配置
89
+ └── package.json
90
+ ```
91
+
92
+ ## 📄 License
93
+
94
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ var localforage = require('localforage');
4
+ var ramda = require('ramda');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var localforage__default = /*#__PURE__*/_interopDefault(localforage);
9
+
10
+ // src/class/ReplaySubject/index.ts
11
+ var ReplaySubject = class {
12
+ constructor(maxBufferSize = 1) {
13
+ this.buffer = [];
14
+ this.subscriptions = [];
15
+ this.maxBufferSize = maxBufferSize;
16
+ }
17
+ subscribe(fn) {
18
+ this.subscriptions.push(fn);
19
+ this.buffer.forEach((data) => fn(data));
20
+ return () => this.unsubscribe(fn);
21
+ }
22
+ unsubscribe(fn) {
23
+ this.subscriptions = this.subscriptions.filter((f) => f !== fn);
24
+ }
25
+ next(data) {
26
+ this.subscriptions.forEach((fn) => fn(data));
27
+ this.buffer.push(data);
28
+ if (this.buffer.length > this.maxBufferSize) {
29
+ this.buffer = this.buffer.slice(1);
30
+ }
31
+ }
32
+ };
33
+
34
+ // src/utils/fnRunner/index.ts
35
+ async function fnRunner(fn, retryTimes = 0) {
36
+ const times = (retryTimes || 0) + 1;
37
+ for (let i = 0; i < times; i++) {
38
+ try {
39
+ return await fn();
40
+ } catch (error) {
41
+ if (i === times - 1) {
42
+ throw error;
43
+ }
44
+ }
45
+ }
46
+ throw new Error("fnRunner: Unexpected error - all retries exhausted.");
47
+ }
48
+
49
+ // src/utils/createQueryWithCache/index.ts
50
+ var db = localforage__default.default.createInstance({
51
+ name: "sh-common-cache"
52
+ // driver: [localforage.LOCALSTORAGE, localforage.INDEXEDDB, localforage.WEBSQL],
53
+ });
54
+ function createQueryWithCache(key, fn, options) {
55
+ const {
56
+ retry,
57
+ maxAge,
58
+ version: version2,
59
+ compareBeforeUpdate,
60
+ remoteMemoryCache,
61
+ equals,
62
+ cacheEnabled,
63
+ beforeRequest,
64
+ onSuccess,
65
+ onError,
66
+ errorHandler
67
+ } = {
68
+ equals: (a, b) => a.length === b.length && a.every((item, index) => item === b[index]),
69
+ retry: 0,
70
+ compareBeforeUpdate: ramda.equals,
71
+ remoteMemoryCache: false,
72
+ ...options
73
+ };
74
+ const cacheUpdateSubject = new ReplaySubject(1);
75
+ const getLocalCache = async (args) => {
76
+ const cache = await db.getItem(key);
77
+ if (cache && (!version2 || cache.version === version2) && (!cache.expires || cache.expires > Date.now()) && equals(cache.args, args)) {
78
+ return cache.result;
79
+ }
80
+ throw new Error("Cache not found");
81
+ };
82
+ const remoteResultCache = {
83
+ success: false,
84
+ args: [],
85
+ result: null
86
+ };
87
+ const clearMemoryCache = () => {
88
+ remoteResultCache.success = false;
89
+ remoteResultCache.result = null;
90
+ remoteResultCache.args = [];
91
+ };
92
+ const queryWithMemoryCache = (args) => {
93
+ if (remoteResultCache.success && equals(remoteResultCache.args, args)) {
94
+ return {
95
+ type: "memory",
96
+ promiseResult: remoteResultCache.result
97
+ };
98
+ }
99
+ remoteResultCache.args = args;
100
+ remoteResultCache.success = true;
101
+ const result = remoteResultCache.result = fnRunner(
102
+ () => fn(...args),
103
+ retry
104
+ );
105
+ Promise.resolve(result).then((result2) => {
106
+ !remoteMemoryCache && clearMemoryCache();
107
+ return result2;
108
+ }).catch(clearMemoryCache);
109
+ return { type: "remote", promiseResult: result };
110
+ };
111
+ const queryRemote = async (args) => {
112
+ const rs = {
113
+ type: "remote",
114
+ result: null
115
+ };
116
+ try {
117
+ const { type, promiseResult } = queryWithMemoryCache(args);
118
+ const result = rs.result = await promiseResult;
119
+ type === "remote" && (!cacheEnabled || await cacheEnabled(args)) && Promise.resolve().then(async () => {
120
+ const oldCache = await getLocalCache(args).catch(() => null);
121
+ const cacheData = {
122
+ args,
123
+ result,
124
+ ...version2 && { version: version2 },
125
+ ...maxAge && { expires: Date.now() + maxAge }
126
+ };
127
+ db.setItem(key, cacheData);
128
+ if (compareBeforeUpdate && oldCache && compareBeforeUpdate(oldCache.result, result)) {
129
+ return;
130
+ }
131
+ cacheUpdateSubject.next({
132
+ result,
133
+ cacheData,
134
+ isCacheHit: !!oldCache
135
+ });
136
+ });
137
+ } catch (error) {
138
+ if (errorHandler) {
139
+ rs.result = await errorHandler(error);
140
+ } else {
141
+ throw error;
142
+ }
143
+ }
144
+ return rs.result;
145
+ };
146
+ const _fn = async (...args) => {
147
+ await beforeRequest?.();
148
+ const [cacheResult, remoteResult] = [
149
+ getLocalCache(args),
150
+ queryRemote(args)
151
+ ];
152
+ cacheResult.then(
153
+ (result2) => onSuccess?.({
154
+ type: "local",
155
+ result: result2
156
+ })
157
+ ).catch(() => {
158
+ });
159
+ remoteResult.then(
160
+ (result2) => onSuccess?.({
161
+ type: "remote",
162
+ result: result2
163
+ })
164
+ ).catch((error) => onError?.(error)).catch(() => {
165
+ });
166
+ const result = await Promise.race([
167
+ cacheResult.catch(() => remoteResult),
168
+ remoteResult
169
+ ]);
170
+ return result;
171
+ };
172
+ _fn.subscribeCacheUpdate = (fn2) => cacheUpdateSubject.subscribe(fn2);
173
+ _fn.clearCache = async () => {
174
+ clearMemoryCache();
175
+ await db.removeItem(key);
176
+ };
177
+ return _fn;
178
+ }
179
+ createQueryWithCache.useDb = (newDb) => {
180
+ db = newDb;
181
+ };
182
+
183
+ // src/index.ts
184
+ var version = "0.0.1";
185
+
186
+ exports.ReplaySubject = ReplaySubject;
187
+ exports.createQueryWithCache = createQueryWithCache;
188
+ exports.fnRunner = fnRunner;
189
+ exports.version = version;
190
+ //# sourceMappingURL=index.cjs.map
191
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/class/ReplaySubject/index.ts","../src/utils/fnRunner/index.ts","../src/utils/createQueryWithCache/index.ts","../src/index.ts"],"names":["localforage","version","deepEquals","result","fn"],"mappings":";;;;;;;;;;AAAA,IAAM,gBAAN,MAAuB;AAAA,EAMrB,WAAA,CAAY,gBAAgB,CAAA,EAAG;AAJ/B,IAAA,IAAA,CAAQ,SAAc,EAAC;AAEvB,IAAA,IAAA,CAAQ,gBAAuC,EAAC;AAG9C,IAAA,IAAA,CAAK,aAAA,GAAgB,aAAA;AAAA,EACvB;AAAA,EAEA,UAAU,EAAA,EAAuB;AAC/B,IAAA,IAAA,CAAK,aAAA,CAAc,KAAK,EAAE,CAAA;AAC1B,IAAA,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,CAAA,IAAA,KAAQ,EAAA,CAAG,IAAI,CAAC,CAAA;AACpC,IAAA,OAAO,MAAM,IAAA,CAAK,WAAA,CAAY,EAAE,CAAA;AAAA,EAClC;AAAA,EAEA,YAAY,EAAA,EAAuB;AACjC,IAAA,IAAA,CAAK,gBAAgB,IAAA,CAAK,aAAA,CAAc,MAAA,CAAO,CAAA,CAAA,KAAK,MAAM,EAAE,CAAA;AAAA,EAC9D;AAAA,EAEA,KAAK,IAAA,EAAS;AACZ,IAAA,IAAA,CAAK,aAAA,CAAc,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,IAAI,CAAC,CAAA;AACzC,IAAA,IAAA,CAAK,MAAA,CAAO,KAAK,IAAI,CAAA;AACrB,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,MAAA,GAAS,IAAA,CAAK,aAAA,EAAe;AAC3C,MAAA,IAAA,CAAK,MAAA,GAAS,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA;AAAA,IACnC;AAAA,EACF;AACF;;;AC3BA,eAAsB,QAAA,CAAY,EAAA,EAAsB,UAAA,GAAqB,CAAA,EAAe;AAC1F,EAAA,MAAM,KAAA,GAAA,CAAS,cAAc,CAAA,IAAK,CAAA;AAClC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,EAAO,CAAA,EAAA,EAAK;AAC9B,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,EAAA,EAAG;AAAA,IAClB,SACO,KAAA,EAAO;AAEZ,MAAA,IAAI,CAAA,KAAM,QAAQ,CAAA,EAAG;AACnB,QAAA,MAAM,KAAA;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,EAAA,MAAM,IAAI,MAAM,qDAAqD,CAAA;AACvE;;;ACAA,IAAI,EAAA,GAAaA,6BAAY,cAAA,CAAe;AAAA,EAC1C,IAAA,EAAM;AAAA;AAER,CAAC,CAAA;AAsED,SAAS,oBAAA,CAAiE,GAAA,EAAa,EAAA,EAAO,OAAA,EAA2B;AACvH,EAAA,MAAM;AAAA,IACJ,KAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA,EAAAC,QAAAA;AAAA,IACA,mBAAA;AAAA,IACA,iBAAA;AAAA,IACA,MAAA;AAAA,IACA,YAAA;AAAA,IACA,aAAA;AAAA,IACA,SAAA;AAAA,IACA,OAAA;AAAA,IACA;AAAA,GACF,GAAI;AAAA,IACF,QAAQ,CAAC,CAAA,EAAG,CAAA,KACV,CAAA,CAAE,WAAW,CAAA,CAAE,MAAA,IAAU,CAAA,CAAE,KAAA,CAAM,CAAC,IAAA,EAAM,KAAA,KAAU,IAAA,KAAS,CAAA,CAAE,KAAK,CAAC,CAAA;AAAA,IACrE,KAAA,EAAO,CAAA;AAAA,IACP,mBAAA,EAAqBC,YAAA;AAAA,IACrB,iBAAA,EAAmB,KAAA;AAAA,IACnB,GAAG;AAAA,GACL;AAEA,EAAA,MAAM,kBAAA,GAAqB,IAAI,aAAA,CAAmC,CAAC,CAAA;AAEnE,EAAA,MAAM,aAAA,GAAgB,OAAO,IAAA,KAAwB;AACnD,IAAA,MAAM,KAAA,GAAQ,MAAM,EAAA,CAAG,OAAA,CAAsB,GAAG,CAAA;AAChD,IAAA,IACE,UACI,CAACD,QAAAA,IAAW,MAAM,OAAA,KAAYA,QAAAA,CAAAA,KAC9B,CAAC,KAAA,CAAM,OAAA,IAAW,KAAA,CAAM,OAAA,GAAU,KAAK,GAAA,EAAI,CAAA,IAC5C,OAAQ,KAAA,CAAM,IAAA,EAAM,IAAI,CAAA,EAC3B;AACA,MAAA,OAAO,KAAA,CAAM,MAAA;AAAA,IACf;AAEA,IAAA,MAAM,IAAI,MAAM,iBAAiB,CAAA;AAAA,EACnC,CAAA;AAEA,EAAA,MAAM,iBAAA,GAIF;AAAA,IACF,OAAA,EAAS,KAAA;AAAA,IACT,MAAM,EAAC;AAAA,IACP,MAAA,EAAQ;AAAA,GACV;AAEA,EAAA,MAAM,mBAAmB,MAAM;AAC7B,IAAA,iBAAA,CAAkB,OAAA,GAAU,KAAA;AAC5B,IAAA,iBAAA,CAAkB,MAAA,GAAS,IAAA;AAC3B,IAAA,iBAAA,CAAkB,OAAO,EAAC;AAAA,EAC5B,CAAA;AAEA,EAAA,MAAM,oBAAA,GAAuB,CAAC,IAAA,KAAwB;AACpD,IAAA,IAAI,kBAAkB,OAAA,IAAW,MAAA,CAAQ,iBAAA,CAAkB,IAAA,EAAM,IAAI,CAAA,EAAG;AACtE,MAAA,OAAO;AAAA,QACL,IAAA,EAAM,QAAA;AAAA,QACN,eAAe,iBAAA,CAAkB;AAAA,OACnC;AAAA,IACF;AAEA,IAAA,iBAAA,CAAkB,IAAA,GAAO,IAAA;AACzB,IAAA,iBAAA,CAAkB,OAAA,GAAU,IAAA;AAC5B,IAAA,MAAM,MAAA,GAAyB,kBAAkB,MAAA,GAAS,QAAA;AAAA,MACxD,MAAM,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,MAChB;AAAA,KACF;AAEA,IAAA,OAAA,CAAQ,OAAA,CAAQ,MAAM,CAAA,CACnB,IAAA,CAAK,CAACE,OAAAA,KAAW;AAChB,MAAA,CAAC,qBAAqB,gBAAA,EAAiB;AACvC,MAAA,OAAOA,OAAAA;AAAA,IACT,CAAC,CAAA,CACA,KAAA,CAAM,gBAAgB,CAAA;AAEzB,IAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAmB,aAAA,EAAe,MAAA,EAAwB;AAAA,EAC3E,CAAA;AAEA,EAAA,MAAM,WAAA,GAAc,OAAO,IAAA,KAAwB;AACjD,IAAA,MAAM,EAAA,GAAK;AAAA,MACT,IAAA,EAAM,QAAA;AAAA,MACN,MAAA,EAAQ;AAAA,KACV;AAEA,IAAA,IAAI;AACF,MAAA,MAAM,EAAE,IAAA,EAAM,aAAA,EAAc,GAAI,qBAAqB,IAAI,CAAA;AACzD,MAAA,MAAM,MAAA,GAAU,EAAA,CAAG,MAAA,GAAS,MAAM,aAAA;AAGlC,MAAA,IAAA,KAAS,QAAA,KACL,CAAC,YAAA,IAAiB,MAAM,YAAA,CAAa,IAAI,CAAA,CAAA,IAC1C,OAAA,CAAQ,OAAA,EAAQ,CAAE,IAAA,CAAK,YAAY;AACpC,QAAA,MAAM,WAAW,MAAM,aAAA,CAAc,IAAI,CAAA,CAAE,KAAA,CAAM,MAAM,IAAI,CAAA;AAE3D,QAAA,MAAM,SAAA,GAAY;AAAA,UAChB,IAAA;AAAA,UACA,MAAA;AAAA,UACA,GAAIF,QAAAA,IAAW,EAAE,OAAA,EAAAA,QAAAA,EAAQ;AAAA,UACzB,GAAI,MAAA,IAAU,EAAE,SAAS,IAAA,CAAK,GAAA,KAAQ,MAAA;AAAO,SAC/C;AACA,QAAA,EAAA,CAAG,OAAA,CAAQ,KAAK,SAAS,CAAA;AAGzB,QAAA,IACE,uBACG,QAAA,IACA,mBAAA,CAAoB,QAAA,CAAS,MAAA,EAAQ,MAAM,CAAA,EAC9C;AACA,UAAA;AAAA,QACF;AACA,QAAA,kBAAA,CAAmB,IAAA,CAAK;AAAA,UACtB,MAAA;AAAA,UACA,SAAA;AAAA,UACA,UAAA,EAAY,CAAC,CAAC;AAAA,SACf,CAAA;AAAA,MACH,CAAC,CAAA;AAAA,IACH,SACO,KAAA,EAAO;AACZ,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,EAAA,CAAG,MAAA,GAAU,MAAM,YAAA,CAAa,KAAK,CAAA;AAAA,MACvC,CAAA,MACK;AACH,QAAA,MAAM,KAAA;AAAA,MACR;AAAA,IACF;AAEA,IAAA,OAAO,EAAA,CAAG,MAAA;AAAA,EACZ,CAAA;AAEA,EAAA,MAAM,GAAA,GAAM,UAAU,IAAA,KAAwB;AAC5C,IAAA,MAAM,aAAA,IAAgB;AAEtB,IAAA,MAAM,CAAC,WAAA,EAAa,YAAY,CAAA,GAAI;AAAA,MAClC,cAAc,IAAI,CAAA;AAAA,MAClB,YAAY,IAAI;AAAA,KAClB;AACA,IAAA,WAAA,CACG,IAAA;AAAA,MAAK,CAAAE,YACJ,SAAA,GAAY;AAAA,QACV,IAAA,EAAM,OAAA;AAAA,QACN,MAAA,EAAQA;AAAA,OACT;AAAA,KACH,CACC,MAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AACjB,IAAA,YAAA,CACG,IAAA;AAAA,MAAK,CAAAA,YACJ,SAAA,GAAY;AAAA,QACV,IAAA,EAAM,QAAA;AAAA,QACN,MAAA,EAAQA;AAAA,OACT;AAAA,KACH,CACC,MAAM,CAAA,KAAA,KAAS,OAAA,GAAU,KAAK,CAAC,CAAA,CAC/B,MAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AACjB,IAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,IAAA,CAAK;AAAA,MAChC,WAAA,CAAY,KAAA,CAAM,MAAM,YAAY,CAAA;AAAA,MACpC;AAAA,KACD,CAAA;AAED,IAAA,OAAO,MAAA;AAAA,EACT,CAAA;AAIA,EAAA,GAAA,CAAI,oBAAA,GAAuB,CAACC,GAAAA,KAC1B,kBAAA,CAAmB,UAAUA,GAAE,CAAA;AAEjC,EAAA,GAAA,CAAI,aAAa,YAAY;AAC3B,IAAA,gBAAA,EAAiB;AACjB,IAAA,MAAM,EAAA,CAAG,WAAW,GAAG,CAAA;AAAA,EACzB,CAAA;AAEA,EAAA,OAAO,GAAA;AACT;AAEA,oBAAA,CAAqB,KAAA,GAAQ,CAAC,KAAA,KAAkB;AAC9C,EAAA,EAAA,GAAK,KAAA;AACP,CAAA;;;ACjQO,IAAM,OAAA,GAAU","file":"index.cjs","sourcesContent":["class ReplaySubject<T> {\n private maxBufferSize: number\n private buffer: T[] = []\n\n private subscriptions: ((data: T) => void)[] = []\n\n constructor(maxBufferSize = 1) {\n this.maxBufferSize = maxBufferSize\n }\n\n subscribe(fn: (data: T) => void) {\n this.subscriptions.push(fn)\n this.buffer.forEach(data => fn(data))\n return () => this.unsubscribe(fn)\n }\n\n unsubscribe(fn: (data: T) => void) {\n this.subscriptions = this.subscriptions.filter(f => f !== fn)\n }\n\n next(data: T) {\n this.subscriptions.forEach(fn => fn(data))\n this.buffer.push(data)\n if (this.buffer.length > this.maxBufferSize) {\n this.buffer = this.buffer.slice(1)\n }\n }\n}\n\nexport { ReplaySubject }\n","export async function fnRunner<T>(fn: () => Promise<T>, retryTimes: number = 0): Promise<T> {\n const times = (retryTimes || 0) + 1\n for (let i = 0; i < times; i++) {\n try {\n return await fn()\n }\n catch (error) {\n // Only throw if this was the last attempt\n if (i === times - 1) {\n throw error\n }\n }\n }\n // This line should never be reached, but it's required for type safety.\n throw new Error('fnRunner: Unexpected error - all retries exhausted.')\n}\n","import localforage from 'localforage'\nimport { equals as deepEquals } from 'ramda'\nimport { ReplaySubject } from '../../class'\nimport { fnRunner } from '../fnRunner'\n\n// TODO:\n// 1. 是否需要增加 reload 方法,只请求远程并刷新缓存\n\ntype DBType = Pick<\n LocalForage,\n 'getItem' | 'setItem' | 'removeItem'\n // 这些可以暂时不考虑\n // | 'clear' | 'keys' | 'length'\n>\n\nlet db: DBType = localforage.createInstance({\n name: 'sh-common-cache',\n // driver: [localforage.LOCALSTORAGE, localforage.INDEXEDDB, localforage.WEBSQL],\n})\n\ninterface CacheItem<T extends (...args: any[]) => Promise<any>> {\n /** 缓存参数 */\n args: Parameters<T>\n /** 缓存版本,用于缓存数据结构变更 */\n version?: string\n /** 缓存过期时间,为空表示永不过期 */\n expires?: number\n /** 缓存结果 */\n result: Awaited<ReturnType<T>>\n}\n\ninterface CacheOptions<T extends (...args: any[]) => Promise<any>> {\n /** 缓存过期时间,为空表示永不过期 */\n maxAge?: number\n /** 缓存版本,用于缓存数据结构变更 */\n version?: string\n /**\n * 重试次数,为0表示不重试\n * @default 0\n */\n retry?: number\n /**\n * 是否比较旧缓存与新缓存后再触发 onCacheUpdate\n * 为 true 时,只有当新旧缓存不一致时才触发 onCacheUpdate\n * 为 false 时,总是触发 onCacheUpdate\n * @default R.equals\n */\n compareBeforeUpdate?:\n | ((prev: Awaited<ReturnType<T>>, next: Awaited<ReturnType<T>>) => boolean)\n | false\n /**\n * 是否启用远程内存缓存\n * 为 true 时,启用远程内存缓存\n * 为 false 时,不启用远程内存缓存\n * @default false\n */\n remoteMemoryCache?: boolean\n /** @default (a, b) => a.length === b.length && a.every((item, index) => item === b[index]) */\n equals?: (prev: Parameters<T>, next: Parameters<T>) => boolean\n /**\n * 根据入参决定是否添加缓存\n * 不传的话默认启用缓存\n */\n cacheEnabled?: (args: Parameters<T>) => Promise<boolean>\n /** 请求前回调 */\n beforeRequest?: () => Promise<void> | void\n /** 请求成功回调 */\n onSuccess?: (result: {\n type: 'local' | 'remote'\n result: Awaited<ReturnType<T>>\n }) => void\n /** 请求错误回调 */\n onError?: (error: any) => void\n /** 远程请求错误处理 */\n errorHandler?: (error: any) => any\n // /** 缓存更新回调 */\n // onCacheUpdate?: (\n // result: Awaited<ReturnType<T>>,\n // cacheData: CacheItem<T>,\n // ) => void;\n}\n\ninterface CacheUpdateEvent<T extends (...args: any[]) => Promise<any>> {\n result: Awaited<ReturnType<T>>\n cacheData: CacheItem<T>\n isCacheHit: boolean\n}\n\nfunction createQueryWithCache<T extends (...args: any[]) => Promise<any>>(key: string, fn: T, options?: CacheOptions<T>) {\n const {\n retry,\n maxAge,\n version,\n compareBeforeUpdate,\n remoteMemoryCache,\n equals,\n cacheEnabled,\n beforeRequest,\n onSuccess,\n onError,\n errorHandler,\n } = {\n equals: (a, b) =>\n a.length === b.length && a.every((item, index) => item === b[index]),\n retry: 0,\n compareBeforeUpdate: deepEquals,\n remoteMemoryCache: false,\n ...options,\n } as CacheOptions<T>\n\n const cacheUpdateSubject = new ReplaySubject<CacheUpdateEvent<T>>(1)\n\n const getLocalCache = async (args: Parameters<T>) => {\n const cache = await db.getItem<CacheItem<T>>(key)\n if (\n cache\n && (!version || cache.version === version)\n && (!cache.expires || cache.expires > Date.now())\n && equals!(cache.args, args)\n ) {\n return cache.result\n }\n\n throw new Error('Cache not found')\n }\n\n const remoteResultCache: {\n success: boolean\n args: Parameters<T>\n result: ReturnType<T> | null\n } = {\n success: false,\n args: [] as any,\n result: null,\n }\n\n const clearMemoryCache = () => {\n remoteResultCache.success = false\n remoteResultCache.result = null\n remoteResultCache.args = [] as any\n }\n\n const queryWithMemoryCache = (args: Parameters<T>) => {\n if (remoteResultCache.success && equals!(remoteResultCache.args, args)) {\n return {\n type: 'memory' as const,\n promiseResult: remoteResultCache.result! as ReturnType<T>,\n }\n }\n\n remoteResultCache.args = args\n remoteResultCache.success = true\n const result: ReturnType<T> = (remoteResultCache.result = fnRunner(\n () => fn(...args),\n retry,\n ) as ReturnType<T>)\n\n Promise.resolve(result)\n .then((result) => {\n !remoteMemoryCache && clearMemoryCache()\n return result\n })\n .catch(clearMemoryCache)\n\n return { type: 'remote' as const, promiseResult: result as ReturnType<T> }\n }\n\n const queryRemote = async (args: Parameters<T>) => {\n const rs = {\n type: 'remote' as const,\n result: null as unknown as Awaited<ReturnType<T>>,\n }\n\n try {\n const { type, promiseResult } = queryWithMemoryCache(args)\n const result = (rs.result = await promiseResult)\n\n // 设置缓存\n type === 'remote'\n && (!cacheEnabled || (await cacheEnabled(args)))\n && Promise.resolve().then(async () => {\n const oldCache = await getLocalCache(args).catch(() => null)\n\n const cacheData = {\n args,\n result,\n ...(version && { version }),\n ...(maxAge && { expires: Date.now() + maxAge }),\n }\n db.setItem(key, cacheData)\n\n // 如果配置了比较,且旧缓存存在且与新结果相等,则不触发 onCacheUpdate\n if (\n compareBeforeUpdate\n && oldCache\n && compareBeforeUpdate(oldCache.result, result)\n ) {\n return\n }\n cacheUpdateSubject.next({\n result,\n cacheData,\n isCacheHit: !!oldCache,\n })\n })\n }\n catch (error) {\n if (errorHandler) {\n rs.result = (await errorHandler(error)) as Awaited<ReturnType<T>>\n }\n else {\n throw error\n }\n }\n\n return rs.result\n }\n\n const _fn = async (...args: Parameters<T>) => {\n await beforeRequest?.()\n\n const [cacheResult, remoteResult] = [\n getLocalCache(args),\n queryRemote(args),\n ]\n cacheResult\n .then(result =>\n onSuccess?.({\n type: 'local' as const,\n result: result as Awaited<ReturnType<T>>,\n }),\n )\n .catch(() => {})\n remoteResult\n .then(result =>\n onSuccess?.({\n type: 'remote' as const,\n result: result as Awaited<ReturnType<T>>,\n }),\n )\n .catch(error => onError?.(error))\n .catch(() => {})\n const result = await Promise.race([\n cacheResult.catch(() => remoteResult),\n remoteResult,\n ])\n\n return result\n }\n\n // _fn.getMemoryCache = async () => remoteResultCache;\n\n _fn.subscribeCacheUpdate = (fn: (data: CacheUpdateEvent<T>) => void) =>\n cacheUpdateSubject.subscribe(fn)\n\n _fn.clearCache = async () => {\n clearMemoryCache()\n await db.removeItem(key)\n }\n\n return _fn\n}\n\ncreateQueryWithCache.useDb = (newDb: DBType) => {\n db = newDb\n}\n\nexport { createQueryWithCache }\n","/**\n * @shihengtech/utils - A collection of utility functions\n */\n\n// Type checking utilities\nexport * from './class'\nexport * from './utils'\n// Version\nexport const version = '0.0.1'\n"]}
@@ -0,0 +1,87 @@
1
+ declare class ReplaySubject<T> {
2
+ private maxBufferSize;
3
+ private buffer;
4
+ private subscriptions;
5
+ constructor(maxBufferSize?: number);
6
+ subscribe(fn: (data: T) => void): () => void;
7
+ unsubscribe(fn: (data: T) => void): void;
8
+ next(data: T): void;
9
+ }
10
+
11
+ type DBType = Pick<LocalForage, 'getItem' | 'setItem' | 'removeItem'>;
12
+ interface CacheItem<T extends (...args: any[]) => Promise<any>> {
13
+ /** 缓存参数 */
14
+ args: Parameters<T>;
15
+ /** 缓存版本,用于缓存数据结构变更 */
16
+ version?: string;
17
+ /** 缓存过期时间,为空表示永不过期 */
18
+ expires?: number;
19
+ /** 缓存结果 */
20
+ result: Awaited<ReturnType<T>>;
21
+ }
22
+ interface CacheOptions<T extends (...args: any[]) => Promise<any>> {
23
+ /** 缓存过期时间,为空表示永不过期 */
24
+ maxAge?: number;
25
+ /** 缓存版本,用于缓存数据结构变更 */
26
+ version?: string;
27
+ /**
28
+ * 重试次数,为0表示不重试
29
+ * @default 0
30
+ */
31
+ retry?: number;
32
+ /**
33
+ * 是否比较旧缓存与新缓存后再触发 onCacheUpdate
34
+ * 为 true 时,只有当新旧缓存不一致时才触发 onCacheUpdate
35
+ * 为 false 时,总是触发 onCacheUpdate
36
+ * @default R.equals
37
+ */
38
+ compareBeforeUpdate?: ((prev: Awaited<ReturnType<T>>, next: Awaited<ReturnType<T>>) => boolean) | false;
39
+ /**
40
+ * 是否启用远程内存缓存
41
+ * 为 true 时,启用远程内存缓存
42
+ * 为 false 时,不启用远程内存缓存
43
+ * @default false
44
+ */
45
+ remoteMemoryCache?: boolean;
46
+ /** @default (a, b) => a.length === b.length && a.every((item, index) => item === b[index]) */
47
+ equals?: (prev: Parameters<T>, next: Parameters<T>) => boolean;
48
+ /**
49
+ * 根据入参决定是否添加缓存
50
+ * 不传的话默认启用缓存
51
+ */
52
+ cacheEnabled?: (args: Parameters<T>) => Promise<boolean>;
53
+ /** 请求前回调 */
54
+ beforeRequest?: () => Promise<void> | void;
55
+ /** 请求成功回调 */
56
+ onSuccess?: (result: {
57
+ type: 'local' | 'remote';
58
+ result: Awaited<ReturnType<T>>;
59
+ }) => void;
60
+ /** 请求错误回调 */
61
+ onError?: (error: any) => void;
62
+ /** 远程请求错误处理 */
63
+ errorHandler?: (error: any) => any;
64
+ }
65
+ interface CacheUpdateEvent<T extends (...args: any[]) => Promise<any>> {
66
+ result: Awaited<ReturnType<T>>;
67
+ cacheData: CacheItem<T>;
68
+ isCacheHit: boolean;
69
+ }
70
+ declare function createQueryWithCache<T extends (...args: any[]) => Promise<any>>(key: string, fn: T, options?: CacheOptions<T>): {
71
+ (...args: Parameters<T>): Promise<ReturnType<T>>;
72
+ subscribeCacheUpdate(fn: (data: CacheUpdateEvent<T>) => void): () => void;
73
+ clearCache(): Promise<void>;
74
+ };
75
+ declare namespace createQueryWithCache {
76
+ var useDb: (newDb: DBType) => void;
77
+ }
78
+
79
+ declare function fnRunner<T>(fn: () => Promise<T>, retryTimes?: number): Promise<T>;
80
+
81
+ /**
82
+ * @shihengtech/utils - A collection of utility functions
83
+ */
84
+
85
+ declare const version = "0.0.1";
86
+
87
+ export { ReplaySubject, createQueryWithCache, fnRunner, version };
@@ -0,0 +1,87 @@
1
+ declare class ReplaySubject<T> {
2
+ private maxBufferSize;
3
+ private buffer;
4
+ private subscriptions;
5
+ constructor(maxBufferSize?: number);
6
+ subscribe(fn: (data: T) => void): () => void;
7
+ unsubscribe(fn: (data: T) => void): void;
8
+ next(data: T): void;
9
+ }
10
+
11
+ type DBType = Pick<LocalForage, 'getItem' | 'setItem' | 'removeItem'>;
12
+ interface CacheItem<T extends (...args: any[]) => Promise<any>> {
13
+ /** 缓存参数 */
14
+ args: Parameters<T>;
15
+ /** 缓存版本,用于缓存数据结构变更 */
16
+ version?: string;
17
+ /** 缓存过期时间,为空表示永不过期 */
18
+ expires?: number;
19
+ /** 缓存结果 */
20
+ result: Awaited<ReturnType<T>>;
21
+ }
22
+ interface CacheOptions<T extends (...args: any[]) => Promise<any>> {
23
+ /** 缓存过期时间,为空表示永不过期 */
24
+ maxAge?: number;
25
+ /** 缓存版本,用于缓存数据结构变更 */
26
+ version?: string;
27
+ /**
28
+ * 重试次数,为0表示不重试
29
+ * @default 0
30
+ */
31
+ retry?: number;
32
+ /**
33
+ * 是否比较旧缓存与新缓存后再触发 onCacheUpdate
34
+ * 为 true 时,只有当新旧缓存不一致时才触发 onCacheUpdate
35
+ * 为 false 时,总是触发 onCacheUpdate
36
+ * @default R.equals
37
+ */
38
+ compareBeforeUpdate?: ((prev: Awaited<ReturnType<T>>, next: Awaited<ReturnType<T>>) => boolean) | false;
39
+ /**
40
+ * 是否启用远程内存缓存
41
+ * 为 true 时,启用远程内存缓存
42
+ * 为 false 时,不启用远程内存缓存
43
+ * @default false
44
+ */
45
+ remoteMemoryCache?: boolean;
46
+ /** @default (a, b) => a.length === b.length && a.every((item, index) => item === b[index]) */
47
+ equals?: (prev: Parameters<T>, next: Parameters<T>) => boolean;
48
+ /**
49
+ * 根据入参决定是否添加缓存
50
+ * 不传的话默认启用缓存
51
+ */
52
+ cacheEnabled?: (args: Parameters<T>) => Promise<boolean>;
53
+ /** 请求前回调 */
54
+ beforeRequest?: () => Promise<void> | void;
55
+ /** 请求成功回调 */
56
+ onSuccess?: (result: {
57
+ type: 'local' | 'remote';
58
+ result: Awaited<ReturnType<T>>;
59
+ }) => void;
60
+ /** 请求错误回调 */
61
+ onError?: (error: any) => void;
62
+ /** 远程请求错误处理 */
63
+ errorHandler?: (error: any) => any;
64
+ }
65
+ interface CacheUpdateEvent<T extends (...args: any[]) => Promise<any>> {
66
+ result: Awaited<ReturnType<T>>;
67
+ cacheData: CacheItem<T>;
68
+ isCacheHit: boolean;
69
+ }
70
+ declare function createQueryWithCache<T extends (...args: any[]) => Promise<any>>(key: string, fn: T, options?: CacheOptions<T>): {
71
+ (...args: Parameters<T>): Promise<ReturnType<T>>;
72
+ subscribeCacheUpdate(fn: (data: CacheUpdateEvent<T>) => void): () => void;
73
+ clearCache(): Promise<void>;
74
+ };
75
+ declare namespace createQueryWithCache {
76
+ var useDb: (newDb: DBType) => void;
77
+ }
78
+
79
+ declare function fnRunner<T>(fn: () => Promise<T>, retryTimes?: number): Promise<T>;
80
+
81
+ /**
82
+ * @shihengtech/utils - A collection of utility functions
83
+ */
84
+
85
+ declare const version = "0.0.1";
86
+
87
+ export { ReplaySubject, createQueryWithCache, fnRunner, version };
package/dist/index.js ADDED
@@ -0,0 +1,182 @@
1
+ import localforage from 'localforage';
2
+ import { equals } from 'ramda';
3
+
4
+ // src/class/ReplaySubject/index.ts
5
+ var ReplaySubject = class {
6
+ constructor(maxBufferSize = 1) {
7
+ this.buffer = [];
8
+ this.subscriptions = [];
9
+ this.maxBufferSize = maxBufferSize;
10
+ }
11
+ subscribe(fn) {
12
+ this.subscriptions.push(fn);
13
+ this.buffer.forEach((data) => fn(data));
14
+ return () => this.unsubscribe(fn);
15
+ }
16
+ unsubscribe(fn) {
17
+ this.subscriptions = this.subscriptions.filter((f) => f !== fn);
18
+ }
19
+ next(data) {
20
+ this.subscriptions.forEach((fn) => fn(data));
21
+ this.buffer.push(data);
22
+ if (this.buffer.length > this.maxBufferSize) {
23
+ this.buffer = this.buffer.slice(1);
24
+ }
25
+ }
26
+ };
27
+
28
+ // src/utils/fnRunner/index.ts
29
+ async function fnRunner(fn, retryTimes = 0) {
30
+ const times = (retryTimes || 0) + 1;
31
+ for (let i = 0; i < times; i++) {
32
+ try {
33
+ return await fn();
34
+ } catch (error) {
35
+ if (i === times - 1) {
36
+ throw error;
37
+ }
38
+ }
39
+ }
40
+ throw new Error("fnRunner: Unexpected error - all retries exhausted.");
41
+ }
42
+
43
+ // src/utils/createQueryWithCache/index.ts
44
+ var db = localforage.createInstance({
45
+ name: "sh-common-cache"
46
+ // driver: [localforage.LOCALSTORAGE, localforage.INDEXEDDB, localforage.WEBSQL],
47
+ });
48
+ function createQueryWithCache(key, fn, options) {
49
+ const {
50
+ retry,
51
+ maxAge,
52
+ version: version2,
53
+ compareBeforeUpdate,
54
+ remoteMemoryCache,
55
+ equals: equals$1,
56
+ cacheEnabled,
57
+ beforeRequest,
58
+ onSuccess,
59
+ onError,
60
+ errorHandler
61
+ } = {
62
+ equals: (a, b) => a.length === b.length && a.every((item, index) => item === b[index]),
63
+ retry: 0,
64
+ compareBeforeUpdate: equals,
65
+ remoteMemoryCache: false,
66
+ ...options
67
+ };
68
+ const cacheUpdateSubject = new ReplaySubject(1);
69
+ const getLocalCache = async (args) => {
70
+ const cache = await db.getItem(key);
71
+ if (cache && (!version2 || cache.version === version2) && (!cache.expires || cache.expires > Date.now()) && equals$1(cache.args, args)) {
72
+ return cache.result;
73
+ }
74
+ throw new Error("Cache not found");
75
+ };
76
+ const remoteResultCache = {
77
+ success: false,
78
+ args: [],
79
+ result: null
80
+ };
81
+ const clearMemoryCache = () => {
82
+ remoteResultCache.success = false;
83
+ remoteResultCache.result = null;
84
+ remoteResultCache.args = [];
85
+ };
86
+ const queryWithMemoryCache = (args) => {
87
+ if (remoteResultCache.success && equals$1(remoteResultCache.args, args)) {
88
+ return {
89
+ type: "memory",
90
+ promiseResult: remoteResultCache.result
91
+ };
92
+ }
93
+ remoteResultCache.args = args;
94
+ remoteResultCache.success = true;
95
+ const result = remoteResultCache.result = fnRunner(
96
+ () => fn(...args),
97
+ retry
98
+ );
99
+ Promise.resolve(result).then((result2) => {
100
+ !remoteMemoryCache && clearMemoryCache();
101
+ return result2;
102
+ }).catch(clearMemoryCache);
103
+ return { type: "remote", promiseResult: result };
104
+ };
105
+ const queryRemote = async (args) => {
106
+ const rs = {
107
+ type: "remote",
108
+ result: null
109
+ };
110
+ try {
111
+ const { type, promiseResult } = queryWithMemoryCache(args);
112
+ const result = rs.result = await promiseResult;
113
+ type === "remote" && (!cacheEnabled || await cacheEnabled(args)) && Promise.resolve().then(async () => {
114
+ const oldCache = await getLocalCache(args).catch(() => null);
115
+ const cacheData = {
116
+ args,
117
+ result,
118
+ ...version2 && { version: version2 },
119
+ ...maxAge && { expires: Date.now() + maxAge }
120
+ };
121
+ db.setItem(key, cacheData);
122
+ if (compareBeforeUpdate && oldCache && compareBeforeUpdate(oldCache.result, result)) {
123
+ return;
124
+ }
125
+ cacheUpdateSubject.next({
126
+ result,
127
+ cacheData,
128
+ isCacheHit: !!oldCache
129
+ });
130
+ });
131
+ } catch (error) {
132
+ if (errorHandler) {
133
+ rs.result = await errorHandler(error);
134
+ } else {
135
+ throw error;
136
+ }
137
+ }
138
+ return rs.result;
139
+ };
140
+ const _fn = async (...args) => {
141
+ await beforeRequest?.();
142
+ const [cacheResult, remoteResult] = [
143
+ getLocalCache(args),
144
+ queryRemote(args)
145
+ ];
146
+ cacheResult.then(
147
+ (result2) => onSuccess?.({
148
+ type: "local",
149
+ result: result2
150
+ })
151
+ ).catch(() => {
152
+ });
153
+ remoteResult.then(
154
+ (result2) => onSuccess?.({
155
+ type: "remote",
156
+ result: result2
157
+ })
158
+ ).catch((error) => onError?.(error)).catch(() => {
159
+ });
160
+ const result = await Promise.race([
161
+ cacheResult.catch(() => remoteResult),
162
+ remoteResult
163
+ ]);
164
+ return result;
165
+ };
166
+ _fn.subscribeCacheUpdate = (fn2) => cacheUpdateSubject.subscribe(fn2);
167
+ _fn.clearCache = async () => {
168
+ clearMemoryCache();
169
+ await db.removeItem(key);
170
+ };
171
+ return _fn;
172
+ }
173
+ createQueryWithCache.useDb = (newDb) => {
174
+ db = newDb;
175
+ };
176
+
177
+ // src/index.ts
178
+ var version = "0.0.1";
179
+
180
+ export { ReplaySubject, createQueryWithCache, fnRunner, version };
181
+ //# sourceMappingURL=index.js.map
182
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/class/ReplaySubject/index.ts","../src/utils/fnRunner/index.ts","../src/utils/createQueryWithCache/index.ts","../src/index.ts"],"names":["version","equals","deepEquals","result","fn"],"mappings":"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","file":"index.js","sourcesContent":["class ReplaySubject<T> {\n private maxBufferSize: number\n private buffer: T[] = []\n\n private subscriptions: ((data: T) => void)[] = []\n\n constructor(maxBufferSize = 1) {\n this.maxBufferSize = maxBufferSize\n }\n\n subscribe(fn: (data: T) => void) {\n this.subscriptions.push(fn)\n this.buffer.forEach(data => fn(data))\n return () => this.unsubscribe(fn)\n }\n\n unsubscribe(fn: (data: T) => void) {\n this.subscriptions = this.subscriptions.filter(f => f !== fn)\n }\n\n next(data: T) {\n this.subscriptions.forEach(fn => fn(data))\n this.buffer.push(data)\n if (this.buffer.length > this.maxBufferSize) {\n this.buffer = this.buffer.slice(1)\n }\n }\n}\n\nexport { ReplaySubject }\n","export async function fnRunner<T>(fn: () => Promise<T>, retryTimes: number = 0): Promise<T> {\n const times = (retryTimes || 0) + 1\n for (let i = 0; i < times; i++) {\n try {\n return await fn()\n }\n catch (error) {\n // Only throw if this was the last attempt\n if (i === times - 1) {\n throw error\n }\n }\n }\n // This line should never be reached, but it's required for type safety.\n throw new Error('fnRunner: Unexpected error - all retries exhausted.')\n}\n","import localforage from 'localforage'\nimport { equals as deepEquals } from 'ramda'\nimport { ReplaySubject } from '../../class'\nimport { fnRunner } from '../fnRunner'\n\n// TODO:\n// 1. 是否需要增加 reload 方法,只请求远程并刷新缓存\n\ntype DBType = Pick<\n LocalForage,\n 'getItem' | 'setItem' | 'removeItem'\n // 这些可以暂时不考虑\n // | 'clear' | 'keys' | 'length'\n>\n\nlet db: DBType = localforage.createInstance({\n name: 'sh-common-cache',\n // driver: [localforage.LOCALSTORAGE, localforage.INDEXEDDB, localforage.WEBSQL],\n})\n\ninterface CacheItem<T extends (...args: any[]) => Promise<any>> {\n /** 缓存参数 */\n args: Parameters<T>\n /** 缓存版本,用于缓存数据结构变更 */\n version?: string\n /** 缓存过期时间,为空表示永不过期 */\n expires?: number\n /** 缓存结果 */\n result: Awaited<ReturnType<T>>\n}\n\ninterface CacheOptions<T extends (...args: any[]) => Promise<any>> {\n /** 缓存过期时间,为空表示永不过期 */\n maxAge?: number\n /** 缓存版本,用于缓存数据结构变更 */\n version?: string\n /**\n * 重试次数,为0表示不重试\n * @default 0\n */\n retry?: number\n /**\n * 是否比较旧缓存与新缓存后再触发 onCacheUpdate\n * 为 true 时,只有当新旧缓存不一致时才触发 onCacheUpdate\n * 为 false 时,总是触发 onCacheUpdate\n * @default R.equals\n */\n compareBeforeUpdate?:\n | ((prev: Awaited<ReturnType<T>>, next: Awaited<ReturnType<T>>) => boolean)\n | false\n /**\n * 是否启用远程内存缓存\n * 为 true 时,启用远程内存缓存\n * 为 false 时,不启用远程内存缓存\n * @default false\n */\n remoteMemoryCache?: boolean\n /** @default (a, b) => a.length === b.length && a.every((item, index) => item === b[index]) */\n equals?: (prev: Parameters<T>, next: Parameters<T>) => boolean\n /**\n * 根据入参决定是否添加缓存\n * 不传的话默认启用缓存\n */\n cacheEnabled?: (args: Parameters<T>) => Promise<boolean>\n /** 请求前回调 */\n beforeRequest?: () => Promise<void> | void\n /** 请求成功回调 */\n onSuccess?: (result: {\n type: 'local' | 'remote'\n result: Awaited<ReturnType<T>>\n }) => void\n /** 请求错误回调 */\n onError?: (error: any) => void\n /** 远程请求错误处理 */\n errorHandler?: (error: any) => any\n // /** 缓存更新回调 */\n // onCacheUpdate?: (\n // result: Awaited<ReturnType<T>>,\n // cacheData: CacheItem<T>,\n // ) => void;\n}\n\ninterface CacheUpdateEvent<T extends (...args: any[]) => Promise<any>> {\n result: Awaited<ReturnType<T>>\n cacheData: CacheItem<T>\n isCacheHit: boolean\n}\n\nfunction createQueryWithCache<T extends (...args: any[]) => Promise<any>>(key: string, fn: T, options?: CacheOptions<T>) {\n const {\n retry,\n maxAge,\n version,\n compareBeforeUpdate,\n remoteMemoryCache,\n equals,\n cacheEnabled,\n beforeRequest,\n onSuccess,\n onError,\n errorHandler,\n } = {\n equals: (a, b) =>\n a.length === b.length && a.every((item, index) => item === b[index]),\n retry: 0,\n compareBeforeUpdate: deepEquals,\n remoteMemoryCache: false,\n ...options,\n } as CacheOptions<T>\n\n const cacheUpdateSubject = new ReplaySubject<CacheUpdateEvent<T>>(1)\n\n const getLocalCache = async (args: Parameters<T>) => {\n const cache = await db.getItem<CacheItem<T>>(key)\n if (\n cache\n && (!version || cache.version === version)\n && (!cache.expires || cache.expires > Date.now())\n && equals!(cache.args, args)\n ) {\n return cache.result\n }\n\n throw new Error('Cache not found')\n }\n\n const remoteResultCache: {\n success: boolean\n args: Parameters<T>\n result: ReturnType<T> | null\n } = {\n success: false,\n args: [] as any,\n result: null,\n }\n\n const clearMemoryCache = () => {\n remoteResultCache.success = false\n remoteResultCache.result = null\n remoteResultCache.args = [] as any\n }\n\n const queryWithMemoryCache = (args: Parameters<T>) => {\n if (remoteResultCache.success && equals!(remoteResultCache.args, args)) {\n return {\n type: 'memory' as const,\n promiseResult: remoteResultCache.result! as ReturnType<T>,\n }\n }\n\n remoteResultCache.args = args\n remoteResultCache.success = true\n const result: ReturnType<T> = (remoteResultCache.result = fnRunner(\n () => fn(...args),\n retry,\n ) as ReturnType<T>)\n\n Promise.resolve(result)\n .then((result) => {\n !remoteMemoryCache && clearMemoryCache()\n return result\n })\n .catch(clearMemoryCache)\n\n return { type: 'remote' as const, promiseResult: result as ReturnType<T> }\n }\n\n const queryRemote = async (args: Parameters<T>) => {\n const rs = {\n type: 'remote' as const,\n result: null as unknown as Awaited<ReturnType<T>>,\n }\n\n try {\n const { type, promiseResult } = queryWithMemoryCache(args)\n const result = (rs.result = await promiseResult)\n\n // 设置缓存\n type === 'remote'\n && (!cacheEnabled || (await cacheEnabled(args)))\n && Promise.resolve().then(async () => {\n const oldCache = await getLocalCache(args).catch(() => null)\n\n const cacheData = {\n args,\n result,\n ...(version && { version }),\n ...(maxAge && { expires: Date.now() + maxAge }),\n }\n db.setItem(key, cacheData)\n\n // 如果配置了比较,且旧缓存存在且与新结果相等,则不触发 onCacheUpdate\n if (\n compareBeforeUpdate\n && oldCache\n && compareBeforeUpdate(oldCache.result, result)\n ) {\n return\n }\n cacheUpdateSubject.next({\n result,\n cacheData,\n isCacheHit: !!oldCache,\n })\n })\n }\n catch (error) {\n if (errorHandler) {\n rs.result = (await errorHandler(error)) as Awaited<ReturnType<T>>\n }\n else {\n throw error\n }\n }\n\n return rs.result\n }\n\n const _fn = async (...args: Parameters<T>) => {\n await beforeRequest?.()\n\n const [cacheResult, remoteResult] = [\n getLocalCache(args),\n queryRemote(args),\n ]\n cacheResult\n .then(result =>\n onSuccess?.({\n type: 'local' as const,\n result: result as Awaited<ReturnType<T>>,\n }),\n )\n .catch(() => {})\n remoteResult\n .then(result =>\n onSuccess?.({\n type: 'remote' as const,\n result: result as Awaited<ReturnType<T>>,\n }),\n )\n .catch(error => onError?.(error))\n .catch(() => {})\n const result = await Promise.race([\n cacheResult.catch(() => remoteResult),\n remoteResult,\n ])\n\n return result\n }\n\n // _fn.getMemoryCache = async () => remoteResultCache;\n\n _fn.subscribeCacheUpdate = (fn: (data: CacheUpdateEvent<T>) => void) =>\n cacheUpdateSubject.subscribe(fn)\n\n _fn.clearCache = async () => {\n clearMemoryCache()\n await db.removeItem(key)\n }\n\n return _fn\n}\n\ncreateQueryWithCache.useDb = (newDb: DBType) => {\n db = newDb\n}\n\nexport { createQueryWithCache }\n","/**\n * @shihengtech/utils - A collection of utility functions\n */\n\n// Type checking utilities\nexport * from './class'\nexport * from './utils'\n// Version\nexport const version = '0.0.1'\n"]}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@shihengtech/utils",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "description": "A collection of utility tools",
6
+ "author": "",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": ""
11
+ },
12
+ "keywords": [
13
+ "tools",
14
+ "utils",
15
+ "utilities"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "import": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "require": {
24
+ "types": "./dist/index.d.cts",
25
+ "default": "./dist/index.cjs"
26
+ }
27
+ }
28
+ },
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "registry": "https://registry.npmjs.org/"
41
+ },
42
+ "scripts": {
43
+ "dev": "tsup --watch",
44
+ "build": "tsup",
45
+ "test": "vitest",
46
+ "test:run": "vitest run",
47
+ "test:coverage": "vitest run --coverage",
48
+ "docs:dev": "vitepress dev docs",
49
+ "docs:build": "vitepress build docs",
50
+ "docs:preview": "vitepress preview docs",
51
+ "lint": "eslint .",
52
+ "lint:fix": "eslint . --fix",
53
+ "prepublishOnly": "npm run build"
54
+ },
55
+ "dependencies": {
56
+ "localforage": "^1.10.0",
57
+ "ramda": "^0.32.0"
58
+ },
59
+ "devDependencies": {
60
+ "@antfu/eslint-config": "^4.13.0",
61
+ "@types/node": "^20.10.0",
62
+ "@types/ramda": "^0.31.1",
63
+ "@vitest/coverage-v8": "^1.0.0",
64
+ "eslint": "^9.27.0",
65
+ "eslint-plugin-format": "^1.0.2",
66
+ "tsup": "^8.0.1",
67
+ "typescript": "^5.3.2",
68
+ "vitepress": "^1.0.0-rc.31",
69
+ "vitest": "^1.0.0"
70
+ }
71
+ }