@a9n-shoji/rvw 0.2.2 → 0.2.3
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/CHANGELOG.md +18 -0
- package/README.md +6 -3
- package/dist/cli.mjs +188 -19
- package/dist/cli.mjs.map +4 -4
- package/dist/web/assets/DocumentViewer-B4LdSL1l.js +1560 -0
- package/dist/web/assets/{MarkdownTable-DVJ0_MIC.js → MarkdownTable-CdbKPft2.js} +1 -1
- package/dist/web/assets/WalkthroughViewer-D1LlSQw2.js +1 -0
- package/dist/web/assets/{abnfDiagram-N423BO3Z-6dQzEuPV.js → abnfDiagram-N423BO3Z-CE1nZSC9.js} +1 -1
- package/dist/web/assets/architecture-TIHT7OUA-edLV2Zq-.js +1 -0
- package/dist/web/assets/{architectureDiagram-T3A2C74G-DP7hHV7T.js → architectureDiagram-T3A2C74G-B88qYFUo.js} +1 -1
- package/dist/web/assets/{blockDiagram-VBNYF7ZC-ZbwzVKWP.js → blockDiagram-VBNYF7ZC-BaX0rhbb.js} +1 -1
- package/dist/web/assets/{c4Diagram-5PPSVZJV-HVD9_uV4.js → c4Diagram-5PPSVZJV-C1ppBaEC.js} +1 -1
- package/dist/web/assets/channel-J8FHSJDG.js +1 -0
- package/dist/web/assets/{chunk-2GRJ4B5K-Bkxs2d4J.js → chunk-2GRJ4B5K-D2TTocfh.js} +1 -1
- package/dist/web/assets/{chunk-4I5QYGJK-BJHE6zC_.js → chunk-4I5QYGJK-ikT25rPN.js} +1 -1
- package/dist/web/assets/{chunk-5RXB4S5H-B-52dfCQ.js → chunk-5RXB4S5H-CGlPaulj.js} +1 -1
- package/dist/web/assets/{chunk-6Q2QTUOP-BpD0vQ66.js → chunk-6Q2QTUOP-BuuMeCo2.js} +1 -1
- package/dist/web/assets/{chunk-7Z6QIM7H-BTI1qgAO.js → chunk-7Z6QIM7H-DT3BAZ4L.js} +1 -1
- package/dist/web/assets/{chunk-GF5L2VYU-B8BxoCCx.js → chunk-GF5L2VYU-mIdlxzfr.js} +1 -1
- package/dist/web/assets/{chunk-I66GZJ75-zwi-Xf01.js → chunk-I66GZJ75-CGl1hx4J.js} +1 -1
- package/dist/web/assets/{chunk-JQJVKLGR-D9mKBWUz.js → chunk-JQJVKLGR-BLG6gAfx.js} +1 -1
- package/dist/web/assets/{chunk-KBJHAD2P-Qmtir3qc.js → chunk-KBJHAD2P-D_MPzBJx.js} +1 -1
- package/dist/web/assets/{chunk-NSK5VX7P-SP2vZdAo.js → chunk-NSK5VX7P-CiYiU8wP.js} +1 -1
- package/dist/web/assets/{chunk-QR6OTTB3-uVDCHurs.js → chunk-QR6OTTB3-BK8Tnvcc.js} +1 -1
- package/dist/web/assets/{chunk-UBXNYLIW-CtNzBWOU.js → chunk-UBXNYLIW-BuzttTFk.js} +1 -1
- package/dist/web/assets/{chunk-W5SLKNZC-BPfDqbmK.js → chunk-W5SLKNZC-UidmFHe2.js} +1 -1
- package/dist/web/assets/{chunk-WRU74C26-BZkEOexq.js → chunk-WRU74C26-nox7WFnh.js} +1 -1
- package/dist/web/assets/classDiagram-JCYQIIEL-CmTWSnH0.js +1 -0
- package/dist/web/assets/classDiagram-v2-OCEON4UE-CmTWSnH0.js +1 -0
- package/dist/web/assets/{cynefin-VYW2F7L2-BRisCyqC.js → cynefin-VYW2F7L2-wDMibS5s.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-MW4NZA55-ClIrmCkB.js → cynefinDiagram-MW4NZA55-CBTrdvOE.js} +1 -1
- package/dist/web/assets/{dagre-VZM6K2ZE-X31uC_JB.js → dagre-VZM6K2ZE-Bbgn4Rr7.js} +1 -1
- package/dist/web/assets/{diagram-7IWD3JNH-DBQFjl0i.js → diagram-7IWD3JNH-Bm9AuXb9.js} +1 -1
- package/dist/web/assets/{diagram-B4RE2ZJO-BZ7934Wg.js → diagram-B4RE2ZJO-D3lZ-r-H.js} +1 -1
- package/dist/web/assets/{diagram-LBJQPF4R-CcJqH9aY.js → diagram-LBJQPF4R-D-GaynX7.js} +1 -1
- package/dist/web/assets/{diagram-Q27KOJAE-DzM7eiil.js → diagram-Q27KOJAE-BeZRiu1d.js} +1 -1
- package/dist/web/assets/{diagram-UB23O5K3-CVCV-d3m.js → diagram-UB23O5K3-C3GP7iMe.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-BXEA7PRR-D8xb9FBp.js → ebnfDiagram-BXEA7PRR-BJWg0NVU.js} +1 -1
- package/dist/web/assets/{erDiagram-JOGREHBK-1l-G_PMG.js → erDiagram-JOGREHBK-XPmhUeB_.js} +1 -1
- package/dist/web/assets/eventmodeling-45OFAUF4-C3c4qddw.js +1 -0
- package/dist/web/assets/flowDiagram-UKHOOZJN-DMA-a1zf.js +1 -0
- package/dist/web/assets/{ganttDiagram-PKOTCBZU-CDJ9a1tp.js → ganttDiagram-PKOTCBZU-CL-xEo-M.js} +1 -1
- package/dist/web/assets/{gitGraph-TEB2WS4Q-DOeTh5wB.js → gitGraph-TEB2WS4Q-BVRyuIx3.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-DS77QQ5N-DYLxc8Nz.js → gitGraphDiagram-DS77QQ5N-BVNIEupW.js} +1 -1
- package/dist/web/assets/{index-9cOaPesh.js → index-D8GJBw23.js} +98 -98
- package/dist/web/assets/{index-kK3hngnz.css → index-DMD2vvQW.css} +1 -1
- package/dist/web/assets/{info-DKCQHKI2-B5u97QuQ.js → info-DKCQHKI2-BKBUVpfc.js} +1 -1
- package/dist/web/assets/{infoDiagram-6WML65LV-zosKW2OD.js → infoDiagram-6WML65LV-_dTFnzSE.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-VYcc_au8.js → ishikawaDiagram-WSZJBQD7-DIRgD4hO.js} +1 -1
- package/dist/web/assets/{journeyDiagram-NVQOT4AX-C_nU8kP4.js → journeyDiagram-NVQOT4AX-bp7nZrfR.js} +1 -1
- package/dist/web/assets/{kanban-definition-27J2QSJJ-DybKaWfx.js → kanban-definition-27J2QSJJ-C6mZIo8S.js} +1 -1
- package/dist/web/assets/{line-DWsGplGj.js → line-DWTym7eJ.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-D_T_3oIQ.js → mermaid-parser.core-CbecsNA1.js} +3 -3
- package/dist/web/assets/{mermaid.core-BoLGzxB5.js → mermaid.core-CdRuLyYI.js} +4 -4
- package/dist/web/assets/{mindmap-definition-FAOFIHXS-CsdpqPIa.js → mindmap-definition-FAOFIHXS-CU2P1z-9.js} +1 -1
- package/dist/web/assets/{packet-7NZHBO7P-CReAEF1h.js → packet-7NZHBO7P-CZsup6Od.js} +1 -1
- package/dist/web/assets/{pegDiagram-VL7TDLO6-CE9-NPMu.js → pegDiagram-VL7TDLO6-DTTyttpe.js} +1 -1
- package/dist/web/assets/{pie-RZYD4A2V-GYixXUka.js → pie-RZYD4A2V-DgrIiWZA.js} +1 -1
- package/dist/web/assets/{pieDiagram-7S7Q4E2Y-CqaAKNg2.js → pieDiagram-7S7Q4E2Y-BBNFyPT1.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-eRMHJqfj.js → quadrantDiagram-CIZ2JOQS-ejp-Aa2l.js} +1 -1
- package/dist/web/assets/{radar-I7S5WNFK-BwHxP0iK.js → radar-I7S5WNFK-Cp_hfjrI.js} +1 -1
- package/dist/web/assets/{railroad-3IZDKUUU-AHc3O4Oz.js → railroad-3IZDKUUU-Dw-oMal2.js} +1 -1
- package/dist/web/assets/railroad-abnf-AHOZXSZD-BN-ZBhE0.js +1 -0
- package/dist/web/assets/railroad-ebnf-EBAXGLYW-CG97zyI2.js +1 -0
- package/dist/web/assets/railroad-peg-LSFZ7HO6-DHLQ1evQ.js +1 -0
- package/dist/web/assets/{railroadDiagram-AXF67PYL-BhAxCokG.js → railroadDiagram-AXF67PYL-DkBUaj6Z.js} +1 -1
- package/dist/web/assets/{requirementDiagram-LRYGKXZP-BRW-kYhg.js → requirementDiagram-LRYGKXZP-Dff1ex_h.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-W5VNT64P-DfnYLbDJ.js → sankeyDiagram-W5VNT64P-DptSL9Bc.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-SI44F4Z6-WEkIPSg8.js → sequenceDiagram-SI44F4Z6-DEnPcs3U.js} +1 -1
- package/dist/web/assets/{stateDiagram-OKZ733FA-CdN7veIN.js → stateDiagram-OKZ733FA-BwJO1MKA.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-UEYNNEHI-LaSNqvWy.js +1 -0
- package/dist/web/assets/{swimlanes-SLNWSIFB-g9wWLf5Y.js → swimlanes-SLNWSIFB-BosKiwZA.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-C5OBtBlv.js +8 -0
- package/dist/web/assets/{timeline-definition-Z64GVDOM-DjkKZIPE.js → timeline-definition-Z64GVDOM-C5isAn4X.js} +1 -1
- package/dist/web/assets/{treeView-QDETBFTQ-WARa9r09.js → treeView-QDETBFTQ-zEyUXRc4.js} +1 -1
- package/dist/web/assets/{treemap-6X3UGDF4-CTFjdb6k.js → treemap-6X3UGDF4-B-X0EQSE.js} +1 -1
- package/dist/web/assets/{vennDiagram-T6HMQDX7-CI9QrBwu.js → vennDiagram-T6HMQDX7-QMdGieCa.js} +1 -1
- package/dist/web/assets/{wardley-OPB4EBWU-Cfo_Nnla.js → wardley-OPB4EBWU-BaeAF8d8.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-T6FBY63Y-CzGJ4fzZ.js → wardleyDiagram-T6FBY63Y-DTQ_uLUq.js} +1 -1
- package/dist/web/assets/{xychartDiagram-ELKLHX3M-BXCiD7gJ.js → xychartDiagram-ELKLHX3M-BMdBWzib.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +17 -15
- package/skills/rvw-watch-comments/SKILL.md +71 -32
- package/skills/rvw-watch-comments/agents/openai.yaml +2 -2
- package/skills/rvw-watch-comments/scripts/auto-ack.mjs +0 -0
- package/skills/rvw-watch-comments/scripts/preflight.mjs +0 -0
- package/skills/rvw-watch-comments/scripts/watch-driver.mjs +223 -48
- package/skills/rvw-watch-comments/scripts/watch-state.mjs +13 -2
- package/dist/web/assets/DocumentViewer-ADzspLC4.js +0 -1560
- package/dist/web/assets/WalkthroughViewer-yQibj2tK.js +0 -1
- package/dist/web/assets/architecture-TIHT7OUA-DEoBuEnc.js +0 -1
- package/dist/web/assets/channel-DjgdV-zW.js +0 -1
- package/dist/web/assets/classDiagram-JCYQIIEL-B6Qjam7Y.js +0 -1
- package/dist/web/assets/classDiagram-v2-OCEON4UE-B6Qjam7Y.js +0 -1
- package/dist/web/assets/eventmodeling-45OFAUF4-ndNeUBOZ.js +0 -1
- package/dist/web/assets/flowDiagram-UKHOOZJN-BVxK_6Tu.js +0 -1
- package/dist/web/assets/railroad-abnf-AHOZXSZD-ZKY5olmV.js +0 -1
- package/dist/web/assets/railroad-ebnf-EBAXGLYW-CrG1UCMo.js +0 -1
- package/dist/web/assets/railroad-peg-LSFZ7HO6-CoY6MIbj.js +0 -1
- package/dist/web/assets/stateDiagram-v2-UEYNNEHI-0xn0gbpn.js +0 -1
- package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-YJ3yh_d3.js +0 -8
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
chmodSync,
|
|
7
|
+
linkSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
4
14
|
import path from "node:path";
|
|
5
15
|
import { StringDecoder } from "node:string_decoder";
|
|
6
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,9 +20,13 @@ const EXIT_WATCH = 20;
|
|
|
10
20
|
const EXIT_SEQUENCE = 21;
|
|
11
21
|
const EXIT_INGEST = 22;
|
|
12
22
|
const EXIT_AUTO_ACK = 23;
|
|
23
|
+
const EXIT_ALREADY_RUNNING = 24;
|
|
24
|
+
const DEFAULT_MAX_IN_FLIGHT = 1;
|
|
25
|
+
const DEFAULT_AUTO_ACK_POLL_MS = 250;
|
|
13
26
|
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
14
27
|
const stateScript = path.join(scriptDirectory, "watch-state.mjs");
|
|
15
28
|
const autoAckScript = path.join(scriptDirectory, "auto-ack.mjs");
|
|
29
|
+
const DRIVER_LOCK_SUFFIX = ".watch-driver.lock";
|
|
16
30
|
|
|
17
31
|
class DriverError extends Error {
|
|
18
32
|
constructor(message, exitCode, details = null) {
|
|
@@ -25,12 +39,27 @@ class DriverError extends Error {
|
|
|
25
39
|
function parseArguments(values) {
|
|
26
40
|
let state = null;
|
|
27
41
|
let autoAck = false;
|
|
42
|
+
let maxInFlight = DEFAULT_MAX_IN_FLIGHT;
|
|
43
|
+
let maxInFlightProvided = false;
|
|
28
44
|
for (let index = 0; index < values.length; index += 1) {
|
|
29
45
|
const value = values[index];
|
|
30
46
|
if (value === "--auto-ack") {
|
|
31
47
|
autoAck = true;
|
|
32
48
|
continue;
|
|
33
49
|
}
|
|
50
|
+
if (value === "--max-in-flight") {
|
|
51
|
+
const next = values[index + 1];
|
|
52
|
+
if (!next || next.startsWith("--")) {
|
|
53
|
+
throw new DriverError("--max-in-flight requires an integer", EXIT_WATCH);
|
|
54
|
+
}
|
|
55
|
+
maxInFlight = Number(next);
|
|
56
|
+
maxInFlightProvided = true;
|
|
57
|
+
if (!Number.isSafeInteger(maxInFlight) || maxInFlight < 1 || maxInFlight > 64) {
|
|
58
|
+
throw new DriverError("--max-in-flight must be an integer from 1 through 64", EXIT_WATCH);
|
|
59
|
+
}
|
|
60
|
+
index += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
34
63
|
if (value === "--state") {
|
|
35
64
|
const next = values[index + 1];
|
|
36
65
|
if (!next || next.startsWith("--")) {
|
|
@@ -47,7 +76,10 @@ function parseArguments(values) {
|
|
|
47
76
|
throw new DriverError(`Unexpected argument: ${value}`, EXIT_WATCH);
|
|
48
77
|
}
|
|
49
78
|
if (!state) throw new DriverError("Pass the task state database path", EXIT_WATCH);
|
|
50
|
-
|
|
79
|
+
if (!autoAck && maxInFlightProvided) {
|
|
80
|
+
throw new DriverError("--max-in-flight requires --auto-ack", EXIT_WATCH);
|
|
81
|
+
}
|
|
82
|
+
return { state: path.resolve(state), autoAck, maxInFlight };
|
|
51
83
|
}
|
|
52
84
|
|
|
53
85
|
async function runNodeScript(script, args, input) {
|
|
@@ -86,12 +118,108 @@ function write(value) {
|
|
|
86
118
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
87
119
|
}
|
|
88
120
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
121
|
+
function fileIdentity(filePath) {
|
|
122
|
+
try {
|
|
123
|
+
const stat = lstatSync(filePath);
|
|
124
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function unlinkIfOwned(filePath, identity) {
|
|
131
|
+
const current = fileIdentity(filePath);
|
|
132
|
+
if (!identity || !current || current.dev !== identity.dev || current.ino !== identity.ino) return;
|
|
133
|
+
try {
|
|
134
|
+
unlinkSync(filePath);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error.code !== "ENOENT") throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function processIsAlive(pid) {
|
|
141
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
142
|
+
try {
|
|
143
|
+
process.kill(pid, 0);
|
|
144
|
+
return true;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return error.code !== "ESRCH";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function lockOwner(lockPath) {
|
|
151
|
+
try {
|
|
152
|
+
const value = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
153
|
+
return Number.isInteger(value?.pid) && value.pid > 0 ? value : null;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function acquireDriverLock(state) {
|
|
160
|
+
const canonicalState = realpathSync(state);
|
|
161
|
+
const lockPath = `${canonicalState}${DRIVER_LOCK_SUFFIX}`;
|
|
162
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
163
|
+
const stagingPath = `${lockPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
164
|
+
try {
|
|
165
|
+
writeFileSync(
|
|
166
|
+
stagingPath,
|
|
167
|
+
`${JSON.stringify({ pid: process.pid, state: canonicalState, startedAt: new Date().toISOString() })}\n`,
|
|
168
|
+
{ flag: "wx", mode: 0o600 },
|
|
169
|
+
);
|
|
170
|
+
chmodSync(stagingPath, 0o600);
|
|
171
|
+
linkSync(stagingPath, lockPath);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
try {
|
|
174
|
+
unlinkSync(stagingPath);
|
|
175
|
+
} catch (unlinkError) {
|
|
176
|
+
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
177
|
+
}
|
|
178
|
+
if (error.code !== "EEXIST") throw error;
|
|
179
|
+
const observedIdentity = fileIdentity(lockPath);
|
|
180
|
+
const owner = lockOwner(lockPath);
|
|
181
|
+
// Preserve unreadable locks rather than risking two live drivers.
|
|
182
|
+
if (!owner || processIsAlive(owner.pid)) {
|
|
183
|
+
throw new DriverError(
|
|
184
|
+
"Another watch-driver process already owns this task state",
|
|
185
|
+
EXIT_ALREADY_RUNNING,
|
|
186
|
+
{
|
|
187
|
+
state: canonicalState,
|
|
188
|
+
lockPath,
|
|
189
|
+
ownerPid: owner?.pid ?? null,
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
unlinkIfOwned(lockPath, observedIdentity);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const identity = fileIdentity(lockPath);
|
|
198
|
+
if (!identity) throw new Error("Could not verify the watch-driver owner lock");
|
|
199
|
+
unlinkSync(stagingPath);
|
|
200
|
+
return { identity, path: lockPath };
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const stagingIdentity = fileIdentity(stagingPath);
|
|
203
|
+
unlinkIfOwned(lockPath, stagingIdentity);
|
|
204
|
+
try {
|
|
205
|
+
unlinkSync(stagingPath);
|
|
206
|
+
} catch (unlinkError) {
|
|
207
|
+
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
94
211
|
}
|
|
212
|
+
throw new DriverError("Could not acquire the watch-driver owner lock", EXIT_ALREADY_RUNNING, {
|
|
213
|
+
state: canonicalState,
|
|
214
|
+
lockPath,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function releaseDriverLock(lock) {
|
|
219
|
+
unlinkIfOwned(lock.path, lock.identity);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function dispatchAutoAck(state, pullRequest) {
|
|
95
223
|
const result = await runNodeScript(autoAckScript, [
|
|
96
224
|
"--state",
|
|
97
225
|
state,
|
|
@@ -104,14 +232,21 @@ async function dispatchAutoAck(state, pullRequest) {
|
|
|
104
232
|
write({ ...result.json, type: "batch-acknowledged" });
|
|
105
233
|
}
|
|
106
234
|
|
|
107
|
-
async function dispatchPendingAutoAcks(state) {
|
|
108
|
-
const
|
|
109
|
-
|
|
235
|
+
async function dispatchPendingAutoAcks(state, maxInFlight) {
|
|
236
|
+
const work = await stateCommand(state, "list");
|
|
237
|
+
const inFlight = Number(work.inFlight);
|
|
238
|
+
if (!Number.isSafeInteger(inFlight) || inFlight < 0 || !Array.isArray(work.pending)) {
|
|
239
|
+
throw new DriverError("watch-state list returned invalid work capacity", EXIT_INGEST, work);
|
|
240
|
+
}
|
|
241
|
+
let available = Math.max(0, maxInFlight - inFlight);
|
|
242
|
+
for (const batch of work.pending) {
|
|
243
|
+
if (available === 0) break;
|
|
110
244
|
await dispatchAutoAck(state, batch.pullRequest);
|
|
245
|
+
available -= 1;
|
|
111
246
|
}
|
|
112
247
|
}
|
|
113
248
|
|
|
114
|
-
async function processFrame(state, frame, autoAck,
|
|
249
|
+
async function processFrame(state, frame, autoAck, maxInFlight) {
|
|
115
250
|
if (!frame || typeof frame !== "object" || typeof frame.type !== "string") {
|
|
116
251
|
throw new DriverError("Invalid RFC 7464 frame", EXIT_SEQUENCE, frame);
|
|
117
252
|
}
|
|
@@ -127,10 +262,10 @@ async function processFrame(state, frame, autoAck, pullRequests) {
|
|
|
127
262
|
databaseId: frame.databaseId,
|
|
128
263
|
anchoredAtCurrent: frame.anchoredAtCurrent,
|
|
129
264
|
autoAck,
|
|
265
|
+
maxInFlight: autoAck ? maxInFlight : null,
|
|
130
266
|
});
|
|
131
267
|
} else if (frame.type === "comment-posted" && ingested.status === "queued") {
|
|
132
|
-
if (autoAck)
|
|
133
|
-
else {
|
|
268
|
+
if (!autoAck) {
|
|
134
269
|
write({
|
|
135
270
|
ok: true,
|
|
136
271
|
type: "pending",
|
|
@@ -142,7 +277,16 @@ async function processFrame(state, frame, autoAck, pullRequests) {
|
|
|
142
277
|
}
|
|
143
278
|
}
|
|
144
279
|
|
|
145
|
-
|
|
280
|
+
function autoAckPollMilliseconds() {
|
|
281
|
+
const configured = Number(
|
|
282
|
+
process.env.RVW_WATCH_AUTO_ACK_POLL_MS ?? String(DEFAULT_AUTO_ACK_POLL_MS),
|
|
283
|
+
);
|
|
284
|
+
return Number.isSafeInteger(configured) && configured >= 10 && configured <= 60_000
|
|
285
|
+
? configured
|
|
286
|
+
: DEFAULT_AUTO_ACK_POLL_MS;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function runWatchOnce(state, autoAck, maxInFlight, stopping) {
|
|
146
290
|
const current = await stateCommand(state, "status");
|
|
147
291
|
const args = ["comment", "watch"];
|
|
148
292
|
if (current.cursor) args.push("--after", current.cursor);
|
|
@@ -160,6 +304,25 @@ async function runWatchOnce(state, autoAck, stopping) {
|
|
|
160
304
|
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
161
305
|
});
|
|
162
306
|
child.stdin.end();
|
|
307
|
+
let autoAckPump = null;
|
|
308
|
+
let autoAckError = null;
|
|
309
|
+
const pumpAutoAcks = async () => {
|
|
310
|
+
if (!autoAck || stopping.requested) return;
|
|
311
|
+
if (autoAckPump) return autoAckPump;
|
|
312
|
+
autoAckPump = dispatchPendingAutoAcks(state, maxInFlight);
|
|
313
|
+
try {
|
|
314
|
+
await autoAckPump;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
autoAckError ??= error;
|
|
317
|
+
child.kill("SIGTERM");
|
|
318
|
+
throw error;
|
|
319
|
+
} finally {
|
|
320
|
+
autoAckPump = null;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
const autoAckTimer = autoAck
|
|
324
|
+
? setInterval(() => void pumpAutoAcks().catch(() => undefined), autoAckPollMilliseconds())
|
|
325
|
+
: null;
|
|
163
326
|
const decoder = new StringDecoder("utf8");
|
|
164
327
|
let buffered = "";
|
|
165
328
|
let readySeen = false;
|
|
@@ -169,7 +332,6 @@ async function runWatchOnce(state, autoAck, stopping) {
|
|
|
169
332
|
buffered += decoder.write(chunk);
|
|
170
333
|
const lines = buffered.split("\n");
|
|
171
334
|
buffered = lines.pop() ?? "";
|
|
172
|
-
const pullRequests = new Set();
|
|
173
335
|
for (const rawLine of lines) {
|
|
174
336
|
if (!rawLine) continue;
|
|
175
337
|
if (!rawLine.startsWith("\u001e")) {
|
|
@@ -183,9 +345,9 @@ async function runWatchOnce(state, autoAck, stopping) {
|
|
|
183
345
|
}
|
|
184
346
|
readySeen ||= frame.type === "ready";
|
|
185
347
|
stoppedSeen ||= frame.type === "stopped";
|
|
186
|
-
await processFrame(state, frame, autoAck,
|
|
348
|
+
await processFrame(state, frame, autoAck, maxInFlight);
|
|
187
349
|
}
|
|
188
|
-
|
|
350
|
+
await pumpAutoAcks();
|
|
189
351
|
}
|
|
190
352
|
buffered += decoder.end();
|
|
191
353
|
if (buffered.trim()) {
|
|
@@ -196,6 +358,8 @@ async function runWatchOnce(state, autoAck, stopping) {
|
|
|
196
358
|
);
|
|
197
359
|
}
|
|
198
360
|
const status = await statusPromise;
|
|
361
|
+
if (autoAckPump) await autoAckPump.catch(() => undefined);
|
|
362
|
+
if (autoAckError) throw autoAckError;
|
|
199
363
|
stopping.child = null;
|
|
200
364
|
return { ...status, stderr, readySeen, stoppedSeen };
|
|
201
365
|
} catch (error) {
|
|
@@ -203,6 +367,8 @@ async function runWatchOnce(state, autoAck, stopping) {
|
|
|
203
367
|
await statusPromise.catch(() => undefined);
|
|
204
368
|
stopping.child = null;
|
|
205
369
|
throw error;
|
|
370
|
+
} finally {
|
|
371
|
+
if (autoAckTimer) clearInterval(autoAckTimer);
|
|
206
372
|
}
|
|
207
373
|
}
|
|
208
374
|
|
|
@@ -217,42 +383,51 @@ function delay(milliseconds) {
|
|
|
217
383
|
}
|
|
218
384
|
|
|
219
385
|
async function main() {
|
|
220
|
-
const { state, autoAck } = parseArguments(process.argv.slice(2));
|
|
221
|
-
const
|
|
222
|
-
const stop = () => {
|
|
223
|
-
stopping.requested = true;
|
|
224
|
-
stopping.child?.kill("SIGTERM");
|
|
225
|
-
};
|
|
226
|
-
process.once("SIGINT", stop);
|
|
227
|
-
process.once("SIGTERM", stop);
|
|
228
|
-
const maxRestarts = Number(process.env.RVW_WATCH_DRIVER_MAX_RESTARTS ?? "5");
|
|
229
|
-
let restarts = 0;
|
|
386
|
+
const { state, autoAck, maxInFlight } = parseArguments(process.argv.slice(2));
|
|
387
|
+
const driverLock = acquireDriverLock(state);
|
|
230
388
|
try {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
389
|
+
const stopping = { requested: false, child: null };
|
|
390
|
+
const stop = () => {
|
|
391
|
+
stopping.requested = true;
|
|
392
|
+
stopping.child?.kill("SIGTERM");
|
|
393
|
+
};
|
|
394
|
+
process.once("SIGINT", stop);
|
|
395
|
+
process.once("SIGTERM", stop);
|
|
396
|
+
const maxRestarts = Number(process.env.RVW_WATCH_DRIVER_MAX_RESTARTS ?? "5");
|
|
397
|
+
let restarts = 0;
|
|
398
|
+
try {
|
|
399
|
+
while (!stopping.requested) {
|
|
400
|
+
const startedAt = Date.now();
|
|
401
|
+
if (autoAck) await dispatchPendingAutoAcks(state, maxInFlight);
|
|
402
|
+
const result = await runWatchOnce(state, autoAck, maxInFlight, stopping);
|
|
403
|
+
if (stopping.requested) return;
|
|
404
|
+
if (Date.now() - startedAt >= 30_000) restarts = 0;
|
|
405
|
+
restarts += 1;
|
|
406
|
+
if (restarts > maxRestarts) {
|
|
407
|
+
throw new DriverError(
|
|
408
|
+
"rvw comment watch exceeded its reconnect limit",
|
|
409
|
+
EXIT_WATCH,
|
|
410
|
+
result,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const delayMs = retryDelay(restarts);
|
|
414
|
+
write({
|
|
415
|
+
ok: true,
|
|
416
|
+
type: "reconnecting",
|
|
417
|
+
attempt: restarts,
|
|
418
|
+
delayMs,
|
|
419
|
+
lastExitCode: result.code,
|
|
420
|
+
lastSignal: result.signal,
|
|
421
|
+
readySeen: result.readySeen,
|
|
422
|
+
});
|
|
423
|
+
await delay(delayMs);
|
|
240
424
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
type: "reconnecting",
|
|
245
|
-
attempt: restarts,
|
|
246
|
-
delayMs,
|
|
247
|
-
lastExitCode: result.code,
|
|
248
|
-
lastSignal: result.signal,
|
|
249
|
-
readySeen: result.readySeen,
|
|
250
|
-
});
|
|
251
|
-
await delay(delayMs);
|
|
425
|
+
} finally {
|
|
426
|
+
process.off("SIGINT", stop);
|
|
427
|
+
process.off("SIGTERM", stop);
|
|
252
428
|
}
|
|
253
429
|
} finally {
|
|
254
|
-
|
|
255
|
-
process.off("SIGTERM", stop);
|
|
430
|
+
releaseDriverLock(driverLock);
|
|
256
431
|
}
|
|
257
432
|
}
|
|
258
433
|
|
|
@@ -68,7 +68,8 @@ function openState(statePath, create) {
|
|
|
68
68
|
fail(`State database does not exist: ${absolute}`);
|
|
69
69
|
}
|
|
70
70
|
const database = new DatabaseSync(absolute);
|
|
71
|
-
database.exec("PRAGMA
|
|
71
|
+
database.exec("PRAGMA busy_timeout = 5000;");
|
|
72
|
+
database.exec("PRAGMA journal_mode = WAL;");
|
|
72
73
|
database.exec(`
|
|
73
74
|
CREATE TABLE IF NOT EXISTS meta (
|
|
74
75
|
key TEXT PRIMARY KEY,
|
|
@@ -455,6 +456,16 @@ function listPending(database) {
|
|
|
455
456
|
});
|
|
456
457
|
}
|
|
457
458
|
|
|
459
|
+
function listWork(database) {
|
|
460
|
+
const inFlight = database
|
|
461
|
+
.prepare("SELECT count(*) AS count FROM batches WHERE status = 'in_flight'")
|
|
462
|
+
.get();
|
|
463
|
+
return {
|
|
464
|
+
inFlight: Number(inFlight.count),
|
|
465
|
+
pending: listPending(database),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
458
469
|
async function waitForPending(database, options) {
|
|
459
470
|
const intervalMs = positiveIntegerOption(options, "interval-ms", 250);
|
|
460
471
|
const follow = options.follow === true;
|
|
@@ -822,7 +833,7 @@ async function main() {
|
|
|
822
833
|
return;
|
|
823
834
|
}
|
|
824
835
|
if (command === "list") {
|
|
825
|
-
write({ ok: true,
|
|
836
|
+
write({ ok: true, ...listWork(database) });
|
|
826
837
|
return;
|
|
827
838
|
}
|
|
828
839
|
if (command === "wait") {
|