@volter/supercode-orchestrator 0.5.11 → 0.5.12

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,6 +1,6 @@
1
1
  // Public native workflow observation. Filesystem custody authorizes this door;
2
2
  // Teams separately authorizes the Org event and its installed subscribers.
3
- import { watch, existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { watch, existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
4
4
  import { dirname, basename, join, resolve } from 'node:path';
5
5
  import { createHash } from 'node:crypto';
6
6
  import { listBoards, openBoard, backingPaths } from './store.mjs';
@@ -9,28 +9,39 @@ import { recordCardPublications } from './published.mjs';
9
9
  const identity = root => createHash('sha256').update(realpathSync(root)).digest('hex');
10
10
  const cursorOf = (home, boards) => Buffer.from(JSON.stringify({version:1,home,boards})).toString('base64url');
11
11
  const resync = reason => Object.assign(new Error(reason), {code:'resync_required'});
12
+ const RESETTLE_MS = 30_000;
12
13
 
13
14
  /** Snapshot once, then replay only committed publications on backing notifications. */
14
15
  export async function* followBoardPublications({root,after,signal}) {
15
16
  const home=identity(root), positions={}, watchers=new Map();
16
17
  let dirty=true, wake=null, failure=null, inventory=null;
17
- const notify=()=>{dirty=true;wake?.();};
18
+ // The stores' files as this follower last left them. Its own pass writes (the publication transaction) and closes
19
+ // the board, which rewrites the WAL and raises a watch event of its own; an event that finds every store file as the
20
+ // pass left it is that echo, not a change, and does not start another pass. Without this the follower woke itself
21
+ // every half second and burned a core with nothing to publish (t_0328d520).
22
+ let stores=[], settled=null;
23
+ const stamp=()=>stores.map(path=>{try{const s=statSync(path);return `${s.size}:${s.mtimeMs}`;}catch{return '-';}}).join('|');
24
+ const notify=(name=null)=>{
25
+ if(name!==null&&settled!==null&&stores.some(path=>basename(path)===name)&&stamp()===settled)return;
26
+ dirty=true;wake?.();
27
+ };
18
28
  const directory=(path,accept)=>{
19
29
  if(!existsSync(path))return;
20
30
  if(watchers.has(path)){watchers.get(path).accept.push(accept);return;}
21
31
  const entry={accept:[accept],handle:null};
22
- entry.handle=watch(path,(_kind,name)=>{if(name==null||entry.accept.some(fn=>fn(String(name))))notify();});
32
+ entry.handle=watch(path,(_kind,name)=>{if(name==null)notify();else if(entry.accept.some(fn=>fn(String(name))))notify(String(name));});
23
33
  entry.handle.on('error',error=>{failure=error;notify();});watchers.set(path,entry);
24
34
  };
25
35
  const file=path=>directory(dirname(path),name=>name===basename(path));
26
36
  const register=slugs=>{
27
37
  for(const entry of watchers.values())entry.accept=[];
38
+ stores=[join(root,'kanban.db'),join(root,'kanban.db-wal')];
28
39
  directory(root,name=>['kanban.db','kanban.db-wal','kanban'].includes(name));
29
40
  directory(join(root,'kanban'),name=>['boards','ztrack.json','board'].includes(name));
30
41
  directory(join(root,'kanban','boards'),()=>true);
31
42
  for(const slug of slugs){
32
43
  const paths=backingPaths(root,slug);
33
- file(paths.sqlite);file(`${paths.sqlite}-wal`);file(paths.ztrack);
44
+ file(paths.sqlite);file(`${paths.sqlite}-wal`);file(paths.ztrack);stores.push(paths.sqlite,`${paths.sqlite}-wal`);
34
45
  directory(paths.files,name=>name.endsWith('.json'));
35
46
  try {const cfg=JSON.parse(readFileSync(paths.ztrack,'utf8'));const document=resolve(dirname(paths.ztrack),cfg.document);file(document);file(`${document}.archive.md`);}catch{/* other backing */}
36
47
  }
@@ -83,7 +94,12 @@ export async function* followBoardPublications({root,after,signal}) {
83
94
  }
84
95
  if(initial){initial=false;yield {type:'snapshot_end',cursor:cursorOf(home,positions)};dirty=true;}
85
96
  else if(checkpointPending){checkpointPending=false;yield {type:'checkpoint',cursor:cursorOf(home,positions)};}
86
- if(!dirty&&!signal?.aborted)await new Promise(done=>{wake=done;if(dirty||signal?.aborted)done();});
97
+ settled=stamp();
98
+ // A write that lands between the pass and the stamp above has its event taken for this pass's echo: a pass
99
+ // every RESETTLE_MS anyway bounds how long that write can wait.
100
+ let timer=null;
101
+ if(!dirty&&!signal?.aborted)await new Promise(done=>{wake=done;timer=setTimeout(()=>{dirty=true;done();},RESETTLE_MS);timer.unref?.();if(dirty||signal?.aborted)done();});
102
+ clearTimeout(timer);
87
103
  wake=null;
88
104
  }
89
105
  }finally{signal?.removeEventListener('abort',aborted);for(const {handle} of watchers.values())handle.close();}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "type": "module",
5
5
  "description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
6
6
  "exports": {