@a9n-shoji/rvw 0.2.1 → 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 +29 -0
- package/README.md +24 -4
- 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-DbDc9VoP.js → MarkdownTable-CdbKPft2.js} +1 -1
- package/dist/web/assets/WalkthroughViewer-D1LlSQw2.js +1 -0
- package/dist/web/assets/{abnfDiagram-N423BO3Z-fQDP4qYL.js → abnfDiagram-N423BO3Z-CE1nZSC9.js} +1 -1
- package/dist/web/assets/architecture-TIHT7OUA-edLV2Zq-.js +1 -0
- package/dist/web/assets/{architectureDiagram-T3A2C74G-D3Aon3Nd.js → architectureDiagram-T3A2C74G-B88qYFUo.js} +1 -1
- package/dist/web/assets/{blockDiagram-VBNYF7ZC-DS1DcAqy.js → blockDiagram-VBNYF7ZC-BaX0rhbb.js} +1 -1
- package/dist/web/assets/{c4Diagram-5PPSVZJV-BHQWz7l2.js → c4Diagram-5PPSVZJV-C1ppBaEC.js} +1 -1
- package/dist/web/assets/channel-J8FHSJDG.js +1 -0
- package/dist/web/assets/{chunk-2GRJ4B5K-CaahxDVT.js → chunk-2GRJ4B5K-D2TTocfh.js} +1 -1
- package/dist/web/assets/{chunk-4I5QYGJK-Bat5hdXD.js → chunk-4I5QYGJK-ikT25rPN.js} +1 -1
- package/dist/web/assets/{chunk-5RXB4S5H-jyonN41f.js → chunk-5RXB4S5H-CGlPaulj.js} +1 -1
- package/dist/web/assets/{chunk-6Q2QTUOP-BBo9TYzL.js → chunk-6Q2QTUOP-BuuMeCo2.js} +1 -1
- package/dist/web/assets/{chunk-7Z6QIM7H-CGSwsKhC.js → chunk-7Z6QIM7H-DT3BAZ4L.js} +1 -1
- package/dist/web/assets/{chunk-GF5L2VYU-Cuca3bAY.js → chunk-GF5L2VYU-mIdlxzfr.js} +1 -1
- package/dist/web/assets/{chunk-I66GZJ75-DRdqLSyH.js → chunk-I66GZJ75-CGl1hx4J.js} +1 -1
- package/dist/web/assets/{chunk-JQJVKLGR-8xhLkhs2.js → chunk-JQJVKLGR-BLG6gAfx.js} +1 -1
- package/dist/web/assets/{chunk-KBJHAD2P-oOV-3e5-.js → chunk-KBJHAD2P-D_MPzBJx.js} +1 -1
- package/dist/web/assets/{chunk-NSK5VX7P-CXit1l3q.js → chunk-NSK5VX7P-CiYiU8wP.js} +1 -1
- package/dist/web/assets/{chunk-QR6OTTB3-DcdCU-01.js → chunk-QR6OTTB3-BK8Tnvcc.js} +1 -1
- package/dist/web/assets/{chunk-UBXNYLIW-i72Hgljr.js → chunk-UBXNYLIW-BuzttTFk.js} +1 -1
- package/dist/web/assets/{chunk-W5SLKNZC-DetAQo7M.js → chunk-W5SLKNZC-UidmFHe2.js} +1 -1
- package/dist/web/assets/{chunk-WRU74C26-DDUvJFNs.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-fd-v6ani.js → cynefin-VYW2F7L2-wDMibS5s.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-MW4NZA55-DRsVpAC3.js → cynefinDiagram-MW4NZA55-CBTrdvOE.js} +1 -1
- package/dist/web/assets/{dagre-VZM6K2ZE-BnRSs_fe.js → dagre-VZM6K2ZE-Bbgn4Rr7.js} +1 -1
- package/dist/web/assets/{diagram-7IWD3JNH-D1g0PR4y.js → diagram-7IWD3JNH-Bm9AuXb9.js} +1 -1
- package/dist/web/assets/{diagram-B4RE2ZJO-DuDKsQLp.js → diagram-B4RE2ZJO-D3lZ-r-H.js} +1 -1
- package/dist/web/assets/{diagram-LBJQPF4R-COpzgGe7.js → diagram-LBJQPF4R-D-GaynX7.js} +1 -1
- package/dist/web/assets/{diagram-Q27KOJAE-DR2vRbPE.js → diagram-Q27KOJAE-BeZRiu1d.js} +1 -1
- package/dist/web/assets/{diagram-UB23O5K3-hs4VGPP7.js → diagram-UB23O5K3-C3GP7iMe.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-BXEA7PRR-B_4NlK9q.js → ebnfDiagram-BXEA7PRR-BJWg0NVU.js} +1 -1
- package/dist/web/assets/{erDiagram-JOGREHBK-JyoZ3U-o.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-f37I0Twm.js → ganttDiagram-PKOTCBZU-CL-xEo-M.js} +1 -1
- package/dist/web/assets/{gitGraph-TEB2WS4Q-B_QWjr7j.js → gitGraph-TEB2WS4Q-BVRyuIx3.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-DS77QQ5N-AZsaC0hu.js → gitGraphDiagram-DS77QQ5N-BVNIEupW.js} +1 -1
- package/dist/web/assets/index-D8GJBw23.js +384 -0
- package/dist/web/assets/{index-kK3hngnz.css → index-DMD2vvQW.css} +1 -1
- package/dist/web/assets/{info-DKCQHKI2-BRfisbEI.js → info-DKCQHKI2-BKBUVpfc.js} +1 -1
- package/dist/web/assets/{infoDiagram-6WML65LV-CaIb64N5.js → infoDiagram-6WML65LV-_dTFnzSE.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-BoJrcTja.js → ishikawaDiagram-WSZJBQD7-DIRgD4hO.js} +1 -1
- package/dist/web/assets/{journeyDiagram-NVQOT4AX-CRR1dZy8.js → journeyDiagram-NVQOT4AX-bp7nZrfR.js} +1 -1
- package/dist/web/assets/{kanban-definition-27J2QSJJ-D5skDuP4.js → kanban-definition-27J2QSJJ-C6mZIo8S.js} +1 -1
- package/dist/web/assets/{line-BKKx0vSh.js → line-DWTym7eJ.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-DvZnIth0.js → mermaid-parser.core-CbecsNA1.js} +3 -3
- package/dist/web/assets/{mermaid.core-hykiOF_o.js → mermaid.core-CdRuLyYI.js} +4 -4
- package/dist/web/assets/{mindmap-definition-FAOFIHXS-BN2LQ4Yn.js → mindmap-definition-FAOFIHXS-CU2P1z-9.js} +1 -1
- package/dist/web/assets/{packet-7NZHBO7P-CS5I0NlV.js → packet-7NZHBO7P-CZsup6Od.js} +1 -1
- package/dist/web/assets/{pegDiagram-VL7TDLO6-WvOdhbxo.js → pegDiagram-VL7TDLO6-DTTyttpe.js} +1 -1
- package/dist/web/assets/{pie-RZYD4A2V-BwNAZrQ1.js → pie-RZYD4A2V-DgrIiWZA.js} +1 -1
- package/dist/web/assets/{pieDiagram-7S7Q4E2Y-CbyrZjm7.js → pieDiagram-7S7Q4E2Y-BBNFyPT1.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-CW9YBxAO.js → quadrantDiagram-CIZ2JOQS-ejp-Aa2l.js} +1 -1
- package/dist/web/assets/{radar-I7S5WNFK-DBoCLDNH.js → radar-I7S5WNFK-Cp_hfjrI.js} +1 -1
- package/dist/web/assets/{railroad-3IZDKUUU-BOjpYyDz.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-BdOOOq8o.js → railroadDiagram-AXF67PYL-DkBUaj6Z.js} +1 -1
- package/dist/web/assets/{requirementDiagram-LRYGKXZP-DIYlSrTb.js → requirementDiagram-LRYGKXZP-Dff1ex_h.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-W5VNT64P-Cc1IGxp_.js → sankeyDiagram-W5VNT64P-DptSL9Bc.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-SI44F4Z6-CJsPO9Z9.js → sequenceDiagram-SI44F4Z6-DEnPcs3U.js} +1 -1
- package/dist/web/assets/{stateDiagram-OKZ733FA-B033gpHZ.js → stateDiagram-OKZ733FA-BwJO1MKA.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-UEYNNEHI-LaSNqvWy.js +1 -0
- package/dist/web/assets/{swimlanes-SLNWSIFB-C0Tstg7e.js → swimlanes-SLNWSIFB-BosKiwZA.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-C5OBtBlv.js +8 -0
- package/dist/web/assets/{timeline-definition-Z64GVDOM-BA3y-16W.js → timeline-definition-Z64GVDOM-C5isAn4X.js} +1 -1
- package/dist/web/assets/{treeView-QDETBFTQ-BUNq-JXr.js → treeView-QDETBFTQ-zEyUXRc4.js} +1 -1
- package/dist/web/assets/{treemap-6X3UGDF4-FIR_NbUo.js → treemap-6X3UGDF4-B-X0EQSE.js} +1 -1
- package/dist/web/assets/{vennDiagram-T6HMQDX7-IeFFjnlq.js → vennDiagram-T6HMQDX7-QMdGieCa.js} +1 -1
- package/dist/web/assets/{wardley-OPB4EBWU-DQAX4LKo.js → wardley-OPB4EBWU-BaeAF8d8.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-T6FBY63Y-guGPPMAG.js → wardleyDiagram-T6FBY63Y-DTQ_uLUq.js} +1 -1
- package/dist/web/assets/{xychartDiagram-ELKLHX3M-Cj0v359_.js → xychartDiagram-ELKLHX3M-BMdBWzib.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +17 -14
- package/skills/rvw/SKILL.md +56 -15
- package/skills/rvw-watch-comments/SKILL.md +117 -47
- 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 +125 -64
- package/dist/web/assets/DocumentViewer-DvrDlIpR.js +0 -1560
- package/dist/web/assets/WalkthroughViewer-aDUeKC3B.js +0 -1
- package/dist/web/assets/architecture-TIHT7OUA-CuFS9GkN.js +0 -1
- package/dist/web/assets/channel-CTWz7tRI.js +0 -1
- package/dist/web/assets/classDiagram-JCYQIIEL-BJQv9QuO.js +0 -1
- package/dist/web/assets/classDiagram-v2-OCEON4UE-BJQv9QuO.js +0 -1
- package/dist/web/assets/eventmodeling-45OFAUF4-CfUhQK5g.js +0 -1
- package/dist/web/assets/flowDiagram-UKHOOZJN-FFZ82SPa.js +0 -1
- package/dist/web/assets/index-CY_JoUL3.js +0 -384
- package/dist/web/assets/railroad-abnf-AHOZXSZD-DQhDS2pf.js +0 -1
- package/dist/web/assets/railroad-ebnf-EBAXGLYW-Dtt2kOaN.js +0 -1
- package/dist/web/assets/railroad-peg-LSFZ7HO6-nfH8zno6.js +0 -1
- package/dist/web/assets/stateDiagram-v2-UEYNNEHI-wS01qj3t.js +0 -1
- package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-DoAqfWAd.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,
|
|
@@ -98,17 +99,11 @@ function openState(statePath, create) {
|
|
|
98
99
|
created_at TEXT NOT NULL,
|
|
99
100
|
updated_at TEXT NOT NULL
|
|
100
101
|
);
|
|
101
|
-
CREATE TABLE IF NOT EXISTS comment_statuses (
|
|
102
|
-
comment_ref TEXT PRIMARY KEY,
|
|
103
|
-
idempotency_key TEXT NOT NULL UNIQUE,
|
|
104
|
-
post_id TEXT UNIQUE,
|
|
105
|
-
created_at TEXT NOT NULL,
|
|
106
|
-
updated_at TEXT NOT NULL
|
|
107
|
-
);
|
|
108
102
|
CREATE TABLE IF NOT EXISTS operations (
|
|
109
103
|
batch_id TEXT NOT NULL,
|
|
110
104
|
comment_ref TEXT NOT NULL,
|
|
111
105
|
idempotency_key TEXT NOT NULL UNIQUE,
|
|
106
|
+
post_id TEXT,
|
|
112
107
|
PRIMARY KEY(batch_id, comment_ref)
|
|
113
108
|
);
|
|
114
109
|
CREATE TABLE IF NOT EXISTS suppressed_posts (
|
|
@@ -123,6 +118,7 @@ function openState(statePath, create) {
|
|
|
123
118
|
CREATE INDEX IF NOT EXISTS batches_pending_pr
|
|
124
119
|
ON batches(status, pull_request_url, next_attempt_at, created_at);
|
|
125
120
|
`);
|
|
121
|
+
migrateStateSchema(database);
|
|
126
122
|
if (create) chmodSync(absolute, 0o600);
|
|
127
123
|
return { database, absolute };
|
|
128
124
|
}
|
|
@@ -156,39 +152,102 @@ function setMeta(database, key, value) {
|
|
|
156
152
|
.run(key, value);
|
|
157
153
|
}
|
|
158
154
|
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
database
|
|
176
|
-
.prepare(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
|
|
155
|
+
function tableExists(database, tableName) {
|
|
156
|
+
return Boolean(
|
|
157
|
+
database
|
|
158
|
+
.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
159
|
+
.get(tableName),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function migrateStateSchema(database) {
|
|
164
|
+
const operationColumns = database.prepare("PRAGMA table_info(operations)").all();
|
|
165
|
+
if (
|
|
166
|
+
operationColumns.some((column) => column.name === "post_id") &&
|
|
167
|
+
getMeta(database, "batch_scoped_status_posts") === "1"
|
|
168
|
+
) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
transaction(database, () => {
|
|
172
|
+
const currentOperationColumns = database.prepare("PRAGMA table_info(operations)").all();
|
|
173
|
+
if (!currentOperationColumns.some((column) => column.name === "post_id")) {
|
|
174
|
+
database.exec("ALTER TABLE operations ADD COLUMN post_id TEXT;");
|
|
175
|
+
}
|
|
176
|
+
if (getMeta(database, "batch_scoped_status_posts") === "1") return;
|
|
177
|
+
if (tableExists(database, "comment_statuses")) {
|
|
178
|
+
const rows = database
|
|
179
|
+
.prepare(
|
|
180
|
+
`SELECT b.id AS batch_id, e.comment_ref,
|
|
181
|
+
b.created_at AS batch_created_at,
|
|
182
|
+
o.idempotency_key AS operation_key, o.post_id AS operation_post_id,
|
|
183
|
+
s.idempotency_key AS legacy_key, s.post_id AS legacy_post_id,
|
|
184
|
+
s.updated_at AS legacy_updated_at
|
|
185
|
+
FROM batches b
|
|
186
|
+
JOIN (
|
|
187
|
+
SELECT DISTINCT batch_id, comment_ref FROM events WHERE batch_id IS NOT NULL
|
|
188
|
+
) e ON e.batch_id = b.id
|
|
189
|
+
LEFT JOIN operations o ON o.batch_id = b.id AND o.comment_ref = e.comment_ref
|
|
190
|
+
LEFT JOIN comment_statuses s ON s.comment_ref = e.comment_ref
|
|
191
|
+
WHERE b.status != 'completed' AND b.attempts > 0
|
|
192
|
+
ORDER BY e.comment_ref,
|
|
193
|
+
CASE b.status WHEN 'in_flight' THEN 0 WHEN 'pending' THEN 1 ELSE 2 END,
|
|
194
|
+
b.created_at DESC`,
|
|
195
|
+
)
|
|
196
|
+
.all();
|
|
197
|
+
const migratedCommentRefs = new Set();
|
|
198
|
+
for (const row of rows) {
|
|
199
|
+
const useLegacyStatus =
|
|
200
|
+
!migratedCommentRefs.has(row.comment_ref) &&
|
|
201
|
+
row.legacy_key &&
|
|
202
|
+
row.legacy_updated_at >= row.batch_created_at;
|
|
203
|
+
if (useLegacyStatus) migratedCommentRefs.add(row.comment_ref);
|
|
204
|
+
if (row.operation_key) {
|
|
205
|
+
if (useLegacyStatus && row.operation_post_id === null && row.legacy_post_id !== null) {
|
|
206
|
+
database
|
|
207
|
+
.prepare("UPDATE operations SET post_id = ? WHERE batch_id = ? AND comment_ref = ?")
|
|
208
|
+
.run(row.legacy_post_id, row.batch_id, row.comment_ref);
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
database
|
|
213
|
+
.prepare(
|
|
214
|
+
`INSERT INTO operations(batch_id, comment_ref, idempotency_key, post_id)
|
|
215
|
+
VALUES (?, ?, ?, ?)`,
|
|
216
|
+
)
|
|
217
|
+
.run(
|
|
218
|
+
row.batch_id,
|
|
219
|
+
row.comment_ref,
|
|
220
|
+
useLegacyStatus
|
|
221
|
+
? row.legacy_key
|
|
222
|
+
: `${getMeta(database, "task_id") ?? "task"}:${row.batch_id}:${randomUUID()}`,
|
|
223
|
+
useLegacyStatus ? row.legacy_post_id : null,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
setMeta(database, "batch_scoped_status_posts", "1");
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function ensureBatchOperations(database, batchId) {
|
|
232
|
+
const commentRefs = database
|
|
233
|
+
.prepare("SELECT DISTINCT comment_ref FROM events WHERE batch_id = ? ORDER BY comment_ref")
|
|
234
|
+
.all(batchId)
|
|
235
|
+
.map((operation) => operation.comment_ref);
|
|
236
|
+
for (let index = 0; index < commentRefs.length; index += 1) {
|
|
237
|
+
database
|
|
238
|
+
.prepare(
|
|
239
|
+
`INSERT OR IGNORE INTO operations(batch_id, comment_ref, idempotency_key, post_id)
|
|
240
|
+
VALUES (?, ?, ?, NULL)`,
|
|
241
|
+
)
|
|
242
|
+
.run(batchId, commentRefs[index], `${getMeta(database, "task_id")}:${batchId}:${index + 1}`);
|
|
243
|
+
}
|
|
183
244
|
}
|
|
184
245
|
|
|
185
246
|
function batchStatusOperations(database, batchId) {
|
|
186
247
|
return database
|
|
187
248
|
.prepare(
|
|
188
|
-
`SELECT
|
|
189
|
-
|
|
190
|
-
LEFT JOIN comment_statuses s ON s.comment_ref = e.comment_ref
|
|
191
|
-
WHERE e.batch_id = ? ORDER BY e.comment_ref`,
|
|
249
|
+
`SELECT comment_ref, post_id FROM operations
|
|
250
|
+
WHERE batch_id = ? ORDER BY comment_ref`,
|
|
192
251
|
)
|
|
193
252
|
.all(batchId)
|
|
194
253
|
.map((operation) => ({
|
|
@@ -311,19 +370,19 @@ function ingestEvent(database, frame) {
|
|
|
311
370
|
now,
|
|
312
371
|
);
|
|
313
372
|
if (event.deleted) {
|
|
314
|
-
const
|
|
315
|
-
.prepare("SELECT comment_ref FROM
|
|
316
|
-
.
|
|
317
|
-
|
|
373
|
+
const operations = database
|
|
374
|
+
.prepare("SELECT batch_id, comment_ref FROM operations WHERE post_id = ?")
|
|
375
|
+
.all(event.postId);
|
|
376
|
+
for (const operation of operations) {
|
|
318
377
|
database
|
|
319
378
|
.prepare(
|
|
320
|
-
`UPDATE
|
|
321
|
-
WHERE comment_ref = ?`,
|
|
379
|
+
`UPDATE operations SET post_id = NULL, idempotency_key = ?
|
|
380
|
+
WHERE batch_id = ? AND comment_ref = ?`,
|
|
322
381
|
)
|
|
323
382
|
.run(
|
|
324
|
-
`${getMeta(database, "task_id")}
|
|
325
|
-
|
|
326
|
-
|
|
383
|
+
`${getMeta(database, "task_id")}:${operation.batch_id}:${randomUUID()}`,
|
|
384
|
+
operation.batch_id,
|
|
385
|
+
operation.comment_ref,
|
|
327
386
|
);
|
|
328
387
|
}
|
|
329
388
|
}
|
|
@@ -397,6 +456,16 @@ function listPending(database) {
|
|
|
397
456
|
});
|
|
398
457
|
}
|
|
399
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
|
+
|
|
400
469
|
async function waitForPending(database, options) {
|
|
401
470
|
const intervalMs = positiveIntegerOption(options, "interval-ms", 250);
|
|
402
471
|
const follow = options.follow === true;
|
|
@@ -448,8 +517,7 @@ function createBatch(database, pullRequestUrl, now) {
|
|
|
448
517
|
"UPDATE events SET batch_id = ?, updated_at = ? WHERE pull_request_url = ? AND status = 'pending' AND batch_id IS NULL",
|
|
449
518
|
)
|
|
450
519
|
.run(batchId, now, pullRequestUrl);
|
|
451
|
-
|
|
452
|
-
for (const commentRef of commentRefs) ensureCommentStatus(database, commentRef, now);
|
|
520
|
+
ensureBatchOperations(database, batchId);
|
|
453
521
|
return batchId;
|
|
454
522
|
}
|
|
455
523
|
|
|
@@ -497,17 +565,11 @@ function claim(database, pullRequestUrl, writeKey) {
|
|
|
497
565
|
commentRef: event.comment_ref,
|
|
498
566
|
pullRequestUrl: event.pull_request_url,
|
|
499
567
|
}));
|
|
500
|
-
|
|
501
|
-
.prepare("SELECT DISTINCT comment_ref FROM events WHERE batch_id = ? ORDER BY comment_ref")
|
|
502
|
-
.all(batchId)
|
|
503
|
-
.map((operation) => operation.comment_ref);
|
|
504
|
-
for (const commentRef of commentRefs) ensureCommentStatus(database, commentRef, now);
|
|
568
|
+
ensureBatchOperations(database, batchId);
|
|
505
569
|
const operations = database
|
|
506
570
|
.prepare(
|
|
507
|
-
`SELECT
|
|
508
|
-
|
|
509
|
-
JOIN comment_statuses s ON s.comment_ref = e.comment_ref
|
|
510
|
-
WHERE e.batch_id = ? ORDER BY e.comment_ref`,
|
|
571
|
+
`SELECT comment_ref, idempotency_key, post_id FROM operations
|
|
572
|
+
WHERE batch_id = ? ORDER BY comment_ref`,
|
|
511
573
|
)
|
|
512
574
|
.all(batchId)
|
|
513
575
|
.map((operation) => ({
|
|
@@ -568,17 +630,16 @@ function acknowledge(database, leaseId, input) {
|
|
|
568
630
|
.get(leaseId);
|
|
569
631
|
if (!batch) fail("Active lease was not found");
|
|
570
632
|
const operation = database
|
|
571
|
-
.prepare("SELECT
|
|
633
|
+
.prepare("SELECT post_id FROM operations WHERE batch_id = ? AND comment_ref = ?")
|
|
572
634
|
.get(batch.id, input.commentRef);
|
|
573
635
|
if (!operation) fail("Comment is not part of the active lease");
|
|
574
636
|
const now = new Date().toISOString();
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
fail("Comment already has another status post");
|
|
637
|
+
if (operation.post_id !== null && operation.post_id !== input.postId) {
|
|
638
|
+
fail("Batch operation already has another status post");
|
|
578
639
|
}
|
|
579
640
|
database
|
|
580
|
-
.prepare("UPDATE
|
|
581
|
-
.run(input.postId,
|
|
641
|
+
.prepare("UPDATE operations SET post_id = ? WHERE batch_id = ? AND comment_ref = ?")
|
|
642
|
+
.run(input.postId, batch.id, input.commentRef);
|
|
582
643
|
database
|
|
583
644
|
.prepare("INSERT OR IGNORE INTO suppressed_posts(post_id, created_at) VALUES (?, ?)")
|
|
584
645
|
.run(input.postId, now);
|
|
@@ -591,7 +652,7 @@ function acknowledge(database, leaseId, input) {
|
|
|
591
652
|
batchId: batch.id,
|
|
592
653
|
commentRef: input.commentRef,
|
|
593
654
|
statusPostId: input.postId,
|
|
594
|
-
status:
|
|
655
|
+
status: operation.post_id === null ? "recorded" : "existing",
|
|
595
656
|
};
|
|
596
657
|
});
|
|
597
658
|
}
|
|
@@ -772,7 +833,7 @@ async function main() {
|
|
|
772
833
|
return;
|
|
773
834
|
}
|
|
774
835
|
if (command === "list") {
|
|
775
|
-
write({ ok: true,
|
|
836
|
+
write({ ok: true, ...listWork(database) });
|
|
776
837
|
return;
|
|
777
838
|
}
|
|
778
839
|
if (command === "wait") {
|