@geekmidas/cache 10.0.0-alpha.99 → 10.0.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 (96) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/{rolldown-runtime-VH7oDXx4.cjs → chunk-CUT6urMc.cjs} +10 -8
  3. package/dist/expo.cjs +10 -10
  4. package/dist/expo.cjs.map +1 -1
  5. package/dist/expo.d.cts +6 -2
  6. package/dist/expo.d.cts.map +1 -1
  7. package/dist/expo.d.mts +6 -2
  8. package/dist/expo.d.mts.map +1 -1
  9. package/dist/expo.mjs +6 -4
  10. package/dist/expo.mjs.map +1 -1
  11. package/dist/file.cjs +19 -16
  12. package/dist/file.cjs.map +1 -1
  13. package/dist/file.d.cts +6 -3
  14. package/dist/file.d.cts.map +1 -1
  15. package/dist/file.d.mts +6 -3
  16. package/dist/file.d.mts.map +1 -1
  17. package/dist/file.mjs +12 -7
  18. package/dist/file.mjs.map +1 -1
  19. package/dist/index-BSYYdwRm.d.cts +36 -0
  20. package/dist/index-BSYYdwRm.d.cts.map +1 -0
  21. package/dist/index-Dx96gr-9.d.mts +36 -0
  22. package/dist/index-Dx96gr-9.d.mts.map +1 -0
  23. package/dist/index.cjs +2 -17
  24. package/dist/index.d.cts +2 -2
  25. package/dist/index.d.mts +2 -2
  26. package/dist/index.mjs +2 -12
  27. package/dist/memory.cjs +11 -8
  28. package/dist/memory.cjs.map +1 -1
  29. package/dist/memory.d.cts +6 -2
  30. package/dist/memory.d.cts.map +1 -1
  31. package/dist/memory.d.mts +6 -2
  32. package/dist/memory.d.mts.map +1 -1
  33. package/dist/memory.mjs +8 -5
  34. package/dist/memory.mjs.map +1 -1
  35. package/dist/src-CrE4Nl5T.mjs +14 -0
  36. package/dist/src-CrE4Nl5T.mjs.map +1 -0
  37. package/dist/src-zfYb7mdS.cjs +20 -0
  38. package/dist/src-zfYb7mdS.cjs.map +1 -0
  39. package/dist/upstash.cjs +6 -46
  40. package/dist/upstash.cjs.map +1 -1
  41. package/dist/upstash.d.cts +6 -27
  42. package/dist/upstash.d.cts.map +1 -1
  43. package/dist/upstash.d.mts +6 -27
  44. package/dist/upstash.d.mts.map +1 -1
  45. package/dist/upstash.mjs +4 -41
  46. package/dist/upstash.mjs.map +1 -1
  47. package/package.json +6 -53
  48. package/src/__benchmarks__/memory.bench.ts +51 -0
  49. package/src/__tests__/cache.spec.ts +144 -0
  50. package/src/__tests__/file.spec.ts +192 -0
  51. package/src/__tests__/integration.spec.ts +282 -0
  52. package/src/__tests__/memory.spec.ts +134 -0
  53. package/src/__tests__/upstash.spec.ts +276 -0
  54. package/src/expo.ts +50 -0
  55. package/src/file.ts +126 -0
  56. package/src/index.ts +48 -0
  57. package/src/memory.ts +38 -0
  58. package/src/upstash.ts +41 -0
  59. package/tsconfig.json +9 -0
  60. package/tsdown.config.ts +5 -0
  61. package/dist/index-BbdGhmWP.d.cts +0 -62
  62. package/dist/index-BbdGhmWP.d.cts.map +0 -1
  63. package/dist/index-BbdGhmWP.d.mts +0 -62
  64. package/dist/index-BbdGhmWP.d.mts.map +0 -1
  65. package/dist/index.cjs.map +0 -1
  66. package/dist/index.mjs.map +0 -1
  67. package/dist/postgres.cjs +0 -185
  68. package/dist/postgres.cjs.map +0 -1
  69. package/dist/postgres.d.cts +0 -71
  70. package/dist/postgres.d.cts.map +0 -1
  71. package/dist/postgres.d.mts +0 -71
  72. package/dist/postgres.d.mts.map +0 -1
  73. package/dist/postgres.mjs +0 -180
  74. package/dist/postgres.mjs.map +0 -1
  75. package/dist/redis.cjs +0 -88
  76. package/dist/redis.cjs.map +0 -1
  77. package/dist/redis.d.cts +0 -27
  78. package/dist/redis.d.cts.map +0 -1
  79. package/dist/redis.d.mts +0 -27
  80. package/dist/redis.d.mts.map +0 -1
  81. package/dist/redis.mjs +0 -85
  82. package/dist/redis.mjs.map +0 -1
  83. package/dist/registry.cjs +0 -48
  84. package/dist/registry.cjs.map +0 -1
  85. package/dist/registry.d.cts +0 -2
  86. package/dist/registry.d.mts +0 -2
  87. package/dist/registry.mjs +0 -44
  88. package/dist/registry.mjs.map +0 -1
  89. package/dist/telemetry.cjs +0 -38
  90. package/dist/telemetry.cjs.map +0 -1
  91. package/dist/telemetry.d.cts +0 -6
  92. package/dist/telemetry.d.cts.map +0 -1
  93. package/dist/telemetry.d.mts +0 -6
  94. package/dist/telemetry.d.mts.map +0 -1
  95. package/dist/telemetry.mjs +0 -37
  96. package/dist/telemetry.mjs.map +0 -1
@@ -0,0 +1,134 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { InMemoryCache } from '../memory';
3
+
4
+ describe('InMemoryCache', () => {
5
+ let cache: InMemoryCache;
6
+
7
+ beforeEach(() => {
8
+ cache = new InMemoryCache();
9
+ });
10
+
11
+ describe('get', () => {
12
+ it('should return undefined for non-existent key', async () => {
13
+ const result = await cache.get('non-existent');
14
+ expect(result).toBeUndefined();
15
+ });
16
+
17
+ it('should return value for existing key', async () => {
18
+ await cache.set('test-key', 'test-value');
19
+ const result = await cache.get('test-key');
20
+ expect(result).toBe('test-value');
21
+ });
22
+
23
+ it('should handle different data types', async () => {
24
+ const objectValue = { id: 1, name: 'test' };
25
+ const arrayValue = [1, 2, 3];
26
+ const numberValue = 42;
27
+ const booleanValue = true;
28
+
29
+ await cache.set('object', objectValue);
30
+ await cache.set('array', arrayValue);
31
+ await cache.set('number', numberValue);
32
+ await cache.set('boolean', booleanValue);
33
+
34
+ expect(await cache.get('object')).toEqual(objectValue);
35
+ expect(await cache.get('array')).toEqual(arrayValue);
36
+ expect(await cache.get('number')).toBe(numberValue);
37
+ expect(await cache.get('boolean')).toBe(booleanValue);
38
+ });
39
+ });
40
+
41
+ describe('set', () => {
42
+ it('should store string value', async () => {
43
+ await cache.set('string-key', 'string-value');
44
+ const result = await cache.get('string-key');
45
+ expect(result).toBe('string-value');
46
+ });
47
+
48
+ it('should store object value', async () => {
49
+ const value = { id: 1, data: 'test' };
50
+ await cache.set('object-key', value);
51
+ const result = await cache.get('object-key');
52
+ expect(result).toEqual(value);
53
+ });
54
+
55
+ it('should overwrite existing value', async () => {
56
+ await cache.set('key', 'initial-value');
57
+ await cache.set('key', 'updated-value');
58
+ const result = await cache.get('key');
59
+ expect(result).toBe('updated-value');
60
+ });
61
+
62
+ it('should handle null values', async () => {
63
+ await cache.set('null-key', null);
64
+ const result = await cache.get('null-key');
65
+ expect(result).toBeNull();
66
+ });
67
+ });
68
+
69
+ describe('delete', () => {
70
+ it('should remove existing key', async () => {
71
+ await cache.set('to-delete', 'value');
72
+ await cache.delete('to-delete');
73
+ const result = await cache.get('to-delete');
74
+ expect(result).toBeUndefined();
75
+ });
76
+
77
+ it('should not throw error for non-existent key', async () => {
78
+ await expect(cache.delete('non-existent')).resolves.not.toThrow();
79
+ });
80
+
81
+ it('should only delete specified key', async () => {
82
+ await cache.set('key1', 'value1');
83
+ await cache.set('key2', 'value2');
84
+ await cache.delete('key1');
85
+
86
+ expect(await cache.get('key1')).toBeUndefined();
87
+ expect(await cache.get('key2')).toBe('value2');
88
+ });
89
+ });
90
+
91
+ describe('multiple operations', () => {
92
+ it('should handle multiple keys independently', async () => {
93
+ await cache.set('key1', 'value1');
94
+ await cache.set('key2', 'value2');
95
+ await cache.set('key3', 'value3');
96
+
97
+ expect(await cache.get('key1')).toBe('value1');
98
+ expect(await cache.get('key2')).toBe('value2');
99
+ expect(await cache.get('key3')).toBe('value3');
100
+ });
101
+
102
+ it('should maintain data integrity across operations', async () => {
103
+ const data = { count: 0 };
104
+ await cache.set('counter', data);
105
+
106
+ const retrieved = await cache.get('counter');
107
+ retrieved.count = 5;
108
+
109
+ const original = await cache.get('counter');
110
+ expect(original.count).toBe(5); // Reference equality in memory
111
+ });
112
+ });
113
+
114
+ describe('edge cases', () => {
115
+ it('should handle empty string as key', async () => {
116
+ await cache.set('', 'empty-key-value');
117
+ const result = await cache.get('');
118
+ expect(result).toBe('empty-key-value');
119
+ });
120
+
121
+ it('should handle special characters in keys', async () => {
122
+ const specialKey = 'key!@#$%^&*()_+{}|:"<>?[]\\;\',./?`~';
123
+ await cache.set(specialKey, 'special-value');
124
+ const result = await cache.get(specialKey);
125
+ expect(result).toBe('special-value');
126
+ });
127
+
128
+ it('should handle undefined values', async () => {
129
+ await cache.set('undefined-key', undefined);
130
+ const result = await cache.get('undefined-key');
131
+ expect(result).toBeUndefined();
132
+ });
133
+ });
134
+ });
@@ -0,0 +1,276 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { UpstashCache } from '../upstash';
3
+
4
+ describe('UpstashCache', () => {
5
+ let cache: UpstashCache;
6
+ const testKeyPrefix = `test:${Date.now()}:`;
7
+ let testKeys: string[] = [];
8
+
9
+ beforeEach(() => {
10
+ cache = new UpstashCache('http://localhost:8079', 'example_token');
11
+ testKeys = [];
12
+ });
13
+
14
+ afterEach(async () => {
15
+ // Clean up test keys
16
+ for (const key of testKeys) {
17
+ try {
18
+ await cache.delete(key);
19
+ } catch (_error) {
20
+ // Ignore cleanup errors
21
+ }
22
+ }
23
+ });
24
+
25
+ const getTestKey = (suffix: string): string => {
26
+ const key = `${testKeyPrefix}${suffix}`;
27
+ testKeys.push(key);
28
+ return key;
29
+ };
30
+
31
+ describe('get', () => {
32
+ it('should return undefined for non-existent key', async () => {
33
+ const key = getTestKey('non-existent');
34
+ const result = await cache.get(key);
35
+
36
+ expect(result).toBeUndefined();
37
+ });
38
+
39
+ it('should parse and return JSON value', async () => {
40
+ const key = getTestKey('object');
41
+ const testValue = { id: 1, name: 'test' };
42
+
43
+ await cache.set(key, testValue);
44
+ const result = await cache.get(key);
45
+
46
+ expect(result).toEqual(testValue);
47
+ });
48
+
49
+ it('should handle string values', async () => {
50
+ const key = getTestKey('string');
51
+ const testValue = 'simple string';
52
+
53
+ await cache.set(key, testValue);
54
+ const result = await cache.get(key);
55
+
56
+ expect(result).toBe(testValue);
57
+ });
58
+
59
+ it('should handle number values', async () => {
60
+ const key = getTestKey('number');
61
+ const testValue = 42;
62
+
63
+ await cache.set(key, testValue);
64
+ const result = await cache.get(key);
65
+
66
+ expect(result).toBe(testValue);
67
+ });
68
+
69
+ it('should handle boolean values', async () => {
70
+ const key = getTestKey('boolean');
71
+ const testValue = true;
72
+
73
+ await cache.set(key, testValue);
74
+ const result = await cache.get(key);
75
+
76
+ expect(result).toBe(testValue);
77
+ });
78
+
79
+ it('should handle array values', async () => {
80
+ const key = getTestKey('array');
81
+ const testArray = [1, 2, 3, 'test'];
82
+
83
+ await cache.set(key, testArray);
84
+ const result = await cache.get(key);
85
+
86
+ expect(result).toEqual(testArray);
87
+ });
88
+
89
+ it('should handle null values as undefined', async () => {
90
+ const key = getTestKey('null');
91
+
92
+ await cache.set(key, null);
93
+ const result = await cache.get(key);
94
+
95
+ expect(result).toBeUndefined();
96
+ });
97
+ });
98
+
99
+ describe('set', () => {
100
+ it('should store and retrieve object value', async () => {
101
+ const key = getTestKey('set-object');
102
+ const testValue = { id: 1, name: 'test' };
103
+
104
+ await cache.set(key, testValue);
105
+ const result = await cache.get(key);
106
+
107
+ expect(result).toEqual(testValue);
108
+ });
109
+
110
+ it('should store and retrieve string values', async () => {
111
+ const key = getTestKey('set-string');
112
+ const testValue = 'test string';
113
+
114
+ await cache.set(key, testValue);
115
+ const result = await cache.get(key);
116
+
117
+ expect(result).toBe(testValue);
118
+ });
119
+
120
+ it('should store and retrieve number values', async () => {
121
+ const key = getTestKey('set-number');
122
+ const testValue = 42;
123
+
124
+ await cache.set(key, testValue);
125
+ const result = await cache.get(key);
126
+
127
+ expect(result).toBe(testValue);
128
+ });
129
+
130
+ it('should store and retrieve boolean values', async () => {
131
+ const key = getTestKey('set-boolean');
132
+ const testValue = true;
133
+
134
+ await cache.set(key, testValue);
135
+ const result = await cache.get(key);
136
+
137
+ expect(result).toBe(testValue);
138
+ });
139
+
140
+ it('should store and retrieve array values', async () => {
141
+ const key = getTestKey('set-array');
142
+ const testArray = [1, 2, 3];
143
+
144
+ await cache.set(key, testArray);
145
+ const result = await cache.get(key);
146
+
147
+ expect(result).toEqual(testArray);
148
+ });
149
+
150
+ it('should not store and retrieve null values', async () => {
151
+ const key = getTestKey('set-null');
152
+
153
+ await cache.set(key, null);
154
+ const result = await cache.get(key);
155
+
156
+ expect(result).toBeUndefined();
157
+ });
158
+
159
+ it('should overwrite existing values', async () => {
160
+ const key = getTestKey('overwrite');
161
+
162
+ await cache.set(key, 'initial');
163
+ await cache.set(key, 'updated');
164
+ const result = await cache.get(key);
165
+
166
+ expect(result).toBe('updated');
167
+ });
168
+ });
169
+
170
+ describe('delete', () => {
171
+ it('should delete existing key', async () => {
172
+ const key = getTestKey('delete-test');
173
+
174
+ await cache.set(key, 'value-to-delete');
175
+ await cache.delete(key);
176
+ const result = await cache.get(key);
177
+
178
+ expect(result).toBeUndefined();
179
+ });
180
+
181
+ it('should handle deletion of non-existent key', async () => {
182
+ const key = getTestKey('non-existent-delete');
183
+
184
+ await expect(cache.delete(key)).resolves.not.toThrow();
185
+ });
186
+
187
+ it('should only delete specified key', async () => {
188
+ const key1 = getTestKey('delete-key1');
189
+ const key2 = getTestKey('delete-key2');
190
+
191
+ await cache.set(key1, 'value1');
192
+ await cache.set(key2, 'value2');
193
+
194
+ await cache.delete(key1);
195
+
196
+ expect(await cache.get(key1)).toBeUndefined();
197
+ expect(await cache.get(key2)).toBe('value2');
198
+ });
199
+ });
200
+
201
+ describe('ttl', () => {
202
+ it('should return 0 for non-existent key', async () => {
203
+ const key = getTestKey('ttl-non-existent');
204
+ const result = await cache.ttl(key);
205
+
206
+ expect(result).toBe(0);
207
+ });
208
+
209
+ it('should return positive TTL for existing key with expiry', async () => {
210
+ const key = getTestKey('ttl-with-expiry');
211
+
212
+ await cache.set(key, 'test-value', 3600);
213
+ const result = await cache.ttl(key);
214
+
215
+ expect(result).toBeGreaterThan(0);
216
+ expect(result).toBeLessThanOrEqual(3600);
217
+ });
218
+
219
+ it('should return TTL in seconds', async () => {
220
+ const key = getTestKey('ttl-seconds');
221
+
222
+ await cache.set(key, 'test-value', 60); // 60 seconds
223
+ const result = await cache.ttl(key);
224
+
225
+ expect(result).toBeGreaterThan(0);
226
+ expect(result).toBeLessThanOrEqual(60);
227
+ });
228
+ });
229
+
230
+ describe('integration scenarios', () => {
231
+ it('should handle round-trip operations', async () => {
232
+ const key = getTestKey('user-session');
233
+ const testData = {
234
+ user: { id: 1, name: 'John' },
235
+ preferences: ['dark-mode', 'notifications'],
236
+ count: 42,
237
+ };
238
+
239
+ await cache.set(key, testData);
240
+ const result = await cache.get(key);
241
+
242
+ expect(result).toEqual(testData);
243
+ });
244
+
245
+ it('should handle complex nested objects', async () => {
246
+ const key = getTestKey('complex');
247
+ const complexObject = {
248
+ level1: {
249
+ level2: {
250
+ level3: {
251
+ data: 'deep nested value',
252
+ array: [{ id: 1 }, { id: 2 }],
253
+ },
254
+ },
255
+ },
256
+ };
257
+
258
+ await cache.set(key, complexObject);
259
+ const result = await cache.get(key);
260
+
261
+ expect(result).toEqual(complexObject);
262
+ });
263
+
264
+ it('should handle concurrent operations', async () => {
265
+ const keys = ['concurrent1', 'concurrent2', 'concurrent3'].map((suffix) =>
266
+ getTestKey(suffix),
267
+ );
268
+ const values = ['value1', 'value2', 'value3'];
269
+
270
+ await Promise.all(keys.map((key, i) => cache.set(key, values[i])));
271
+ const results = await Promise.all(keys.map((key) => cache.get(key)));
272
+
273
+ expect(results).toEqual(values);
274
+ });
275
+ });
276
+ });
package/src/expo.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { addSeconds } from 'date-fns';
2
+ import * as SecureStore from 'expo-secure-store';
3
+ import { type Cache, getExpirationInSeconds } from './index';
4
+
5
+ export class ExpoSecureCache implements Cache {
6
+ static getExpiryKey(key: string): string {
7
+ return `${key}:expiresAt`;
8
+ }
9
+
10
+ async ttl(key: string): Promise<number> {
11
+ const result = await SecureStore.getItemAsync(
12
+ ExpoSecureCache.getExpiryKey(key),
13
+ );
14
+ if (!result) {
15
+ return 0;
16
+ }
17
+
18
+ const secondsLeft = getExpirationInSeconds(result);
19
+
20
+ return Math.max(secondsLeft, 0);
21
+ }
22
+
23
+ async get<T>(key: string): Promise<T | undefined> {
24
+ const result = await SecureStore.getItemAsync(key);
25
+ if (!result) {
26
+ return undefined;
27
+ }
28
+
29
+ const expiresAt = await this.ttl(key);
30
+ if (expiresAt === 0) {
31
+ return undefined;
32
+ }
33
+
34
+ return JSON.parse(result) as T;
35
+ }
36
+
37
+ async set<T>(key: string, value: T, ttl: number = 600): Promise<void> {
38
+ const expiresAt = addSeconds(new Date(), ttl).toISOString();
39
+ await SecureStore.setItemAsync(key, JSON.stringify(value));
40
+ await SecureStore.setItemAsync(
41
+ ExpoSecureCache.getExpiryKey(key),
42
+ expiresAt,
43
+ );
44
+ }
45
+
46
+ async delete(key: string): Promise<void> {
47
+ await SecureStore.deleteItemAsync(key);
48
+ await SecureStore.deleteItemAsync(ExpoSecureCache.getExpiryKey(key));
49
+ }
50
+ }
package/src/file.ts ADDED
@@ -0,0 +1,126 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { addSeconds } from 'date-fns';
5
+ import lockfile from 'proper-lockfile';
6
+ import { type Cache, getExpirationInSeconds } from './';
7
+
8
+ interface FileCacheEntry {
9
+ value: unknown;
10
+ expiresAt: string;
11
+ }
12
+
13
+ type FileCacheData = Record<string, FileCacheEntry>;
14
+
15
+ export interface FileCacheOptions {
16
+ /** Path to the cache JSON file. Default: `process.cwd()/.gkm/cache.json` */
17
+ path?: string;
18
+ /** Default TTL in seconds. Default: 600 */
19
+ ttl?: number;
20
+ }
21
+
22
+ export class FileCache implements Cache {
23
+ private readonly path: string;
24
+ private readonly defaultTtl: number;
25
+ /** In-process mutex to serialize concurrent writes within the same process. */
26
+ private mutex: Promise<void> = Promise.resolve();
27
+
28
+ constructor(options: FileCacheOptions = {}) {
29
+ this.path = options.path ?? join(process.cwd(), '.gkm', 'cache.json');
30
+ this.defaultTtl = options.ttl ?? 600;
31
+ }
32
+
33
+ async get<T>(key: string): Promise<T | undefined> {
34
+ const data = await this.readData();
35
+ const entry = data[key];
36
+ if (!entry) return undefined;
37
+
38
+ const remaining = getExpirationInSeconds(entry.expiresAt);
39
+ if (remaining <= 0) {
40
+ await this.withLock(async (current) => {
41
+ delete current[key];
42
+ return current;
43
+ });
44
+ return undefined;
45
+ }
46
+
47
+ return entry.value as T;
48
+ }
49
+
50
+ async set<T>(key: string, value: T, ttl?: number): Promise<void> {
51
+ const seconds = ttl ?? this.defaultTtl;
52
+ const expiresAt = addSeconds(new Date(), seconds).toISOString();
53
+
54
+ await this.withLock(async (data) => {
55
+ data[key] = { value, expiresAt };
56
+ return data;
57
+ });
58
+ }
59
+
60
+ async delete(key: string): Promise<void> {
61
+ await this.withLock(async (data) => {
62
+ delete data[key];
63
+ return data;
64
+ });
65
+ }
66
+
67
+ async ttl(key: string): Promise<number> {
68
+ const data = await this.readData();
69
+ const entry = data[key];
70
+ if (!entry) return 0;
71
+
72
+ return getExpirationInSeconds(entry.expiresAt);
73
+ }
74
+
75
+ private async readData(): Promise<FileCacheData> {
76
+ try {
77
+ const content = await readFile(this.path, 'utf-8');
78
+ return JSON.parse(content) as FileCacheData;
79
+ } catch {
80
+ return {};
81
+ }
82
+ }
83
+
84
+ private async ensureDir(): Promise<void> {
85
+ const dir = dirname(this.path);
86
+ if (!existsSync(dir)) {
87
+ await mkdir(dir, { recursive: true });
88
+ }
89
+ }
90
+
91
+ private async withLock(
92
+ fn: (data: FileCacheData) => Promise<FileCacheData>,
93
+ ): Promise<void> {
94
+ // Queue behind any pending in-process operations
95
+ const prev = this.mutex;
96
+ let resolve: () => void;
97
+ this.mutex = new Promise<void>((r) => {
98
+ resolve = r;
99
+ });
100
+
101
+ try {
102
+ await prev;
103
+ await this.ensureDir();
104
+
105
+ // Ensure file exists before locking (proper-lockfile requires it)
106
+ if (!existsSync(this.path)) {
107
+ await writeFile(this.path, '{}');
108
+ }
109
+
110
+ const release = await lockfile.lock(this.path, {
111
+ retries: { retries: 3, minTimeout: 100, maxTimeout: 1000 },
112
+ stale: 10000,
113
+ });
114
+
115
+ try {
116
+ const data = await this.readData();
117
+ const updated = await fn(data);
118
+ await writeFile(this.path, JSON.stringify(updated, null, 2));
119
+ } finally {
120
+ await release();
121
+ }
122
+ } finally {
123
+ resolve!();
124
+ }
125
+ }
126
+ }
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { differenceInSeconds } from 'date-fns';
2
+
3
+ export const getExpirationInSeconds = (
4
+ expiresAt: string | number | Date | undefined | null,
5
+ ): number => {
6
+ if (!expiresAt) {
7
+ return 0;
8
+ }
9
+
10
+ const expirationDate = new Date(expiresAt);
11
+ const now = new Date();
12
+
13
+ const secondsLeft = differenceInSeconds(expirationDate, now);
14
+
15
+ return Math.max(secondsLeft, 0);
16
+ };
17
+
18
+ export interface Cache {
19
+ /**
20
+ * Retrieves a value from the cache.
21
+ *
22
+ * @param key The key to retrieve the value for.
23
+ * @returns The cached value, or undefined if not found.
24
+ */
25
+ get<T>(key: string): Promise<T | undefined>;
26
+ /**
27
+ * Sets a value in the cache.
28
+ *
29
+ * @param key The key to set the value for.
30
+ * @param value The value to cache.
31
+ * @param ttl Optional time-to-live in seconds. If not provided, the value will not expire.
32
+ */
33
+ set<T>(key: string, value: T, ttl?: number): Promise<void>;
34
+ /**
35
+ * Deletes a value from the cache.
36
+ *
37
+ * @param key The key to delete from the cache.
38
+ */
39
+ delete(key: string): Promise<void>;
40
+
41
+ /**
42
+ * Retrieves the time-to-live (TTL) for a cached value.
43
+ *
44
+ * @param key The key to retrieve the TTL for.
45
+ * @returns The TTL in seconds, or 0 if the key does not exist.
46
+ */
47
+ ttl(key: string): Promise<number>;
48
+ }
package/src/memory.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { addSeconds } from 'date-fns';
2
+ import { type Cache, getExpirationInSeconds } from './';
3
+
4
+ export class InMemoryCache implements Cache {
5
+ private store: Map<string, unknown> = new Map();
6
+ private expirations: Map<string, Date> = new Map();
7
+
8
+ async get<T>(key: string): Promise<T | undefined> {
9
+ const expiration = await this.ttl(key);
10
+ if (!expiration) {
11
+ await this.delete(key);
12
+ return undefined;
13
+ }
14
+
15
+ return this.store.get(key) as T | undefined;
16
+ }
17
+
18
+ async ttl(key: string): Promise<number> {
19
+ const expiration = this.expirations.get(key);
20
+ if (!expiration) {
21
+ return 0;
22
+ }
23
+
24
+ return getExpirationInSeconds(expiration);
25
+ }
26
+
27
+ async set<T>(key: string, value: T, ttl: number = 600): Promise<void> {
28
+ this.store.set(key, value);
29
+ const now = new Date();
30
+ const expiresAt = addSeconds(now, ttl);
31
+ this.expirations.set(key, expiresAt);
32
+ }
33
+
34
+ async delete(key: string): Promise<void> {
35
+ this.store.delete(key);
36
+ this.expirations.delete(key);
37
+ }
38
+ }