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

alovajs / alova / #190

22 Jul 2024 07:20AM UTC coverage: 93.187% (-0.1%) from 93.292%
#190

push

github

web-flow
Merge pull request #470 from alovajs/changeset-release/next

ci: release next (beta)

1579 of 1740 branches covered (90.75%)

Branch coverage included in aggregate %.

9473 of 10120 relevant lines covered (93.61%)

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

31✔
52
    // save the relationship between this method and its hitSources.
31✔
53
    // cache structure is like this:
31✔
54
    /*
31✔
55
      {
31✔
56
        "$a.[namespace][methodKey]": [cache data],
31✔
57
        ...
31✔
58
        "hss.[sourceMethodKey]": "{
31✔
59
          [targetMethodKey1]: 0,
31✔
60
          [targetMethodKey2]: 0,
31✔
61
          ...
31✔
62
        }",
31✔
63
        "hss.[sourceMethodName]": "{
31✔
64
          [targetMethodKey3]: 0,
31✔
65
          [targetMethodKey4]: 0,
31✔
66
          ...
31✔
67
        }",
31✔
68
        "hsr.[sourceMethodNameRegexpSource]": "{
31✔
69
          [targetMethodKey5]: 0,
31✔
70
          [targetMethodKey6]: 0,
31✔
71
          ...
31✔
72
        }",
31✔
73
        "hsr.regexp1": ["hss.key1", "hss.key2"],
31✔
74
        "hsr.regexp2": ["hss.key1", "hss.key2"]
31✔
75
      }
31✔
76
    */
31✔
77
    if (hitSource) {
31!
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
  }
31✔
119
};
44✔
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,
81✔
141
  key: string,
81✔
142
  cacheAdapter: AlovaGlobalCacheAdapter,
81✔
143
  tag?: DetailCacheConfig<AG>['tag']
81✔
144
) => {
81✔
145
  const storagedData = await cacheAdapter.get<[any, number, DetailCacheConfig<AG>['tag']]>(
81✔
146
    buildNamespacedCacheKey(namespace, key)
81✔
147
  );
81✔
148
  if (storagedData) {
81✔
149
    // eslint-disable-next-line
29✔
150
    const [_, expireTimestamp, storedTag] = storagedData;
29✔
151
    // 如果没有过期时间则表示数据永不过期,否则需要判断是否过期
29✔
152
    if (storedTag === tag && (!expireTimestamp || expireTimestamp > getTime())) {
29✔
153
      return storagedData;
29✔
154
    }
29✔
155
    // 如果过期,则删除缓存
×
156
    await removeWithCacheAdapter(namespace, key, cacheAdapter);
×
157
  }
×
158
};
81✔
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,
78✔
169
  key: string,
78✔
170
  cacheAdapter: AlovaGlobalCacheAdapter,
78✔
171
  tag?: DetailCacheConfig<AG>['tag']
78✔
172
) => {
78✔
173
  const rawData = await getRawWithCacheAdapter(namespace, key, cacheAdapter, tag);
78✔
174
  return rawData ? rawData[0] : undefinedValue;
78✔
175
};
78✔
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,
628✔
191
  sourceName: AlovaMethodConfig<any, any, any>['name'],
628✔
192
  cacheAdapter: AlovaGlobalCacheAdapter
628✔
193
) => {
628✔
194
  const sourceNameStr = `${sourceName}`;
628✔
195
  // map that recording the source key and target method keys.
628✔
196
  const sourceTargetKeyMap = {} as Record<string, UniqueKeyPromised | undefined>;
628✔
197
  // get hit key by method key.
628✔
198
  const hitSourceKey = hitSourceStringCacheKey(sourceKey);
628✔
199
  sourceTargetKeyMap[hitSourceKey] = await cacheAdapter.get(hitSourceKey);
628✔
200
  let unifiedHitSourceRegexpChannel: string[] | undefined;
628✔
201

628✔
202
  if (sourceName) {
628!
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

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

628✔
243
  // now let's start to delete target caches.
628✔
244
  // and filter the finished keys.
628✔
245
  const accessedKeys: UniqueKeyPromised = {};
628✔
246
  await PromiseCls.all(
628✔
247
    mapItem(objectKeys(sourceTargetKeyMap), async sourceKey => {
628✔
248
      const targetKeys = sourceTargetKeyMap[sourceKey];
628✔
249
      if (targetKeys) {
628!
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
    })
628✔
260
  );
628✔
261

628✔
262
  // update source key if there is still has keys.
628✔
263
  // remove source key if its keys is empty.
628✔
264
  const unifiedHitSourceRegexpChannelLen = len(unifiedHitSourceRegexpChannel || []);
628✔
265
  await PromiseCls.all(
628✔
266
    mapItem(objectKeys(sourceTargetKeyMap), async sourceKey => {
628✔
267
      const targetKeys = sourceTargetKeyMap[sourceKey];
628✔
268
      if (targetKeys) {
628!
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
    })
628✔
283
  );
628✔
284

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