@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.
- package/dist/accept.js +0 -1
- package/dist/actions/actionSets.d.ts +3 -3
- package/dist/actions/actions.d.ts +69 -48
- package/dist/actions/actions.js +39 -4
- package/dist/actions/context.d.ts +15 -11
- package/dist/actions/context.js +5 -0
- package/dist/actions/meta.d.ts +18 -12
- package/dist/actions/meta.js +114 -38
- package/dist/actions/spec.d.ts +3 -3
- package/dist/actions/types.d.ts +45 -16
- package/dist/actions/writer.d.ts +1 -1
- package/dist/actions/writer.test.js +2 -2
- package/dist/cache/cache.d.ts +3 -3
- package/dist/cache/cache.js +111 -42
- package/dist/cache/etag.test.js +1 -1
- package/dist/cache/file.d.ts +33 -1
- package/dist/cache/file.js +92 -10
- package/dist/cache/memory.d.ts +12 -2
- package/dist/cache/memory.js +63 -1
- package/dist/cache/types.d.ts +51 -22
- package/dist/errors.d.ts +1 -1
- package/dist/jsonld.d.ts +1 -1
- package/dist/makeTypeDefs.d.ts +2 -2
- package/dist/mod.d.ts +17 -15
- package/dist/mod.js +17 -15
- package/dist/processAction.d.ts +2 -2
- package/dist/processAction.js +1 -1
- package/dist/registry.d.ts +74 -8
- package/dist/registry.js +70 -8
- package/dist/registry.test.js +1 -1
- package/dist/scopes.d.ts +8 -8
- package/dist/scopes.js +8 -5
- package/dist/utils/contextBuilder.d.ts +1 -1
- package/dist/utils/getActionContext.d.ts +2 -2
- package/dist/utils/getPropertyValueSpecifications.d.ts +1 -1
- package/dist/utils/getRequestBodyValues.d.ts +3 -3
- package/dist/utils/getRequestIRIValues.d.ts +2 -2
- package/dist/utils/isPopulatedObject.js +1 -1
- package/dist/utils/makeAppendProblemDetails.d.ts +1 -1
- package/dist/utils/makeURLPattern.js +1 -0
- package/dist/utils/parseSearchParams.d.ts +2 -2
- package/dist/validators.d.ts +2 -2
- package/dist/validators.js +2 -2
- package/lib/accept.test.ts +1 -1
- package/lib/accept.ts +0 -2
- package/lib/actions/actionSets.ts +4 -4
- package/lib/actions/actions.ts +159 -99
- package/lib/actions/context.ts +22 -10
- package/lib/actions/meta.ts +140 -55
- package/lib/actions/path.test.ts +1 -1
- package/lib/actions/path.ts +1 -1
- package/lib/actions/spec.ts +3 -3
- package/lib/actions/types.ts +60 -15
- package/lib/actions/writer.test.ts +2 -2
- package/lib/actions/writer.ts +1 -1
- package/lib/cache/cache.ts +138 -52
- package/lib/cache/etag.test.ts +1 -1
- package/lib/cache/file.ts +113 -12
- package/lib/cache/memory.ts +85 -3
- package/lib/cache/types.ts +70 -23
- package/lib/errors.ts +1 -1
- package/lib/jsonld.ts +1 -1
- package/lib/makeTypeDefs.ts +5 -5
- package/lib/mod.ts +17 -15
- package/lib/processAction.ts +14 -14
- package/lib/registry.test.ts +1 -1
- package/lib/registry.ts +96 -19
- package/lib/request.ts +1 -1
- package/lib/scopes.test.ts +2 -2
- package/lib/scopes.ts +14 -11
- package/lib/utils/contextBuilder.ts +3 -3
- package/lib/utils/getActionContext.ts +4 -4
- package/lib/utils/getInternalName.ts +1 -1
- package/lib/utils/getPropertyValueSpecifications.ts +4 -4
- package/lib/utils/getRequestBodyValues.ts +5 -5
- package/lib/utils/getRequestIRIValues.ts +4 -4
- package/lib/utils/isPopulatedObject.ts +1 -1
- package/lib/utils/makeAppendProblemDetails.ts +1 -1
- package/lib/utils/makeURLPattern.ts +1 -0
- package/lib/utils/parseSearchParams.ts +2 -2
- package/lib/validators.ts +5 -5
- package/package.json +4 -2
package/lib/cache/cache.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import {createHash} from 'node:crypto';
|
|
2
|
-
import {CacheContext, NextFn, Registry} from '../mod.
|
|
3
|
-
import {EtagConditions} from './etag.
|
|
4
|
-
import type {CacheBuilder, CacheEntryDescriptor, CacheETagArgs, CacheETagInstanceArgs, CacheHitHandle, CacheHTTPArgs, CacheHTTPInstanceArgs, CacheMeta, CacheMissHandle, CacheStorage, CacheStoreArgs, CacheStoreInstanceArgs, LockedCacheMissHandle, UpstreamCache} from './types.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
212
|
-
|
|
281
|
+
if (resourceState?.type === 'cache-hit') {
|
|
282
|
+
if (this.#isNotModified(ctx.req.headers, resourceState.etag)) {
|
|
213
283
|
ctx.hit = true;
|
|
214
|
-
ctx.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
|
-
}
|
|
223
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
374
|
+
return `"${hash}"`;
|
|
289
375
|
}
|
|
290
376
|
|
|
291
377
|
}
|
package/lib/cache/etag.test.ts
CHANGED
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.
|
|
3
|
-
import {
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
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('
|
|
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
|
}
|
package/lib/cache/memory.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
+
}
|