@occultist/occultist 0.0.4 → 0.0.6

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 (82) hide show
  1. package/dist/accept.js +0 -1
  2. package/dist/actions/actionSets.d.ts +3 -3
  3. package/dist/actions/actions.d.ts +69 -48
  4. package/dist/actions/actions.js +39 -4
  5. package/dist/actions/context.d.ts +15 -11
  6. package/dist/actions/context.js +5 -0
  7. package/dist/actions/meta.d.ts +18 -12
  8. package/dist/actions/meta.js +114 -38
  9. package/dist/actions/spec.d.ts +3 -3
  10. package/dist/actions/types.d.ts +45 -16
  11. package/dist/actions/writer.d.ts +1 -1
  12. package/dist/actions/writer.test.js +2 -2
  13. package/dist/cache/cache.d.ts +3 -3
  14. package/dist/cache/cache.js +111 -42
  15. package/dist/cache/etag.test.js +1 -1
  16. package/dist/cache/file.d.ts +33 -1
  17. package/dist/cache/file.js +92 -10
  18. package/dist/cache/memory.d.ts +12 -2
  19. package/dist/cache/memory.js +63 -1
  20. package/dist/cache/types.d.ts +51 -22
  21. package/dist/errors.d.ts +1 -1
  22. package/dist/jsonld.d.ts +1 -1
  23. package/dist/makeTypeDefs.d.ts +2 -2
  24. package/dist/mod.d.ts +17 -15
  25. package/dist/mod.js +17 -15
  26. package/dist/processAction.d.ts +2 -2
  27. package/dist/processAction.js +1 -1
  28. package/dist/registry.d.ts +74 -8
  29. package/dist/registry.js +70 -8
  30. package/dist/registry.test.js +1 -1
  31. package/dist/scopes.d.ts +8 -8
  32. package/dist/scopes.js +8 -5
  33. package/dist/utils/contextBuilder.d.ts +1 -1
  34. package/dist/utils/getActionContext.d.ts +2 -2
  35. package/dist/utils/getPropertyValueSpecifications.d.ts +1 -1
  36. package/dist/utils/getRequestBodyValues.d.ts +3 -3
  37. package/dist/utils/getRequestIRIValues.d.ts +2 -2
  38. package/dist/utils/isPopulatedObject.js +1 -1
  39. package/dist/utils/makeAppendProblemDetails.d.ts +1 -1
  40. package/dist/utils/makeURLPattern.js +1 -0
  41. package/dist/utils/parseSearchParams.d.ts +2 -2
  42. package/dist/validators.d.ts +2 -2
  43. package/dist/validators.js +2 -2
  44. package/lib/accept.test.ts +1 -1
  45. package/lib/accept.ts +0 -2
  46. package/lib/actions/actionSets.ts +4 -4
  47. package/lib/actions/actions.ts +159 -99
  48. package/lib/actions/context.ts +22 -10
  49. package/lib/actions/meta.ts +140 -55
  50. package/lib/actions/path.test.ts +1 -1
  51. package/lib/actions/path.ts +1 -1
  52. package/lib/actions/spec.ts +3 -3
  53. package/lib/actions/types.ts +60 -15
  54. package/lib/actions/writer.test.ts +2 -2
  55. package/lib/actions/writer.ts +1 -1
  56. package/lib/cache/cache.ts +138 -52
  57. package/lib/cache/etag.test.ts +1 -1
  58. package/lib/cache/file.ts +113 -12
  59. package/lib/cache/memory.ts +85 -3
  60. package/lib/cache/types.ts +70 -23
  61. package/lib/errors.ts +1 -1
  62. package/lib/jsonld.ts +1 -1
  63. package/lib/makeTypeDefs.ts +5 -5
  64. package/lib/mod.ts +17 -15
  65. package/lib/processAction.ts +14 -14
  66. package/lib/registry.test.ts +1 -1
  67. package/lib/registry.ts +96 -19
  68. package/lib/request.ts +1 -1
  69. package/lib/scopes.test.ts +2 -2
  70. package/lib/scopes.ts +14 -11
  71. package/lib/utils/contextBuilder.ts +3 -3
  72. package/lib/utils/getActionContext.ts +4 -4
  73. package/lib/utils/getInternalName.ts +1 -1
  74. package/lib/utils/getPropertyValueSpecifications.ts +4 -4
  75. package/lib/utils/getRequestBodyValues.ts +5 -5
  76. package/lib/utils/getRequestIRIValues.ts +4 -4
  77. package/lib/utils/isPopulatedObject.ts +1 -1
  78. package/lib/utils/makeAppendProblemDetails.ts +1 -1
  79. package/lib/utils/makeURLPattern.ts +1 -0
  80. package/lib/utils/parseSearchParams.ts +2 -2
  81. package/lib/validators.ts +5 -5
  82. package/package.json +4 -2
@@ -1,8 +1,17 @@
1
1
  import {createHash} from 'node:crypto';
2
- import {CacheContext, NextFn, Registry} from '../mod.js';
3
- import {EtagConditions} from './etag.js';
4
- import type {CacheBuilder, CacheEntryDescriptor, CacheETagArgs, CacheETagInstanceArgs, CacheHitHandle, CacheHTTPArgs, CacheHTTPInstanceArgs, CacheMeta, CacheMissHandle, CacheStorage, CacheStoreArgs, CacheStoreInstanceArgs, LockedCacheMissHandle, UpstreamCache} from './types.js';
5
-
2
+ import {CacheContext, type NextFn, Registry} from '../mod.ts';
3
+ import {EtagConditions} from './etag.ts';
4
+ import type {CacheBuilder, CacheEntryDescriptor, CacheETagArgs, CacheETagInstanceArgs, CacheHitHandle, CacheHTTPArgs, CacheHTTPInstanceArgs, CacheMeta, CacheMissHandle, CacheSemantics, CacheStorage, CacheStoreArgs, CacheStoreInstanceArgs, LockedCacheMissHandle, UpstreamCache} from './types.ts';
5
+
6
+ const supportedSemantics: CacheSemantics[] = [
7
+ 'options',
8
+ 'head',
9
+ 'get',
10
+ 'post',
11
+ 'put',
12
+ 'delete',
13
+ 'query',
14
+ ];
6
15
 
7
16
  export class Cache implements CacheBuilder {
8
17
  #registry: Registry;
@@ -74,8 +83,16 @@ export class Cache implements CacheBuilder {
74
83
 
75
84
  }
76
85
 
77
- async invalidate(_req: Request): Promise<void> {
86
+ async invalidate(key: string, url: string): Promise<void> {
87
+ const promises = [
88
+ this.#cacheMeta.invalidate(key),
89
+ this.#storage.invalidate(key),
90
+ ];
91
+
92
+ if (this.#upstream != null)
93
+ promises.push(this.upstream.invalidate(url));
78
94
 
95
+ await Promise.all(promises);
79
96
  }
80
97
  }
81
98
 
@@ -103,10 +120,15 @@ export class CacheMiddleware {
103
120
  return false;
104
121
  });
105
122
 
106
- if (descriptor == null) {
123
+ if (descriptor == null || !supportedSemantics.includes(descriptor.semantics)) {
107
124
  return await next();
108
125
  }
109
126
 
127
+ if (descriptor.semantics === 'put' || descriptor.semantics === 'delete') {
128
+ this.#useInvalidate(descriptor, ctx, next);
129
+ return;
130
+ }
131
+
110
132
  switch (descriptor.args.strategy) {
111
133
  case 'http': {
112
134
  await this.#useHTTP(descriptor, ctx, next);
@@ -126,22 +148,69 @@ export class CacheMiddleware {
126
148
  /**
127
149
  * @todo Implement vary rules.
128
150
  */
129
- #makeKey(descriptor: CacheEntryDescriptor): string {
151
+ #makeKey(
152
+ descriptor: CacheEntryDescriptor,
153
+ ctx: CacheContext,
154
+ ): string {
155
+ const { authKey } = ctx;
130
156
  const { contentType } = descriptor;
131
157
  const { version } = descriptor.args;
132
158
  const { name } = descriptor.action;
133
159
  const { url } = descriptor.request;
134
160
 
135
- return 'v' + (version ?? 0) + '|' + name + '|' + contentType.toLowerCase() + '|' + url.toString();
161
+ if (authKey == null)
162
+ return 'v' + (version ?? 0) + '|' + name + '|' + contentType.toLowerCase() + '|' + url.toString();
163
+
164
+ return 'v' + (version ?? 0) + '|' + name + '|' + contentType.toLowerCase() + '|' + url.toString() + '|' + authKey;
136
165
  }
137
166
 
167
+
168
+ /**
169
+ * Sets response headers based of the cache args and authorization status.
170
+ */
138
171
  #setHeaders(
139
172
  descriptor: CacheEntryDescriptor,
140
173
  ctx: CacheContext,
141
174
  ): void {
142
- const {
143
-
144
- } = descriptor.args;
175
+ const args = descriptor.args;
176
+ const cacheControl: string[] = [];
177
+
178
+ if (ctx.authKey != null && args.publicWhenAuthenticated) {
179
+ cacheControl.push('public');
180
+ } else if (ctx.authKey != null || args.private) {
181
+ cacheControl.push('private');
182
+ } else if (ctx.public) {
183
+ cacheControl.push('public');
184
+ }
185
+
186
+ if (cacheControl.length !== 0) {
187
+ ctx.headers.set('Cache-Control', cacheControl.join(', '));
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Used for PUT and DELETE requests which should invalidate the cache
193
+ * when successful.
194
+ */
195
+ async #useInvalidate(
196
+ descriptor: CacheEntryDescriptor,
197
+ ctx: CacheContext,
198
+ next: NextFn,
199
+ ): Promise<void> {
200
+ await next();
201
+
202
+ if (ctx.status != null || ctx.status.toString()[0] !== '2') {
203
+ return;
204
+ }
205
+
206
+ const key = this.#makeKey(descriptor, ctx);
207
+ const args = descriptor.args;
208
+ const cache = args.cache;
209
+
210
+ await cache.invalidate(
211
+ key,
212
+ ctx.url,
213
+ );
145
214
  }
146
215
 
147
216
  async #useHTTP(
@@ -159,10 +228,12 @@ export class CacheMiddleware {
159
228
  ctx: CacheContext,
160
229
  next: NextFn,
161
230
  ): Promise<void> {
162
- const key = this.#makeKey(descriptor);
231
+ const key = this.#makeKey(descriptor, ctx);
163
232
  const rules = new EtagConditions(ctx.req.headers);
164
233
  const resourceState = await descriptor.args.cache.meta.get(key);
165
234
 
235
+ this.#setHeaders(descriptor, ctx);
236
+
166
237
  if (resourceState.type === 'cache-hit') {
167
238
  if (rules.ifMatch(resourceState.etag)) {
168
239
  return;
@@ -174,8 +245,6 @@ export class CacheMiddleware {
174
245
  }
175
246
  }
176
247
 
177
- this.#setHeaders(descriptor, ctx);
178
-
179
248
  await next();
180
249
  }
181
250
 
@@ -184,50 +253,66 @@ export class CacheMiddleware {
184
253
  ctx: CacheContext,
185
254
  next: NextFn,
186
255
  ): Promise<void> {
187
- const key = this.#makeKey(descriptor);
256
+ const key = this.#makeKey(descriptor, ctx);
188
257
  let resourceState: CacheHitHandle | CacheMissHandle | LockedCacheMissHandle | undefined;
258
+ const args = descriptor.args;
259
+ const cache = args.cache;
189
260
 
190
- if (
191
- typeof descriptor.args.cache.meta.getOrLock === 'function' &&
192
- descriptor.args.lock
193
- ) {
194
- try {
195
- resourceState = await descriptor.args.cache.meta.getOrLock(key);
196
- } catch (err) {
197
- resourceState = await descriptor.args.cache.meta.get(key);
198
- }
199
- } else {
200
- resourceState = await descriptor.args.cache.meta.get(key);
201
- }
202
-
203
- if (resourceState?.type === 'cache-hit') {
204
- if (this.#isNotModified(ctx.req.headers, resourceState.etag)) {
205
- ctx.hit = true;
206
- ctx.status = 304;
261
+ this.#setHeaders(descriptor, ctx);
207
262
 
208
- return;
263
+ if (descriptor.semantics === 'get' ||
264
+ descriptor.semantics === 'head' ||
265
+ descriptor.semantics === 'options' ||
266
+ descriptor.semantics === 'query'
267
+ ) {
268
+ if (
269
+ typeof cache.meta.getOrLock === 'function' &&
270
+ args.lock
271
+ ) {
272
+ try {
273
+ resourceState = await cache.meta.getOrLock(key);
274
+ } catch (err) {
275
+ resourceState = await cache.meta.get(key);
276
+ }
277
+ } else {
278
+ resourceState = await cache.meta.get(key);
209
279
  }
210
280
 
211
- if (resourceState.hasContent) {
212
- try {
281
+ if (resourceState?.type === 'cache-hit') {
282
+ if (this.#isNotModified(ctx.req.headers, resourceState.etag)) {
213
283
  ctx.hit = true;
214
- ctx.status = resourceState.status;
215
- ctx.body = await descriptor.args.cache.storage.get(key);
216
-
217
- for (const [key, value] of resourceState.headers.entries()) {
218
- ctx.headers.set(key, value);
219
- }
284
+ ctx.status = 304;
220
285
 
221
286
  return;
222
- } catch (err) {
223
- console.log(err);
287
+ }
288
+
289
+ if (resourceState.hasContent) {
290
+ try {
291
+ ctx.hit = true;
292
+ ctx.status = resourceState.status;
293
+ ctx.body = await cache.storage.get(key);
294
+
295
+ for (const [key, value] of Object.entries(resourceState.headers)) {
296
+ if (Array.isArray(value)) {
297
+ ctx.headers.delete(key);
298
+
299
+ for (let i = 0; i < value.length; i++) {
300
+ ctx.headers.append(key, value[i]);
301
+ }
302
+ } else {
303
+ ctx.headers.set(key, value);
304
+ }
305
+ }
306
+
307
+ return;
308
+ } catch (err) {
309
+ console.log(err);
310
+ }
224
311
  }
225
312
  }
226
313
  }
227
314
 
228
315
  try {
229
- this.#setHeaders(descriptor, ctx);
230
-
231
316
  await next();
232
317
 
233
318
  const body = await new Response(ctx.body).blob();
@@ -236,19 +321,19 @@ export class CacheMiddleware {
236
321
  ctx.etag = etag;
237
322
  ctx.headers.set('Etag', etag);
238
323
 
239
- await descriptor.args.cache.meta.set(key, {
324
+ await cache.meta.set(key, {
240
325
  key,
241
326
  authKey: ctx.authKey,
242
327
  iri: ctx.url,
243
328
  status: ctx.status ?? 200,
244
329
  hasContent: ctx.body != null,
245
- headers: ctx.headers,
330
+ headers: Object.fromEntries(ctx.headers.entries()),
246
331
  contentType: ctx.contentType,
247
332
  etag,
248
333
  });
249
334
 
250
335
  if (ctx.body != null) {
251
- await descriptor.args.cache.storage.set(key, body);
336
+ await cache.storage.set(key, body);
252
337
  }
253
338
 
254
339
  if (resourceState.type === 'locked-cache-miss') {
@@ -259,7 +344,6 @@ export class CacheMiddleware {
259
344
  ctx.hit = true;
260
345
  ctx.status = 304;
261
346
  ctx.body = null;
262
- ctx.headers.delete('Etag');
263
347
 
264
348
  return;
265
349
  }
@@ -280,12 +364,14 @@ export class CacheMiddleware {
280
364
  return rules.isNotModified(etag);
281
365
  }
282
366
 
283
- async #createEtag(body: Blob, weak: boolean = true): Promise<string> {
367
+ /**
368
+ * Creates a strong etag using a sha1 hashing algorithim.
369
+ */
370
+ async #createEtag(body: Blob): Promise<string> {
284
371
  const buff = await body.bytes();
285
372
  const hash = createHash('sha1').update(buff).digest('hex');
286
- const quoted = `"${hash}"`;
287
373
 
288
- return weak ? `W/${quoted}` : quoted;
374
+ return `"${hash}"`;
289
375
  }
290
376
 
291
377
  }
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
- import {EtagConditions} from './etag.js';
3
+ import {EtagConditions} from './etag.ts';
4
4
 
5
5
 
6
6
  test.describe('conditions.ifMatch()', () => {
package/lib/cache/file.ts CHANGED
@@ -1,31 +1,49 @@
1
1
  import {createHash} from 'node:crypto';
2
- import type {CacheDetails, CacheHitHandle, CacheMeta, CacheStorage, CacheMissHandle} from './types.js';
3
- import {FileHandle, open, readFile, writeFile, rm} from 'node:fs/promises';
2
+ import type {CacheDetails, CacheHitHandle, CacheMeta, CacheStorage, CacheMissHandle, UpstreamCache} from './types.ts';
3
+ import {readFile, writeFile, rm} from 'node:fs/promises';
4
4
  import {join} from 'node:path';
5
+ import {type FSWatcher, watchFile} from 'node:fs';
6
+ import {Registry} from '../registry.ts';
7
+ import {Cache} from './cache.ts';
5
8
 
6
9
 
7
-
10
+ /**
11
+ * Stores cache meta information in a file on the filesystem.
12
+ *
13
+ * This is not robust enough for multi-process use.
14
+ */
8
15
  export class FileCacheMeta implements CacheMeta {
9
16
 
10
17
  #filePath: string;
11
- #handle: FileHandle | undefined;
12
18
  #details: Record<string, CacheDetails> = {};
19
+ #watcher: FSWatcher | undefined;
20
+ #writing: boolean = false;
13
21
 
14
22
  constructor(filePath: string) {
15
23
  this.#filePath = filePath;
16
24
  }
17
25
 
18
26
  async #init(): Promise<void> {
19
- this.#handle = await open(this.#filePath, 'w+');
20
- const content = await this.#handle.readFile({ encoding: 'utf-8' });
27
+ try {
28
+ const content = await readFile(this.#filePath, 'utf-8');
29
+
30
+
31
+ this.#details = JSON.parse(content);
32
+ } catch (err) {
33
+ await writeFile(this.#filePath, JSON.stringify(this.#details));
34
+ }
35
+
36
+ this.#watcher = watchFile(this.#filePath, async () => {
37
+ if (this.#writing) return;
21
38
 
22
- this.#details = JSON.parse(content);
39
+ const content = await readFile(this.#filePath, 'utf-8');
40
+ this.#details = JSON.parse(content);
41
+ }) as FSWatcher;
42
+ this.#watcher.unref();
23
43
  }
24
44
 
25
45
  async get(key: string): Promise<CacheHitHandle| CacheMissHandle> {
26
- if (this.#handle == null) {
27
- this.#init();
28
- }
46
+ if (this.#watcher == null) await this.#init();
29
47
 
30
48
  const details = this.#details[key];
31
49
  async function set(details: CacheDetails) {
@@ -46,24 +64,69 @@ export class FileCacheMeta implements CacheMeta {
46
64
  };
47
65
  }
48
66
 
67
+ /**
68
+ * Sets a cached value.
69
+ *
70
+ * @param key Unique key for this cached value.
71
+ * @param details Details of the cache to store.
72
+ */
49
73
  async set(key: string, details: CacheDetails) {
74
+ if (this.#watcher == null) await this.#init();
75
+
50
76
  this.#details[key] = details;
51
- this.#handle.writeFile(JSON.stringify(details));
77
+
78
+ await this.#write();
79
+ }
80
+
81
+ /**
82
+ * Invalidates a cached value by key.
83
+ *
84
+ * @param key Unique key for this cached value.
85
+ */
86
+ async invalidate(key: string): Promise<void> {
87
+ if (this.#watcher == null) await this.#init();
88
+
89
+ delete this.#details[key];
90
+
91
+ await this.#write();
52
92
  }
53
93
 
94
+ /**
95
+ * Flushes the entire cache of values.
96
+ *
97
+ * @param key Unique key for this cached value.
98
+ */
99
+ async flush(): Promise<void> {
100
+ this.#details = {};
101
+ await this.#write();
102
+ }
103
+
104
+ async #write(): Promise<void> {
105
+ this.#writing = true;
106
+
107
+ try {
108
+ await writeFile(this.#filePath, JSON.stringify(this.#details));
109
+ } catch {}
110
+
111
+ this.#writing = false;
112
+ }
54
113
  }
55
114
 
56
115
  export class FileSystemCacheStorage implements CacheStorage {
57
116
 
58
117
  #directory: string;
59
118
  #hashes: Map<string, string> = new Map();
119
+
120
+ constructor(directory: string) {
121
+ this.#directory = directory;
122
+ }
60
123
 
61
124
  hash(key: string): string {
62
125
  let hash = this.#hashes.get(key);
63
126
 
64
127
  if (hash != null) return hash;
65
128
 
66
- hash = createHash('md5').update(key).digest('hex');
129
+ hash = createHash('sha256').update(key).digest('hex');
67
130
 
68
131
  this.#hashes.set(key, hash);
69
132
 
@@ -83,4 +146,42 @@ export class FileSystemCacheStorage implements CacheStorage {
83
146
  async invalidate(key: string): Promise<void> {
84
147
  await rm(join(this.#directory, this.hash(key)));
85
148
  }
149
+
150
+ async flush(): Promise<void> {
151
+ const promises: Array<Promise<void>> = [];
152
+
153
+ for (const hash of this.#hashes.values()) {
154
+ promises.push(rm(join(this.#directory, hash)));
155
+ }
156
+
157
+ this.#hashes = new Map();
158
+ await Promise.all(promises);
159
+ }
160
+ }
161
+
162
+ export class FileSystemCache extends Cache {
163
+ #fileMeta: FileCacheMeta;
164
+ #fileSystemStorage: FileSystemCacheStorage;
165
+
166
+ constructor(registry: Registry, filePath: string, directory: string, upstream?: UpstreamCache) {
167
+ const meta = new FileCacheMeta(filePath);
168
+ const storage = new FileSystemCacheStorage(directory);
169
+
170
+ super(
171
+ registry,
172
+ meta,
173
+ storage,
174
+ upstream,
175
+ );
176
+
177
+ this.#fileMeta = meta;
178
+ this.#fileSystemStorage = storage;
179
+ }
180
+
181
+ async flush() {
182
+ return Promise.all([
183
+ this.#fileMeta.flush(),
184
+ this.#fileSystemStorage.flush(),
185
+ ]);
186
+ }
86
187
  }
@@ -1,10 +1,18 @@
1
- import type {CacheDetails, CacheHitHandle, CacheMeta, CacheStorage, CacheMissHandle} from './types.js';
1
+ import {Registry} from '../registry.ts';
2
+ import type {CacheDetails, CacheHitHandle, CacheMeta, CacheStorage, CacheMissHandle, UpstreamCache, LockedCacheMissHandle} from './types.ts';
3
+ import {Cache} from './cache.ts';
2
4
 
3
5
 
4
6
  export class InMemoryCacheMeta implements CacheMeta {
5
7
  #details: Map<string, CacheDetails> = new Map();
8
+ #locks: Map<string, Promise<void>> = new Map();
9
+ #flushLock: Promise<void> | undefined;
10
+
11
+ async get(key: string): Promise<CacheHitHandle | CacheMissHandle> {
12
+ if (this.#flushLock) {
13
+ await this.#flushLock;
14
+ }
6
15
 
7
- async get(key: string): Promise<CacheHitHandle| CacheMissHandle> {
8
16
  const details = this.#details.get(key);
9
17
  async function set(details: CacheDetails) {
10
18
  this.#details.set(key, details);
@@ -24,10 +32,67 @@ export class InMemoryCacheMeta implements CacheMeta {
24
32
  };
25
33
  }
26
34
 
27
- async set(key: string, details: CacheDetails) {
35
+ set(key: string, details: CacheDetails): void {
28
36
  this.#details.set(key, details);
29
37
  }
30
38
 
39
+ async getOrLock(key: string): Promise<CacheHitHandle | LockedCacheMissHandle> {
40
+ if (this.#flushLock) {
41
+ await this.#flushLock;
42
+ }
43
+
44
+ const lock = this.#locks.get(key);
45
+
46
+ if (lock != null) {
47
+ await lock;
48
+ }
49
+
50
+ const details = this.#details.get(key);
51
+ const { resolve, promise } = Promise.withResolvers<void>();
52
+
53
+ this.#locks.set(key, promise);
54
+
55
+ function set(details: CacheDetails) {
56
+ this.#details.set(key, details);
57
+ }
58
+
59
+ const release = () => {
60
+ resolve();
61
+ this.#locks.delete(key);
62
+ }
63
+
64
+ if (details == null) {
65
+ return {
66
+ type: 'locked-cache-miss',
67
+ set,
68
+ release,
69
+ };
70
+ }
71
+
72
+ return {
73
+ ...details,
74
+ type: 'cache-hit',
75
+ set,
76
+ };
77
+ }
78
+
79
+ invalidate(key: string): void {
80
+ this.#details.delete(key);
81
+ }
82
+
83
+ async flush(): Promise<void> {
84
+ const { resolve, promise } = Promise.withResolvers<void>();
85
+ this.#flushLock = promise;
86
+
87
+ // there could be a race condition here where the values are
88
+ // flused before queued requests can get them.
89
+ await Promise.all(this.#locks.values());
90
+
91
+ this.#details = new Map();
92
+ this.#locks = new Map();
93
+
94
+ resolve();
95
+ }
31
96
  }
32
97
 
33
98
  export class InMemoryCacheStorage implements CacheStorage {
@@ -47,6 +112,23 @@ export class InMemoryCacheStorage implements CacheStorage {
47
112
  this.#cache.delete(key);
48
113
  }
49
114
 
115
+ flush(): void {
116
+ this.#cache = new Map();
117
+ }
50
118
  }
51
119
 
120
+ export class InMemoryCache extends Cache {
121
+ constructor(registry: Registry, upstream?: UpstreamCache) {
122
+ super(
123
+ registry,
124
+ new InMemoryCacheMeta(),
125
+ new InMemoryCacheStorage(),
126
+ upstream,
127
+ );
128
+ }
52
129
 
130
+ async flush() {
131
+ await this.meta.flush();
132
+ this.storage.flush();
133
+ }
134
+ }