limited-cache 1.1.1 → 2.1.0

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +19 -5
  2. package/CODE_OF_CONDUCT.md +103 -47
  3. package/{CONTRIBUTING.md → CONTRIBUTING.template.md} +4 -2
  4. package/LICENSE +1 -1
  5. package/README.md +26 -18
  6. package/dist/core/LimitedCache.d.ts +4 -3
  7. package/dist/core/LimitedCache.d.ts.map +1 -0
  8. package/dist/core/LimitedCache.js +29 -0
  9. package/dist/core/LimitedCache.js.map +1 -0
  10. package/dist/core/LimitedCacheObject.d.ts +5 -3
  11. package/dist/core/LimitedCacheObject.d.ts.map +1 -0
  12. package/dist/core/LimitedCacheObject.js +52 -0
  13. package/dist/core/LimitedCacheObject.js.map +1 -0
  14. package/dist/core/builtIns.d.ts +12 -11
  15. package/dist/core/builtIns.d.ts.map +1 -0
  16. package/dist/core/builtIns.js +5 -0
  17. package/dist/core/builtIns.js.map +1 -0
  18. package/dist/core/defaultOptions.d.ts +6 -6
  19. package/dist/core/defaultOptions.d.ts.map +1 -0
  20. package/dist/core/defaultOptions.js +14 -0
  21. package/dist/core/defaultOptions.js.map +1 -0
  22. package/dist/core/limitedCacheUtil.d.ts +13 -12
  23. package/dist/core/limitedCacheUtil.d.ts.map +1 -0
  24. package/dist/core/limitedCacheUtil.js +14 -0
  25. package/dist/core/limitedCacheUtil.js.map +1 -0
  26. package/dist/core/lowLevelFunctions.d.ts +14 -13
  27. package/dist/core/lowLevelFunctions.d.ts.map +1 -0
  28. package/dist/core/lowLevelFunctions.js +240 -0
  29. package/dist/core/lowLevelFunctions.js.map +1 -0
  30. package/dist/index.cjs +388 -0
  31. package/dist/index.d.ts +7 -10
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +7 -8
  34. package/dist/index.js.map +1 -0
  35. package/dist/types.d.ts +58 -60
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +2 -0
  38. package/dist/types.js.map +1 -0
  39. package/package.json +86 -73
  40. package/src/__tests__/LimitedCache.test.ts +186 -0
  41. package/src/__tests__/LimitedCacheObject.test.ts +150 -0
  42. package/src/__tests__/lowLevelFunctions.test.ts +430 -0
  43. package/src/__tests__/scenarios/keys.test.ts +67 -0
  44. package/src/__tests__/scenarios/maxCacheSize.test.ts +126 -0
  45. package/src/__tests__/scenarios/maxCacheTime.test.ts +150 -0
  46. package/src/__tests__/scenarios/values.test.ts +49 -0
  47. package/src/__tests__/typeChecks/LimitedCacheObjectTypes.test.ts +70 -0
  48. package/src/__tests__/typeChecks/LimitedCacheTypes.test.ts +70 -0
  49. package/src/__tests__/typeChecks/lowLevelFunctionsTypes.test.ts +70 -0
  50. package/src/core/LimitedCache.ts +1 -1
  51. package/src/core/LimitedCacheObject.ts +15 -3
  52. package/src/core/defaultOptions.ts +1 -2
  53. package/src/core/limitedCacheUtil.ts +1 -1
  54. package/src/core/lowLevelFunctions.ts +6 -6
  55. package/src/index.ts +0 -8
  56. package/dist/limited-cache.cjs.development.js +0 -456
  57. package/dist/limited-cache.cjs.development.js.map +0 -1
  58. package/dist/limited-cache.cjs.production.min.js +0 -2
  59. package/dist/limited-cache.cjs.production.min.js.map +0 -1
  60. package/dist/limited-cache.esm.js +0 -436
  61. package/dist/limited-cache.esm.js.map +0 -1
  62. package/legacy-types/ts3.x/dist/core/LimitedCache.d.ts +0 -3
  63. package/legacy-types/ts3.x/dist/core/LimitedCacheObject.d.ts +0 -3
  64. package/legacy-types/ts3.x/dist/core/builtIns.d.ts +0 -11
  65. package/legacy-types/ts3.x/dist/core/defaultOptions.d.ts +0 -6
  66. package/legacy-types/ts3.x/dist/core/limitedCacheUtil.d.ts +0 -12
  67. package/legacy-types/ts3.x/dist/core/lowLevelFunctions.d.ts +0 -13
  68. package/legacy-types/ts3.x/dist/index.d.ts +0 -10
  69. package/legacy-types/ts3.x/dist/types.d.ts +0 -60
@@ -0,0 +1,430 @@
1
+ import { describe, beforeEach, expect, it, vitest } from 'vitest';
2
+ import { defaultOptions } from '../core/defaultOptions';
3
+ import {
4
+ isCacheMeta,
5
+ upgradeCacheMeta,
6
+ lowLevelInit,
7
+ lowLevelGetOne,
8
+ lowLevelGetAll,
9
+ lowLevelHas,
10
+ lowLevelSet,
11
+ lowLevelRemove,
12
+ lowLevelReset,
13
+ } from '../core/lowLevelFunctions';
14
+ import { LimitedCacheMeta } from '../types';
15
+
16
+ describe('lowLevelFunctions', () => {
17
+ describe('isCacheMeta', () => {
18
+ it('accepts cacheMeta shapes', () => {
19
+ expect(
20
+ isCacheMeta({
21
+ limitedCacheMetaVersion: 123,
22
+ options: defaultOptions,
23
+ cache: {},
24
+ keyList: [],
25
+ keyInfo: {},
26
+ opsLeft: 100,
27
+ }),
28
+ ).toBe(true);
29
+ });
30
+ it('rejects invalid cacheMeta shapes', () => {
31
+ expect(
32
+ // @ts-expect-error Invalid cacheMeta shape
33
+ isCacheMeta({
34
+ cache: {},
35
+ }),
36
+ ).toBe(false);
37
+ });
38
+ });
39
+
40
+ describe('upgradeCacheMeta', () => {
41
+ it('warns and upgrades if given an older, incompatible cacheMeta', () => {
42
+ const consoleWarnSpy = vitest.spyOn(console, 'warn').mockReturnValueOnce();
43
+
44
+ const cacheMeta = lowLevelInit();
45
+ cacheMeta.limitedCacheMetaVersion = 1;
46
+ upgradeCacheMeta(cacheMeta);
47
+
48
+ expect(cacheMeta.limitedCacheMetaVersion).toEqual(2);
49
+
50
+ const consoleErrorCalls = consoleWarnSpy.mock.calls;
51
+ expect(consoleErrorCalls.length).toBe(1);
52
+ });
53
+
54
+ it('throws if given an invalid cacheMeta', () => {
55
+ expect(() => {
56
+ // @ts-expect-error Invalid cacheMeta shape
57
+ upgradeCacheMeta({
58
+ cache: {},
59
+ });
60
+ }).toThrowError();
61
+ });
62
+ });
63
+
64
+ describe('lowLevelInit', () => {
65
+ beforeEach(() => {
66
+ vitest.restoreAllMocks();
67
+ });
68
+
69
+ it('clones the default options', () => {
70
+ const myCacheMeta = lowLevelInit();
71
+
72
+ expect(myCacheMeta.options).toEqual(defaultOptions);
73
+ expect(myCacheMeta.options === defaultOptions).toBe(false);
74
+ });
75
+
76
+ it('clones provided options', () => {
77
+ const myOptions = {
78
+ maxCacheSize: 123,
79
+ maxCacheTime: 456,
80
+ warnIfItemPurgedBeforeTime: 999,
81
+ opLimit: 10,
82
+ scanLimit: 100,
83
+ };
84
+
85
+ const myCacheMeta = lowLevelInit(myOptions);
86
+
87
+ expect(myCacheMeta.options).toEqual(myOptions);
88
+ expect(myCacheMeta.options === myOptions).toBe(false);
89
+ });
90
+
91
+ it('merges provided options with defaults', () => {
92
+ const myOptions = {
93
+ maxCacheSize: 123,
94
+ maxCacheTime: 456,
95
+ };
96
+
97
+ const myCacheMeta = lowLevelInit(myOptions);
98
+
99
+ expect(myCacheMeta.options).toEqual({
100
+ maxCacheSize: 123,
101
+ maxCacheTime: 456,
102
+ warnIfItemPurgedBeforeTime: 5000,
103
+ opLimit: 200,
104
+ scanLimit: 50,
105
+ });
106
+ });
107
+
108
+ it('accepts an existing cacheMeta', () => {
109
+ const existingCacheMeta = lowLevelInit();
110
+ const myCacheMeta = lowLevelInit(existingCacheMeta);
111
+
112
+ expect(existingCacheMeta).toEqual(myCacheMeta);
113
+ });
114
+
115
+ it('warns and upgrades if given an older, incompatible cacheMeta', () => {
116
+ const consoleWarnSpy = vitest.spyOn(console, 'warn').mockReturnValueOnce();
117
+
118
+ const existingCacheMeta = lowLevelInit();
119
+ existingCacheMeta.limitedCacheMetaVersion = 1;
120
+ const myCacheMeta = lowLevelInit(existingCacheMeta);
121
+
122
+ expect(existingCacheMeta).toEqual(myCacheMeta);
123
+ expect(existingCacheMeta.limitedCacheMetaVersion).toEqual(2);
124
+
125
+ const consoleErrorCalls = consoleWarnSpy.mock.calls;
126
+ expect(consoleErrorCalls.length).toBe(1);
127
+ });
128
+ });
129
+
130
+ describe('lowLevelHas', () => {
131
+ let myCacheMeta: LimitedCacheMeta;
132
+ beforeEach(() => {
133
+ vitest.restoreAllMocks();
134
+ myCacheMeta = lowLevelInit({
135
+ maxCacheTime: 1000,
136
+ });
137
+ });
138
+
139
+ it('has a missing key', () => {
140
+ const result = lowLevelHas(myCacheMeta, 'abc');
141
+ expect(result).toBe(false);
142
+ });
143
+
144
+ it('has a present key', () => {
145
+ myCacheMeta.cache['abc'] = 123;
146
+ myCacheMeta.keyInfo['abc'] = [Date.now(), 0];
147
+
148
+ const result = lowLevelHas(myCacheMeta, 'abc');
149
+ expect(result).toBe(true);
150
+ });
151
+
152
+ it('has an expired key', () => {
153
+ myCacheMeta.cache['abc'] = 123;
154
+ myCacheMeta.keyInfo['abc'] = [1, 0];
155
+
156
+ const result = lowLevelHas(myCacheMeta, 'abc');
157
+ expect(result).toBe(false);
158
+ });
159
+ });
160
+
161
+ describe('lowLevelGetOne', () => {
162
+ let myCacheMeta: LimitedCacheMeta;
163
+ beforeEach(() => {
164
+ vitest.restoreAllMocks();
165
+ myCacheMeta = lowLevelInit({
166
+ maxCacheTime: 1000,
167
+ });
168
+ });
169
+
170
+ it('get a missing key', () => {
171
+ const result = lowLevelGetOne(myCacheMeta, 'abc');
172
+ expect(result).toBeUndefined();
173
+ });
174
+
175
+ it('get a present key', () => {
176
+ // Danger: Manually manipulating internals, because otherwise we can't test 'get' separately from 'set'
177
+ myCacheMeta.cache['abc'] = 123;
178
+ myCacheMeta.keyList = ['abc'];
179
+ myCacheMeta.keyInfo['abc'] = [Date.now(), 0];
180
+
181
+ const result = lowLevelGetOne(myCacheMeta, 'abc');
182
+ expect(result).toEqual(123);
183
+ });
184
+
185
+ it('get an expired key', () => {
186
+ // Danger: Manually manipulating internals, because otherwise we can't test 'get' separately from 'set'
187
+ myCacheMeta.cache['abc'] = 123;
188
+ myCacheMeta.keyList = ['abc'];
189
+ myCacheMeta.keyInfo['abc'] = [1, 0];
190
+
191
+ const result = lowLevelGetOne(myCacheMeta, 'abc');
192
+ expect(result).toEqual(undefined);
193
+ });
194
+ });
195
+
196
+ describe('lowLevelGetAll', () => {
197
+ let myCacheMeta: LimitedCacheMeta;
198
+ beforeEach(() => {
199
+ vitest.restoreAllMocks();
200
+ myCacheMeta = lowLevelInit({
201
+ maxCacheTime: 1000,
202
+ });
203
+ });
204
+
205
+ it('get all when empty', () => {
206
+ const result = lowLevelGetAll(myCacheMeta);
207
+ expect(result).toEqual({});
208
+ });
209
+
210
+ it('get all when not empty', () => {
211
+ // Danger: Manually manipulating internals, because otherwise we can't test 'get' separately from 'set'
212
+ myCacheMeta.cache['abc'] = 123;
213
+ myCacheMeta.cache['def'] = 456;
214
+ myCacheMeta.keyList = ['abc', 'def'];
215
+ myCacheMeta.keyInfo['abc'] = [Date.now(), 0];
216
+ myCacheMeta.keyInfo['def'] = [Date.now(), 0];
217
+
218
+ const result = lowLevelGetAll(myCacheMeta);
219
+ expect(result).toEqual({ abc: 123, def: 456 });
220
+ });
221
+
222
+ it('get all when all expired', () => {
223
+ // Danger: Manually manipulating internals, because otherwise we can't test 'get' separately from 'set'
224
+ myCacheMeta.cache['abc'] = 123;
225
+ myCacheMeta.cache['def'] = 456;
226
+ myCacheMeta.keyList = ['abc', 'def'];
227
+ myCacheMeta.keyInfo['abc'] = [1, 0];
228
+ myCacheMeta.keyInfo['def'] = [1, 0];
229
+
230
+ const result = lowLevelGetAll(myCacheMeta);
231
+ expect(result).toEqual({});
232
+ });
233
+
234
+ it('get all when some expired', () => {
235
+ // Danger: Manually manipulating internals, because otherwise we can't test 'get' separately from 'set'
236
+ myCacheMeta.cache['abc'] = 123;
237
+ myCacheMeta.cache['def'] = 456;
238
+ myCacheMeta.keyList = ['abc', 'def'];
239
+ myCacheMeta.keyInfo['abc'] = [1, 0];
240
+ myCacheMeta.keyInfo['def'] = [Date.now(), 0];
241
+
242
+ const result = lowLevelGetAll(myCacheMeta);
243
+ expect(result).toEqual({ def: 456 });
244
+ });
245
+ });
246
+
247
+ describe('lowLevelSet', () => {
248
+ let myCacheMeta: LimitedCacheMeta;
249
+ beforeEach(() => {
250
+ vitest.restoreAllMocks();
251
+ myCacheMeta = lowLevelInit({
252
+ maxCacheTime: 1000,
253
+ });
254
+ });
255
+
256
+ it('returns a mutated cacheMeta', () => {
257
+ const returnedCacheMeta = lowLevelSet(myCacheMeta, 'abc', 123);
258
+
259
+ expect(returnedCacheMeta).toStrictEqual(myCacheMeta);
260
+ });
261
+
262
+ it('set a new key', () => {
263
+ myCacheMeta = lowLevelSet(myCacheMeta, 'abc', 123);
264
+
265
+ expect(myCacheMeta.cache).toEqual({ abc: 123 });
266
+ });
267
+
268
+ it('set several new keys', () => {
269
+ myCacheMeta = lowLevelSet(myCacheMeta, 'abc', 123);
270
+ myCacheMeta = lowLevelSet(myCacheMeta, 'def', 456);
271
+
272
+ expect(myCacheMeta.cache).toEqual({ abc: 123, def: 456 });
273
+ });
274
+
275
+ it('overwrite a key', () => {
276
+ myCacheMeta = lowLevelSet(myCacheMeta, 'abc', 123);
277
+ myCacheMeta = lowLevelSet(myCacheMeta, 'abc', 456);
278
+
279
+ expect(myCacheMeta.cache).toEqual({ abc: 456 });
280
+ });
281
+
282
+ it('removes items to stay within the cache size', () => {
283
+ myCacheMeta = lowLevelInit({
284
+ maxCacheSize: 5,
285
+ maxCacheTime: 1000,
286
+ opLimit: Number.MAX_SAFE_INTEGER,
287
+ warnIfItemPurgedBeforeTime: 0,
288
+ });
289
+
290
+ // Set 5 items, then 5 more
291
+ for (let n = 1; n <= 5; n++) {
292
+ myCacheMeta = lowLevelSet(myCacheMeta, `n=${n}`, n);
293
+ }
294
+ expect(myCacheMeta.cache).toEqual({
295
+ 'n=1': 1,
296
+ 'n=2': 2,
297
+ 'n=3': 3,
298
+ 'n=4': 4,
299
+ 'n=5': 5,
300
+ });
301
+
302
+ for (let n = 6; n <= 10; n++) {
303
+ myCacheMeta = lowLevelSet(myCacheMeta, `n=${n}`, n);
304
+ }
305
+
306
+ expect(myCacheMeta.cache).toEqual({
307
+ 'n=1': undefined,
308
+ 'n=2': undefined,
309
+ 'n=3': undefined,
310
+ 'n=4': undefined,
311
+ 'n=5': undefined,
312
+ 'n=6': 6,
313
+ 'n=7': 7,
314
+ 'n=8': 8,
315
+ 'n=9': 9,
316
+ 'n=10': 10,
317
+ });
318
+ // Double-check the keys, because jest can give false positives on the undefined ones above
319
+ expect(Object.keys(myCacheMeta.cache)).toEqual([
320
+ 'n=1',
321
+ 'n=2',
322
+ 'n=3',
323
+ 'n=4',
324
+ 'n=5',
325
+ 'n=6',
326
+ 'n=7',
327
+ 'n=8',
328
+ 'n=9',
329
+ 'n=10',
330
+ ]);
331
+
332
+ // And even though the cache hasn't been cleaned up yet, the keys should reflect the proper state
333
+ expect(lowLevelHas(myCacheMeta, 'n=1')).toEqual(false);
334
+ expect(lowLevelHas(myCacheMeta, 'n=5')).toEqual(false);
335
+ expect(lowLevelHas(myCacheMeta, 'n=8')).toEqual(true);
336
+ expect(lowLevelHas(myCacheMeta, 'n=10')).toEqual(true);
337
+ });
338
+
339
+ it('performs maintenance after many actions', () => {
340
+ myCacheMeta = lowLevelInit({
341
+ maxCacheSize: 10,
342
+ maxCacheTime: 1000,
343
+ opLimit: 1,
344
+ });
345
+
346
+ // Automaintenance should be done after 10 "set" actions (i.e., on the 11th)
347
+ for (let n = 1; n <= 10; n++) {
348
+ myCacheMeta = lowLevelSet(myCacheMeta, `n=${n}`, n);
349
+ }
350
+ for (let n = 2; n <= 9; n++) {
351
+ myCacheMeta = lowLevelRemove(myCacheMeta, `n=${n}`);
352
+ }
353
+ expect(myCacheMeta.cache).toEqual({
354
+ 'n=1': 1,
355
+ 'n=2': undefined,
356
+ 'n=3': undefined,
357
+ 'n=4': undefined,
358
+ 'n=5': undefined,
359
+ 'n=6': undefined,
360
+ 'n=7': undefined,
361
+ 'n=8': undefined,
362
+ 'n=9': undefined,
363
+ 'n=10': 10,
364
+ });
365
+ // Double-check the keys, because jest can give false positives on the undefined ones above
366
+ expect(Object.keys(myCacheMeta.cache)).toEqual([
367
+ 'n=1',
368
+ 'n=2',
369
+ 'n=3',
370
+ 'n=4',
371
+ 'n=5',
372
+ 'n=6',
373
+ 'n=7',
374
+ 'n=8',
375
+ 'n=9',
376
+ 'n=10',
377
+ ]);
378
+
379
+ myCacheMeta = lowLevelSet(myCacheMeta, 'n=11', 11);
380
+
381
+ // Now the internal data (including keys) should be trimmed
382
+ expect(myCacheMeta.cache).toEqual({ 'n=1': 1, 'n=10': 10, 'n=11': 11 });
383
+ expect(Object.keys(myCacheMeta.cache)).toEqual(['n=1', 'n=10', 'n=11']);
384
+ });
385
+ });
386
+
387
+ describe('lowLevelRemove', () => {
388
+ let myCacheMeta: LimitedCacheMeta;
389
+ beforeEach(() => {
390
+ vitest.restoreAllMocks();
391
+ myCacheMeta = lowLevelInit();
392
+ myCacheMeta = lowLevelSet(myCacheMeta, 'abc', 123);
393
+ });
394
+
395
+ it('removes an absent key', () => {
396
+ myCacheMeta = lowLevelRemove(myCacheMeta, 'ghi');
397
+
398
+ expect(myCacheMeta.cache).toEqual({ abc: 123 });
399
+ });
400
+
401
+ it('removes a present key', () => {
402
+ myCacheMeta = lowLevelRemove(myCacheMeta, 'abc');
403
+
404
+ expect(myCacheMeta.cache).toEqual({ abc: undefined });
405
+ });
406
+
407
+ it('removes a present key multiple times', () => {
408
+ myCacheMeta = lowLevelRemove(myCacheMeta, 'abc');
409
+ expect(myCacheMeta.cache).toEqual({ abc: undefined });
410
+
411
+ myCacheMeta = lowLevelRemove(myCacheMeta, 'abc');
412
+ expect(myCacheMeta.cache).toEqual({ abc: undefined });
413
+ });
414
+ });
415
+
416
+ describe('lowLevelReset', () => {
417
+ let myCacheMeta: LimitedCacheMeta;
418
+ beforeEach(() => {
419
+ vitest.restoreAllMocks();
420
+ myCacheMeta = lowLevelInit();
421
+ myCacheMeta = lowLevelSet(myCacheMeta, 'abc', 123);
422
+ });
423
+
424
+ it('does what it says', () => {
425
+ myCacheMeta = lowLevelReset(myCacheMeta);
426
+
427
+ expect(myCacheMeta.cache).toEqual({});
428
+ });
429
+ });
430
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, beforeEach, expect, it } from 'vitest';
2
+ import { limitedCacheUtil, LimitedCacheMeta } from '../../index';
3
+
4
+ describe('key names', () => {
5
+ let myCacheMeta: LimitedCacheMeta;
6
+ beforeEach(() => {
7
+ myCacheMeta = limitedCacheUtil.init();
8
+ });
9
+
10
+ it('builtins: hasOwnProperty', () => {
11
+ expect(limitedCacheUtil.has(myCacheMeta, 'hasOwnProperty')).toEqual(false);
12
+ expect(limitedCacheUtil.get(myCacheMeta, 'hasOwnProperty')).toEqual(undefined);
13
+
14
+ limitedCacheUtil.set(myCacheMeta, 'hasOwnProperty', 123);
15
+
16
+ expect(limitedCacheUtil.has(myCacheMeta, 'hasOwnProperty')).toEqual(true);
17
+ expect(limitedCacheUtil.get(myCacheMeta, 'hasOwnProperty')).toEqual(123);
18
+ });
19
+
20
+ it('builtins: constructor', () => {
21
+ expect(limitedCacheUtil.has(myCacheMeta, 'constructor')).toEqual(false);
22
+ expect(limitedCacheUtil.get(myCacheMeta, 'constructor')).toEqual(undefined);
23
+
24
+ limitedCacheUtil.set(myCacheMeta, 'constructor', 123);
25
+
26
+ expect(limitedCacheUtil.has(myCacheMeta, 'constructor')).toEqual(true);
27
+ expect(limitedCacheUtil.get(myCacheMeta, 'constructor')).toEqual(123);
28
+ });
29
+
30
+ it('empty string', () => {
31
+ limitedCacheUtil.set(myCacheMeta, '', 123);
32
+
33
+ expect(limitedCacheUtil.has(myCacheMeta, '')).toEqual(true);
34
+ expect(limitedCacheUtil.get(myCacheMeta, '')).toEqual(123);
35
+ });
36
+
37
+ it('number', () => {
38
+ // @ts-expect-error description here
39
+ limitedCacheUtil.set(myCacheMeta, 123, 123);
40
+
41
+ // @ts-expect-error description here
42
+ expect(limitedCacheUtil.has(myCacheMeta, 123)).toEqual(true);
43
+ // @ts-expect-error description here
44
+ expect(limitedCacheUtil.get(myCacheMeta, 123)).toEqual(123);
45
+ });
46
+
47
+ it('special character: "-"', () => {
48
+ limitedCacheUtil.set(myCacheMeta, 'abc-123', 123);
49
+
50
+ expect(limitedCacheUtil.has(myCacheMeta, 'abc-123')).toEqual(true);
51
+ expect(limitedCacheUtil.get(myCacheMeta, 'abc-123')).toEqual(123);
52
+ });
53
+
54
+ it('special character: "."', () => {
55
+ limitedCacheUtil.set(myCacheMeta, 'abc.123', 123);
56
+
57
+ expect(limitedCacheUtil.has(myCacheMeta, 'abc.123')).toEqual(true);
58
+ expect(limitedCacheUtil.get(myCacheMeta, 'abc.123')).toEqual(123);
59
+ });
60
+
61
+ it('special character: " "', () => {
62
+ limitedCacheUtil.set(myCacheMeta, 'abc 123', 123);
63
+
64
+ expect(limitedCacheUtil.has(myCacheMeta, 'abc 123')).toEqual(true);
65
+ expect(limitedCacheUtil.get(myCacheMeta, 'abc 123')).toEqual(123);
66
+ });
67
+ });
@@ -0,0 +1,126 @@
1
+ import { describe, beforeEach, expect, it, vitest } from 'vitest';
2
+ import { LimitedCache, LimitedCacheInstance } from '../../index';
3
+
4
+ // To avoid race conditions or timing issues, since some expect() checks can take 10+ ms when busy,
5
+ // we use a long cache timeout even for 'immediate' expiration, and use delays slightly longer than that
6
+ const CACHE_TIMEOUT = 20;
7
+ const timeoutPromise = (): Promise<null> =>
8
+ new Promise((resolve) => setTimeout(resolve, CACHE_TIMEOUT + 2));
9
+
10
+ describe('maxCacheSize scenarios', () => {
11
+ let myCache: LimitedCacheInstance;
12
+ beforeEach(() => {
13
+ vitest.restoreAllMocks();
14
+ myCache = LimitedCache({
15
+ maxCacheSize: 20,
16
+ maxCacheTime: Number.MAX_SAFE_INTEGER,
17
+ opLimit: Number.MAX_SAFE_INTEGER,
18
+ warnIfItemPurgedBeforeTime: 0,
19
+ });
20
+ });
21
+
22
+ it('removes items to stay within the cache size, with unique keys', () => {
23
+ // Populate items into new keys: there should remain 20 items
24
+ for (let n = 1; n <= 1000; n++) {
25
+ myCache.set(`n=${n}`, n);
26
+
27
+ const allKeys = Object.keys(myCache.getAll());
28
+ expect(allKeys.length).toEqual(Math.min(20, n));
29
+ }
30
+ });
31
+
32
+ it('removes items to stay within the cache size, with overlapping keys', () => {
33
+ // Populate 20 items
34
+ for (let n = 1; n <= 20; n++) {
35
+ myCache.set(`n=${n}`, n);
36
+ }
37
+
38
+ // Populate items into progressive larger sets of keys: there should remain 20 items
39
+ for (let n = 1; n <= 10; n++) {
40
+ myCache.set(`n=${n}`, n);
41
+
42
+ const allKeys = Object.keys(myCache.getAll());
43
+ expect(allKeys.length).toEqual(20);
44
+ }
45
+ for (let n = 1; n <= 100; n++) {
46
+ myCache.set(`n=${n}`, n);
47
+
48
+ const allKeys = Object.keys(myCache.getAll());
49
+ expect(allKeys.length).toEqual(20);
50
+ }
51
+ for (let n = 1; n <= 1000; n++) {
52
+ myCache.set(`n=${n}`, n);
53
+
54
+ const allKeys = Object.keys(myCache.getAll());
55
+ expect(allKeys.length).toEqual(20);
56
+ }
57
+ });
58
+
59
+ it('removes items to stay within the cache size, with randomized keys', () => {
60
+ // Populate 20 items
61
+ for (let n = 1; n <= 20; n++) {
62
+ myCache.set(`n=${n}`, n);
63
+ }
64
+
65
+ // Now keep populating random values into a limited set of keys: there should remain 20 items
66
+ for (let i = 0; i < 1000; i++) {
67
+ // Keys will span 1 to 100
68
+ const n = Math.floor(Math.random() * 100) + 1;
69
+ myCache.set(`n=${n}`, i);
70
+
71
+ const allKeys = Object.keys(myCache.getAll());
72
+ expect(allKeys.length).toEqual(20);
73
+ }
74
+ });
75
+
76
+ it('removes expired items aggressively', async () => {
77
+ myCache = LimitedCache({
78
+ maxCacheSize: 5,
79
+ maxCacheTime: CACHE_TIMEOUT,
80
+ opLimit: Number.MAX_SAFE_INTEGER,
81
+ });
82
+
83
+ // Fill the cache
84
+ for (let n = 1; n <= 5; n++) {
85
+ myCache.set(`n=${n}`, n);
86
+ }
87
+ expect(Object.keys(myCache.getAll())).toEqual(['n=1', 'n=2', 'n=3', 'n=4', 'n=5']);
88
+
89
+ await timeoutPromise();
90
+
91
+ // Fill with different values
92
+ for (let n = 6; n <= 10; n++) {
93
+ myCache.set(`n=${n}`, n);
94
+ }
95
+ expect(Object.keys(myCache.getAll())).toEqual(['n=6', 'n=7', 'n=8', 'n=9', 'n=10']);
96
+ });
97
+
98
+ it('warns if a freshly-added item is pushed out too quickly', async () => {
99
+ myCache = LimitedCache({
100
+ maxCacheSize: 5,
101
+ maxCacheTime: 1000,
102
+ opLimit: Number.MAX_SAFE_INTEGER,
103
+ warnIfItemPurgedBeforeTime: 1000,
104
+ });
105
+
106
+ const consoleWarnSpy = vitest.spyOn(console, 'warn').mockReturnValueOnce();
107
+
108
+ for (let n = 1; n <= 5; n++) {
109
+ myCache.set(`n=${n}`, n);
110
+ }
111
+ // This pushes out the still-fresh `n=1`
112
+ myCache.set('abc', 123);
113
+
114
+ expect(Object.keys(myCache.getAll())).toEqual(['n=2', 'n=3', 'n=4', 'n=5', 'abc']);
115
+
116
+ // And we should have seen a warning about the force-removed key
117
+ const consoleWarnCalls = consoleWarnSpy.mock.calls;
118
+ expect(consoleWarnCalls.length).toBe(1);
119
+ const [warningString, warningObject] = consoleWarnCalls[0];
120
+ expect(warningString).toBe(
121
+ 'Purged an item from cache while it was still fresh: you may want to increase maxCacheSize',
122
+ );
123
+ expect(typeof warningObject).toBe('object');
124
+ expect(warningObject.key).toBe('n=1');
125
+ });
126
+ });