@atolis-hq/wake 0.3.71 → 0.3.72
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.
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, open, readFile, readdir, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, open, readFile, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
export class FileProjectionStore {
|
|
5
5
|
root;
|
|
6
6
|
constructor(root) {
|
|
7
7
|
this.root = root;
|
|
8
8
|
}
|
|
9
|
+
listCache = new Map();
|
|
9
10
|
async read(namespace, key) {
|
|
10
11
|
try {
|
|
11
12
|
return JSON.parse(await readFile(this.path(namespace, key), 'utf8'));
|
|
@@ -18,23 +19,47 @@ export class FileProjectionStore {
|
|
|
18
19
|
}
|
|
19
20
|
async write(projection) {
|
|
20
21
|
await atomicJson(this.path(projection.namespace, projection.key), projection);
|
|
22
|
+
this.listCache.delete(projection.namespace);
|
|
21
23
|
}
|
|
24
|
+
// Callers (advance-once, orchestration-service, DeliveryService, ...) list()
|
|
25
|
+
// an entire namespace unconditionally on every control-plane tick, even when
|
|
26
|
+
// fully idle. Without a cache that's a readdir+open+read+JSON.parse of every
|
|
27
|
+
// projection file in the namespace every tick forever, mirroring the same
|
|
28
|
+
// cost FileEventJournal.scan() already avoids for the event journal via a
|
|
29
|
+
// fingerprint cache. Same fix here: only re-read files whose directory
|
|
30
|
+
// listing (name:size:mtimeMs) actually changed since the last list().
|
|
22
31
|
async list(namespace) {
|
|
23
32
|
const directory = join(this.root, 'projections', encode(namespace));
|
|
33
|
+
let files;
|
|
24
34
|
try {
|
|
25
|
-
|
|
26
|
-
return Promise.all(files.map(async (file) => JSON.parse(await readFile(join(directory, file), 'utf8'))));
|
|
35
|
+
files = (await readdir(directory)).filter((file) => file.endsWith('.json')).sort();
|
|
27
36
|
}
|
|
28
37
|
catch (error) {
|
|
29
|
-
if (error.code === 'ENOENT')
|
|
38
|
+
if (error.code === 'ENOENT') {
|
|
39
|
+
this.listCache.delete(namespace);
|
|
30
40
|
return [];
|
|
41
|
+
}
|
|
31
42
|
throw error;
|
|
32
43
|
}
|
|
44
|
+
const fingerprint = (await Promise.all(files.map(async (file) => {
|
|
45
|
+
const info = await stat(join(directory, file));
|
|
46
|
+
return `${file}:${info.size}:${info.mtimeMs}`;
|
|
47
|
+
}))).join('|');
|
|
48
|
+
const cached = this.listCache.get(namespace);
|
|
49
|
+
if (cached?.fingerprint === fingerprint)
|
|
50
|
+
return cached.entries;
|
|
51
|
+
const entries = await Promise.all(files.map(async (file) => JSON.parse(await readFile(join(directory, file), 'utf8'))));
|
|
52
|
+
this.listCache.set(namespace, { fingerprint, entries });
|
|
53
|
+
return entries;
|
|
33
54
|
}
|
|
34
55
|
async clear(namespace) {
|
|
35
56
|
await rm(namespace === undefined
|
|
36
57
|
? join(this.root, 'projections')
|
|
37
58
|
: join(this.root, 'projections', encode(namespace)), { recursive: true, force: true });
|
|
59
|
+
if (namespace === undefined)
|
|
60
|
+
this.listCache.clear();
|
|
61
|
+
else
|
|
62
|
+
this.listCache.delete(namespace);
|
|
38
63
|
}
|
|
39
64
|
path(namespace, key) {
|
|
40
65
|
return join(this.root, 'projections', encode(namespace), `${encode(key)}.json`);
|