@volter/supercode-orchestrator 0.5.41 → 0.5.42
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/board/cli.mjs +24 -2
- package/board/engine.mjs +13 -1
- package/board/event-stream.mjs +5 -3
- package/board/published.mjs +0 -35
- package/board/wake.mjs +1 -7
- package/board.mjs +4 -0
- package/package.json +2 -2
- package/board/event-relay.mjs +0 -51
package/board/cli.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// which lands here. The home is `--root`, else `SUPERCODE_ORCHESTRATOR_HOME`, else `HERMES_HOME`; the board is
|
|
6
6
|
// `--board <slug>`, else `HERMES_KANBAN_BOARD`, else the home's current board (`kanban/current`), else `default`.
|
|
7
7
|
import { taskStatusOf } from './published.mjs';
|
|
8
|
-
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
10
|
import { recordSlow, takeSteps } from '@volter/supercode-harness-sdk/slow-log';
|
|
11
11
|
import { basename, join, resolve } from 'node:path';
|
|
@@ -15,6 +15,19 @@ import { actorOf, view, backingPaths, layOut, backingOf, boardExists, boardPath,
|
|
|
15
15
|
import { isLocalWorkspace } from './workspace.mjs';
|
|
16
16
|
import { openZtrackBoard } from './ztrack.mjs';
|
|
17
17
|
import * as engine from './engine.mjs';
|
|
18
|
+
import { runtimeDoor } from './runtime-door.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What this machine's connector reports of publishing this board home (`harness.v1.boards.list`): whether the team
|
|
22
|
+
* refused its credential, the publication it is retrying and the ones it abandoned. Null when no daemon answers or the
|
|
23
|
+
* home is not registered: diagnostics then say nothing of publishing.
|
|
24
|
+
*/
|
|
25
|
+
function boardPublishing(root) {
|
|
26
|
+
const read = runtimeDoor(null, 'harness.v1.boards.list', {}, { timeout: 15_000 });
|
|
27
|
+
if (read.code !== 0) return null;
|
|
28
|
+
let here; try { here = realpathSync(root); } catch { here = resolve(root); }
|
|
29
|
+
try { const listed = JSON.parse(read.stdout); const board = listed.boards?.find((b) => b.root === here); return board ? { ...board, refused: listed.refused ?? null } : null; } catch { return null; }
|
|
30
|
+
}
|
|
18
31
|
import { requestAdoption, alertManagers, boardAddress, boardLabel, boardSettings, callerSession, commandEnv, reconcileCallerLaunch, closeRequested, lanesOf, localMachine, observeFleet, paused, plan, startCard, stopDispatching, supercodeBin, tick, deliver, startDelivery } from './dispatch.mjs';
|
|
19
32
|
import { watchBoard } from './wake.mjs';
|
|
20
33
|
import { streamBoardPublications } from './event-stream.mjs';
|
|
@@ -819,10 +832,12 @@ export async function runWorkflow(argv) {
|
|
|
819
832
|
case 'diagnostics': case 'diag': {
|
|
820
833
|
const { settings } = await laneState(root).catch(() => ({ settings: boardSettings({}) }));
|
|
821
834
|
const fleet = observeFleet(root);
|
|
835
|
+
const publishing = boardPublishing(root);
|
|
822
836
|
const order = ['warning', 'error', 'critical'];
|
|
823
837
|
const found = withBoard((b) => {
|
|
824
838
|
const cards = args.task ? [engine.getTask(b.db, args.task)].filter(Boolean) : engine.listCards(b.db, {});
|
|
825
|
-
return
|
|
839
|
+
return [...(args.task ? [] : engine.diagnosePublishing(publishing).map((d) => ({ task: '(board)', title_of_task: null, ...d }))),
|
|
840
|
+
...cards.flatMap((t) => engine.diagnose(b.db, t, { failureLimit: settings.failureLimit, fleet, launchWindowSeconds: settings.launchWindow, publishing }).map((d) => ({ task: t.id, title_of_task: t.title, ...d })))]
|
|
826
841
|
.filter((d) => !args.severity || order.indexOf(d.severity) >= order.indexOf(args.severity));
|
|
827
842
|
});
|
|
828
843
|
if (args.json) { say(args, '', found); return 0; }
|
|
@@ -1448,6 +1463,13 @@ async function serve(root, args = {}) {
|
|
|
1448
1463
|
const running = installed();
|
|
1449
1464
|
log(`dispatching the boards of ${root} from ${localMachine()} (orchestrator ${running ?? 'unknown'})`);
|
|
1450
1465
|
events = watchBoard({ root, binary: supercodeBin(), env: commandEnv(), log });
|
|
1466
|
+
// The board's events are published by this machine's connector, not by the dispatcher (D137, D138 step 4): the home is
|
|
1467
|
+
// named to the daemon once, and the connector follows its `workflow events` from then on, with or without this serve.
|
|
1468
|
+
{ const named = runtimeDoor(null, 'harness.v1.boards.register', { root }, { timeout: 30_000 });
|
|
1469
|
+
const refused = named.code === 0 ? JSON.parse(named.stdout || '{}').refused ?? null : null;
|
|
1470
|
+
log(named.code !== 0 ? `board events: not published; the machine daemon did not take this home: ${(named.stderr || named.stdout).trim().split('\n').at(-1)}`
|
|
1471
|
+
: refused ? `board events: registered, but the team refused this machine's credential (${refused.status}); nothing is published until it is re-enrolled`
|
|
1472
|
+
: `board events: this home is published by the machine's connector`); }
|
|
1451
1473
|
log('dispatch wakes on board writes, document edits and board mail; the interval sweep reads each machine\'s own door');
|
|
1452
1474
|
const stuck = {};
|
|
1453
1475
|
const warned = {};
|
package/board/engine.mjs
CHANGED
|
@@ -912,12 +912,20 @@ export function gcEvents(board, seconds) {
|
|
|
912
912
|
|
|
913
913
|
const hours = (s) => s / 3600;
|
|
914
914
|
|
|
915
|
+
/** The board home's publishing, as its machine's connector reports it (`harness.v1.boards.list`): what holds it all. */
|
|
916
|
+
export function diagnosePublishing(publishing) {
|
|
917
|
+
if (!publishing) return [];
|
|
918
|
+
if (publishing.refused) return [{ kind: 'publishing_refused', severity: 'error', title: `the team refused this machine's credential (${publishing.refused.status}); no card of this home is published until the machine is re-enrolled`, data: publishing.refused, actions: ['supercode teams enroll'] }];
|
|
919
|
+
if (publishing.error) return [{ kind: 'publishing_waits', severity: 'warning', title: `publishing to the team waits and is retried: ${publishing.error}`, data: { failing: publishing.failing ?? null }, actions: [] }];
|
|
920
|
+
return [];
|
|
921
|
+
}
|
|
922
|
+
|
|
915
923
|
/**
|
|
916
924
|
* Distress signals on a card, computed on demand and never stored (Hermes's diagnostics, the same kinds where the
|
|
917
925
|
* same thing can go wrong, and supercode's own where only its sessions can): each `{ kind, severity, title, data,
|
|
918
926
|
* actions }`, worst first.
|
|
919
927
|
*/
|
|
920
|
-
export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, strandedSeconds = 30 * 60, blockedHours = 24, fleet = null, launchWindowSeconds = 300 } = {}) {
|
|
928
|
+
export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, strandedSeconds = 30 * 60, blockedHours = 24, fleet = null, launchWindowSeconds = 300, publishing = null } = {}) {
|
|
921
929
|
const out = [];
|
|
922
930
|
const t = now();
|
|
923
931
|
const events = all(db, 'SELECT kind, payload, created_at FROM task_events WHERE task_id = ? ORDER BY id', task.id);
|
|
@@ -979,6 +987,10 @@ export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, stran
|
|
|
979
987
|
const p = json(row.payload, {}) ?? {};
|
|
980
988
|
out.push({ kind: 'notice_abandoned', severity: 'warning', title: `the notice of event ${p.source_event} to ${p.address} was abandoned after ${p.attempts} attempts: ${p.error ?? 'no reason recorded'}`, data: { event: p.source_event, address: p.address, attempts: p.attempts }, actions: [] });
|
|
981
989
|
}
|
|
990
|
+
// publications of the card the machine's connector abandoned: the team refused each one past its attempts
|
|
991
|
+
for (const a of (publishing?.abandoned ?? []).filter((a) => a.card === task.id).slice(0, 20)) {
|
|
992
|
+
out.push({ kind: 'publication_abandoned', severity: 'warning', title: `its ${a.kind} (sequence ${a.sequence}) was not published to the team: refused ${a.attempts} times (${a.status} ${a.code ?? ''}): ${a.error}`, data: { publication: a.publication, sequence: a.sequence, status: a.status, code: a.code }, actions: [] });
|
|
993
|
+
}
|
|
982
994
|
const order = ['critical', 'error', 'warning'];
|
|
983
995
|
return out.sort((a, b) => order.indexOf(a.severity) - order.indexOf(b.severity));
|
|
984
996
|
}
|
package/board/event-stream.mjs
CHANGED
|
@@ -81,9 +81,11 @@ export async function* followBoardPublications({root,after,signal}) {
|
|
|
81
81
|
// The transaction's watermark fences concurrent mutations. Replay
|
|
82
82
|
// after it repairs any head updated while this inventory is read.
|
|
83
83
|
for(const sequence of boundary.heads){
|
|
84
|
-
const row=board.db.prepare('SELECT snapshot FROM supercode_publications WHERE sequence=?').get(sequence);
|
|
84
|
+
const row=board.db.prepare('SELECT id,kind,sequence,snapshot FROM supercode_publications WHERE sequence=?').get(sequence);
|
|
85
85
|
if(!row)throw resync('Workflow snapshot unavailable');
|
|
86
|
-
|
|
86
|
+
// each item names the publication it is (its id, kind and sequence), as an event does, so a follower that
|
|
87
|
+
// republishes it keys and orders it the same way however it first read it
|
|
88
|
+
yield {type:'snapshot_item',id:row.id,kind:row.kind,sequence:row.sequence,data:{board:slug,card:JSON.parse(row.snapshot)}};
|
|
87
89
|
}
|
|
88
90
|
positions[slug]={log:boundary.log,sequence:boundary.high};
|
|
89
91
|
}else{
|
|
@@ -91,7 +93,7 @@ export async function* followBoardPublications({root,after,signal}) {
|
|
|
91
93
|
if(!position||position.log!==boundary.log||!Number.isSafeInteger(position.sequence)||position.sequence<boundary.floor||position.sequence>boundary.high)throw resync(`Workflow publication log changed: ${slug}`);
|
|
92
94
|
for(;;){
|
|
93
95
|
const rows=board.db.prepare('SELECT * FROM supercode_publications WHERE sequence>? ORDER BY sequence LIMIT 32').all(positions[slug].sequence);if(!rows.length)break;
|
|
94
|
-
for(const row of rows){positions[slug].sequence=row.sequence;yield {type:'event',id:row.id,kind:row.kind,cursor:cursorOf(home,positions),data:{board:slug,card:JSON.parse(row.snapshot)}};}
|
|
96
|
+
for(const row of rows){positions[slug].sequence=row.sequence;yield {type:'event',id:row.id,kind:row.kind,sequence:row.sequence,cursor:cursorOf(home,positions),data:{board:slug,card:JSON.parse(row.snapshot)}};}
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
}finally{board.close();const paths=backingPaths(root,slug);for(const path of [paths.sqlite,`${paths.sqlite}-wal`])settled.set(path,statOf(path));}
|
package/board/published.mjs
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
// The card writer commits the full snapshot and a retained delivery obligation together.
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { realpathSync } from 'node:fs';
|
|
4
3
|
export const PUBLICATION_SCHEMA = `
|
|
5
|
-
CREATE TABLE IF NOT EXISTS supercode_publication_receipts (
|
|
6
|
-
event_id TEXT NOT NULL, destination TEXT NOT NULL, PRIMARY KEY(event_id,destination)
|
|
7
|
-
);
|
|
8
4
|
CREATE TABLE IF NOT EXISTS supercode_publications (
|
|
9
5
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, resource_id TEXT NOT NULL,
|
|
10
6
|
kind TEXT NOT NULL, snapshot TEXT NOT NULL, actor TEXT, created_at INTEGER NOT NULL
|
|
@@ -103,39 +99,8 @@ export function recordCardPublications(db,actor) {
|
|
|
103
99
|
if(history>2*RETAINED_CHANGES){
|
|
104
100
|
const cut=Number(db.prepare('SELECT sequence FROM supercode_publications WHERE sequence NOT IN (SELECT sequence FROM supercode_publication_heads) ORDER BY sequence DESC LIMIT 1 OFFSET ?').get(RETAINED_CHANGES).sequence);
|
|
105
101
|
const old='SELECT id FROM supercode_publications WHERE sequence<=? AND sequence NOT IN (SELECT sequence FROM supercode_publication_heads)';
|
|
106
|
-
db.prepare(`DELETE FROM supercode_publication_receipts WHERE event_id IN (${old})`).run(cut);
|
|
107
102
|
db.prepare(`DELETE FROM supercode_publications WHERE id IN (${old})`).run(cut);
|
|
108
103
|
db.prepare('UPDATE supercode_publication_log SET pruned_through=MAX(pruned_through,?)').run(cut);
|
|
109
104
|
}
|
|
110
105
|
return changed;
|
|
111
106
|
}
|
|
112
|
-
export const boardSourceName = (root,slug) => `board:${hash(realpathSync(root)).slice(0,24)}:${slug}`;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* The card as one published event may carry it (`limit`: the bytes its `data`, `{ board, card }`, may serialize to).
|
|
116
|
-
* The board keeps every card whole; a card too large to send keeps its newest history: the oldest comments, attempts
|
|
117
|
-
* and reviews go first, then the body's tail, and `omitted` counts what the event leaves out.
|
|
118
|
-
*/
|
|
119
|
-
export function publishableCard(card,board,limit) {
|
|
120
|
-
const bytes=value=>Buffer.byteLength(JSON.stringify(value));
|
|
121
|
-
const size=value=>bytes({board,card:value});
|
|
122
|
-
if(size(card)<=limit)return card;
|
|
123
|
-
const omitted={comments:0,attempts:0,reviews:0,body_chars:0};
|
|
124
|
-
const fitted={...card,omitted};
|
|
125
|
-
// Counted once per item (its bytes and a separator), then checked whole: a card of megabytes fits in one pass.
|
|
126
|
-
let over=size(fitted)-limit;
|
|
127
|
-
for(const field of ['comments','attempts','reviews']){
|
|
128
|
-
const items=card[field]??[];let drop=0;
|
|
129
|
-
while(drop<items.length&&over>0){over-=bytes(items[drop])+1;drop+=1;}
|
|
130
|
-
fitted[field]=items.slice(drop);omitted[field]=drop;
|
|
131
|
-
}
|
|
132
|
-
for(const field of ['comments','attempts','reviews']){
|
|
133
|
-
while(fitted[field].length&&size(fitted)>limit){fitted[field]=fitted[field].slice(1);omitted[field]+=1;}
|
|
134
|
-
}
|
|
135
|
-
const body=String(card.body??'');
|
|
136
|
-
for(let keep=body.length;size(fitted)>limit&&keep>0;){
|
|
137
|
-
keep=Math.max(0,Math.min(keep-1,keep-(size(fitted)-limit)-64));
|
|
138
|
-
fitted.body=body.slice(0,keep);omitted.body_chars=body.length-keep;
|
|
139
|
-
}
|
|
140
|
-
return fitted;
|
|
141
|
-
}
|
package/board/wake.mjs
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { watch, existsSync, readFileSync, readdirSync, writeFileSync, renameSync, unlinkSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
-
import { boardEventRelay } from './event-relay.mjs';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
8
7
|
|
|
9
8
|
export function signalBoardWrite(root, actor) {
|
|
@@ -20,13 +19,11 @@ export function signalBoardWrite(root, actor) {
|
|
|
20
19
|
export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
21
20
|
const watchers = new Map();
|
|
22
21
|
let pending = null, waiter = null, closed = false;
|
|
23
|
-
let relay = null;
|
|
24
22
|
const wake = (source, at = Date.now()) => {
|
|
25
23
|
if (closed) return;
|
|
26
24
|
if (!pending) pending = { at, sources: new Set() };
|
|
27
25
|
pending.at = Math.min(pending.at, at);
|
|
28
26
|
pending.sources.add(source);
|
|
29
|
-
void relay?.wake();
|
|
30
27
|
waiter?.();
|
|
31
28
|
};
|
|
32
29
|
const directory = (path, key, accept) => {
|
|
@@ -91,9 +88,6 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
|
91
88
|
}
|
|
92
89
|
};
|
|
93
90
|
refresh();
|
|
94
|
-
const supercodeHome = env.SUPERCODE_HOME || (env.XDG_CONFIG_HOME ? join(env.XDG_CONFIG_HOME,'supercode') : join(env.HOME ?? '', '.config','supercode'));
|
|
95
|
-
relay = boardEventRelay({root,supercodeHome,log});
|
|
96
|
-
void relay.wake();
|
|
97
91
|
return {
|
|
98
92
|
refresh,
|
|
99
93
|
wake,
|
|
@@ -109,7 +103,7 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
|
109
103
|
return result ? { at: result.at, sources: [...result.sources] } : null;
|
|
110
104
|
},
|
|
111
105
|
close() {
|
|
112
|
-
closed = true; waiter?.();
|
|
106
|
+
closed = true; waiter?.();
|
|
113
107
|
for (const handle of watchers.values()) handle.close();
|
|
114
108
|
watchers.clear();
|
|
115
109
|
},
|
package/board.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
15
|
import { summary } from './board/cli.mjs';
|
|
16
16
|
import { commandEnv, notifyChats, startDelivery, supercodeBin } from './board/dispatch.mjs';
|
|
17
17
|
import { watchBoard } from './board/wake.mjs';
|
|
18
|
+
import { runtimeDoor } from './board/runtime-door.mjs';
|
|
18
19
|
import { workflowEntry } from './board/workflow.mjs';
|
|
19
20
|
import { evaluate, holds } from './board/expr.mjs';
|
|
20
21
|
|
|
@@ -101,6 +102,9 @@ export async function startBoard({ root, orchestration = null, log = () => {}, a
|
|
|
101
102
|
}
|
|
102
103
|
};
|
|
103
104
|
const events = watchBoard({ root, binary: supercodeBin(), env: commandEnv(), log: message => log('warn', message) });
|
|
105
|
+
// The board's events are published by this machine's connector (D137, D138 step 4): the home is named to it once.
|
|
106
|
+
{ const named = runtimeDoor(null, 'harness.v1.boards.register', { root }, { timeout: 30_000 });
|
|
107
|
+
log(named.code === 0 ? 'info' : 'warn', named.code === 0 ? 'board: its events are published by the machine\'s connector' : `board: events not published; the machine daemon did not take this home: ${(named.stderr || named.stdout).trim().split('\n').at(-1)}`); }
|
|
104
108
|
void (async () => {
|
|
105
109
|
let wakeReason = null;
|
|
106
110
|
while (!stopped) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.42",
|
|
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": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@volter/supercode-harness-sdk": "^0.3.60",
|
|
28
|
-
"@volter/teams": "0.2.
|
|
28
|
+
"@volter/teams": "0.2.14",
|
|
29
29
|
"@volter/ztrack": "^2.4.23",
|
|
30
30
|
"yaml": "^2.9.0"
|
|
31
31
|
},
|
package/board/event-relay.mjs
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// App-owned source names/maps live here; the Teams core knows only a scoped event publisher.
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { eventClient, spentClause } from '@volter/teams/event-client';
|
|
4
|
-
import { EVENT_DATA_LIMIT } from '@volter/teams/event-limits';
|
|
5
|
-
import { boardSourceName, publishableCard, recordCardPublications } from './published.mjs';
|
|
6
|
-
import { listBoards, openBoard } from './store.mjs';
|
|
7
|
-
|
|
8
|
-
export function boardEventRelay({root,context,supercodeHome,log=()=>{}}) {
|
|
9
|
-
let work=null,dirty=false,closed=false,retry=null,reconcile=true,heldUntil=0;const sources=new Map();
|
|
10
|
-
const run=async()=>{
|
|
11
|
-
if(reconcile){
|
|
12
|
-
// Establish every native source head before any network operation. A
|
|
13
|
-
// delivery retry cannot leave later boards without their initial inventory.
|
|
14
|
-
for(const slug of listBoards(root)){const board=openBoard(root,slug);try{board.tx(db=>recordCardPublications(db,null));}finally{board.close();}}
|
|
15
|
-
reconcile=false;
|
|
16
|
-
}
|
|
17
|
-
const {team,context:audience}=eventClient({context,supercodeHome});
|
|
18
|
-
for(const slug of listBoards(root)){
|
|
19
|
-
const board=openBoard(root,slug);
|
|
20
|
-
try{
|
|
21
|
-
const key=JSON.stringify({server:audience.server,org:audience.org,principal:audience.principal,board:slug});
|
|
22
|
-
let source=sources.get(key);
|
|
23
|
-
if(!source){source=(await team.events.createSource({name:boardSourceName(root,slug)},{caller:'board-relay source'})).data.id;sources.set(key,source);}
|
|
24
|
-
const destination=createHash('sha256').update(`${key}:${source}`).digest('hex');
|
|
25
|
-
for(;;){
|
|
26
|
-
const rows=board.db.prepare('SELECT * FROM supercode_publications p WHERE NOT EXISTS (SELECT 1 FROM supercode_publication_receipts r WHERE r.event_id=p.id AND r.destination=?) ORDER BY sequence LIMIT 32').all(destination);if(!rows.length)break;
|
|
27
|
-
for(const row of rows){
|
|
28
|
-
if(closed)return;
|
|
29
|
-
const id=`event_${createHash('sha256').update(`${row.id}:${destination}`).digest('hex')}`;
|
|
30
|
-
await team.events.publish(source,{id,kind:row.kind,resource:{kind:'task',id:`${source}:${row.resource_id}`},revision:row.sequence,data:{board:slug,card:publishableCard(JSON.parse(row.snapshot),slug,EVENT_DATA_LIMIT)},actor:{session:row.actor?`session:${row.actor}`:null}},{idempotencyKey:id,caller:'board-relay publish'})
|
|
31
|
-
.catch(error=>{throw Object.assign(error,{message:`${row.resource_id} ${row.kind} (sequence ${row.sequence}): ${error.message}`});});
|
|
32
|
-
board.tx(db=>db.prepare('INSERT OR IGNORE INTO supercode_publication_receipts VALUES (?,?)').run(row.id,destination));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}finally{board.close();}
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
const wake=(refresh=true)=>{
|
|
39
|
-
reconcile ||= refresh;dirty=true;if(work||closed)return work;
|
|
40
|
-
// Refused for the rate limit, delivery waits out the server's retry-after: a board write meanwhile would only ask
|
|
41
|
-
// again and spend the allowance the credential shares with every other caller.
|
|
42
|
-
if(Date.now()<heldUntil)return null;
|
|
43
|
-
work=(async()=>{while(dirty&&!closed){dirty=false;await run();}})().catch(error=>{
|
|
44
|
-
log(`board event delivery pending: ${error.code?`${error.code}: `:''}${error.message}${spentClause(error)}`);
|
|
45
|
-
// Retry durable delivery, not a read of the board's current state.
|
|
46
|
-
const wait=error.code==='rate_limited'?Math.max(5000,Number(error.details?.retry_after_seconds??30)*1000):5000;heldUntil=Date.now()+wait;
|
|
47
|
-
clearTimeout(retry);retry=setTimeout(()=>{heldUntil=0;wake(false);},wait);retry.unref?.();
|
|
48
|
-
}).finally(()=>{work=null;});return work;
|
|
49
|
-
};
|
|
50
|
-
return {wake,close(){closed=true;clearTimeout(retry);}};
|
|
51
|
-
}
|