• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alovajs / alova / #195

29 Jul 2024 07:40AM UTC coverage: 93.645% (+0.3%) from 93.318%
#195

push

github

web-flow
Merge pull request #487 from alovajs/changeset-release/main

ci: release

1583 of 1741 branches covered (90.92%)

Branch coverage included in aggregate %.

9527 of 10123 relevant lines covered (94.11%)

58.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.26
/packages/alova/src/storage/cacheWrapper.ts
1
import { buildNamespacedCacheKey, getTime, instanceOf, newInstance } from '@alova/shared/function';
1✔
2
import {
1✔
3
  PromiseCls,
1✔
4
  RegExpCls,
1✔
5
  deleteAttr,
1✔
6
  filterItem,
1✔
7
  forEach,
1✔
8
  len,
1✔
9
  mapItem,
1✔
10
  objectKeys,
1✔
11
  pushItem,
1✔
12
  undefinedValue
1✔
13
} from '@alova/shared/vars';
1✔
14
import { AlovaGenerics, AlovaGlobalCacheAdapter, AlovaMethodConfig, DetailCacheConfig, Method } from '~/typings';
1✔
15

1✔
16
type UniqueKeyPromised = Record<string, 0>;
1✔
17
const hitSourceStringCacheKey = (key: string) => `hss.${key}`;
1✔
18
const hitSourceRegexpPrefix = 'hsr.';
1✔
19
const hitSourceRegexpCacheKey = (regexpStr: string) => hitSourceRegexpPrefix + regexpStr;
1✔
20
const unifiedHitSourceRegexpCacheKey = '$$hsrs';
1✔
21
const regexpSourceFlagSeparator = '__$<>$__';
1✔
22
const addItem = (obj: UniqueKeyPromised, item: string) => {
1✔
23
  obj[item] = 0;
71✔
24
};
71✔
25

1✔
26
/**
1✔
27
 * set or update cache
1✔
28
 * @param namespace 命名空间
1✔
29
 * @param key 存储的key
1✔
30
 * @param response 存储的响应内容
1✔
31
 * @param expireTimestamp 过期时间点的时间戳表示
1✔
32
 * @param storage 存储对象
1✔
33
 * @param tag 存储标签,用于区分不同的存储标记
1✔
34
 */
1✔
35
export const setWithCacheAdapter = async <AG extends AlovaGenerics = AlovaGenerics>(
1✔
36
  namespace: string,
50✔
37
  key: string,
50✔
38
  data: any,
50✔
39
  expireTimestamp: number,
50✔
40
  cacheAdapter: AlovaGlobalCacheAdapter,
50✔
41
  hitSource: Method['hitSource'],
50✔
42
  tag?: DetailCacheConfig<AG>['tag']
50✔
43
) => {
50✔
44
  // not to cache if expireTimestamp is less than current timestamp
50✔
45
  if (expireTimestamp > getTime() && data) {
50✔
46
    const methodCacheKey = buildNamespacedCacheKey(namespace, key);
34✔
47
    await cacheAdapter.set(
34✔
48
      methodCacheKey,
34✔
49
      filterItem([data, expireTimestamp === Infinity ? undefinedValue : expireTimestamp, tag], Boolean)
34!
50
    );
34✔
51

34✔
52
    // save the relationship between this method and its hitSources.
34✔
53
    // cache structure is like this:
34✔
54
    /*
34✔
55
      {
34✔
56
        "$a.[namespace][methodKey]": [cache data],
34✔
57
        ...
34✔
58
        "hss.[sourceMethodKey]": "{
34✔
59
          [targetMethodKey1]: 0,
34✔
60
          [targetMethodKey2]: 0,
34✔
61
          ...
34✔
62
        }",
34✔
63
        "hss.[sourceMethodName]": "{
34✔
64
          [targetMethodKey3]: 0,
34✔
65
          [targetMethodKey4]: 0,
34✔
66
          ...
34✔
67
        }",
34✔
68
        "hsr.[sourceMethodNameRegexpSource]": "{
34✔
69
          [targetMethodKey5]: 0,
34✔
70
          [targetMethodKey6]: 0,
34✔
71
          ...
34✔
72
        }",
34✔
73
        "hsr.regexp1": ["hss.key1", "hss.key2"],
34✔
74
        "hsr.regexp2": ["hss.key1", "hss.key2"]
34✔
75
      }
34✔
76
    */
34✔
77
    if (hitSource) {
34✔
78
      // filter repeat items and categorize the regexp, to prevent unnecessary cost of IO
14✔
79
      const hitSourceKeys = {} as UniqueKeyPromised;
14✔
80
      const hitSourceRegexpSources = [] as string[];
14✔
81
      forEach(hitSource, sourceItem => {
14✔
82
        const isRegexp = instanceOf(sourceItem, RegExpCls);
26✔
83
        const targetHitSourceKey = isRegexp
26✔
84
          ? sourceItem.source + (sourceItem.flags ? regexpSourceFlagSeparator + sourceItem.flags : '')
26!
85
          : sourceItem;
26✔
86

26✔
87
        if (targetHitSourceKey) {
26✔
88
          if (isRegexp && !hitSourceKeys[targetHitSourceKey]) {
26✔
89
            pushItem(hitSourceRegexpSources, targetHitSourceKey);
7✔
90
          }
7✔
91
          addItem(
26✔
92
            hitSourceKeys,
26✔
93
            isRegexp ? hitSourceRegexpCacheKey(targetHitSourceKey) : hitSourceStringCacheKey(targetHitSourceKey)
26✔
94
          );
26✔
95
        }
26✔
96
      });
14✔
97

14✔
98
      // save the relationship. Minimize IO as much as possible
14✔
99
      const promises = mapItem(objectKeys(hitSourceKeys), async hitSourceKey => {
14✔
100
        // filter the empty strings.
26✔
101
        const targetMethodKeys = (await cacheAdapter.get<UniqueKeyPromised>(hitSourceKey)) || {};
26✔
102
        addItem(targetMethodKeys, methodCacheKey);
26✔
103
        await cacheAdapter.set(hitSourceKey, targetMethodKeys);
26✔
104
      });
14✔
105
      const saveRegexp = async () => {
14✔
106
        // save the regexp source if regexp exists.
14✔
107
        if (len(hitSourceRegexpSources)) {
14✔
108
          const regexpList = (await cacheAdapter.get<string[]>(unifiedHitSourceRegexpCacheKey)) || [];
7✔
109
          // TODO: hitSourceRegexpSources 需要去重
7✔
110
          pushItem(regexpList, ...hitSourceRegexpSources);
7✔
111
          await cacheAdapter.set(unifiedHitSourceRegexpCacheKey, regexpList);
7✔
112
        }
7✔
113
      };
14✔
114

14✔
115
      // parallel executing all async tasks.
14✔
116
      await PromiseCls.all([...promises, saveRegexp()]);
14✔
117
    }
14✔
118
  }
34✔
119
};
50✔
120

1✔
121
/**
1✔
122
 * 删除存储的响应数据
1✔
123
 * @param namespace 命名空间
1✔
124
 * @param key 存储的key
1✔
125
 * @param storage 存储对象
1✔
126
 */
1✔
127
export const removeWithCacheAdapter = async (namespace: string, key: string, cacheAdapter: AlovaGlobalCacheAdapter) => {
1✔
128
  const methodStoreKey = buildNamespacedCacheKey(namespace, key);
11✔
129
  await cacheAdapter.remove(methodStoreKey);
11✔
130
};
11✔
131

1✔
132
/**
1✔
133
 * 获取存储的响应数据
1✔
134
 * @param namespace 命名空间
1✔
135
 * @param key 存储的key
1✔
136
 * @param storage 存储对象
1✔
137
 * @param tag 存储标签,标记改变了数据将会失效
1✔
138
 */
1✔
139
export const getRawWithCacheAdapter = async <AG extends AlovaGenerics = AlovaGenerics>(
1✔
140
  namespace: string,
96✔
141
  key: string,
96✔
142
  cacheAdapter: AlovaGlobalCacheAdapter,
96✔
143
  tag?: DetailCacheConfig<AG>['tag']
96✔
144
) => {
96✔
145
  const storagedData = await cacheAdapter.get<[any, number, DetailCacheConfig<AG>['tag']]>(
96✔
146
    buildNamespacedCacheKey(namespace, key)
96✔
147
  );
96✔
148
  if (storagedData) {
96✔
149
    // eslint-disable-next-line
23✔
150
    const [_, expireTimestamp, storedTag] = storagedData;
23✔
151
    // 如果没有过期时间则表示数据永不过期,否则需要判断是否过期
23✔
152
    if (storedTag === tag && (!expireTimestamp || expireTimestamp > getTime())) {
23✔
153
      return storagedData;
23✔
154
    }
23✔
155
    // 如果过期,则删除缓存
×
156
    await removeWithCacheAdapter(namespace, key, cacheAdapter);
×
157
  }
×
158
};
96✔
159

1✔
160
/**
1✔
161
 * 获取存储的响应数据
1✔
162
 * @param namespace 命名空间
1✔
163
 * @param key 存储的key
1✔
164
 * @param storage 存储对象
1✔
165
 * @param tag 存储标签,标记改变了数据将会失效
1✔
166
 */
1✔
167
export const getWithCacheAdapter = async <AG extends AlovaGenerics = AlovaGenerics>(
1✔
168
  namespace: string,
92✔
169
  key: string,
92✔
170
  cacheAdapter: AlovaGlobalCacheAdapter,
92✔
171
  tag?: DetailCacheConfig<AG>['tag']
92✔
172
) => {
92✔
173
  const rawData = await getRawWithCacheAdapter(namespace, key, cacheAdapter, tag);
92✔
174
  return rawData ? rawData[0] : undefinedValue;
92✔
175
};
92✔
176

1✔
177
/**
1✔
178
 * clear all cached data
1✔
179
 */
1✔
180
export const clearWithCacheAdapter = async (cacheAdapters: AlovaGlobalCacheAdapter[]) =>
1✔
181
  PromiseCls.all(cacheAdapters.map(cacheAdapter => cacheAdapter.clear()));
1✔
182

1✔
183
/**
1✔
184
 * query and delete target cache with key and name of source method instance.
1✔
185
 * @param sourceKey source method instance key
1✔
186
 * @param sourceName source method instance name
1✔
187
 * @param cacheAdapter cache adapter
1✔
188
 */
1✔
189
export const hitTargetCacheWithCacheAdapter = async (
1✔
190
  sourceKey: string,
796✔
191
  sourceName: AlovaMethodConfig<any, any, any>['name'],
796✔
192
  cacheAdapter: AlovaGlobalCacheAdapter
796✔
193
) => {
796✔
194
  const sourceNameStr = `${sourceName}`;
796✔
195
  // map that recording the source key and target method keys.
796✔
196
  const sourceTargetKeyMap = {} as Record<string, UniqueKeyPromised | undefined>;
796✔
197
  // get hit key by method key.
796✔
198
  const hitSourceKey = hitSourceStringCacheKey(sourceKey);
796✔
199
  sourceTargetKeyMap[hitSourceKey] = await cacheAdapter.get(hitSourceKey);
796✔
200
  let unifiedHitSourceRegexpChannel: string[] | undefined;
796✔
201

796✔
202
  if (sourceName) {
796✔
203
    const hitSourceName = hitSourceStringCacheKey(sourceNameStr);
50✔
204
    // get hit key by method name if it is exists.
50✔
205
    sourceTargetKeyMap[hitSourceName] = await cacheAdapter.get(hitSourceName);
50✔
206

50✔
207
    // match regexped key by source method name and get hit key by method name.
50✔
208
    unifiedHitSourceRegexpChannel = await cacheAdapter.get<string[]>(unifiedHitSourceRegexpCacheKey);
50✔
209
    const matchedRegexpStrings = [] as string[];
50✔
210
    if (unifiedHitSourceRegexpChannel && len(unifiedHitSourceRegexpChannel)) {
50✔
211
      forEach(unifiedHitSourceRegexpChannel, regexpStr => {
4✔
212
        const [source, flag] = regexpStr.split(regexpSourceFlagSeparator);
5✔
213
        if (newInstance(RegExpCls, source, flag).test(sourceNameStr)) {
5✔
214
          pushItem(matchedRegexpStrings, regexpStr);
3✔
215
        }
3✔
216
      });
4✔
217

4✔
218
      // parallel get hit key by matched regexps.
4✔
219
      await PromiseCls.all(
4✔
220
        mapItem(matchedRegexpStrings, async regexpString => {
4✔
221
          const hitSourceRegexpString = hitSourceRegexpCacheKey(regexpString);
3✔
222
          sourceTargetKeyMap[hitSourceRegexpString] = await cacheAdapter.get(hitSourceRegexpString);
3✔
223
        })
4✔
224
      );
4✔
225
    }
4✔
226
  }
50✔
227

796✔
228
  const removeWithTargetKey = async (targetKey: string) => {
796✔
229
    try {
19✔
230
      await cacheAdapter.remove(targetKey);
19✔
231
      // loop sourceTargetKeyMap and remove this key to prevent unnecessary cost of IO.
17✔
232
      for (const sourceKey in sourceTargetKeyMap) {
19✔
233
        const targetKeys = sourceTargetKeyMap[sourceKey];
31✔
234
        if (targetKeys) {
31✔
235
          deleteAttr(targetKeys, targetKey);
17✔
236
        }
17✔
237
      }
31✔
238
    } catch (error) {
19✔
239
      // the try-catch is used to prevent throwing error, cause throwing error in `Promise.all` below.
2✔
240
    }
2✔
241
  };
796✔
242

796✔
243
  // now let's start to delete target caches.
796✔
244
  // and filter the finished keys.
796✔
245
  const accessedKeys: UniqueKeyPromised = {};
796✔
246
  await PromiseCls.all(
796✔
247
    mapItem(objectKeys(sourceTargetKeyMap), async sourceKey => {
796✔
248
      const targetKeys = sourceTargetKeyMap[sourceKey];
848✔
249
      if (targetKeys) {
848✔
250
        const removingPromises = [] as Promise<void>[];
17✔
251
        for (const key in targetKeys) {
17✔
252
          if (!accessedKeys[key]) {
19✔
253
            addItem(accessedKeys, key);
19✔
254
            pushItem(removingPromises, removeWithTargetKey(key));
19✔
255
          }
19✔
256
        }
19✔
257
        await PromiseCls.all(removingPromises);
17✔
258
      }
17✔
259
    })
796✔
260
  );
796✔
261

796✔
262
  // update source key if there is still has keys.
796✔
263
  // remove source key if its keys is empty.
796✔
264
  const unifiedHitSourceRegexpChannelLen = len(unifiedHitSourceRegexpChannel || []);
796✔
265
  await PromiseCls.all(
796✔
266
    mapItem(objectKeys(sourceTargetKeyMap), async sourceKey => {
796✔
267
      const targetKeys = sourceTargetKeyMap[sourceKey];
848✔
268
      if (targetKeys) {
848✔
269
        if (len(objectKeys(targetKeys))) {
17✔
270
          await cacheAdapter.set(sourceKey, targetKeys);
1✔
271
        } else {
17✔
272
          await cacheAdapter.remove(sourceKey);
16✔
273
          // if this is a regexped key, need to remove it from unified regexp channel.
16✔
274
          if (sourceKey.includes(hitSourceRegexpPrefix) && unifiedHitSourceRegexpChannel) {
16✔
275
            unifiedHitSourceRegexpChannel = filterItem(
2✔
276
              unifiedHitSourceRegexpChannel,
2✔
277
              rawRegexpStr => hitSourceRegexpCacheKey(rawRegexpStr) !== sourceKey
2✔
278
            );
2✔
279
          }
2✔
280
        }
16✔
281
      }
17✔
282
    })
796✔
283
  );
796✔
284

796✔
285
  // update unified hit source regexp channel if its length was changed.
796✔
286
  if (unifiedHitSourceRegexpChannelLen !== len(unifiedHitSourceRegexpChannel || [])) {
796✔
287
    await cacheAdapter.set(unifiedHitSourceRegexpCacheKey, unifiedHitSourceRegexpChannel);
2✔
288
  }
2✔
289
};
796✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc