@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.
@@ -108,4 +108,4 @@ export function resolveWakeVersion(options = {}) {
108
108
  return `g${headHash.slice(0, 7)}`;
109
109
  return '0.1.0-dev';
110
110
  }
111
- export const wakeVersion = "g3d78f06";
111
+ export const wakeVersion = "g06dff25";
@@ -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
- const files = (await readdir(directory)).filter((file) => file.endsWith('.json')).sort();
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`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.3.71",
3
+ "version": "0.3.72",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {