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

alovajs / alova / #210

08 Oct 2024 07:26AM UTC coverage: 93.734% (-0.09%) from 93.826%
#210

push

github

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

ci: release

1608 of 1761 branches covered (91.31%)

Branch coverage included in aggregate %.

9537 of 10129 relevant lines covered (94.16%)

60.43 hits per line

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

79.76
/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,
51✔
37
  key: string,
51✔
38
  data: any,
51✔
39
  expireTimestamp: number,
51✔
40
  cacheAdapter: AlovaGlobalCacheAdapter,
51✔
41
  hitSource: Method['hitSource'],
51✔
42
  tag?: DetailCacheConfig<AG>['tag']
51✔
43
) => {
51✔
44
  // not to cache if expireTimestamp is less than current timestamp
51✔
45
  if (expireTimestamp > getTime() && data) {
51✔
46
    const methodCacheKey = buildNamespacedCacheKey(namespace, key);
46✔
47
    await cacheAdapter.set(
46✔
48
      methodCacheKey,
46✔
49
      filterItem([data, expireTimestamp === Infinity ? undefinedValue : expireTimestamp, tag], Boolean)
46!
50
    );
46✔
51

46✔
52
    // save the relationship between this method and its hitSources.
46✔
53
    // cache structure is like this:
46✔
54
    /*
46✔
55
      {
46✔
56
        "$a.[namespace][methodKey]": [cache data],
46✔
57
        ...
46✔
58
        "hss.[sourceMethodKey]": "{
46✔
59
          [targetMethodKey1]: 0,
46✔
60
          [targetMethodKey2]: 0,
46✔
61
          ...
46✔
62
        }",
46✔
63
        "hss.[sourceMethodName]": "{
46✔
64
          [targetMethodKey3]: 0,
46✔
65
          [targetMethodKey4]: 0,
46✔
66
          ...
46✔
67
        }",
46✔
68
        "hsr.[sourceMethodNameRegexpSource]": "{
46✔
69
          [targetMethodKey5]: 0,
46✔
70
          [targetMethodKey6]: 0,
46✔
71
          ...
46✔
72
        }",
46✔
73
        "hsr.regexp1": ["hss.key1", "hss.key2"],
46✔
74
        "hsr.regexp2": ["hss.key1", "hss.key2"]
46✔
75
      }
46✔
76
    */
46✔
77
    if (hitSource) {
46!
78
      // filter repeat items and categorize the regexp, to prevent unnecessary cost of IO
×
79
      const hitSourceKeys = {} as UniqueKeyPromised;
×
80
      const hitSourceRegexpSources = [] as string[];
×
81
      forEach(hitSource, sourceItem => {
✔
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
      });
×
97

×
98
      // save the relationship. Minimize IO as much as possible
×
99
      const promises = mapItem(objectKeys(hitSourceKeys), async hitSourceKey => {
✔
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
      });
×
105
      const saveRegexp = async () => {
✔
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
      };
×
114

×
115
      // parallel executing all async tasks.
×
116
      await PromiseCls.all([...promises, saveRegexp()]);
×
117
    }
×
118
  }
46✔
119
};
51✔
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);
24✔
129
  await cacheAdapter.remove(methodStoreKey);
24✔
130
};
24✔
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,
92✔
141
  key: string,
92✔
142
  cacheAdapter: AlovaGlobalCacheAdapter,
92✔
143
  tag?: DetailCacheConfig<AG>['tag']
92✔
144
) => {
92✔
145
  const storagedData = await cacheAdapter.get<[any, number, DetailCacheConfig<AG>['tag']]>(
92✔
146
    buildNamespacedCacheKey(namespace, key)
92✔
147
  );
92✔
148
  if (storagedData) {
92✔
149
    // eslint-disable-next-line
32✔
150
    const [_, expireTimestamp, storedTag] = storagedData;
32✔
151
    // 如果没有过期时间则表示数据永不过期,否则需要判断是否过期
32✔
152
    if (storedTag === tag && (!expireTimestamp || expireTimestamp > getTime())) {
32✔
153
      return storagedData;
32✔
154
    }
32✔
155
    // 如果过期,则删除缓存
×
156
    await removeWithCacheAdapter(namespace, key, cacheAdapter);
×
157
  }
×
158
};
92✔
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,
89✔
169
  key: string,
89✔
170
  cacheAdapter: AlovaGlobalCacheAdapter,
89✔
171
  tag?: DetailCacheConfig<AG>['tag']
89✔
172
) => {
89✔
173
  const rawData = await getRawWithCacheAdapter(namespace, key, cacheAdapter, tag);
89✔
174
  return rawData ? rawData[0] : undefinedValue;
89✔
175
};
89✔
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,
744✔
191
  sourceName: AlovaMethodConfig<any, any, any>['name'],
744✔
192
  cacheAdapter: AlovaGlobalCacheAdapter
744✔
193
) => {
744✔
194
  const sourceNameStr = `${sourceName}`;
744✔
195
  // map that recording the source key and target method keys.
744✔
196
  const sourceTargetKeyMap = {} as Record<string, UniqueKeyPromised | undefined>;
744✔
197
  // get hit key by method key.
744✔
198
  const hitSourceKey = hitSourceStringCacheKey(sourceKey);
744✔
199
  sourceTargetKeyMap[hitSourceKey] = await cacheAdapter.get(hitSourceKey);
744✔
200
  let unifiedHitSourceRegexpChannel: string[] | undefined;
744✔
201

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

×
207
    // match regexped key by source method name and get hit key by method name.
×
208
    unifiedHitSourceRegexpChannel = await cacheAdapter.get<string[]>(unifiedHitSourceRegexpCacheKey);
×
209
    const matchedRegexpStrings = [] as string[];
×
210
    if (unifiedHitSourceRegexpChannel && len(unifiedHitSourceRegexpChannel)) {
×
211
      forEach(unifiedHitSourceRegexpChannel, regexpStr => {
✔
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
      });
×
217

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

744✔
228
  const removeWithTargetKey = async (targetKey: string) => {
744✔
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
  };
744✔
242

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

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

744✔
285
  // update unified hit source regexp channel if its length was changed.
744✔
286
  if (unifiedHitSourceRegexpChannelLen !== len(unifiedHitSourceRegexpChannel || [])) {
744!
287
    await cacheAdapter.set(unifiedHitSourceRegexpCacheKey, unifiedHitSourceRegexpChannel);
×
288
  }
×
289
};
744✔
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