@gentbajko/slopify 0.8.4 → 1.0.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/README.md +28 -6
- package/dist/adapters/llm/claude-code.js +6 -3
- package/dist/adapters/llm/codex.js +102 -12
- package/dist/adapters/llm/gemini.js +11 -8
- package/dist/adapters/llm/run-cli.js +61 -6
- package/dist/adapters/tts/inworld-async.js +4 -4
- package/dist/adapters/tts/inworld.js +73 -68
- package/dist/catalog/registry.js +13 -73
- package/dist/edge/cli.js +5 -2
- package/dist/edge/events/hub.js +7 -2
- package/dist/edge/events/preview-cache.js +7 -4
- package/dist/edge/events/visibility.js +33 -0
- package/dist/edge/http/actions.js +74 -79
- package/dist/edge/http/app.js +39 -6
- package/dist/edge/http/audio-preview.js +9 -1
- package/dist/edge/http/checkpoints.js +95 -0
- package/dist/edge/http/diagnostics.js +31 -0
- package/dist/edge/http/draft-files.js +69 -0
- package/dist/edge/http/draft-problem.js +15 -0
- package/dist/edge/http/drafts.js +95 -0
- package/dist/edge/http/multipart.js +86 -0
- package/dist/edge/http/mutations.js +49 -0
- package/dist/edge/http/planning.js +12 -1
- package/dist/edge/http/project-templates.js +74 -0
- package/dist/edge/http/projects.js +19 -2
- package/dist/edge/http/revision-files.js +84 -0
- package/dist/edge/http/revisions.js +103 -0
- package/dist/edge/http/schedules.js +100 -0
- package/dist/edge/http/staging.js +11 -52
- package/dist/edge/http/storage.js +110 -0
- package/dist/edge/http/subtitles.js +9 -79
- package/dist/edge/http/tutorial.js +26 -0
- package/dist/edge/signal-shutdown.js +23 -0
- package/dist/kernel/audio-preview.js +11 -5
- package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
- package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
- package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
- package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
- package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
- package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
- package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
- package/dist/kernel/ports/model.js +5 -0
- package/dist/kernel/runner/attempt-repo.js +15 -0
- package/dist/kernel/runner/attempt.js +14 -2
- package/dist/kernel/runner/checkpoint-authority.js +23 -0
- package/dist/kernel/runner/index.js +35 -11
- package/dist/kernel/runner/providers.js +18 -8
- package/dist/kernel/runner/work-attempt.js +47 -0
- package/dist/kernel/runner/work-authority.js +37 -0
- package/dist/kernel/runner/work.js +23 -0
- package/dist/main.js +227 -69
- package/dist/slices/admission/repo.js +5 -56
- package/dist/slices/admission/schema.js +64 -0
- package/dist/slices/admission/start.js +13 -4
- package/dist/slices/article/continuation.js +9 -3
- package/dist/slices/article/provided-entries.js +7 -3
- package/dist/slices/article/run.js +20 -7
- package/dist/slices/article/segments.js +13 -4
- package/dist/slices/batch/index.js +8 -11
- package/dist/slices/cancel/index.js +78 -34
- package/dist/slices/checkpoints/change.js +250 -0
- package/dist/slices/checkpoints/fingerprint.js +33 -0
- package/dist/slices/checkpoints/index.js +2 -0
- package/dist/slices/checkpoints/model.js +1 -0
- package/dist/slices/checkpoints/recovery.js +116 -0
- package/dist/slices/checkpoints/repo.js +132 -0
- package/dist/slices/checkpoints/rules.js +30 -0
- package/dist/slices/checkpoints/schema.js +59 -0
- package/dist/slices/control/index.js +88 -22
- package/dist/slices/control/providers.js +3 -2
- package/dist/slices/control/revision-control-schema.js +7 -0
- package/dist/slices/control/revision-control.js +59 -0
- package/dist/slices/estimate/index.js +77 -59
- package/dist/slices/estimate/requests.js +102 -0
- package/dist/slices/images/run.js +14 -3
- package/dist/slices/library/slots.js +11 -11
- package/dist/slices/library/snapshot.js +33 -0
- package/dist/slices/narration/legacy-plan.js +58 -0
- package/dist/slices/narration/live.js +2 -2
- package/dist/slices/narration/plan.js +48 -54
- package/dist/slices/narration/run.js +21 -8
- package/dist/slices/play-drafts/convert.js +120 -0
- package/dist/slices/play-drafts/model.js +1 -0
- package/dist/slices/play-drafts/readiness.js +80 -0
- package/dist/slices/play-drafts/repo.js +76 -0
- package/dist/slices/play-drafts/review-inputs.js +150 -0
- package/dist/slices/play-drafts/review.js +109 -0
- package/dist/slices/play-drafts/schema.js +218 -0
- package/dist/slices/play-drafts/service.js +242 -0
- package/dist/slices/play-drafts/start-repo.js +113 -0
- package/dist/slices/play-drafts/start.js +148 -0
- package/dist/slices/play-drafts/uploads.js +67 -0
- package/dist/slices/project-templates/from-project.js +159 -0
- package/dist/slices/project-templates/model.js +1 -0
- package/dist/slices/project-templates/repo.js +41 -0
- package/dist/slices/project-templates/schema.js +37 -0
- package/dist/slices/project-templates/service.js +136 -0
- package/dist/slices/project-templates/setup.js +55 -0
- package/dist/slices/rebuild/admission-repo.js +120 -0
- package/dist/slices/rebuild/dependencies.js +33 -0
- package/dist/slices/rebuild/model.js +94 -0
- package/dist/slices/rebuild/narration-history.js +43 -0
- package/dist/slices/rebuild/narration-reuse.js +20 -0
- package/dist/slices/rebuild/preview-details.js +144 -0
- package/dist/slices/rebuild/preview-plan.js +223 -0
- package/dist/slices/rebuild/preview-retained.js +128 -0
- package/dist/slices/rebuild/provided-review.js +32 -0
- package/dist/slices/rebuild/recipe-audio.js +172 -0
- package/dist/slices/rebuild/recipe-build.js +17 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
- package/dist/slices/rebuild/recipe-exports.js +65 -0
- package/dist/slices/rebuild/recipe-fixture.js +150 -0
- package/dist/slices/rebuild/recipe-input-schema.js +82 -0
- package/dist/slices/rebuild/recipe-legacy.js +135 -0
- package/dist/slices/rebuild/recipe-model.js +77 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
- package/dist/slices/rebuild/recipe-save.js +221 -0
- package/dist/slices/rebuild/recipe-text.js +170 -0
- package/dist/slices/rebuild/recipe-validation.js +86 -0
- package/dist/slices/rebuild/recipe-visual.js +111 -0
- package/dist/slices/rebuild/recipe-work.js +212 -0
- package/dist/slices/rebuild/recipes.js +4 -0
- package/dist/slices/rebuild/repo.js +64 -0
- package/dist/slices/rebuild/runtime-actions.js +76 -0
- package/dist/slices/rebuild/runtime-admission.js +71 -0
- package/dist/slices/rebuild/runtime-article.js +117 -0
- package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
- package/dist/slices/rebuild/runtime-export.js +139 -0
- package/dist/slices/rebuild/runtime-instructions.js +51 -0
- package/dist/slices/rebuild/runtime-local.js +137 -0
- package/dist/slices/rebuild/runtime-materialize.js +105 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
- package/dist/slices/rebuild/runtime-plan.js +103 -0
- package/dist/slices/rebuild/runtime-provider.js +184 -0
- package/dist/slices/rebuild/runtime-publication.js +172 -0
- package/dist/slices/rebuild/runtime-run.js +35 -0
- package/dist/slices/rebuild/runtime-store.js +166 -0
- package/dist/slices/rebuild/runtime-subtitles.js +202 -0
- package/dist/slices/rebuild/service-readiness.js +110 -0
- package/dist/slices/rebuild/service.js +149 -0
- package/dist/slices/rebuild/transition-repo.js +108 -0
- package/dist/slices/rebuild/work-records.js +44 -0
- package/dist/slices/reruns/index.js +5 -0
- package/dist/slices/research/run.js +33 -12
- package/dist/slices/revisions/adopt-assets.js +31 -0
- package/dist/slices/revisions/adopt-content.js +112 -0
- package/dist/slices/revisions/adopt.js +100 -0
- package/dist/slices/revisions/downloads.js +80 -0
- package/dist/slices/revisions/index.js +5 -0
- package/dist/slices/revisions/manifest-repo.js +125 -0
- package/dist/slices/revisions/model.js +1 -0
- package/dist/slices/revisions/mutation-assets.js +143 -0
- package/dist/slices/revisions/mutation-cues.js +95 -0
- package/dist/slices/revisions/mutation-prepare.js +182 -0
- package/dist/slices/revisions/mutation-request.js +58 -0
- package/dist/slices/revisions/mutation-work.js +47 -0
- package/dist/slices/revisions/mutations.js +194 -0
- package/dist/slices/revisions/projection.js +82 -0
- package/dist/slices/revisions/publication-model.js +1 -0
- package/dist/slices/revisions/publication-rules.js +166 -0
- package/dist/slices/revisions/publish.js +115 -0
- package/dist/slices/revisions/repo.js +83 -0
- package/dist/slices/revisions/restore.js +60 -0
- package/dist/slices/revisions/rules.js +99 -0
- package/dist/slices/revisions/schema.js +160 -0
- package/dist/slices/revisions/view.js +44 -0
- package/dist/slices/schedules/calendar.js +47 -0
- package/dist/slices/schedules/model.js +1 -0
- package/dist/slices/schedules/repo.js +281 -0
- package/dist/slices/schedules/scheduler.js +144 -0
- package/dist/slices/schedules/schema.js +80 -0
- package/dist/slices/schedules/service.js +178 -0
- package/dist/slices/settings/cli-paths.js +1 -1
- package/dist/slices/settings/cli-status.js +45 -5
- package/dist/slices/settings/tutorial-schema.js +39 -0
- package/dist/slices/settings/tutorial.js +66 -0
- package/dist/slices/storage/assets.js +108 -0
- package/dist/slices/storage/delete-project.js +2 -1
- package/dist/slices/storage/downloads.js +4 -1
- package/dist/slices/storage/portable.js +871 -0
- package/dist/slices/storage/prepare.js +67 -0
- package/dist/slices/storage/reconcile.js +25 -5
- package/dist/slices/storage/repo.js +2 -15
- package/dist/slices/storage/schema.js +35 -0
- package/dist/slices/storage/staging-refs.js +26 -0
- package/dist/slices/storage/staging.js +16 -11
- package/dist/slices/thumbnail/run.js +26 -9
- package/dist/slices/video/audio-export-args.js +33 -0
- package/dist/slices/video/audio-export.js +13 -36
- package/dist/slices/video/audio-inputs.js +4 -3
- package/dist/slices/video/ffmpeg.js +6 -0
- package/dist/slices/video/reuse-audio.js +11 -4
- package/dist/updater/install-flow.js +27 -2
- package/dist/updater/plan.js +28 -2
- package/dist/updater/service.js +27 -2
- package/dist/updater/worker.js +2 -1
- package/dist/web/assets/index-CO7DnVX9.js +137 -0
- package/dist/web/assets/index-DX1_chKs.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/adapters/tts/inworld-text.js +0 -32
- package/dist/web/assets/index-Ce2HApus.css +0 -1
- package/dist/web/assets/index-xbD0MZOY.js +0 -130
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { zValidator } from "@hono/zod-validator";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { findRevisionDownload, revisionImagesZip } from "../../slices/revisions/downloads.js";
|
|
8
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
9
|
+
const id = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(64)
|
|
13
|
+
.regex(/^[0-9A-Za-z_-]+$/);
|
|
14
|
+
const revisionParam = z.object({ projectId: id, revisionId: id });
|
|
15
|
+
const recordParam = revisionParam.extend({ recordId: id });
|
|
16
|
+
const folderParam = z.object({ id, revisionId: id, recordId: id });
|
|
17
|
+
export function revisionFileRoutes(deps) {
|
|
18
|
+
return new Hono()
|
|
19
|
+
.get("/files/:projectId/revisions/:revisionId/images.zip", zValidator("param", revisionParam, onInvalid), (c) => {
|
|
20
|
+
const { projectId, revisionId } = c.req.valid("param");
|
|
21
|
+
const result = revisionImagesZip(deps, projectId, revisionId);
|
|
22
|
+
if (!result.ok)
|
|
23
|
+
return unavailable(c, result.reason);
|
|
24
|
+
return c.body(result.bytes, 200, {
|
|
25
|
+
"content-type": "application/zip",
|
|
26
|
+
"content-length": String(result.bytes.byteLength),
|
|
27
|
+
"content-disposition": `attachment; filename="${result.filename}"`,
|
|
28
|
+
});
|
|
29
|
+
})
|
|
30
|
+
.get("/files/:projectId/revisions/:revisionId/:recordId", zValidator("param", recordParam, onInvalid), (c) => {
|
|
31
|
+
const { projectId, revisionId, recordId } = c.req.valid("param");
|
|
32
|
+
const result = findRevisionDownload(deps, projectId, revisionId, recordId);
|
|
33
|
+
if (!result.ok)
|
|
34
|
+
return unavailable(c, result.reason);
|
|
35
|
+
const value = result.download;
|
|
36
|
+
return c.body(Readable.toWeb(createReadStream(value.path)), 200, {
|
|
37
|
+
"content-type": value.contentType,
|
|
38
|
+
"content-length": String(value.bytes),
|
|
39
|
+
"content-disposition": `attachment; filename="${value.filename}"`,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function revisionFolderRoutes(deps) {
|
|
44
|
+
return new Hono().post("/:id/revisions/:revisionId/:recordId/open-folder", zValidator("param", folderParam, onInvalid), async (c) => {
|
|
45
|
+
const origin = c.req.header("origin");
|
|
46
|
+
if (origin !== undefined && origin !== new URL(c.req.url).origin)
|
|
47
|
+
return problem(c, {
|
|
48
|
+
status: 403,
|
|
49
|
+
title: titleOf(403),
|
|
50
|
+
detail: "Open folders from Slopify itself.",
|
|
51
|
+
});
|
|
52
|
+
const { id: projectId, revisionId, recordId } = c.req.valid("param");
|
|
53
|
+
const result = findRevisionDownload(deps, projectId, revisionId, recordId);
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
return unavailable(c, result.reason);
|
|
56
|
+
try {
|
|
57
|
+
if (deps.openFolder === undefined)
|
|
58
|
+
throw new Error("No folder opener configured");
|
|
59
|
+
await deps.openFolder(dirname(result.download.path));
|
|
60
|
+
return c.json({ opened: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
deps.log.write("warn", "project.open-folder", {
|
|
64
|
+
projectId,
|
|
65
|
+
detail: "The file manager could not be opened for a retained file.",
|
|
66
|
+
});
|
|
67
|
+
return problem(c, {
|
|
68
|
+
status: 503,
|
|
69
|
+
title: titleOf(503),
|
|
70
|
+
detail: "Could not open the file manager on the machine running Slopify. Make sure a desktop session is available.",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function unavailable(c, reason) {
|
|
76
|
+
return problem(c, {
|
|
77
|
+
status: 404,
|
|
78
|
+
title: titleOf(404),
|
|
79
|
+
detail: reason === "missing-file"
|
|
80
|
+
? "This retained file is no longer on disk. Review the affected rebuild to recreate it."
|
|
81
|
+
: "The retained file is unavailable.",
|
|
82
|
+
extensions: { reason },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { projectExists } from "../../slices/admission/repo.js";
|
|
5
|
+
import { previewRebuildSchema, startRebuildSchema } from "../../slices/rebuild/model.js";
|
|
6
|
+
import { previewRebuild, startRebuild } from "../../slices/rebuild/service.js";
|
|
7
|
+
import { ensureBaseline, restoreRevision, saveRevision } from "../../slices/revisions/index.js";
|
|
8
|
+
import { currentRevisionId, listRevisionHistory } from "../../slices/revisions/repo.js";
|
|
9
|
+
import { restoreRevisionSchema, saveRevisionSchema } from "../../slices/revisions/schema.js";
|
|
10
|
+
import { getRevisionView } from "../../slices/revisions/view.js";
|
|
11
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
12
|
+
const id = z
|
|
13
|
+
.string()
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(64)
|
|
16
|
+
.regex(/^[0-9A-Za-z_-]+$/);
|
|
17
|
+
const projectParam = z.object({ id });
|
|
18
|
+
const revisionParam = projectParam.extend({ revisionId: id });
|
|
19
|
+
const saveBody = saveRevisionSchema.extend({ idempotencyKey: z.uuid() });
|
|
20
|
+
const restoreBody = restoreRevisionSchema.extend({ idempotencyKey: z.uuid() });
|
|
21
|
+
const statuses = {
|
|
22
|
+
"no-project": 404,
|
|
23
|
+
"no-revision": 404,
|
|
24
|
+
conflict: 409,
|
|
25
|
+
"idempotency-conflict": 409,
|
|
26
|
+
"invalid-edit": 400,
|
|
27
|
+
"stale-preview": 409,
|
|
28
|
+
"invalid-selection": 400,
|
|
29
|
+
"review-required": 409,
|
|
30
|
+
"cost-ack-required": 409,
|
|
31
|
+
readiness: 409,
|
|
32
|
+
};
|
|
33
|
+
function refused(c, deps, projectId, value) {
|
|
34
|
+
const status = statuses[value.reason];
|
|
35
|
+
return problem(c, {
|
|
36
|
+
status,
|
|
37
|
+
title: titleOf(status),
|
|
38
|
+
detail: value.reason.replaceAll("-", " "),
|
|
39
|
+
extensions: {
|
|
40
|
+
reason: value.reason,
|
|
41
|
+
currentRevisionId: currentRevisionId(deps.db, projectId) ?? null,
|
|
42
|
+
fields: value.fields ?? [],
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function unavailable(c) {
|
|
47
|
+
return problem(c, {
|
|
48
|
+
status: 503,
|
|
49
|
+
title: titleOf(503),
|
|
50
|
+
detail: "Rebuild services are unavailable.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function revisionRoutes(deps) {
|
|
54
|
+
return new Hono()
|
|
55
|
+
.post("/:id/revisions/prepare", zValidator("param", projectParam, onInvalid), async (c) => {
|
|
56
|
+
const projectId = c.req.valid("param").id;
|
|
57
|
+
const result = await ensureBaseline(deps, projectId);
|
|
58
|
+
return result.ok ? c.json(result) : refused(c, deps, projectId, result);
|
|
59
|
+
})
|
|
60
|
+
.get("/:id/revisions", zValidator("param", projectParam, onInvalid), (c) => {
|
|
61
|
+
const projectId = c.req.valid("param").id;
|
|
62
|
+
return projectExists(deps.db, projectId)
|
|
63
|
+
? c.json({ revisions: listRevisionHistory(deps.db, projectId) })
|
|
64
|
+
: refused(c, deps, projectId, { reason: "no-project" });
|
|
65
|
+
})
|
|
66
|
+
.get("/:id/revisions/:revisionId", zValidator("param", revisionParam, onInvalid), (c) => {
|
|
67
|
+
const { id: projectId, revisionId } = c.req.valid("param");
|
|
68
|
+
const view = getRevisionView(deps, projectId, revisionId);
|
|
69
|
+
return view === undefined
|
|
70
|
+
? refused(c, deps, projectId, { reason: "no-revision" })
|
|
71
|
+
: c.json({ view });
|
|
72
|
+
})
|
|
73
|
+
.post("/:id/revisions", zValidator("param", projectParam, onInvalid), zValidator("json", saveBody, onInvalid), async (c) => {
|
|
74
|
+
const projectId = c.req.valid("param").id;
|
|
75
|
+
const result = await saveRevision(deps, { projectId, ...c.req.valid("json") });
|
|
76
|
+
if (!result.ok)
|
|
77
|
+
return refused(c, deps, projectId, result);
|
|
78
|
+
deps.hub.emit(projectId, { type: "project.updated", projectId });
|
|
79
|
+
return c.json(result);
|
|
80
|
+
})
|
|
81
|
+
.post("/:id/revisions/restore", zValidator("param", projectParam, onInvalid), zValidator("json", restoreBody, onInvalid), async (c) => {
|
|
82
|
+
const projectId = c.req.valid("param").id;
|
|
83
|
+
const result = await restoreRevision(deps, { projectId, ...c.req.valid("json") });
|
|
84
|
+
if (!result.ok)
|
|
85
|
+
return refused(c, deps, projectId, result);
|
|
86
|
+
deps.hub.emit(projectId, { type: "project.updated", projectId });
|
|
87
|
+
return c.json(result);
|
|
88
|
+
})
|
|
89
|
+
.post("/:id/rebuild/preview", zValidator("param", projectParam, onInvalid), zValidator("json", previewRebuildSchema, onInvalid), async (c) => {
|
|
90
|
+
if (deps.rebuild === undefined)
|
|
91
|
+
return unavailable(c);
|
|
92
|
+
const projectId = c.req.valid("param").id;
|
|
93
|
+
const result = await previewRebuild(deps.rebuild, { projectId, ...c.req.valid("json") });
|
|
94
|
+
return result.ok ? c.json(result) : refused(c, deps, projectId, result);
|
|
95
|
+
})
|
|
96
|
+
.post("/:id/rebuild", zValidator("param", projectParam, onInvalid), zValidator("json", startRebuildSchema, onInvalid), async (c) => {
|
|
97
|
+
if (deps.rebuild === undefined)
|
|
98
|
+
return unavailable(c);
|
|
99
|
+
const projectId = c.req.valid("param").id;
|
|
100
|
+
const result = await startRebuild(deps.rebuild, { projectId, ...c.req.valid("json") });
|
|
101
|
+
return result.ok ? c.json(result, 202) : refused(c, deps, projectId, result);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { scheduleCreateSchema, scheduleRunSchema, scheduleSummarySchema, scheduleUpdateSchema, } from "../../slices/schedules/schema.js";
|
|
6
|
+
import { cancelSchedule, createSchedule, deleteSchedule, listScheduleRuns, listSchedules, pauseSchedule, resumeSchedule, updateSchedule, } from "../../slices/schedules/service.js";
|
|
7
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
8
|
+
const id = z.object({ id: z.uuid() });
|
|
9
|
+
function refused(c, result) {
|
|
10
|
+
const status = ["not-found", "missing-template"].includes(result.reason)
|
|
11
|
+
? 404
|
|
12
|
+
: ["conflict", "cancel-required", "spend-limit", "unsupported-media"].includes(result.reason)
|
|
13
|
+
? 409
|
|
14
|
+
: 400;
|
|
15
|
+
const detail = {
|
|
16
|
+
"invalid-input": "Check the schedule fields and try again.",
|
|
17
|
+
"not-found": "The schedule was not found.",
|
|
18
|
+
conflict: "The schedule changed elsewhere. Reload it before trying again.",
|
|
19
|
+
"cancel-required": "Cancel this schedule before deleting it. Completed schedules can be deleted directly.",
|
|
20
|
+
"missing-template": "The selected template version is no longer available.",
|
|
21
|
+
"unsupported-media": "This template contains provided media and cannot run unattended.",
|
|
22
|
+
"not-due": "Choose a future one-off time.",
|
|
23
|
+
"spend-limit": "The estimate exceeds the schedule spend limit or contains unknown pricing.",
|
|
24
|
+
readiness: "Provider or local readiness checks refused this scheduled run.",
|
|
25
|
+
};
|
|
26
|
+
return problem(c, {
|
|
27
|
+
status,
|
|
28
|
+
title: titleOf(status),
|
|
29
|
+
detail: detail[result.reason],
|
|
30
|
+
extensions: { reason: result.reason },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function scheduleRoutes(deps) {
|
|
34
|
+
return routes(deps);
|
|
35
|
+
}
|
|
36
|
+
function routes(deps) {
|
|
37
|
+
const service = () => {
|
|
38
|
+
if (deps === undefined)
|
|
39
|
+
throw new HTTPException(404);
|
|
40
|
+
return deps;
|
|
41
|
+
};
|
|
42
|
+
return new Hono()
|
|
43
|
+
.get("/", (c) => c.json({
|
|
44
|
+
schedules: listSchedules(service()).map((schedule) => scheduleSummarySchema.parse(schedule)),
|
|
45
|
+
}))
|
|
46
|
+
.post("/", zValidator("json", scheduleCreateSchema, onInvalid), (c) => {
|
|
47
|
+
const result = createSchedule(service(), c.req.valid("json"));
|
|
48
|
+
return result.ok
|
|
49
|
+
? c.json(scheduleSummarySchema.parse(result.value), 201)
|
|
50
|
+
: refused(c, result);
|
|
51
|
+
})
|
|
52
|
+
.get("/:id", zValidator("param", id, onInvalid), (c) => {
|
|
53
|
+
const input = c.req.valid("param");
|
|
54
|
+
const schedule = listSchedules(service()).find((item) => item.id === input.id);
|
|
55
|
+
if (!schedule)
|
|
56
|
+
return refused(c, { ok: false, reason: "not-found" });
|
|
57
|
+
const runs = listScheduleRuns(service(), input.id);
|
|
58
|
+
return runs.ok
|
|
59
|
+
? c.json({
|
|
60
|
+
schedule: scheduleSummarySchema.parse(schedule),
|
|
61
|
+
runs: runs.value.map((run) => scheduleRunSchema.parse(run)),
|
|
62
|
+
})
|
|
63
|
+
: refused(c, runs);
|
|
64
|
+
})
|
|
65
|
+
.put("/:id", zValidator("param", id, onInvalid), zValidator("json", scheduleUpdateSchema.unwrap().omit({ id: true }), onInvalid), (c) => {
|
|
66
|
+
const result = updateSchedule(service(), {
|
|
67
|
+
...c.req.valid("json"),
|
|
68
|
+
id: c.req.valid("param").id,
|
|
69
|
+
});
|
|
70
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
71
|
+
})
|
|
72
|
+
.post("/:id/pause", zValidator("param", id, onInvalid), zValidator("json", z.object({ baseVersion: z.number().int().positive() }).strict(), onInvalid), (c) => {
|
|
73
|
+
const result = pauseSchedule(service(), {
|
|
74
|
+
...c.req.valid("json"),
|
|
75
|
+
id: c.req.valid("param").id,
|
|
76
|
+
});
|
|
77
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
78
|
+
})
|
|
79
|
+
.post("/:id/resume", zValidator("param", id, onInvalid), zValidator("json", z.object({ baseVersion: z.number().int().positive() }).strict(), onInvalid), (c) => {
|
|
80
|
+
const result = resumeSchedule(service(), {
|
|
81
|
+
...c.req.valid("json"),
|
|
82
|
+
id: c.req.valid("param").id,
|
|
83
|
+
});
|
|
84
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
85
|
+
})
|
|
86
|
+
.post("/:id/cancel", zValidator("param", id, onInvalid), zValidator("json", z.object({ baseVersion: z.number().int().positive() }).strict(), onInvalid), (c) => {
|
|
87
|
+
const result = cancelSchedule(service(), {
|
|
88
|
+
...c.req.valid("json"),
|
|
89
|
+
id: c.req.valid("param").id,
|
|
90
|
+
});
|
|
91
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
92
|
+
})
|
|
93
|
+
.delete("/:id", zValidator("param", id, onInvalid), zValidator("json", z.object({ baseVersion: z.number().int().positive() }).strict(), onInvalid), (c) => {
|
|
94
|
+
const result = deleteSchedule(service(), {
|
|
95
|
+
...c.req.valid("json"),
|
|
96
|
+
id: c.req.valid("param").id,
|
|
97
|
+
});
|
|
98
|
+
return result.ok ? c.json(result.value) : refused(c, result);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { Readable } from "node:stream";
|
|
2
|
-
import { Busboy } from "@fastify/busboy";
|
|
3
1
|
import { zValidator } from "@hono/zod-validator";
|
|
4
2
|
import { Hono } from "hono";
|
|
5
3
|
import { z } from "zod";
|
|
6
4
|
import { uploadableStageKinds } from "../../slices/storage/model.js";
|
|
7
5
|
import { stagedFiles } from "../../slices/storage/repo.js";
|
|
8
6
|
import { discardStagedFile, stageUpload } from "../../slices/storage/staging.js";
|
|
7
|
+
import { readMultipart } from "./multipart.js";
|
|
9
8
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
10
9
|
const kindParam = z.object({ kind: z.enum(uploadableStageKinds) });
|
|
11
10
|
const idParam = z.object({ id: z.string().min(1).max(64) });
|
|
@@ -26,23 +25,11 @@ export function stagingRoutes(deps) {
|
|
|
26
25
|
return new Hono()
|
|
27
26
|
.get("/", (c) => c.json({ files: stagedFiles(deps.db) }))
|
|
28
27
|
.post("/:kind", zValidator("param", kindParam, onInvalid), async (c) => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
detail: "An upload is sent as multipart/form-data with the file in one part.",
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
const body = c.req.raw.body;
|
|
38
|
-
if (body === null) {
|
|
39
|
-
return problem(c, {
|
|
40
|
-
status: 400,
|
|
41
|
-
title: titleOf(400),
|
|
42
|
-
detail: "The request carried no body.",
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
const result = await readUpload(storage, c.req.valid("param").kind, contentType, body);
|
|
28
|
+
const result = await readMultipart(c.req.raw, (content, originalFilename) => stageUpload(storage, {
|
|
29
|
+
stageKind: c.req.valid("param").kind,
|
|
30
|
+
originalFilename,
|
|
31
|
+
content,
|
|
32
|
+
}));
|
|
46
33
|
if (result === undefined) {
|
|
47
34
|
return problem(c, {
|
|
48
35
|
status: 400,
|
|
@@ -59,44 +46,16 @@ export function stagingRoutes(deps) {
|
|
|
59
46
|
const result = discardStagedFile(storage, c.req.valid("param").id);
|
|
60
47
|
if (!result.ok) {
|
|
61
48
|
return problem(c, {
|
|
62
|
-
status: 404,
|
|
63
|
-
title: titleOf(404),
|
|
64
|
-
detail:
|
|
49
|
+
status: result.reason === "in-use" ? 409 : 404,
|
|
50
|
+
title: titleOf(result.reason === "in-use" ? 409 : 404),
|
|
51
|
+
detail: result.reason === "in-use"
|
|
52
|
+
? "This file is used by a saved draft."
|
|
53
|
+
: "No staged file has that id.",
|
|
65
54
|
});
|
|
66
55
|
}
|
|
67
56
|
return c.body(null, 204);
|
|
68
57
|
});
|
|
69
58
|
}
|
|
70
|
-
// The bytes go from the socket to the parser to the disk. Nothing collects them: the
|
|
71
|
-
// platform's own formData() materialises a whole part in memory first, which an
|
|
72
|
-
// uncapped audio or video upload cannot afford.
|
|
73
|
-
function readUpload(storage, stageKind, contentType, body) {
|
|
74
|
-
return new Promise((resolve, reject) => {
|
|
75
|
-
const parser = new Busboy({
|
|
76
|
-
headers: { "content-type": contentType },
|
|
77
|
-
limits: { files: 1, fields: 8 },
|
|
78
|
-
// The parser hands the name through untouched: silently renaming a file the user
|
|
79
|
-
// picked would record a name nobody chose, so the slice rejects instead.
|
|
80
|
-
preservePath: true,
|
|
81
|
-
});
|
|
82
|
-
let started = false;
|
|
83
|
-
parser.on("file", (_field, stream, filename) => {
|
|
84
|
-
started = true;
|
|
85
|
-
stageUpload(storage, {
|
|
86
|
-
stageKind,
|
|
87
|
-
originalFilename: typeof filename === "string" ? filename : "",
|
|
88
|
-
content: stream,
|
|
89
|
-
}).then(resolve, reject);
|
|
90
|
-
});
|
|
91
|
-
parser.on("error", reject);
|
|
92
|
-
parser.on("finish", () => {
|
|
93
|
-
if (!started) {
|
|
94
|
-
resolve(undefined);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
Readable.fromWeb(body).pipe(parser);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
59
|
function detailOf(reason) {
|
|
101
60
|
return reason === "empty-file"
|
|
102
61
|
? "The uploaded file is empty."
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { exportPortable, importPortable, portableMaxArchiveBytes, storageUsage, } from "../../slices/storage/portable.js";
|
|
3
|
+
import { reconcileStorage } from "../../slices/storage/reconcile.js";
|
|
4
|
+
import { problem, titleOf } from "./problem.js";
|
|
5
|
+
export function storageRoutes(deps) {
|
|
6
|
+
return new Hono()
|
|
7
|
+
.get("/", (c) => c.json(storageUsage({ db: deps.db, paths: deps.paths })))
|
|
8
|
+
.get("/export", (c) => {
|
|
9
|
+
try {
|
|
10
|
+
const bytes = exportPortable({
|
|
11
|
+
db: deps.db,
|
|
12
|
+
paths: deps.paths,
|
|
13
|
+
ids: deps.ids,
|
|
14
|
+
now: () => deps.clock.now().toISOString(),
|
|
15
|
+
});
|
|
16
|
+
return c.body(bytes, 200, {
|
|
17
|
+
"content-type": "application/zip",
|
|
18
|
+
"content-length": String(bytes.byteLength),
|
|
19
|
+
"content-disposition": 'attachment; filename="slopify-backup.zip"',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return problem(c, {
|
|
24
|
+
status: 500,
|
|
25
|
+
title: titleOf(500),
|
|
26
|
+
detail: "The backup could not be created.",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.put("/import", async (c) => {
|
|
31
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
32
|
+
if (!contentType.includes("application/zip"))
|
|
33
|
+
return problem(c, {
|
|
34
|
+
status: 415,
|
|
35
|
+
title: titleOf(415),
|
|
36
|
+
detail: "Upload a Slopify backup ZIP.",
|
|
37
|
+
});
|
|
38
|
+
const upload = await readPortableUpload(c.req.raw);
|
|
39
|
+
if (!upload.ok)
|
|
40
|
+
return problem(c, {
|
|
41
|
+
status: 413,
|
|
42
|
+
title: titleOf(413),
|
|
43
|
+
detail: "The backup must be between 1 byte and 100 MB.",
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
const imported = importPortable({
|
|
47
|
+
db: deps.db,
|
|
48
|
+
paths: deps.paths,
|
|
49
|
+
ids: deps.ids,
|
|
50
|
+
now: () => deps.clock.now().toISOString(),
|
|
51
|
+
}, upload.bytes);
|
|
52
|
+
return c.json(imported);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return problem(c, {
|
|
56
|
+
status: 400,
|
|
57
|
+
title: titleOf(400),
|
|
58
|
+
detail: "The backup is invalid or incomplete.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.post("/cleanup", (c) => c.json(reconcileStorage(deps.db, deps.paths)));
|
|
63
|
+
}
|
|
64
|
+
async function readPortableUpload(request) {
|
|
65
|
+
const declared = request.headers.get("content-length");
|
|
66
|
+
if (declared !== null && /^\d+$/.test(declared) && Number(declared) > portableMaxArchiveBytes) {
|
|
67
|
+
try {
|
|
68
|
+
await request.body?.cancel();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// The request is already rejected; cancellation only releases transport resources sooner.
|
|
72
|
+
}
|
|
73
|
+
return { ok: false };
|
|
74
|
+
}
|
|
75
|
+
if (request.body === null)
|
|
76
|
+
return { ok: false };
|
|
77
|
+
const reader = request.body.getReader();
|
|
78
|
+
const chunks = [];
|
|
79
|
+
let total = 0;
|
|
80
|
+
try {
|
|
81
|
+
while (true) {
|
|
82
|
+
const next = await reader.read();
|
|
83
|
+
if (next.done)
|
|
84
|
+
break;
|
|
85
|
+
if (next.value.byteLength > portableMaxArchiveBytes - total) {
|
|
86
|
+
try {
|
|
87
|
+
await reader.cancel();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// The response is already decided; an upload transport failing cancellation is harmless.
|
|
91
|
+
}
|
|
92
|
+
return { ok: false };
|
|
93
|
+
}
|
|
94
|
+
total += next.value.byteLength;
|
|
95
|
+
chunks.push(next.value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
reader.releaseLock();
|
|
100
|
+
}
|
|
101
|
+
if (total === 0)
|
|
102
|
+
return { ok: false };
|
|
103
|
+
const bytes = new Uint8Array(total);
|
|
104
|
+
let offset = 0;
|
|
105
|
+
for (const chunk of chunks) {
|
|
106
|
+
bytes.set(chunk, offset);
|
|
107
|
+
offset += chunk.byteLength;
|
|
108
|
+
}
|
|
109
|
+
return { ok: true, bytes };
|
|
110
|
+
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { zValidator } from "@hono/zod-validator";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { transact } from "../../kernel/db/tx.js";
|
|
5
|
-
import { derive } from "../../kernel/runner/graph.js";
|
|
6
|
-
import { projectById, resetStage, stagesOf, updateProjectConfig, } from "../../slices/admission/repo.js";
|
|
7
|
-
import { withProjectControl } from "../../slices/control/lock.js";
|
|
8
|
-
import { resolveFont } from "../../slices/fonts/index.js";
|
|
9
|
-
import { outputsOf } from "../../slices/storage/repo.js";
|
|
10
|
-
import { subtitleConfigSchema } from "../../slices/subtitles/model.js";
|
|
11
4
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
12
5
|
const idParam = z.object({
|
|
13
6
|
id: z
|
|
@@ -16,77 +9,14 @@ const idParam = z.object({
|
|
|
16
9
|
.max(64)
|
|
17
10
|
.regex(/^[0-9A-Za-z_-]+$/),
|
|
18
11
|
});
|
|
19
|
-
export function subtitleRoutes(
|
|
20
|
-
return new Hono().patch("/:id/subtitles", zValidator("param", idParam, onInvalid),
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
detail: "No project has that id.",
|
|
29
|
-
});
|
|
30
|
-
const stages = stagesOf(deps.db, id);
|
|
31
|
-
if (stages.some((stage) => stage.state === "running") || deps.runner.hasInflight?.(id))
|
|
32
|
-
return problem(c, {
|
|
33
|
-
status: 409,
|
|
34
|
-
title: titleOf(409),
|
|
35
|
-
detail: "Pause the run and wait for its active work to stop before changing subtitles.",
|
|
36
|
-
});
|
|
37
|
-
const chosen = c.req.valid("json");
|
|
38
|
-
if (chosen.mode !== "off" && project.config.sources.audio === "off")
|
|
39
|
-
return problem(c, {
|
|
40
|
-
status: 400,
|
|
41
|
-
title: titleOf(400),
|
|
42
|
-
detail: "Subtitles need narration audio.",
|
|
43
|
-
});
|
|
44
|
-
const subtitles = project.config.sources.video === "off" && chosen.mode === "burn-in"
|
|
45
|
-
? { ...chosen, mode: "files" }
|
|
46
|
-
: chosen;
|
|
47
|
-
if (subtitles.mode !== "off" && subtitles.fontId !== project.config.subtitles?.fontId) {
|
|
48
|
-
try {
|
|
49
|
-
await resolveFont(deps.paths, subtitles.fontId);
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return problem(c, {
|
|
53
|
-
status: 400,
|
|
54
|
-
title: titleOf(400),
|
|
55
|
-
detail: "That font is no longer available. Choose another font or upload it again.",
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// Font discovery awaits filesystem work; a pending runner may have claimed a
|
|
60
|
-
// stage during that time. Recheck synchronously immediately before the write.
|
|
61
|
-
if (stagesOf(deps.db, id).some((stage) => stage.state === "running") ||
|
|
62
|
-
deps.runner.hasInflight?.(id)) {
|
|
63
|
-
return problem(c, {
|
|
64
|
-
status: 409,
|
|
65
|
-
title: titleOf(409),
|
|
66
|
-
detail: "Pause the run before changing subtitles.",
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
const video = stages.find((stage) => stage.kind === "video");
|
|
70
|
-
transact(deps.db, () => {
|
|
71
|
-
updateProjectConfig(deps.db, id, { ...project.config, subtitles }, deps.clock.now().toISOString());
|
|
72
|
-
// Only the local final export is invalidated. Existing provider outputs and
|
|
73
|
-
// the last finished export remain intact until its replacement succeeds.
|
|
74
|
-
if (video !== undefined &&
|
|
75
|
-
(project.config.sources.audio !== "off" || project.config.sources.video !== "off"))
|
|
76
|
-
resetStage(deps.db, video.id);
|
|
77
|
-
});
|
|
78
|
-
deps.hub.emit(id, { type: "project.updated", projectId: id });
|
|
79
|
-
if (project.paused !== true)
|
|
80
|
-
deps.runner.tick(id);
|
|
81
|
-
const current = stagesOf(deps.db, id);
|
|
82
|
-
return c.json({
|
|
83
|
-
project: {
|
|
84
|
-
...projectById(deps.db, id),
|
|
85
|
-
status: derive(current, project.paused === true),
|
|
86
|
-
},
|
|
87
|
-
stages: current,
|
|
88
|
-
outputs: outputsOf(deps.db, id),
|
|
89
|
-
});
|
|
90
|
-
});
|
|
12
|
+
export function subtitleRoutes(_deps) {
|
|
13
|
+
return new Hono().patch("/:id/subtitles", zValidator("param", idParam, onInvalid), revisionRequired);
|
|
14
|
+
}
|
|
15
|
+
function revisionRequired(c) {
|
|
16
|
+
return problem(c, {
|
|
17
|
+
status: 409,
|
|
18
|
+
title: titleOf(409),
|
|
19
|
+
detail: "Open Edit project to save changes, then review the affected rebuild.",
|
|
20
|
+
extensions: { reason: "revision-required" },
|
|
91
21
|
});
|
|
92
22
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { readTutorial, resetTutorial, saveTutorial, tutorialWriteSchema, } from "../../slices/settings/tutorial.js";
|
|
4
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
5
|
+
// Preserve Hono's inferred route types for the web client.
|
|
6
|
+
export function tutorialRoutes(deps) {
|
|
7
|
+
return new Hono()
|
|
8
|
+
.get("/", (c) => c.json(readTutorial(deps.db)))
|
|
9
|
+
.put("/", zValidator("json", tutorialWriteSchema, onInvalid), (c) => {
|
|
10
|
+
const result = saveTutorial(deps.db, c.req.valid("json"));
|
|
11
|
+
if (!result.ok)
|
|
12
|
+
return problem(c, {
|
|
13
|
+
status: 409,
|
|
14
|
+
title: titleOf(409),
|
|
15
|
+
detail: result.reason === "unreadable"
|
|
16
|
+
? "Saved tutorial progress cannot be read. Choose Restart tutorial to reset it."
|
|
17
|
+
: "Tutorial progress changed. Reload the saved progress before trying again.",
|
|
18
|
+
extensions: { reason: result.reason },
|
|
19
|
+
});
|
|
20
|
+
return c.json(result.value);
|
|
21
|
+
})
|
|
22
|
+
.delete("/", (c) => {
|
|
23
|
+
resetTutorial(deps.db);
|
|
24
|
+
return c.body(null, 204);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function installSignalShutdown(deps) {
|
|
2
|
+
let stopping;
|
|
3
|
+
let forced = false;
|
|
4
|
+
const shutdown = () => {
|
|
5
|
+
// A second explicit signal is the operator's escape hatch if an external resource
|
|
6
|
+
// ignores every graceful cancellation path.
|
|
7
|
+
if (stopping !== undefined) {
|
|
8
|
+
forced = true;
|
|
9
|
+
deps.exit(1);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
stopping = Promise.resolve().then(deps.stop);
|
|
13
|
+
void stopping.then(() => {
|
|
14
|
+
if (!forced)
|
|
15
|
+
deps.exit(0);
|
|
16
|
+
}, () => {
|
|
17
|
+
if (!forced)
|
|
18
|
+
deps.exit(1);
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
deps.on("SIGINT", shutdown);
|
|
22
|
+
deps.on("SIGTERM", shutdown);
|
|
23
|
+
}
|