@tapflowio/relay 0.27.0 → 0.28.0
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/dist/RelayServer.d.ts +74 -0
- package/dist/RelayServer.d.ts.map +1 -1
- package/dist/RelayServer.js +482 -43
- package/dist/RelayServer.js.map +1 -1
- package/dist/SessionManager.d.ts +63 -2
- package/dist/SessionManager.d.ts.map +1 -1
- package/dist/SessionManager.js +123 -1
- package/dist/SessionManager.js.map +1 -1
- package/dist/api/apps.d.ts +2 -1
- package/dist/api/apps.d.ts.map +1 -1
- package/dist/api/apps.js +20 -7
- package/dist/api/apps.js.map +1 -1
- package/dist/api/builds.d.ts +2 -1
- package/dist/api/builds.d.ts.map +1 -1
- package/dist/api/builds.js +23 -45
- package/dist/api/builds.js.map +1 -1
- package/dist/api/comments.d.ts.map +1 -1
- package/dist/api/comments.js +5 -7
- package/dist/api/comments.js.map +1 -1
- package/dist/api/invitations.d.ts.map +1 -1
- package/dist/api/invitations.js +3 -0
- package/dist/api/invitations.js.map +1 -1
- package/dist/api/profile.d.ts.map +1 -1
- package/dist/api/profile.js +28 -9
- package/dist/api/profile.js.map +1 -1
- package/dist/api/recordings.d.ts.map +1 -1
- package/dist/api/recordings.js +5 -6
- package/dist/api/recordings.js.map +1 -1
- package/dist/api/runVideos.d.ts +85 -0
- package/dist/api/runVideos.d.ts.map +1 -0
- package/dist/api/runVideos.js +494 -0
- package/dist/api/runVideos.js.map +1 -0
- package/dist/api/runs.d.ts +77 -0
- package/dist/api/runs.d.ts.map +1 -0
- package/dist/api/runs.js +579 -0
- package/dist/api/runs.js.map +1 -0
- package/dist/api/settings.d.ts +1 -1
- package/dist/api/settings.d.ts.map +1 -1
- package/dist/api/settings.js +67 -10
- package/dist/api/settings.js.map +1 -1
- package/dist/db.d.ts +2 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +4 -0
- package/dist/db.js.map +1 -1
- package/dist/lib/buildDeletion.d.ts +21 -0
- package/dist/lib/buildDeletion.d.ts.map +1 -0
- package/dist/lib/buildDeletion.js +58 -0
- package/dist/lib/buildDeletion.js.map +1 -0
- package/dist/lib/clientAddress.d.ts +12 -0
- package/dist/lib/clientAddress.d.ts.map +1 -1
- package/dist/lib/clientAddress.js +22 -0
- package/dist/lib/clientAddress.js.map +1 -1
- package/dist/lib/uploads.d.ts +13 -0
- package/dist/lib/uploads.d.ts.map +1 -1
- package/dist/lib/uploads.js +41 -0
- package/dist/lib/uploads.js.map +1 -1
- package/dist/lib/watchActions.d.ts +10 -0
- package/dist/lib/watchActions.d.ts.map +1 -0
- package/dist/lib/watchActions.js +27 -0
- package/dist/lib/watchActions.js.map +1 -0
- package/dist/middleware/auth.d.ts +1 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +1 -1
- package/dist/middleware/auth.js.map +1 -1
- package/dist/migrations/015_flow_runs.sql +59 -0
- package/dist/migrations/016_flow_run_videos.sql +51 -0
- package/dist/router.d.ts +5 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +24 -0
- package/dist/router.js.map +1 -1
- package/package.json +9 -9
- package/public/assets/AISessionWatch-Bxew762e.js +36 -0
- package/public/assets/AISessionWatch-Bxew762e.js.br +0 -0
- package/public/assets/AISessionWatch-Bxew762e.js.gz +0 -0
- package/public/assets/AndroidDeviceScreen-C55msKfh.js +35 -0
- package/public/assets/AndroidDeviceScreen-C55msKfh.js.br +0 -0
- package/public/assets/AndroidDeviceScreen-C55msKfh.js.gz +0 -0
- package/public/assets/AppCenter-93hnrrdy.js +26 -0
- package/public/assets/AppCenter-93hnrrdy.js.br +0 -0
- package/public/assets/AppCenter-93hnrrdy.js.gz +0 -0
- package/public/assets/Default-BhgcUKBm.js +6 -0
- package/public/assets/Default-BhgcUKBm.js.br +0 -0
- package/public/assets/Default-BhgcUKBm.js.gz +0 -0
- package/public/assets/Invite-DQBwLaOl.js +1 -0
- package/public/assets/Invite-DQBwLaOl.js.br +0 -0
- package/public/assets/Invite-DQBwLaOl.js.gz +0 -0
- package/public/assets/MacResources-D4LMGxgV.js +1 -0
- package/public/assets/MacResources-D4LMGxgV.js.br +0 -0
- package/public/assets/MacResources-D4LMGxgV.js.gz +0 -0
- package/public/assets/{NotFound-wHOnT2Fl.js → NotFound-DVq_rCKc.js} +1 -1
- package/public/assets/NotFound-DVq_rCKc.js.br +0 -0
- package/public/assets/NotFound-DVq_rCKc.js.gz +0 -0
- package/public/assets/QASession-Dnuq0yrN.js +81 -0
- package/public/assets/QASession-Dnuq0yrN.js.br +0 -0
- package/public/assets/QASession-Dnuq0yrN.js.gz +0 -0
- package/public/assets/ResetPassword-inTbbral.js +1 -0
- package/public/assets/ResetPassword-inTbbral.js.br +0 -0
- package/public/assets/ResetPassword-inTbbral.js.gz +0 -0
- package/public/assets/RunDetail-B3dw8_U0.js +16 -0
- package/public/assets/RunDetail-B3dw8_U0.js.br +0 -0
- package/public/assets/RunDetail-B3dw8_U0.js.gz +0 -0
- package/public/assets/Runs-DqZl9Y6M.js +6 -0
- package/public/assets/Runs-DqZl9Y6M.js.br +0 -0
- package/public/assets/Runs-DqZl9Y6M.js.gz +0 -0
- package/public/assets/Setup-CPa_UWpa.js +1 -0
- package/public/assets/Setup-CPa_UWpa.js.br +0 -0
- package/public/assets/Setup-CPa_UWpa.js.gz +0 -0
- package/public/assets/Team-DZie8W7R.js +6 -0
- package/public/assets/Team-DZie8W7R.js.br +0 -0
- package/public/assets/Team-DZie8W7R.js.gz +0 -0
- package/public/assets/Tokens-u3-uRAwd.js +1 -0
- package/public/assets/Tokens-u3-uRAwd.js.br +0 -0
- package/public/assets/Tokens-u3-uRAwd.js.gz +0 -0
- package/public/assets/WatchedDevice-CpaKHa1x.js +1 -0
- package/public/assets/WatchedDevice-CpaKHa1x.js.br +0 -0
- package/public/assets/WatchedDevice-CpaKHa1x.js.gz +0 -0
- package/public/assets/aiSession-BsmDnyV1.js +31 -0
- package/public/assets/aiSession-BsmDnyV1.js.br +0 -0
- package/public/assets/aiSession-BsmDnyV1.js.gz +0 -0
- package/public/assets/alert-dialog-Bq_Ln5bp.js +1 -0
- package/public/assets/alert-dialog-Bq_Ln5bp.js.br +0 -0
- package/public/assets/alert-dialog-Bq_Ln5bp.js.gz +0 -0
- package/public/assets/{badge-B3vaBVIQ.js → badge-D-bt97WR.js} +1 -1
- package/public/assets/badge-D-bt97WR.js.br +0 -0
- package/public/assets/badge-D-bt97WR.js.gz +0 -0
- package/public/assets/build-format-ofd2iIzf.js +1 -0
- package/public/assets/build-format-ofd2iIzf.js.br +0 -0
- package/public/assets/build-format-ofd2iIzf.js.gz +0 -0
- package/public/assets/dialog-CtzPkg0O.js +1 -0
- package/public/assets/dialog-CtzPkg0O.js.br +0 -0
- package/public/assets/dialog-CtzPkg0O.js.gz +0 -0
- package/public/assets/field-hint-B774BIWK.js +1 -0
- package/public/assets/field-hint-B774BIWK.js.br +1 -0
- package/public/assets/field-hint-B774BIWK.js.gz +0 -0
- package/public/assets/index-D17hSi50.js +1 -0
- package/public/assets/index-D17hSi50.js.br +0 -0
- package/public/assets/index-D17hSi50.js.gz +0 -0
- package/public/assets/index-EPyP3g9A.css +1 -0
- package/public/assets/index-EPyP3g9A.css.br +0 -0
- package/public/assets/index-EPyP3g9A.css.gz +0 -0
- package/public/assets/index-cgl1yEKu.js +223 -0
- package/public/assets/index-cgl1yEKu.js.br +0 -0
- package/public/assets/index-cgl1yEKu.js.gz +0 -0
- package/public/assets/list-view-BcjDCJy5.js +1 -0
- package/public/assets/list-view-BcjDCJy5.js.br +0 -0
- package/public/assets/list-view-BcjDCJy5.js.gz +0 -0
- package/public/assets/{pencil-CycAOoC_.js → pencil-1XSs32nb.js} +2 -2
- package/public/assets/pencil-1XSs32nb.js.br +0 -0
- package/public/assets/pencil-1XSs32nb.js.gz +0 -0
- package/public/assets/play-DCvrorIC.js +6 -0
- package/public/assets/play-DCvrorIC.js.br +0 -0
- package/public/assets/play-DCvrorIC.js.gz +0 -0
- package/public/assets/resource-health-oWJ8hy9X.js +1 -0
- package/public/assets/resource-health-oWJ8hy9X.js.br +0 -0
- package/public/assets/resource-health-oWJ8hy9X.js.gz +0 -0
- package/public/assets/runs-CInQb6v1.js +1 -0
- package/public/assets/runs-CInQb6v1.js.br +0 -0
- package/public/assets/runs-CInQb6v1.js.gz +0 -0
- package/public/assets/search-input-CFWgLcKA.js +6 -0
- package/public/assets/search-input-CFWgLcKA.js.br +0 -0
- package/public/assets/search-input-CFWgLcKA.js.gz +0 -0
- package/public/assets/select-5ZtzWRYG.js +11 -0
- package/public/assets/select-5ZtzWRYG.js.br +0 -0
- package/public/assets/select-5ZtzWRYG.js.gz +0 -0
- package/public/assets/slider-DHuMDXcP.js +1 -0
- package/public/assets/slider-DHuMDXcP.js.br +0 -0
- package/public/assets/slider-DHuMDXcP.js.gz +0 -0
- package/public/assets/tabs-CmgV_sSI.js +1 -0
- package/public/assets/tabs-CmgV_sSI.js.br +0 -0
- package/public/assets/tabs-CmgV_sSI.js.gz +0 -0
- package/public/assets/tech-label-qLodPeUI.js +1 -0
- package/public/assets/tech-label-qLodPeUI.js.br +0 -0
- package/public/assets/tech-label-qLodPeUI.js.gz +0 -0
- package/public/assets/{trash-BXQnOI5U.js → trash-CXjB9oU1.js} +3 -3
- package/public/assets/trash-CXjB9oU1.js.br +0 -0
- package/public/assets/trash-CXjB9oU1.js.gz +0 -0
- package/public/assets/{useFocusAfterSwap-BY3N9f9v.js → useFocusAfterSwap-BdItkQCu.js} +1 -1
- package/public/assets/useFocusAfterSwap-BdItkQCu.js.br +0 -0
- package/public/assets/useFocusAfterSwap-BdItkQCu.js.gz +0 -0
- package/public/assets/useRelay-CES3I1Mt.js +1 -0
- package/public/assets/useRelay-CES3I1Mt.js.br +0 -0
- package/public/assets/useRelay-CES3I1Mt.js.gz +0 -0
- package/public/assets/useShownAfter-By1Yxysz.js +1 -0
- package/public/assets/useShownAfter-By1Yxysz.js.br +0 -0
- package/public/assets/useShownAfter-By1Yxysz.js.gz +0 -0
- package/public/assets/{vendor-charts-BykzUV2D.js → vendor-charts-BE7R_8yA.js} +2 -2
- package/public/assets/vendor-charts-BE7R_8yA.js.br +0 -0
- package/public/assets/vendor-charts-BE7R_8yA.js.gz +0 -0
- package/public/assets/{vendor-react-BlPSzrEe.js → vendor-react-DTSdA8m-.js} +4 -4
- package/public/assets/vendor-react-DTSdA8m-.js.br +0 -0
- package/public/assets/{vendor-react-BlPSzrEe.js.gz → vendor-react-DTSdA8m-.js.gz} +0 -0
- package/public/assets/{vendor-router-B_lVHrKl.js → vendor-router-B1pQim9Z.js} +3 -3
- package/public/assets/vendor-router-B1pQim9Z.js.br +0 -0
- package/public/assets/vendor-router-B1pQim9Z.js.gz +0 -0
- package/public/assets/video-CK2q3-0B.js +6 -0
- package/public/assets/video-CK2q3-0B.js.br +0 -0
- package/public/assets/video-CK2q3-0B.js.gz +0 -0
- package/public/index.html +18 -4
- package/public/index.html.br +0 -0
- package/public/index.html.gz +0 -0
- package/public/assets/AppCenter-BMdYtQbV.js +0 -26
- package/public/assets/AppCenter-BMdYtQbV.js.br +0 -0
- package/public/assets/AppCenter-BMdYtQbV.js.gz +0 -0
- package/public/assets/Default-0DHHVGaN.js +0 -1
- package/public/assets/Default-0DHHVGaN.js.br +0 -0
- package/public/assets/Default-0DHHVGaN.js.gz +0 -0
- package/public/assets/Invite-C0ZB_HiU.js +0 -1
- package/public/assets/Invite-C0ZB_HiU.js.br +0 -0
- package/public/assets/Invite-C0ZB_HiU.js.gz +0 -0
- package/public/assets/MacResources-DmY5pRV2.js +0 -1
- package/public/assets/MacResources-DmY5pRV2.js.br +0 -0
- package/public/assets/MacResources-DmY5pRV2.js.gz +0 -0
- package/public/assets/NotFound-wHOnT2Fl.js.br +0 -0
- package/public/assets/NotFound-wHOnT2Fl.js.gz +0 -0
- package/public/assets/QASession-2QogWEX7.js +0 -150
- package/public/assets/QASession-2QogWEX7.js.br +0 -0
- package/public/assets/QASession-2QogWEX7.js.gz +0 -0
- package/public/assets/ResetPassword-jNRuertx.js +0 -1
- package/public/assets/ResetPassword-jNRuertx.js.br +0 -0
- package/public/assets/ResetPassword-jNRuertx.js.gz +0 -0
- package/public/assets/Setup-CF_FN9Dt.js +0 -1
- package/public/assets/Setup-CF_FN9Dt.js.br +0 -0
- package/public/assets/Setup-CF_FN9Dt.js.gz +0 -0
- package/public/assets/Team-BbiyOGau.js +0 -6
- package/public/assets/Team-BbiyOGau.js.br +0 -0
- package/public/assets/Team-BbiyOGau.js.gz +0 -0
- package/public/assets/Tokens-CZo4sPTx.js +0 -1
- package/public/assets/Tokens-CZo4sPTx.js.br +0 -0
- package/public/assets/Tokens-CZo4sPTx.js.gz +0 -0
- package/public/assets/alert-dialog-DFz8G_zu.js +0 -1
- package/public/assets/alert-dialog-DFz8G_zu.js.br +0 -0
- package/public/assets/alert-dialog-DFz8G_zu.js.gz +0 -0
- package/public/assets/badge-B3vaBVIQ.js.br +0 -0
- package/public/assets/badge-B3vaBVIQ.js.gz +0 -0
- package/public/assets/build-format-dbQPU3A4.js +0 -11
- package/public/assets/build-format-dbQPU3A4.js.br +0 -0
- package/public/assets/build-format-dbQPU3A4.js.gz +0 -0
- package/public/assets/dialog-DeBPk-I3.js +0 -11
- package/public/assets/dialog-DeBPk-I3.js.br +0 -0
- package/public/assets/dialog-DeBPk-I3.js.gz +0 -0
- package/public/assets/field-hint-CMKmbVpy.js +0 -1
- package/public/assets/field-hint-CMKmbVpy.js.br +0 -0
- package/public/assets/field-hint-CMKmbVpy.js.gz +0 -0
- package/public/assets/index-B4w4Lh3e.js +0 -218
- package/public/assets/index-B4w4Lh3e.js.br +0 -0
- package/public/assets/index-B4w4Lh3e.js.gz +0 -0
- package/public/assets/index-CDWu0GQL.css +0 -1
- package/public/assets/index-CDWu0GQL.css.br +0 -0
- package/public/assets/index-CDWu0GQL.css.gz +0 -0
- package/public/assets/list-view-BvINA5t4.js +0 -1
- package/public/assets/list-view-BvINA5t4.js.br +0 -0
- package/public/assets/list-view-BvINA5t4.js.gz +0 -0
- package/public/assets/pencil-CycAOoC_.js.br +0 -0
- package/public/assets/pencil-CycAOoC_.js.gz +0 -0
- package/public/assets/resource-health-C1qSG3MK.js +0 -1
- package/public/assets/resource-health-C1qSG3MK.js.br +0 -0
- package/public/assets/resource-health-C1qSG3MK.js.gz +0 -0
- package/public/assets/trash-BXQnOI5U.js.br +0 -0
- package/public/assets/trash-BXQnOI5U.js.gz +0 -0
- package/public/assets/useFocusAfterSwap-BY3N9f9v.js.br +0 -0
- package/public/assets/useFocusAfterSwap-BY3N9f9v.js.gz +0 -0
- package/public/assets/vendor-charts-BykzUV2D.js.br +0 -0
- package/public/assets/vendor-charts-BykzUV2D.js.gz +0 -0
- package/public/assets/vendor-react-BlPSzrEe.js.br +0 -0
- package/public/assets/vendor-router-B_lVHrKl.js.br +0 -0
- package/public/assets/vendor-router-B_lVHrKl.js.gz +0 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import { getDb } from '../db.js';
|
|
5
|
+
import { requireViewAuth } from '../middleware/auth.js';
|
|
6
|
+
import { json, readBodyLimited } from '../router.js';
|
|
7
|
+
import { unlinkSafe } from '../lib/uploads.js';
|
|
8
|
+
import { findRun, finishedWithin, ownRun, settleOrphanedRuns } from './runs.js';
|
|
9
|
+
// A flow's video (A4): the flow runner uploads the stream it already receives, in chunks, while the flow runs.
|
|
10
|
+
//
|
|
11
|
+
// **The relay never reads a chunk.** It reads the open call's JSON and nothing else; chunks are bytes it writes at a
|
|
12
|
+
// committed offset and serves back by range. That is the line the "no screen data" rule now draws (relay
|
|
13
|
+
// AGENTS.md): what a runner uploads to its own run is kept under a policy, and a stream the relay forwards is not.
|
|
14
|
+
/** Seconds after `finish` the runner may still send the last flow's tail and end call. */
|
|
15
|
+
export const VIDEO_GRACE_SECONDS = 120;
|
|
16
|
+
const MAX_META_BYTES = 16 * 1024;
|
|
17
|
+
/** A chunk is ~2 s or ~2 MB of frames; this leaves room for a larger screen than any measured. */
|
|
18
|
+
const MAX_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
19
|
+
/** ~1.5 min of continuous motion at the measured 8–9 Mbit/s; a flow-like minute is ~25 MB. */
|
|
20
|
+
const MAX_VIDEO_BYTES = 100 * 1024 * 1024;
|
|
21
|
+
/** Free space the relay keeps for itself whatever the cap says: a full disk takes SQLite, sign-in and uploads down. */
|
|
22
|
+
const MIN_FREE_BYTES = 1024 * 1024 * 1024;
|
|
23
|
+
const MIN_FREE_FRACTION = 0.1;
|
|
24
|
+
/**
|
|
25
|
+
* The highest chunk number taken. The index lists every missing number up to the last one, synchronously, so an
|
|
26
|
+
* unbounded seq or `lastSeq` would let one request hang the relay. ~5.5 h of 2 s chunks, far past any flow.
|
|
27
|
+
*/
|
|
28
|
+
const MAX_SEQ = 10_000;
|
|
29
|
+
const END_REASONS = new Set(['done', 'cancelled', 'size-limit', 'storage-full', 'runner-lost', 'truncated']);
|
|
30
|
+
export function runVideosDirFor(uploadsDir) {
|
|
31
|
+
return path.join(uploadsDir, '../run-videos');
|
|
32
|
+
}
|
|
33
|
+
function findVideo(runSeq, idx) {
|
|
34
|
+
return getDb().prepare('SELECT * FROM flow_run_videos WHERE run_seq = ? AND idx = ?').get(runSeq, idx);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* **Ended is decided here, not only by the end call**, because that call can be lost with the runner: an explicit
|
|
38
|
+
* end, or the run finished and the grace has passed, or the relay closed the run because its runner went away.
|
|
39
|
+
*/
|
|
40
|
+
export function videoEnded(video, run) {
|
|
41
|
+
if (video.ended_at !== null)
|
|
42
|
+
return true;
|
|
43
|
+
if (run.status === 'running')
|
|
44
|
+
return false;
|
|
45
|
+
if (run.finished_by === 'relay')
|
|
46
|
+
return true;
|
|
47
|
+
return !finishedWithin(run, VIDEO_GRACE_SECONDS);
|
|
48
|
+
}
|
|
49
|
+
function flowIndex(run, raw) {
|
|
50
|
+
const idx = Number(raw);
|
|
51
|
+
return /^\d+$/.test(raw) && Number.isSafeInteger(idx) && idx < run.flows_total ? idx : null;
|
|
52
|
+
}
|
|
53
|
+
// ── writes (the run's creator, until the video has ended) ────────────────────
|
|
54
|
+
/** Chunk writes of one video run one at a time: two writes at one committed offset would interleave. */
|
|
55
|
+
const writing = new Map();
|
|
56
|
+
function serialised(key, op) {
|
|
57
|
+
const prev = writing.get(key) ?? Promise.resolve();
|
|
58
|
+
const next = prev.then(op, op);
|
|
59
|
+
const tail = next.then(() => undefined, () => undefined);
|
|
60
|
+
writing.set(key, tail);
|
|
61
|
+
void tail.then(() => { if (writing.get(key) === tail)
|
|
62
|
+
writing.delete(key); });
|
|
63
|
+
return next;
|
|
64
|
+
}
|
|
65
|
+
export async function handleOpenVideo(req, res, params, videosDir) {
|
|
66
|
+
const run = ownRun(req, res, params.id, VIDEO_GRACE_SECONDS);
|
|
67
|
+
if (!run)
|
|
68
|
+
return;
|
|
69
|
+
const idx = flowIndex(run, params.idx);
|
|
70
|
+
if (idx === null)
|
|
71
|
+
return json(res, 404, { error: 'No such flow in this run' });
|
|
72
|
+
const buf = await readBodyLimited(req, MAX_META_BYTES);
|
|
73
|
+
if (buf === null)
|
|
74
|
+
return json(res, 413, { error: 'Video metadata too large' });
|
|
75
|
+
let meta;
|
|
76
|
+
try {
|
|
77
|
+
meta = JSON.parse(buf.toString('utf-8'));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
meta = null;
|
|
81
|
+
}
|
|
82
|
+
if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) {
|
|
83
|
+
return json(res, 400, { error: 'Video metadata must be a JSON object' });
|
|
84
|
+
}
|
|
85
|
+
if (meta.formatVersion !== 1) {
|
|
86
|
+
return json(res, 400, { error: 'Unsupported video format version' });
|
|
87
|
+
}
|
|
88
|
+
const db = getDb();
|
|
89
|
+
// The row and its empty file in one synchronous transaction: nothing can delete the run between them, the orphan
|
|
90
|
+
// sweep never sees a file without a row, and a file that cannot be created (a full disk, a permission) rolls the
|
|
91
|
+
// row back — left behind, a retried open would answer 200 for a video every chunk then finds gone.
|
|
92
|
+
const existing = findVideo(run.seq, idx);
|
|
93
|
+
if (existing) {
|
|
94
|
+
// An open retried after a timeout. The same video carries on; one that has ended is not reopened.
|
|
95
|
+
if (existing.ended_at !== null || existing.evicted_at !== null)
|
|
96
|
+
return json(res, 409, { error: 'Video already ended' });
|
|
97
|
+
return json(res, 200, { ok: true, nextSeq: existing.next_seq });
|
|
98
|
+
}
|
|
99
|
+
fs.mkdirSync(videosDir, { recursive: true });
|
|
100
|
+
const file = `${randomUUID()}.tfv`;
|
|
101
|
+
db.transaction(() => {
|
|
102
|
+
db.prepare('INSERT INTO flow_run_videos (run_seq, idx, file, meta_json) VALUES (?, ?, ?, ?)')
|
|
103
|
+
.run(run.seq, idx, file, JSON.stringify(meta));
|
|
104
|
+
fs.writeFileSync(path.join(videosDir, file), '', { flag: 'wx' });
|
|
105
|
+
})();
|
|
106
|
+
json(res, 201, { ok: true, nextSeq: 0 });
|
|
107
|
+
}
|
|
108
|
+
export function diskRoomFor(dir) {
|
|
109
|
+
try {
|
|
110
|
+
const st = fs.statfsSync(dir);
|
|
111
|
+
const total = st.blocks * st.bsize;
|
|
112
|
+
return { free: st.bavail * st.bsize, floor: Math.max(MIN_FREE_BYTES, Math.floor(total * MIN_FREE_FRACTION)) };
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// A filesystem that cannot be asked is not reported as full; the cap still holds.
|
|
116
|
+
return { free: Number.MAX_SAFE_INTEGER, floor: 0 };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function videoUsageBytes() {
|
|
120
|
+
const { used, reserved } = videoUsage();
|
|
121
|
+
return used + reserved;
|
|
122
|
+
}
|
|
123
|
+
/** Committed bytes and bytes reserved by writes in flight, over the videos still kept. */
|
|
124
|
+
function videoUsage() {
|
|
125
|
+
return getDb().prepare(`
|
|
126
|
+
SELECT COALESCE(SUM(bytes), 0) AS used, COALESCE(SUM(reserved_bytes), 0) AS reserved FROM flow_run_videos WHERE evicted_at IS NULL
|
|
127
|
+
`).get();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A reservation lives only as long as the write holding it, and no write survives a restart: a crash between reserve
|
|
131
|
+
* and release would otherwise count those bytes against the cap forever. Called once at start, before any write.
|
|
132
|
+
*/
|
|
133
|
+
export function releaseStaleVideoReservations() {
|
|
134
|
+
getDb().prepare('UPDATE flow_run_videos SET reserved_bytes = 0 WHERE reserved_bytes != 0').run();
|
|
135
|
+
}
|
|
136
|
+
export function videoSettings() {
|
|
137
|
+
const row = getDb().prepare('SELECT video_pass_retention_days, video_storage_cap_mb FROM team_settings WHERE id = 1')
|
|
138
|
+
.get();
|
|
139
|
+
return {
|
|
140
|
+
passRetentionDays: row?.video_pass_retention_days ?? 3,
|
|
141
|
+
storageCapBytes: (row?.video_storage_cap_mb ?? 20480) * 1024 * 1024,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* How far `len` more bytes would go past the cap and past the disk's floor; zero or less is room. Both count: the
|
|
146
|
+
* cap is the operator's setting, the floor is what the disk can actually give. Reserved bytes count against the
|
|
147
|
+
* floor too: they are not on disk yet, so free space does not show them, and two writers to different videos would
|
|
148
|
+
* otherwise both see the same free space. Between a write landing and its release the same bytes count twice, once
|
|
149
|
+
* in free space and once reserved: at most a chunk per writer, and on the side of keeping the floor.
|
|
150
|
+
*/
|
|
151
|
+
export function shortfall(usage, len, capBytes, room) {
|
|
152
|
+
return { overCap: usage.used + usage.reserved + len - capBytes, overDisk: usage.reserved + len - (room.free - room.floor) };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Make room for `len` bytes, in the caller's transaction: evict ended videos of finished runs — passed flows
|
|
156
|
+
* first, oldest first, then the rest — until both the cap and the disk floor allow it. Never a video still being
|
|
157
|
+
* written. Returns the files to unlink once the transaction commits, or null when nothing more can go.
|
|
158
|
+
*/
|
|
159
|
+
function makeRoom(len, videosDir, room, keep) {
|
|
160
|
+
const db = getDb();
|
|
161
|
+
let { overCap, overDisk } = shortfall(videoUsage(), len, videoSettings().storageCapBytes, room);
|
|
162
|
+
if (overCap <= 0 && overDisk <= 0)
|
|
163
|
+
return [];
|
|
164
|
+
const candidates = db.prepare(`
|
|
165
|
+
SELECT v.run_seq, v.idx, v.file, v.bytes, v.ended_at, r.status, r.finished_by, r.finished_at
|
|
166
|
+
FROM flow_run_videos v
|
|
167
|
+
JOIN flow_runs r ON r.seq = v.run_seq
|
|
168
|
+
LEFT JOIN flow_run_flows f ON f.run_seq = v.run_seq AND f.idx = v.idx
|
|
169
|
+
WHERE v.evicted_at IS NULL AND r.status != 'running'
|
|
170
|
+
ORDER BY CASE WHEN f.status = 'passed' THEN 0 ELSE 1 END, v.created_at, v.run_seq, v.idx
|
|
171
|
+
`).all();
|
|
172
|
+
const evict = db.prepare(`UPDATE flow_run_videos SET evicted_at = datetime('now') WHERE run_seq = ? AND idx = ?`);
|
|
173
|
+
const files = [];
|
|
174
|
+
for (const c of candidates) {
|
|
175
|
+
if (overCap <= 0 && overDisk <= 0)
|
|
176
|
+
break;
|
|
177
|
+
if (c.run_seq === keep.runSeq && c.idx === keep.idx)
|
|
178
|
+
continue;
|
|
179
|
+
if (writing.has(`${c.run_seq}:${c.idx}`) || !videoEnded(c, c))
|
|
180
|
+
continue;
|
|
181
|
+
evict.run(c.run_seq, c.idx);
|
|
182
|
+
files.push(path.join(videosDir, c.file));
|
|
183
|
+
overCap -= c.bytes;
|
|
184
|
+
overDisk -= c.bytes;
|
|
185
|
+
}
|
|
186
|
+
return overCap <= 0 && overDisk <= 0 ? files : null;
|
|
187
|
+
}
|
|
188
|
+
export async function handleVideoChunk(req, res, params, videosDir, holders, diskRoom = diskRoomFor) {
|
|
189
|
+
const run = ownRun(req, res, params.id, VIDEO_GRACE_SECONDS);
|
|
190
|
+
if (!run)
|
|
191
|
+
return;
|
|
192
|
+
const idx = flowIndex(run, params.idx);
|
|
193
|
+
if (idx === null)
|
|
194
|
+
return json(res, 404, { error: 'No such flow in this run' });
|
|
195
|
+
if (!/^\d+$/.test(params.seq) || Number(params.seq) > MAX_SEQ)
|
|
196
|
+
return json(res, 400, { error: `seq must be an integer from 0 to ${MAX_SEQ}` });
|
|
197
|
+
const seq = Number(params.seq);
|
|
198
|
+
const startRaw = String(req.headers['x-start-ms'] ?? '');
|
|
199
|
+
if (!/^\d+$/.test(startRaw) || !Number.isSafeInteger(Number(startRaw)))
|
|
200
|
+
return json(res, 400, { error: 'X-Start-Ms must be a non-negative integer' });
|
|
201
|
+
const keyframe = req.headers['x-keyframe'] === '1' ? 1 : 0;
|
|
202
|
+
const pre = findVideo(run.seq, idx);
|
|
203
|
+
if (!pre)
|
|
204
|
+
return json(res, 404, { error: 'Open the video before sending chunks' });
|
|
205
|
+
if (pre.evicted_at !== null)
|
|
206
|
+
return json(res, 410, { error: 'Video no longer kept' });
|
|
207
|
+
if (videoEnded(pre, run))
|
|
208
|
+
return json(res, 409, { error: 'Video already ended' });
|
|
209
|
+
const buf = await readBodyLimited(req, MAX_CHUNK_BYTES);
|
|
210
|
+
if (buf === null)
|
|
211
|
+
return json(res, 413, { error: 'Chunk too large', reason: 'chunk-too-large' });
|
|
212
|
+
if (buf.length === 0)
|
|
213
|
+
return json(res, 400, { error: 'Empty chunk' });
|
|
214
|
+
const key = `${run.seq}:${idx}`;
|
|
215
|
+
const db = getDb();
|
|
216
|
+
await serialised(key, async () => {
|
|
217
|
+
// Read again: the video, its run, or both may have changed while the body arrived or an earlier write ran.
|
|
218
|
+
const video = findVideo(run.seq, idx);
|
|
219
|
+
const current = findRun(params.id);
|
|
220
|
+
if (!video || !current)
|
|
221
|
+
return json(res, 404, { error: 'Run not found' });
|
|
222
|
+
if (video.evicted_at !== null)
|
|
223
|
+
return json(res, 410, { error: 'Video no longer kept' });
|
|
224
|
+
// An end call is not serialised with the chunks, so one may have landed while this chunk waited its turn.
|
|
225
|
+
if (videoEnded(video, current))
|
|
226
|
+
return json(res, 409, { error: 'Video already ended' });
|
|
227
|
+
// A retry of a chunk already written, or one that arrived after a later chunk: the gap it left stays a gap.
|
|
228
|
+
if (seq < video.next_seq)
|
|
229
|
+
return json(res, 200, { ok: true, duplicate: true, nextSeq: video.next_seq });
|
|
230
|
+
if (video.bytes + buf.length > MAX_VIDEO_BYTES) {
|
|
231
|
+
db.prepare(`UPDATE flow_run_videos SET ended_at = datetime('now'), end_reason = 'size-limit' WHERE run_seq = ? AND idx = ?`).run(run.seq, idx);
|
|
232
|
+
return json(res, 413, { error: 'Video reached its size limit', reason: 'size-limit' });
|
|
233
|
+
}
|
|
234
|
+
// **Check, evict and reserve in one synchronous transaction** — two videos must not both pass the cap.
|
|
235
|
+
const room = diskRoom(videosDir);
|
|
236
|
+
let offset = 0;
|
|
237
|
+
const toUnlink = db.transaction(() => {
|
|
238
|
+
settleOrphanedRuns(holders);
|
|
239
|
+
const files = makeRoom(buf.length, videosDir, room, { runSeq: run.seq, idx });
|
|
240
|
+
if (files === null)
|
|
241
|
+
return null;
|
|
242
|
+
const row = findVideo(run.seq, idx);
|
|
243
|
+
offset = row.bytes;
|
|
244
|
+
db.prepare('UPDATE flow_run_videos SET reserved_bytes = reserved_bytes + ? WHERE run_seq = ? AND idx = ?').run(buf.length, run.seq, idx);
|
|
245
|
+
return files;
|
|
246
|
+
})();
|
|
247
|
+
if (toUnlink === null) {
|
|
248
|
+
db.prepare(`UPDATE flow_run_videos SET ended_at = datetime('now'), end_reason = 'storage-full' WHERE run_seq = ? AND idx = ? AND ended_at IS NULL`).run(run.seq, idx);
|
|
249
|
+
return json(res, 507, { error: 'Relay storage for videos is full', reason: 'storage-full' });
|
|
250
|
+
}
|
|
251
|
+
for (const f of toUnlink)
|
|
252
|
+
unlinkSafe(f, 'evicted run video');
|
|
253
|
+
// At the committed length, not appended: a write torn by a crash is overwritten by the next one. `r+` never
|
|
254
|
+
// creates, so a write racing a delete cannot bring back a file no row names.
|
|
255
|
+
const release = db.prepare('UPDATE flow_run_videos SET reserved_bytes = MAX(reserved_bytes - ?, 0) WHERE run_seq = ? AND idx = ?');
|
|
256
|
+
let fh;
|
|
257
|
+
try {
|
|
258
|
+
fh = await fs.promises.open(path.join(videosDir, video.file), 'r+');
|
|
259
|
+
await writeFully(fh, buf, offset);
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
release.run(buf.length, run.seq, idx);
|
|
263
|
+
if (e.code === 'ENOENT')
|
|
264
|
+
return json(res, 410, { error: 'Video no longer kept' });
|
|
265
|
+
throw e;
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
await fh?.close();
|
|
269
|
+
}
|
|
270
|
+
db.transaction(() => {
|
|
271
|
+
release.run(buf.length, run.seq, idx);
|
|
272
|
+
// Gone while the bytes were written (its run deleted, its file evicted): nothing to commit.
|
|
273
|
+
if (!findVideo(run.seq, idx) || findVideo(run.seq, idx).evicted_at !== null)
|
|
274
|
+
return;
|
|
275
|
+
db.prepare(`INSERT INTO flow_run_video_chunks (run_seq, idx, seq, offset, length, start_ms, keyframe) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
276
|
+
.run(run.seq, idx, seq, offset, buf.length, Number(startRaw), keyframe);
|
|
277
|
+
db.prepare(`UPDATE flow_run_videos SET bytes = ?, next_seq = ?, last_chunk_at = datetime('now') WHERE run_seq = ? AND idx = ?`)
|
|
278
|
+
.run(offset + buf.length, seq + 1, run.seq, idx);
|
|
279
|
+
})();
|
|
280
|
+
json(res, 200, { ok: true, nextSeq: seq + 1 });
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* A write may land fewer bytes than asked (a disk filling up mid-write), and the chunk row records the whole length:
|
|
285
|
+
* write the rest, and fail rather than commit a chunk whose tail is not there.
|
|
286
|
+
*/
|
|
287
|
+
export async function writeFully(fh, buf, offset) {
|
|
288
|
+
let done = 0;
|
|
289
|
+
while (done < buf.length) {
|
|
290
|
+
const { bytesWritten } = await fh.write(buf, done, buf.length - done, offset + done);
|
|
291
|
+
if (bytesWritten <= 0)
|
|
292
|
+
throw new Error(`Video chunk write made no progress at ${offset + done}`);
|
|
293
|
+
done += bytesWritten;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
export async function handleEndVideo(req, res, params) {
|
|
297
|
+
const run = ownRun(req, res, params.id, VIDEO_GRACE_SECONDS);
|
|
298
|
+
if (!run)
|
|
299
|
+
return;
|
|
300
|
+
const idx = flowIndex(run, params.idx);
|
|
301
|
+
if (idx === null)
|
|
302
|
+
return json(res, 404, { error: 'No such flow in this run' });
|
|
303
|
+
const buf = await readBodyLimited(req, MAX_META_BYTES);
|
|
304
|
+
if (buf === null)
|
|
305
|
+
return json(res, 413, { error: 'Body too large' });
|
|
306
|
+
let body = {};
|
|
307
|
+
try {
|
|
308
|
+
body = JSON.parse(buf.toString('utf-8'));
|
|
309
|
+
}
|
|
310
|
+
catch { /* answered below */ }
|
|
311
|
+
const lastSeq = body.lastSeq;
|
|
312
|
+
if (!(lastSeq === -1 || (typeof lastSeq === 'number' && Number.isInteger(lastSeq) && lastSeq >= 0 && lastSeq <= MAX_SEQ))) {
|
|
313
|
+
return json(res, 400, { error: `lastSeq must be -1 or an integer from 0 to ${MAX_SEQ}` });
|
|
314
|
+
}
|
|
315
|
+
if (typeof body.reason !== 'string' || !END_REASONS.has(body.reason))
|
|
316
|
+
return json(res, 400, { error: 'Unknown end reason' });
|
|
317
|
+
const video = findVideo(run.seq, idx);
|
|
318
|
+
if (!video)
|
|
319
|
+
return json(res, 404, { error: 'No video for this flow' });
|
|
320
|
+
// Idempotent, and the first ending wins: a size-limit or storage-full the relay decided is not overwritten.
|
|
321
|
+
getDb().prepare(`
|
|
322
|
+
UPDATE flow_run_videos SET ended_at = COALESCE(ended_at, datetime('now')), end_reason = COALESCE(end_reason, ?),
|
|
323
|
+
end_last_seq = COALESCE(end_last_seq, ?)
|
|
324
|
+
WHERE run_seq = ? AND idx = ?
|
|
325
|
+
`).run(body.reason, lastSeq, run.seq, idx);
|
|
326
|
+
json(res, 200, { ok: true });
|
|
327
|
+
}
|
|
328
|
+
// ── reads (anyone who may view) ─────────────────────────────────────────────
|
|
329
|
+
function readTarget(req, res, params, holders) {
|
|
330
|
+
if (!requireViewAuth(req, res))
|
|
331
|
+
return null;
|
|
332
|
+
settleOrphanedRuns(holders);
|
|
333
|
+
const run = findRun(params.id);
|
|
334
|
+
if (!run) {
|
|
335
|
+
json(res, 404, { error: 'Run not found' });
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
const idx = flowIndex(run, params.idx);
|
|
339
|
+
const video = idx === null ? undefined : findVideo(run.seq, idx);
|
|
340
|
+
if (!video) {
|
|
341
|
+
json(res, 404, { error: 'No video for this flow' });
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
return { run, video };
|
|
345
|
+
}
|
|
346
|
+
function endReasonOf(video, run, ended) {
|
|
347
|
+
return video.end_reason ?? (ended && video.ended_at === null ? (run.finished_by === 'relay' ? 'runner-lost' : 'truncated') : null);
|
|
348
|
+
}
|
|
349
|
+
/** Each flow's video by flow index, for `GET /api/v1/runs/:id`. Passed in by the server so runs.ts needs no import of this file. */
|
|
350
|
+
export function videoSummaries(run) {
|
|
351
|
+
const rows = getDb().prepare('SELECT * FROM flow_run_videos WHERE run_seq = ?').all(run.seq);
|
|
352
|
+
return new Map(rows.map((v) => {
|
|
353
|
+
const ended = videoEnded(v, run);
|
|
354
|
+
let notRecorded = null;
|
|
355
|
+
try {
|
|
356
|
+
const meta = JSON.parse(v.meta_json);
|
|
357
|
+
if (typeof meta.notRecorded === 'string')
|
|
358
|
+
notRecorded = meta.notRecorded;
|
|
359
|
+
}
|
|
360
|
+
catch { /* the open call's JSON was validated when stored */ }
|
|
361
|
+
return [v.idx, { ended, endReason: endReasonOf(v, run, ended), evicted: v.evicted_at !== null, notRecorded, bytes: v.bytes }];
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
/** The player's map of the video: its metadata, where each chunk lies, which seqs never arrived, and its state. */
|
|
365
|
+
export function handleVideoIndex(req, res, params, holders) {
|
|
366
|
+
const t = readTarget(req, res, params, holders);
|
|
367
|
+
if (!t)
|
|
368
|
+
return;
|
|
369
|
+
const { run, video } = t;
|
|
370
|
+
const chunks = getDb().prepare(`
|
|
371
|
+
SELECT seq, offset, length, start_ms, keyframe FROM flow_run_video_chunks WHERE run_seq = ? AND idx = ? ORDER BY seq
|
|
372
|
+
`).all(run.seq, video.idx);
|
|
373
|
+
const present = new Set(chunks.map((c) => c.seq));
|
|
374
|
+
const last = Math.max(video.next_seq - 1, video.end_last_seq ?? -1);
|
|
375
|
+
const gaps = [];
|
|
376
|
+
for (let s = 0; s <= last; s++)
|
|
377
|
+
if (!present.has(s))
|
|
378
|
+
gaps.push(s);
|
|
379
|
+
const ended = videoEnded(video, run);
|
|
380
|
+
json(res, 200, {
|
|
381
|
+
meta: JSON.parse(video.meta_json),
|
|
382
|
+
bytes: video.bytes,
|
|
383
|
+
ended,
|
|
384
|
+
endReason: endReasonOf(video, run, ended),
|
|
385
|
+
evicted: video.evicted_at !== null,
|
|
386
|
+
chunks: chunks.map((c) => ({ seq: c.seq, offset: c.offset, length: c.length, startMs: c.start_ms, keyframe: c.keyframe === 1 })),
|
|
387
|
+
gaps,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/** The bytes, by range, never past the committed length: a growing video is read while it is written. */
|
|
391
|
+
export function handleVideoData(req, res, params, holders, videosDir) {
|
|
392
|
+
const t = readTarget(req, res, params, holders);
|
|
393
|
+
if (!t)
|
|
394
|
+
return;
|
|
395
|
+
const { run, video } = t;
|
|
396
|
+
if (video.evicted_at !== null)
|
|
397
|
+
return json(res, 410, { error: 'Video no longer kept' });
|
|
398
|
+
const size = video.bytes;
|
|
399
|
+
let start = 0;
|
|
400
|
+
let end = size - 1;
|
|
401
|
+
const range = req.headers.range;
|
|
402
|
+
if (range) {
|
|
403
|
+
const m = /^bytes=(\d+)-(\d*)$/.exec(range);
|
|
404
|
+
if (!m || Number(m[1]) >= size) {
|
|
405
|
+
res.writeHead(416, { 'Content-Range': `bytes */${size}` });
|
|
406
|
+
return void res.end();
|
|
407
|
+
}
|
|
408
|
+
start = Number(m[1]);
|
|
409
|
+
end = m[2] === '' ? size - 1 : Math.min(Number(m[2]), size - 1);
|
|
410
|
+
if (end < start) {
|
|
411
|
+
res.writeHead(416, { 'Content-Range': `bytes */${size}` });
|
|
412
|
+
return void res.end();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const ended = videoEnded(video, run);
|
|
416
|
+
const headers = {
|
|
417
|
+
'Content-Type': 'application/octet-stream',
|
|
418
|
+
'X-Content-Type-Options': 'nosniff',
|
|
419
|
+
'Content-Disposition': 'attachment',
|
|
420
|
+
'Accept-Ranges': 'bytes',
|
|
421
|
+
// A video still being written must not be cached as it was; an ended one is fixed by its committed length.
|
|
422
|
+
'Cache-Control': ended ? 'private, max-age=3600' : 'no-store',
|
|
423
|
+
...(ended ? { ETag: `"${size}"` } : {}),
|
|
424
|
+
};
|
|
425
|
+
if (size === 0) {
|
|
426
|
+
res.writeHead(200, { ...headers, 'Content-Length': 0 });
|
|
427
|
+
return void res.end();
|
|
428
|
+
}
|
|
429
|
+
const stream = fs.createReadStream(path.join(videosDir, video.file), { start, end });
|
|
430
|
+
stream.once('open', () => {
|
|
431
|
+
res.writeHead(range ? 206 : 200, {
|
|
432
|
+
...headers,
|
|
433
|
+
'Content-Length': end - start + 1,
|
|
434
|
+
...(range ? { 'Content-Range': `bytes ${start}-${end}/${size}` } : {}),
|
|
435
|
+
});
|
|
436
|
+
stream.pipe(res);
|
|
437
|
+
});
|
|
438
|
+
stream.once('error', (e) => {
|
|
439
|
+
if (res.headersSent)
|
|
440
|
+
return void res.destroy();
|
|
441
|
+
if (e.code === 'ENOENT')
|
|
442
|
+
return json(res, 410, { error: 'Video no longer kept' });
|
|
443
|
+
json(res, 500, { error: 'Could not read the video' });
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// ── retention (the daily purge) ─────────────────────────────────────────────
|
|
447
|
+
/**
|
|
448
|
+
* Passed flows' videos past the retention window go; files no live row names (a runner killed mid-open, a crash)
|
|
449
|
+
* are swept; rows whose file is gone (a restored database) are marked evicted, so usage stops counting them.
|
|
450
|
+
*/
|
|
451
|
+
export function purgeRunVideos(videosDir, holders) {
|
|
452
|
+
settleOrphanedRuns(holders);
|
|
453
|
+
const db = getDb();
|
|
454
|
+
const { passRetentionDays } = videoSettings();
|
|
455
|
+
const expired = db.prepare(`
|
|
456
|
+
SELECT v.run_seq, v.idx, v.file, v.ended_at, r.status, r.finished_by, r.finished_at
|
|
457
|
+
FROM flow_run_videos v
|
|
458
|
+
JOIN flow_runs r ON r.seq = v.run_seq
|
|
459
|
+
JOIN flow_run_flows f ON f.run_seq = v.run_seq AND f.idx = v.idx
|
|
460
|
+
WHERE v.evicted_at IS NULL AND r.status != 'running' AND f.status = 'passed' AND v.created_at < datetime('now', ?)
|
|
461
|
+
`).all(`-${passRetentionDays} days`);
|
|
462
|
+
const evict = db.prepare(`UPDATE flow_run_videos SET evicted_at = datetime('now') WHERE run_seq = ? AND idx = ?`);
|
|
463
|
+
const files = [];
|
|
464
|
+
db.transaction(() => {
|
|
465
|
+
for (const v of expired) {
|
|
466
|
+
if (writing.has(`${v.run_seq}:${v.idx}`) || !videoEnded(v, v))
|
|
467
|
+
continue;
|
|
468
|
+
evict.run(v.run_seq, v.idx);
|
|
469
|
+
files.push(v.file);
|
|
470
|
+
}
|
|
471
|
+
})();
|
|
472
|
+
for (const f of files)
|
|
473
|
+
unlinkSafe(path.join(videosDir, f), 'run video');
|
|
474
|
+
let onDisk = [];
|
|
475
|
+
// No directory is what a restore that left the videos out produces: every live row is then missing its file. Any
|
|
476
|
+
// other failure says nothing about the files, and must not mark every video gone.
|
|
477
|
+
try {
|
|
478
|
+
onDisk = fs.readdirSync(videosDir);
|
|
479
|
+
}
|
|
480
|
+
catch (e) {
|
|
481
|
+
if (e.code !== 'ENOENT')
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const live = new Set(db.prepare('SELECT file FROM flow_run_videos WHERE evicted_at IS NULL').all().map((r) => r.file));
|
|
485
|
+
for (const f of onDisk)
|
|
486
|
+
if (!live.has(f))
|
|
487
|
+
unlinkSafe(path.join(videosDir, f), 'orphaned run video');
|
|
488
|
+
const present = new Set(onDisk);
|
|
489
|
+
const missing = db.prepare(`UPDATE flow_run_videos SET evicted_at = datetime('now') WHERE file = ? AND evicted_at IS NULL`);
|
|
490
|
+
for (const f of live)
|
|
491
|
+
if (!present.has(f))
|
|
492
|
+
missing.run(f);
|
|
493
|
+
}
|
|
494
|
+
//# sourceMappingURL=runVideos.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runVideos.js","sourceRoot":"","sources":["../../src/api/runVideos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,kBAAkB,EAAgC,MAAM,WAAW,CAAA;AAE7G,+GAA+G;AAC/G,EAAE;AACF,qHAAqH;AACrH,yGAAyG;AACzG,mHAAmH;AAEnH,0FAA0F;AAC1F,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAA;AACtC,MAAM,cAAc,GAAG,EAAE,GAAG,IAAI,CAAA;AAChC,kGAAkG;AAClG,MAAM,eAAe,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAA;AACvC,8FAA8F;AAC9F,MAAM,eAAe,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAA;AACzC,uHAAuH;AACvH,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AACzC,MAAM,iBAAiB,GAAG,GAAG,CAAA;AAC7B;;;GAGG;AACH,MAAM,OAAO,GAAG,MAAM,CAAA;AAEtB,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC,CAAA;AAE5G,MAAM,UAAU,eAAe,CAAC,UAAkB;IAChD,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,eAAe,CAAC,CAAA;AAC/C,CAAC;AAiBD,SAAS,SAAS,CAAC,MAAc,EAAE,GAAW;IAC5C,OAAO,KAAK,EAAE,CAAC,OAAO,CAAC,6DAA6D,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAyB,CAAA;AAChI,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,KAAiC,EAAE,GAA2D;IACvH,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IACxC,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IAC1C,IAAI,GAAG,CAAC,WAAW,KAAK,OAAO;QAAE,OAAO,IAAI,CAAA;IAC5C,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAA;AAClD,CAAC;AAED,SAAS,SAAS,CAAC,GAAW,EAAE,GAAW;IACzC,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAA;IACvB,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAA;AAC7F,CAAC;AAED,gFAAgF;AAEhF,wGAAwG;AACxG,MAAM,OAAO,GAAG,IAAI,GAAG,EAAyB,CAAA;AAEhD,SAAS,UAAU,CAAI,GAAW,EAAE,EAAoB;IACtD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAA;IAClD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;IAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IACxD,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IACtB,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI;QAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;IAC5E,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAyB,EACzB,GAAwB,EACxB,MAA8B,EAC9B,SAAiB;IAEjB,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,EAAE,mBAAmB,CAAC,CAAA;IAC5D,IAAI,CAAC,GAAG;QAAE,OAAM;IAChB,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IACtC,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAA;IAC9E,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,cAAc,CAAC,CAAA;IACtD,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAA;IAC9E,IAAI,IAAa,CAAA;IACjB,IAAI,CAAC;QAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,IAAI,GAAG,IAAI,CAAA;IAAC,CAAC;IACtE,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACrE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,sCAAsC,EAAE,CAAC,CAAA;IAC1E,CAAC;IACD,IAAK,IAAoC,CAAC,aAAa,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,kCAAkC,EAAE,CAAC,CAAA;IACtE,CAAC;IAED,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,iHAAiH;IACjH,iHAAiH;IACjH,mGAAmG;IACnG,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IACxC,IAAI,QAAQ,EAAE,CAAC;QACb,kGAAkG;QAClG,IAAI,QAAQ,CAAC,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,UAAU,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAA;QACvH,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAA;IACjE,CAAC;IACD,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5C,MAAM,IAAI,GAAG,GAAG,UAAU,EAAE,MAAM,CAAA;IAClC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;QAClB,EAAE,CAAC,OAAO,CAAC,iFAAiF,CAAC;aAC1F,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;QAChD,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;IAClE,CAAC,CAAC,EAAE,CAAA;IACJ,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAA;AAC1C,CAAC;AAKD,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;QAC7B,MAAM,KAAK,GAAG,EAAE,CAAC,MAAM,GAAG,EAAE,CAAC,KAAK,CAAA;QAClC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,iBAAiB,CAAC,CAAC,EAAE,CAAA;IAC/G,CAAC;IAAC,MAAM,CAAC;QACP,kFAAkF;QAClF,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE,CAAA;IACpD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,UAAU,EAAE,CAAA;IACvC,OAAO,IAAI,GAAG,QAAQ,CAAA;AACxB,CAAC;AAED,0FAA0F;AAC1F,SAAS,UAAU;IACjB,OAAO,KAAK,EAAE,CAAC,OAAO,CAAC;;GAEtB,CAAC,CAAC,GAAG,EAAwC,CAAA;AAChD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,6BAA6B;IAC3C,KAAK,EAAE,CAAC,OAAO,CAAC,yEAAyE,CAAC,CAAC,GAAG,EAAE,CAAA;AAClG,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,MAAM,GAAG,GAAG,KAAK,EAAE,CAAC,OAAO,CAAC,wFAAwF,CAAC;SAClH,GAAG,EAAqF,CAAA;IAC3F,OAAO;QACL,iBAAiB,EAAE,GAAG,EAAE,yBAAyB,IAAI,CAAC;QACtD,eAAe,EAAE,CAAC,GAAG,EAAE,oBAAoB,IAAI,KAAK,CAAC,GAAG,IAAI,GAAG,IAAI;KACpE,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CACvB,KAAyC,EAAE,GAAW,EAAE,QAAgB,EAAE,IAAc;IAExF,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,QAAQ,GAAG,GAAG,GAAG,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAA;AAC7H,CAAC;AAED;;;;GAIG;AACH,SAAS,QAAQ,CAAC,GAAW,EAAE,SAAiB,EAAE,IAAc,EAAE,IAAqC;IACrG,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAC,UAAU,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,CAAC,eAAe,EAAE,IAAI,CAAC,CAAA;IAC/F,IAAI,OAAO,IAAI,CAAC,IAAI,QAAQ,IAAI,CAAC;QAAE,OAAO,EAAE,CAAA;IAE5C,MAAM,UAAU,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;;;GAO7B,CAAC,CAAC,GAAG,EAAoI,CAAA;IAC1I,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,uFAAuF,CAAC,CAAA;IACjH,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,OAAO,IAAI,CAAC,IAAI,QAAQ,IAAI,CAAC;YAAE,MAAK;QACxC,IAAI,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG;YAAE,SAAQ;QAC7D,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC;YAAE,SAAQ;QACvE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;QAC3B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;QACxC,OAAO,IAAI,CAAC,CAAC,KAAK,CAAA;QAClB,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAA;IACrB,CAAC;IACD,OAAO,OAAO,IAAI,CAAC,IAAI,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AACrD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAyB,EACzB,GAAwB,EACxB,MAA8B,EAC9B,SAAiB,EACjB,OAAmB,EACnB,WAAsC,WAAW;IAEjD,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,EAAE,mBAAmB,CAAC,CAAA;IAC5D,IAAI,CAAC,GAAG;QAAE,OAAM;IAChB,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IACtC,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAA;IAC9E,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,OAAO;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,oCAAoC,OAAO,EAAE,EAAE,CAAC,CAAA;IAC9I,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC9B,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAA;IACxD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,2CAA2C,EAAE,CAAC,CAAA;IACrJ,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAE1D,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IACnC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,sCAAsC,EAAE,CAAC,CAAA;IAClF,IAAI,GAAG,CAAC,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,CAAA;IACrF,IAAI,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAA;IAEjF,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,eAAe,CAAC,CAAA;IACvD,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAAA;IAChG,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC,CAAA;IAErE,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,CAAA;IAC/B,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,MAAM,UAAU,CAAC,GAAG,EAAE,KAAK,IAAI,EAAE;QAC/B,2GAA2G;QAC3G,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QACrC,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QAClC,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAA;QACzE,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,CAAA;QACvF,0GAA0G;QAC1G,IAAI,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAA;QACvF,4GAA4G;QAC5G,IAAI,GAAG,GAAG,KAAK,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAA;QACvG,IAAI,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;YAC/C,EAAE,CAAC,OAAO,CAAC,gHAAgH,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;YAC9I,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,8BAA8B,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAA;QACxF,CAAC;QAED,uGAAuG;QACvG,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAA;QAChC,IAAI,MAAM,GAAG,CAAC,CAAA;QACd,MAAM,QAAQ,GAAG,EAAE,CAAC,WAAW,CAAC,GAAoB,EAAE;YACpD,kBAAkB,CAAC,OAAO,CAAC,CAAA;YAC3B,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAA;YAC7E,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAA;YAC/B,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAE,CAAA;YACpC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAA;YAClB,EAAE,CAAC,OAAO,CAAC,8FAA8F,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;YACxI,OAAO,KAAK,CAAA;QACd,CAAC,CAAC,EAAE,CAAA;QACJ,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,EAAE,CAAC,OAAO,CAAC,uIAAuI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;YACrK,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,kCAAkC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAA;QAC9F,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,QAAQ;YAAE,UAAU,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAA;QAE5D,4GAA4G;QAC5G,6EAA6E;QAC7E,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,sGAAsG,CAAC,CAAA;QAClI,IAAI,EAAsC,CAAA;QAC1C,IAAI,CAAC;YACH,EAAE,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAA;YACnE,MAAM,UAAU,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,CAAA;QACnC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;YACrC,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,CAAA;YAC5G,MAAM,CAAC,CAAA;QACT,CAAC;gBAAS,CAAC;YACT,MAAM,EAAE,EAAE,KAAK,EAAE,CAAA;QACnB,CAAC;QAED,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YAClB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;YACrC,4FAA4F;YAC5F,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAE,CAAC,UAAU,KAAK,IAAI;gBAAE,OAAM;YACpF,EAAE,CAAC,OAAO,CAAC,wHAAwH,CAAC;iBACjI,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAA;YACzE,EAAE,CAAC,OAAO,CAAC,mHAAmH,CAAC;iBAC5H,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QACpD,CAAC,CAAC,EAAE,CAAA;QACJ,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,EAAE,CAAC,CAAA;IAChD,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,EAAyC,EAAE,GAAW,EAAE,MAAc;IACrG,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,OAAO,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACzB,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,CAAA;QACpF,IAAI,YAAY,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,MAAM,GAAG,IAAI,EAAE,CAAC,CAAA;QAChG,IAAI,IAAI,YAAY,CAAA;IACtB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAyB,EACzB,GAAwB,EACxB,MAA8B;IAE9B,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,EAAE,mBAAmB,CAAC,CAAA;IAC5D,IAAI,CAAC,GAAG;QAAE,OAAM;IAChB,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IACtC,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAA;IAC9E,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,cAAc,CAAC,CAAA;IACtD,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAA;IACpE,IAAI,IAAI,GAA4C,EAAE,CAAA;IACtD,IAAI,CAAC;QAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAgB,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,CAAC;IAC9F,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;IAC5B,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,OAAO,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,IAAI,OAAO,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC;QAC1H,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,8CAA8C,OAAO,EAAE,EAAE,CAAC,CAAA;IAC3F,CAAC;IACD,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC,CAAA;IAC5H,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IACrC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC,CAAA;IACtE,4GAA4G;IAC5G,KAAK,EAAE,CAAC,OAAO,CAAC;;;;GAIf,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAC1C,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;AAC9B,CAAC;AAED,+EAA+E;AAE/E,SAAS,UAAU,CAAC,GAAyB,EAAE,GAAwB,EAAE,MAA8B,EAAE,OAAmB;IAC1H,IAAI,CAAC,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IAC3C,kBAAkB,CAAC,OAAO,CAAC,CAAA;IAC3B,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAC9B,IAAI,CAAC,GAAG,EAAE,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC;QAAC,OAAO,IAAI,CAAA;IAAC,CAAC;IACrE,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IACtC,MAAM,KAAK,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAChE,IAAI,CAAC,KAAK,EAAE,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC,CAAC;QAAC,OAAO,IAAI,CAAA;IAAC,CAAC;IAChF,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;AACvB,CAAC;AAED,SAAS,WAAW,CAAC,KAAe,EAAE,GAAW,EAAE,KAAc;IAC/D,OAAO,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;AACpI,CAAC;AAYD,oIAAoI;AACpI,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,IAAI,GAAG,KAAK,EAAE,CAAC,OAAO,CAAC,iDAAiD,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAe,CAAA;IAC1G,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC5B,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;QAChC,IAAI,WAAW,GAAkB,IAAI,CAAA;QACrC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAA8B,CAAA;YACjE,IAAI,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ;gBAAE,WAAW,GAAG,IAAI,CAAC,WAAW,CAAA;QAC1E,CAAC;QAAC,MAAM,CAAC,CAAC,oDAAoD,CAAC,CAAC;QAChE,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,UAAU,KAAK,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAA;IAC/H,CAAC,CAAC,CAAC,CAAA;AACL,CAAC;AAED,mHAAmH;AACnH,MAAM,UAAU,gBAAgB,CAAC,GAAyB,EAAE,GAAwB,EAAE,MAA8B,EAAE,OAAmB;IACvI,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/C,IAAI,CAAC,CAAC;QAAE,OAAM;IACd,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAAA;IACxB,MAAM,MAAM,GAAG,KAAK,EAAE,CAAC,OAAO,CAAC;;GAE9B,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,CAA0F,CAAA;IACnH,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,EAAE,KAAK,CAAC,YAAY,IAAI,CAAC,CAAC,CAAC,CAAA;IACnE,MAAM,IAAI,GAAa,EAAE,CAAA;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjE,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IACpC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE;QACb,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAY;QAC5C,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,KAAK;QACL,SAAS,EAAE,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC;QACzC,OAAO,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI;QAClC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC,CAAC;QAChI,IAAI;KACL,CAAC,CAAA;AACJ,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,eAAe,CAAC,GAAyB,EAAE,GAAwB,EAAE,MAA8B,EAAE,OAAmB,EAAE,SAAiB;IACzJ,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/C,IAAI,CAAC,CAAC;QAAE,OAAM;IACd,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAAA;IACxB,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,CAAA;IACvF,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAA;IACxB,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,GAAG,GAAG,IAAI,GAAG,CAAC,CAAA;IAClB,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,CAAA;IAC/B,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3C,IAAI,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;YAC/B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,eAAe,EAAE,WAAW,IAAI,EAAE,EAAE,CAAC,CAAA;YAC1D,OAAO,KAAK,GAAG,CAAC,GAAG,EAAE,CAAA;QACvB,CAAC;QACD,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACpB,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,CAAA;QAC/D,IAAI,GAAG,GAAG,KAAK,EAAE,CAAC;YAChB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,eAAe,EAAE,WAAW,IAAI,EAAE,EAAE,CAAC,CAAA;YAC1D,OAAO,KAAK,GAAG,CAAC,GAAG,EAAE,CAAA;QACvB,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IACpC,MAAM,OAAO,GAA6B;QACxC,cAAc,EAAE,0BAA0B;QAC1C,wBAAwB,EAAE,SAAS;QACnC,qBAAqB,EAAE,YAAY;QACnC,eAAe,EAAE,OAAO;QACxB,2GAA2G;QAC3G,eAAe,EAAE,KAAK,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,UAAU;QAC7D,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxC,CAAA;IACD,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;QACf,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,GAAG,OAAO,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC,CAAA;QACvD,OAAO,KAAK,GAAG,CAAC,GAAG,EAAE,CAAA;IACvB,CAAC;IACD,MAAM,MAAM,GAAG,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;IACpF,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;QACvB,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE;YAC/B,GAAG,OAAO;YACV,gBAAgB,EAAE,GAAG,GAAG,KAAK,GAAG,CAAC;YACjC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,SAAS,KAAK,IAAI,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvE,CAAC,CAAA;QACF,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAClB,CAAC,CAAC,CAAA;IACF,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;QACzB,IAAI,GAAG,CAAC,WAAW;YAAE,OAAO,KAAK,GAAG,CAAC,OAAO,EAAE,CAAA;QAC9C,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,CAAA;QAC5G,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,+EAA+E;AAE/E;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB,EAAE,OAAmB;IACnE,kBAAkB,CAAC,OAAO,CAAC,CAAA;IAC3B,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,MAAM,EAAE,iBAAiB,EAAE,GAAG,aAAa,EAAE,CAAA;IAC7C,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;;GAM1B,CAAC,CAAC,GAAG,CAAC,IAAI,iBAAiB,OAAO,CAAyH,CAAA;IAC5J,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,uFAAuF,CAAC,CAAA;IACjH,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;QAClB,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC;gBAAE,SAAQ;YACvE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;YAC3B,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QACpB,CAAC;IACH,CAAC,CAAC,EAAE,CAAA;IACJ,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAA;IAEvE,IAAI,MAAM,GAAa,EAAE,CAAA;IACzB,iHAAiH;IACjH,kFAAkF;IAClF,IAAI,CAAC;QACH,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,CAAA;IACpC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAM;IAC5D,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAE,EAAE,CAAC,OAAO,CAAC,2DAA2D,CAAC,CAAC,GAAG,EAAyB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IAC9I,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAA;IACnG,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/B,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,+FAA+F,CAAC,CAAA;IAC3H,KAAK,MAAM,CAAC,IAAI,IAAI;QAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAC3D,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
/**
|
|
3
|
+
* What the relay knows about the client that holds a run. A run is live exactly while its holder's socket is
|
|
4
|
+
* open: the flow runner does not reconnect, so a closed socket is the end of that process's run.
|
|
5
|
+
*/
|
|
6
|
+
export interface RunHolders {
|
|
7
|
+
/** The owner keys of every open socket, built once per pass so a check costs a lookup, not a scan. */
|
|
8
|
+
connectedKeys(): Set<string>;
|
|
9
|
+
/** The session a socket with one of these keys holds now — the one a watch link should open — or null. */
|
|
10
|
+
heldSession(keys: string[]): string | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The owner keys the runner's socket can have (`ownerKeyFor` in `RelayServer`). **Two, because the socket
|
|
14
|
+
* and REST disagree on loopback**: the handshake does not read a PAT from a local connection, so a runner on
|
|
15
|
+
* the relay's own Mac — a common CI setup — holds its sessions as `anon:<client>`, while the same token on
|
|
16
|
+
* REST names its user. Matching the user form only left every such run unconnected and aborted it a minute
|
|
17
|
+
* in. The anonymous form is reachable only from loopback, and only with the client id, which nothing serves.
|
|
18
|
+
*/
|
|
19
|
+
export declare function holderKeys(createdBy: number | null, client: string): string[];
|
|
20
|
+
/** Where run screenshots live, alongside `uploads/` and `recordings/`. */
|
|
21
|
+
export declare function runScreenshotsDirFor(uploadsDir: string): string;
|
|
22
|
+
export interface RunRow {
|
|
23
|
+
seq: number;
|
|
24
|
+
id: string;
|
|
25
|
+
build_id: number | null;
|
|
26
|
+
holder_client: string;
|
|
27
|
+
created_by: number | null;
|
|
28
|
+
pat_id: number | null;
|
|
29
|
+
is_ci: number;
|
|
30
|
+
ci_provider: string | null;
|
|
31
|
+
ci_branch: string | null;
|
|
32
|
+
ci_commit: string | null;
|
|
33
|
+
ci_job_url: string | null;
|
|
34
|
+
status: 'running' | 'passed' | 'failed' | 'aborted';
|
|
35
|
+
finished_by: 'client' | 'relay' | null;
|
|
36
|
+
failure_kind: string | null;
|
|
37
|
+
error_message: string | null;
|
|
38
|
+
exit_code: number | null;
|
|
39
|
+
flows_json: string;
|
|
40
|
+
flows_total: number;
|
|
41
|
+
created_at: string;
|
|
42
|
+
last_activity_at: string;
|
|
43
|
+
finished_at: string | null;
|
|
44
|
+
}
|
|
45
|
+
export declare function findRun(id: string): RunRow | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* A socket with `socketKey` closed: the runs it held are over unless another socket of the same runner is
|
|
48
|
+
* still open. Called from the socket close handler, so it tolerates a relay started without a database
|
|
49
|
+
* (tests that never touch REST).
|
|
50
|
+
*/
|
|
51
|
+
export declare function abandonRunsOf(socketKey: string, holders: RunHolders): void;
|
|
52
|
+
/**
|
|
53
|
+
* Close every running run whose holder is gone and has been quiet past the grace. Read paths call this
|
|
54
|
+
* before answering, and the daily purge persists it, so no timer exists for it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function settleOrphanedRuns(holders: RunHolders): void;
|
|
57
|
+
/**
|
|
58
|
+
* The run, when this request may write to it: a `builds:write` credential whose owner may write, belonging
|
|
59
|
+
* to the user who created the run — and, when it was created with a PAT, that same PAT. A run id leaking
|
|
60
|
+
* into a CI log must not let another job, or another person's token, rewrite its result.
|
|
61
|
+
*/
|
|
62
|
+
export declare function ownRun(req: http.IncomingMessage, res: http.ServerResponse, id: string,
|
|
63
|
+
/** Seconds after the runner finished the run during which it may still write. Only video uses it: the last
|
|
64
|
+
* flow's tail is sent after `finish`, which goes first so a slow video never costs the run its own ending. */
|
|
65
|
+
graceAfterFinishSeconds?: number): RunRow | null;
|
|
66
|
+
/** Whether the run was finished no more than `seconds` ago. */
|
|
67
|
+
export declare function finishedWithin(run: Pick<RunRow, 'finished_at'>, seconds: number): boolean;
|
|
68
|
+
export declare function handleCreateRun(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
|
|
69
|
+
export declare function handleReportFlow(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>): Promise<void>;
|
|
70
|
+
export declare function handleUploadFlowScreenshot(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>, screenshotsDir: string): Promise<void>;
|
|
71
|
+
export declare function handleFinishRun(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>): Promise<void>;
|
|
72
|
+
export declare function handleListRuns(req: http.IncomingMessage, res: http.ServerResponse, holders: RunHolders): void;
|
|
73
|
+
export declare function handleGetRun(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>, holders: RunHolders, videosOf?: (run: RunRow) => Map<number, unknown>): void;
|
|
74
|
+
export declare function handleGetFlowScreenshot(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>, screenshotsDir: string): void;
|
|
75
|
+
/** Runs with no build, past their own expiry. A build's runs go with the build (`deleteBuildsWithDependents`). */
|
|
76
|
+
export declare function purgeExpiredRuns(screenshotsDir: string, videosDir: string, holders: RunHolders): void;
|
|
77
|
+
//# sourceMappingURL=runs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runs.d.ts","sourceRoot":"","sources":["../../src/api/runs.ts"],"names":[],"mappings":"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"}
|