@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
|
@@ -2,6 +2,9 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { attempt } from "./attempt.js";
|
|
3
3
|
export function stageProviders(deps, context, pieceId) {
|
|
4
4
|
const ctx = {
|
|
5
|
+
work: context.work,
|
|
6
|
+
maySubmit: () => context.maySubmit(pieceId),
|
|
7
|
+
...(pieceId === undefined ? {} : { workPieceId: pieceId }),
|
|
5
8
|
clock: deps.clock,
|
|
6
9
|
log: deps.log,
|
|
7
10
|
attempts: deps.attempts,
|
|
@@ -20,6 +23,7 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
20
23
|
const callId = randomUUID();
|
|
21
24
|
const preview = (text, reset) => context.emit({
|
|
22
25
|
type: "llm.preview",
|
|
26
|
+
...(pieceId === undefined ? {} : { workPieceId: pieceId }),
|
|
23
27
|
projectId: context.stage.projectId,
|
|
24
28
|
stage: context.stage.kind,
|
|
25
29
|
callId,
|
|
@@ -34,6 +38,7 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
34
38
|
let finishReason = null;
|
|
35
39
|
for await (const event of port.complete({
|
|
36
40
|
model: call.model,
|
|
41
|
+
...(call.thinkingConfig === undefined ? {} : { thinkingConfig: call.thinkingConfig }),
|
|
37
42
|
...(call.thinking === undefined ? {} : { thinking: call.thinking }),
|
|
38
43
|
messages: call.messages,
|
|
39
44
|
...(call.webSearch === undefined ? {} : { webSearch: call.webSearch }),
|
|
@@ -64,7 +69,17 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
64
69
|
},
|
|
65
70
|
tts: (call, observe) => {
|
|
66
71
|
const port = deps.registry.tts(call.provider);
|
|
67
|
-
let
|
|
72
|
+
let transientContinuation;
|
|
73
|
+
const continuation = {
|
|
74
|
+
read: () => pieceId === undefined
|
|
75
|
+
? transientContinuation
|
|
76
|
+
: (deps.attempts.readContinuation?.(context.work, pieceId) ?? transientContinuation),
|
|
77
|
+
write: (token) => {
|
|
78
|
+
if (pieceId !== undefined)
|
|
79
|
+
deps.attempts.writeContinuation?.(context.work, pieceId, token);
|
|
80
|
+
transientContinuation = token;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
68
83
|
const notify = (event) => {
|
|
69
84
|
try {
|
|
70
85
|
observe?.(event);
|
|
@@ -78,7 +93,7 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
78
93
|
});
|
|
79
94
|
}
|
|
80
95
|
};
|
|
81
|
-
return schedule(call.provider, () => attempt(ctx, async (signal, progress) => {
|
|
96
|
+
return schedule(call.provider, () => attempt({ ...ctx, continuation }, async (signal, progress) => {
|
|
82
97
|
notify({ type: "start" });
|
|
83
98
|
try {
|
|
84
99
|
const spoken = await port.synthesize({
|
|
@@ -87,12 +102,7 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
87
102
|
text: call.text,
|
|
88
103
|
signal,
|
|
89
104
|
onActivity: progress,
|
|
90
|
-
continuation
|
|
91
|
-
read: () => continuation,
|
|
92
|
-
write: (token) => {
|
|
93
|
-
continuation = token;
|
|
94
|
-
},
|
|
95
|
-
},
|
|
105
|
+
continuation,
|
|
96
106
|
});
|
|
97
107
|
const reader = spoken.audio.getReader();
|
|
98
108
|
const chunks = [];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { transact } from "../db/tx.js";
|
|
2
|
+
import { maySubmit, ownsCurrentWork, workExists } from "./work-authority.js";
|
|
3
|
+
export function startWorkAttempt(db, ids, input, work, pieceId, operation = "submit") {
|
|
4
|
+
return transact(db, () => {
|
|
5
|
+
if (input.stageId !== work.stageId || !workExists(db, work))
|
|
6
|
+
return { ok: false, reason: "held" };
|
|
7
|
+
if (operation === "retrieve") {
|
|
8
|
+
if (pieceId === null || readWorkContinuation(db, work, pieceId) === null)
|
|
9
|
+
return { ok: false, reason: "held" };
|
|
10
|
+
const submitted = db
|
|
11
|
+
.prepare("SELECT id FROM attempts WHERE work_id=? AND work_piece_id=? ORDER BY rowid DESC LIMIT 1")
|
|
12
|
+
.get(work.workId, pieceId);
|
|
13
|
+
return typeof submitted?.id === "string"
|
|
14
|
+
? { ok: true, value: submitted.id }
|
|
15
|
+
: { ok: false, reason: "held" };
|
|
16
|
+
}
|
|
17
|
+
else if (!maySubmit(db, work, pieceId ?? undefined) ||
|
|
18
|
+
(pieceId !== null && readWorkContinuation(db, work, pieceId) !== null))
|
|
19
|
+
return { ok: false, reason: "held" };
|
|
20
|
+
const id = ids.next();
|
|
21
|
+
db.prepare(`INSERT INTO attempts(id,stage_id,piece_id,n,started_at,revision_id,work_id,work_piece_id) VALUES (?,?,?,?,?,?,?,?)`).run(id, work.stageId, null, input.n, input.startedAt, work.revisionId, work.workId, pieceId);
|
|
22
|
+
if (operation === "submit") {
|
|
23
|
+
if (pieceId !== null)
|
|
24
|
+
db.prepare(`UPDATE revision_work_pieces SET submitted_at=COALESCE(submitted_at,?),state='running' WHERE id=? AND work_id=?`).run(input.startedAt, pieceId, work.workId);
|
|
25
|
+
if (ownsCurrentWork(db, work, pieceId))
|
|
26
|
+
db.prepare("UPDATE stages SET attempt_count=? WHERE id=?").run(input.n, work.stageId);
|
|
27
|
+
}
|
|
28
|
+
return { ok: true, value: id };
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function readWorkContinuation(db, work, pieceId) {
|
|
32
|
+
if (!workExists(db, work))
|
|
33
|
+
return null;
|
|
34
|
+
const row = db
|
|
35
|
+
.prepare("SELECT continuation FROM revision_work_pieces WHERE id=? AND work_id=? AND submitted_at IS NOT NULL")
|
|
36
|
+
.get(pieceId, work.workId);
|
|
37
|
+
// A malformed persisted token still records an accepted operation. Let the adapter
|
|
38
|
+
// reject it rather than turning corruption into another paid submission.
|
|
39
|
+
return typeof row?.continuation === "string" ? row.continuation : null;
|
|
40
|
+
}
|
|
41
|
+
export function writeWorkContinuation(db, work, pieceId, continuation) {
|
|
42
|
+
if (!workExists(db, work) || continuation.length === 0)
|
|
43
|
+
return false;
|
|
44
|
+
return (db
|
|
45
|
+
.prepare(`UPDATE revision_work_pieces SET continuation=? WHERE id=? AND work_id=? AND submitted_at IS NOT NULL AND (continuation IS NULL OR continuation=?)`)
|
|
46
|
+
.run(continuation, pieceId, work.workId, continuation).changes === 1);
|
|
47
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function workExists(db, work) {
|
|
2
|
+
return (db
|
|
3
|
+
.prepare(`SELECT 1 FROM revision_work WHERE id=? AND project_id=? AND revision_id=? AND stage_id=? AND kind=? AND fingerprint=?`)
|
|
4
|
+
.get(work.workId, work.projectId, work.revisionId, work.stageId, work.kind, work.fingerprint) !== undefined);
|
|
5
|
+
}
|
|
6
|
+
export function claimWork(db, work) {
|
|
7
|
+
return (db
|
|
8
|
+
.prepare(`UPDATE revision_work SET state='running',progress_current=NULL,progress_total=NULL WHERE id=? AND project_id=? AND revision_id=? AND stage_id=? AND kind=? AND fingerprint=? AND state='pending' AND dispatch_state='allowed'`)
|
|
9
|
+
.run(work.workId, work.projectId, work.revisionId, work.stageId, work.kind, work.fingerprint)
|
|
10
|
+
.changes === 1);
|
|
11
|
+
}
|
|
12
|
+
export function maySubmit(db, work, pieceId) {
|
|
13
|
+
if (!workExists(db, work))
|
|
14
|
+
return false;
|
|
15
|
+
const allowed = db
|
|
16
|
+
.prepare(`SELECT 1 FROM revision_work WHERE id=? AND state IN ('pending','running') AND dispatch_state='allowed'`)
|
|
17
|
+
.get(work.workId) !== undefined;
|
|
18
|
+
if (!allowed || !ownsCurrentWork(db, work, pieceId ?? null))
|
|
19
|
+
return false;
|
|
20
|
+
return (pieceId === undefined ||
|
|
21
|
+
db
|
|
22
|
+
.prepare(`SELECT 1 FROM revision_work_pieces WHERE id=? AND work_id=? AND dispatch_state='allowed' AND state IN ('pending','running')`)
|
|
23
|
+
.get(pieceId, work.workId) !== undefined);
|
|
24
|
+
}
|
|
25
|
+
export function ownsCurrentWork(db, work, pieceId) {
|
|
26
|
+
if (!workExists(db, work))
|
|
27
|
+
return false;
|
|
28
|
+
return (db
|
|
29
|
+
.prepare(`SELECT 1 FROM revision_work_reservations r JOIN project_heads h ON h.project_id=r.project_id AND h.revision_id=r.revision_id JOIN project_revisions v ON v.id=r.revision_id JOIN json_each(v.fingerprints) f ON f.key=COALESCE(r.logical_key,r.work_key) AND f.value=COALESCE(r.desired_fingerprint,r.fingerprint) WHERE r.project_id=? AND r.work_id=? AND r.piece_id IS ? AND (r.piece_id IS NULL OR EXISTS(SELECT 1 FROM revision_work_pieces p WHERE p.id=r.piece_id AND p.work_id=r.work_id AND p.work_key=r.work_key AND p.fingerprint=r.fingerprint))`)
|
|
30
|
+
.get(work.projectId, work.workId, pieceId) !== undefined);
|
|
31
|
+
}
|
|
32
|
+
export function finishWork(db, work, state, reason) {
|
|
33
|
+
if (!workExists(db, work))
|
|
34
|
+
return;
|
|
35
|
+
const normalized = state === "provided" || state === "skipped" ? "done" : state;
|
|
36
|
+
db.prepare(`UPDATE revision_work SET state=?,failure_reason=?,dispatch_state=CASE WHEN ?='pending' THEN 'held' ELSE dispatch_state END WHERE id=?`).run(normalized, reason, normalized, work.workId);
|
|
37
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export function fingerprint(value) {
|
|
4
|
+
function canonical(item) {
|
|
5
|
+
if (typeof item === "number")
|
|
6
|
+
z.number().finite().parse(item);
|
|
7
|
+
if (item === null || typeof item !== "object")
|
|
8
|
+
return JSON.stringify(item);
|
|
9
|
+
if (Array.isArray(item))
|
|
10
|
+
return `[${item.map(canonical).join(",")}]`;
|
|
11
|
+
const record = item;
|
|
12
|
+
return `{${Object.keys(record)
|
|
13
|
+
.sort()
|
|
14
|
+
.map((key) => {
|
|
15
|
+
const part = record[key];
|
|
16
|
+
if (part === undefined)
|
|
17
|
+
throw new Error("Missing fingerprint input");
|
|
18
|
+
return `${JSON.stringify(key)}:${canonical(part)}`;
|
|
19
|
+
})
|
|
20
|
+
.join(",")}}`;
|
|
21
|
+
}
|
|
22
|
+
return createHash("sha256").update(canonical(value)).digest("hex");
|
|
23
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
@@ -10,37 +10,45 @@ import { nodeRunCli } from "./adapters/llm/run-cli.js";
|
|
|
10
10
|
import { curateRegistry } from "./catalog/registry.js";
|
|
11
11
|
import { createCatalogueStore } from "./catalog/store.js";
|
|
12
12
|
import { createHub } from "./edge/events/hub.js";
|
|
13
|
+
import { currentProjectEvent } from "./edge/events/visibility.js";
|
|
13
14
|
import { createApp } from "./edge/http/app.js";
|
|
15
|
+
import { createMutationLifecycle, drainMutationsWithDeadline } from "./edge/http/mutations.js";
|
|
14
16
|
import { openFolder } from "./edge/open-folder.js";
|
|
15
17
|
import { createAudioPreviewStore } from "./kernel/audio-preview.js";
|
|
16
18
|
import { systemClock } from "./kernel/clock.js";
|
|
17
19
|
import { openDb } from "./kernel/db/index.js";
|
|
18
20
|
import { migrate } from "./kernel/db/migrate.js";
|
|
21
|
+
import { transact } from "./kernel/db/tx.js";
|
|
19
22
|
import { ulidIds } from "./kernel/ids.js";
|
|
20
23
|
import { acquireInstanceLock } from "./kernel/lock.js";
|
|
21
24
|
import { openLog } from "./kernel/log.js";
|
|
22
25
|
import { ensureDirs, layout } from "./kernel/paths.js";
|
|
23
26
|
import { sqliteAttempts } from "./kernel/runner/attempt-repo.js";
|
|
24
|
-
import {
|
|
27
|
+
import { createCheckpointAuthority, } from "./kernel/runner/checkpoint-authority.js";
|
|
25
28
|
import { createRunner } from "./kernel/runner/index.js";
|
|
26
29
|
import { stageProviders } from "./kernel/runner/providers.js";
|
|
27
30
|
import { createProviderQueue } from "./kernel/runner/queue.js";
|
|
28
31
|
import { readVersion } from "./kernel/version.js";
|
|
29
32
|
import { modelSources } from "./model-catalog.js";
|
|
30
|
-
import {
|
|
31
|
-
import { prepareProvidedArticleSegments } from "./slices/article/provided-entries.js";
|
|
32
|
-
import { runArticle } from "./slices/article/run.js";
|
|
33
|
+
import { projectPaused } from "./slices/admission/repo.js";
|
|
33
34
|
import { pumpQueue, queueWaiting } from "./slices/batch/index.js";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
35
|
+
import { approveCheckpoint } from "./slices/checkpoints/index.js";
|
|
36
|
+
import { checkpointDecisionForWork, recoverCheckpointWork, settleReleasedCheckpoints, } from "./slices/checkpoints/recovery.js";
|
|
37
|
+
import { resolveFont } from "./slices/fonts/index.js";
|
|
38
|
+
import { templateById } from "./slices/project-templates/repo.js";
|
|
39
|
+
import { claimWork, finishWork, maySubmit } from "./slices/rebuild/repo.js";
|
|
40
|
+
import { materializeAdmittedWork } from "./slices/rebuild/runtime-materialize.js";
|
|
41
|
+
import { runRevisionInvocation } from "./slices/rebuild/runtime-run.js";
|
|
42
|
+
import { executionStages, executionStandings, invocationReady, projectStandings, recordWorkProgress, } from "./slices/rebuild/runtime-store.js";
|
|
43
|
+
import { settleTerminalScheduleRuns } from "./slices/schedules/repo.js";
|
|
44
|
+
import { createScheduleRunner } from "./slices/schedules/scheduler.js";
|
|
37
45
|
import { nodeCliProbe } from "./slices/settings/cli-status.js";
|
|
46
|
+
import { providerStatuses } from "./slices/settings/readiness.js";
|
|
38
47
|
import { reconcileStorage } from "./slices/storage/reconcile.js";
|
|
39
48
|
import { collectorEndpoint, httpPostEvents } from "./slices/telemetry/collector-client.js";
|
|
40
49
|
import { createFlusher } from "./slices/telemetry/flush.js";
|
|
41
50
|
import { record } from "./slices/telemetry/record.js";
|
|
42
|
-
import {
|
|
43
|
-
import { renderVideo } from "./slices/video/run.js";
|
|
51
|
+
import { probeDurationMs } from "./slices/video/ffmpeg.js";
|
|
44
52
|
import { watchActivation } from "./updater/candidate.js";
|
|
45
53
|
import { launchUpdate } from "./updater/install.js";
|
|
46
54
|
import { isUpdateToken } from "./updater/model.js";
|
|
@@ -52,10 +60,58 @@ import { createUpdater } from "./updater/service.js";
|
|
|
52
60
|
// stay queued.
|
|
53
61
|
const flushDelayMs = 1000;
|
|
54
62
|
const collectorTimeoutMs = 10_000;
|
|
63
|
+
const mutationDrainTimeoutMs = 5_000;
|
|
64
|
+
export function createScheduleTickLifecycle(deps) {
|
|
65
|
+
let stopping = false;
|
|
66
|
+
let inflight;
|
|
67
|
+
const tick = () => {
|
|
68
|
+
if (stopping)
|
|
69
|
+
return Promise.resolve();
|
|
70
|
+
if (inflight !== undefined)
|
|
71
|
+
return inflight;
|
|
72
|
+
const release = deps.beginMutation();
|
|
73
|
+
if (release === undefined)
|
|
74
|
+
return Promise.resolve();
|
|
75
|
+
const running = (async () => {
|
|
76
|
+
try {
|
|
77
|
+
await deps.tick();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
try {
|
|
81
|
+
deps.report();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// A logging failure must not turn a timer callback into an unhandled rejection.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
release();
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
inflight = running;
|
|
92
|
+
void running.then(() => {
|
|
93
|
+
if (inflight === running)
|
|
94
|
+
inflight = undefined;
|
|
95
|
+
}, () => {
|
|
96
|
+
if (inflight === running)
|
|
97
|
+
inflight = undefined;
|
|
98
|
+
});
|
|
99
|
+
return running;
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
tick,
|
|
103
|
+
stop: () => {
|
|
104
|
+
stopping = true;
|
|
105
|
+
return inflight ?? Promise.resolve();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
55
109
|
export async function boot(config) {
|
|
56
110
|
const clock = systemClock;
|
|
57
111
|
const ids = ulidIds;
|
|
58
112
|
const paths = layout(config.dataDir);
|
|
113
|
+
const candidateToken = process.env.SLOPIFY_UPDATE_TOKEN ?? "";
|
|
114
|
+
const pendingActivation = isUpdateToken(candidateToken) && process.env.SLOPIFY_UPDATE_PENDING === "1";
|
|
59
115
|
ensureDirs(paths, { mode: 0o700 });
|
|
60
116
|
const lock = acquireInstanceLock(paths.lock);
|
|
61
117
|
let db;
|
|
@@ -68,13 +124,26 @@ export async function boot(config) {
|
|
|
68
124
|
});
|
|
69
125
|
db = openDb(paths.db);
|
|
70
126
|
migrate(db, clock);
|
|
127
|
+
const runtimeDb = db;
|
|
71
128
|
const interrupted = markInterruptedStages(db, clock);
|
|
72
|
-
|
|
129
|
+
recoverCheckpointWork(db);
|
|
130
|
+
const settledSchedules = settleTerminalScheduleRuns(db, clock.now().toISOString());
|
|
131
|
+
// The updater can restore the database after a failed candidate boot, but it cannot
|
|
132
|
+
// restore files deleted by reconciliation. Leave the filesystem untouched until the
|
|
133
|
+
// candidate's committed activation pointer has been verified.
|
|
134
|
+
const reconciled = pendingActivation ? undefined : reconcileStorage(db, paths);
|
|
73
135
|
const log = openLog(paths.logs, clock);
|
|
74
136
|
log.write("info", "boot", {
|
|
75
|
-
detail:
|
|
137
|
+
detail: reconciled === undefined
|
|
138
|
+
? `interrupted stages ${interrupted}, settled schedules ${settledSchedules}, storage reconciliation deferred during update activation`
|
|
139
|
+
: `interrupted stages ${interrupted}, settled schedules ${settledSchedules}, orphan files ${reconciled.orphanFiles}, staged files ${reconciled.stagedFiles}`,
|
|
140
|
+
});
|
|
141
|
+
const eventDb = db;
|
|
142
|
+
const hub = createHub({
|
|
143
|
+
ids,
|
|
144
|
+
log,
|
|
145
|
+
acceptEvent: (event) => currentProjectEvent(eventDb, event),
|
|
76
146
|
});
|
|
77
|
-
const hub = createHub({ ids, log });
|
|
78
147
|
const version = readVersion();
|
|
79
148
|
const telemetry = { db, ids, clock, log, appVersion: version };
|
|
80
149
|
const flusher = createFlusher({
|
|
@@ -92,7 +161,7 @@ export async function boot(config) {
|
|
|
92
161
|
probe: nodeCliProbe,
|
|
93
162
|
}), catalogue);
|
|
94
163
|
const audioPreviews = createAudioPreviewStore();
|
|
95
|
-
const runner =
|
|
164
|
+
const runner = wireRunner({
|
|
96
165
|
db,
|
|
97
166
|
paths,
|
|
98
167
|
clock,
|
|
@@ -122,8 +191,6 @@ export async function boot(config) {
|
|
|
122
191
|
throw new Error("The server is not ready.");
|
|
123
192
|
};
|
|
124
193
|
let listeningPort = config.port;
|
|
125
|
-
const candidateToken = process.env.SLOPIFY_UPDATE_TOKEN ?? "";
|
|
126
|
-
const pendingActivation = isUpdateToken(candidateToken) && process.env.SLOPIFY_UPDATE_PENDING === "1";
|
|
127
194
|
const updater = createUpdater({
|
|
128
195
|
...(isUpdateToken(candidateToken)
|
|
129
196
|
? {
|
|
@@ -131,6 +198,21 @@ export async function boot(config) {
|
|
|
131
198
|
token: candidateToken,
|
|
132
199
|
pending: pendingActivation,
|
|
133
200
|
committed: () => updateCommitted(paths.dataDir, version, candidateToken),
|
|
201
|
+
settle: () => {
|
|
202
|
+
try {
|
|
203
|
+
const settled = reconcileStorage(updateDb, paths);
|
|
204
|
+
log.write("info", "update", {
|
|
205
|
+
detail: `activation storage reconciliation removed ${settled.orphanFiles} orphan files and ${settled.stagedFiles} staged files`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// The pointer is already committed, so storage maintenance can no longer
|
|
210
|
+
// safely reject or roll back this candidate. A normal restart retries it.
|
|
211
|
+
log.write("error", "update", {
|
|
212
|
+
detail: "Storage reconciliation failed after update activation.",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
},
|
|
134
216
|
},
|
|
135
217
|
}
|
|
136
218
|
: {}),
|
|
@@ -169,13 +251,64 @@ export async function boot(config) {
|
|
|
169
251
|
});
|
|
170
252
|
},
|
|
171
253
|
});
|
|
254
|
+
const rebuild = {
|
|
255
|
+
db,
|
|
256
|
+
paths,
|
|
257
|
+
ids,
|
|
258
|
+
clock,
|
|
259
|
+
log,
|
|
260
|
+
runner,
|
|
261
|
+
catalogue,
|
|
262
|
+
measureAudio: (path, signal) => probeDurationMs(ffmpeg, path, signal ?? AbortSignal.timeout(30_000), log),
|
|
263
|
+
providers: () => providerStatuses({ db: updateDb, probe: nodeCliProbe }),
|
|
264
|
+
modelsFor: modelSources(registry).modelsFor,
|
|
265
|
+
emit: (projectId, event) => hub.emit(projectId, event),
|
|
266
|
+
};
|
|
267
|
+
const draftDeps = {
|
|
268
|
+
db,
|
|
269
|
+
paths,
|
|
270
|
+
ids,
|
|
271
|
+
clock,
|
|
272
|
+
log,
|
|
273
|
+
runner,
|
|
274
|
+
catalogue,
|
|
275
|
+
uuid: randomUUID,
|
|
276
|
+
resolveFont: (fontId) => resolveFont(paths, fontId),
|
|
277
|
+
providers: rebuild.providers,
|
|
278
|
+
modelsFor: rebuild.modelsFor,
|
|
279
|
+
emit: (event) => hub.emitGlobal(event),
|
|
280
|
+
recordStarted: (projectIds) => {
|
|
281
|
+
for (const _projectId of projectIds)
|
|
282
|
+
record(telemetry, "project.created", {});
|
|
283
|
+
flusher.soon();
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
const scheduleDeps = {
|
|
287
|
+
...draftDeps,
|
|
288
|
+
template: (id, templateVersion) => templateById(runtimeDb, id, templateVersion),
|
|
289
|
+
};
|
|
290
|
+
const scheduleRunner = createScheduleRunner(scheduleDeps);
|
|
291
|
+
scheduleRunner.recover(clock.now());
|
|
292
|
+
const scheduleTicks = createScheduleTickLifecycle({
|
|
293
|
+
beginMutation: updater.beginMutation,
|
|
294
|
+
tick: scheduleRunner.tick,
|
|
295
|
+
report: () => log.write("error", "schedule.tick", {
|
|
296
|
+
detail: "Scheduled jobs could not be advanced.",
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
const mutations = createMutationLifecycle();
|
|
172
300
|
const app = createApp({
|
|
301
|
+
rebuild,
|
|
302
|
+
drafts: draftDeps,
|
|
303
|
+
schedules: scheduleDeps,
|
|
304
|
+
...(rebuild.measureAudio === undefined ? {} : { measureAudio: rebuild.measureAudio }),
|
|
173
305
|
openFolder,
|
|
174
306
|
db,
|
|
175
307
|
paths,
|
|
176
308
|
hub,
|
|
177
309
|
runner,
|
|
178
310
|
updater,
|
|
311
|
+
mutations,
|
|
179
312
|
audioPreviews,
|
|
180
313
|
...modelSources(registry),
|
|
181
314
|
catalogue,
|
|
@@ -202,6 +335,10 @@ export async function boot(config) {
|
|
|
202
335
|
release();
|
|
203
336
|
}
|
|
204
337
|
}, 1000);
|
|
338
|
+
const scheduleTimer = setInterval(() => {
|
|
339
|
+
void scheduleTicks.tick();
|
|
340
|
+
}, 15_000);
|
|
341
|
+
void scheduleTicks.tick();
|
|
205
342
|
listeningPort = portOf(server) ?? config.port;
|
|
206
343
|
// Whatever last run left queued goes out at start. Nothing waits for
|
|
207
344
|
// it, and an unreachable collector costs one refused socket.
|
|
@@ -212,25 +349,34 @@ export async function boot(config) {
|
|
|
212
349
|
shutdown = () => {
|
|
213
350
|
stopping ??= (async () => {
|
|
214
351
|
clearInterval(queueTimer);
|
|
352
|
+
clearInterval(scheduleTimer);
|
|
353
|
+
const mutationDrain = mutations.stop();
|
|
354
|
+
const scheduleDrain = scheduleTicks.stop();
|
|
355
|
+
const serverClose = beginServerClose(server);
|
|
215
356
|
stopActivation();
|
|
216
357
|
audioPreviews.close();
|
|
217
358
|
try {
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
await close(server);
|
|
359
|
+
// Admission closes before the listener. Requests already admitted keep the
|
|
360
|
+
// database until their response settles; anything racing shutdown receives 503.
|
|
361
|
+
await drainMutationsWithDeadline(mutationDrain, serverClose.terminate, mutationDrainTimeoutMs);
|
|
362
|
+
await scheduleDrain;
|
|
223
363
|
await runner.abortAll();
|
|
224
364
|
}
|
|
225
365
|
finally {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
366
|
+
serverClose.terminate();
|
|
367
|
+
try {
|
|
368
|
+
await serverClose.closed;
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
// The pending timer is cancelled rather than awaited: a shutdown must not wait
|
|
372
|
+
// on the collector. A flush already in flight may land after the database
|
|
373
|
+
// closes and fail to mark its batch delivered, which costs one re-send that
|
|
374
|
+
// the collector deduplicates by event id.
|
|
375
|
+
flusher.stop();
|
|
376
|
+
log.write("info", "shutdown");
|
|
377
|
+
open.close();
|
|
378
|
+
lock.release();
|
|
379
|
+
}
|
|
234
380
|
}
|
|
235
381
|
})();
|
|
236
382
|
return stopping;
|
|
@@ -245,7 +391,7 @@ export async function boot(config) {
|
|
|
245
391
|
throw error;
|
|
246
392
|
}
|
|
247
393
|
}
|
|
248
|
-
function
|
|
394
|
+
export function wireRunner({ audioPreviews, db, paths, clock, ids, log, hub, telemetry, flusher, registry, catalogue, ffmpeg, }) {
|
|
249
395
|
// A stage counts what it did and the queue is flushed after each new event. `record`
|
|
250
396
|
// swallows its own failures, so this can neither fail a stage nor widen what leaves the
|
|
251
397
|
// machine - the payload allow-list is checked inside it.
|
|
@@ -253,12 +399,7 @@ function wire({ audioPreviews, db, paths, clock, ids, log, hub, telemetry, flush
|
|
|
253
399
|
record(telemetry, type, counters);
|
|
254
400
|
flusher.soon();
|
|
255
401
|
};
|
|
256
|
-
|
|
257
|
-
// the same seven dependencies.
|
|
258
|
-
const video = { db, paths, ids, clock, log, ffmpeg, count, alignSubtitles };
|
|
259
|
-
// Research, the article, the images and the thumbnail all write into the same project
|
|
260
|
-
// folder from the same database handle, so they take the same six dependencies.
|
|
261
|
-
const writing = { db, paths, ids, clock, log, count };
|
|
402
|
+
const execution = { db, paths, ids, clock, log, ffmpeg, alignSubtitles, audioPreviews, count };
|
|
262
403
|
// A stage slice is handed the wrapped calls, never the registry: every provider call
|
|
263
404
|
// it makes is already inside the retry policy (kernel/runner/providers.ts).
|
|
264
405
|
const providers = {
|
|
@@ -268,43 +409,55 @@ function wire({ audioPreviews, db, paths, clock, ids, log, hub, telemetry, flush
|
|
|
268
409
|
log,
|
|
269
410
|
queue: createProviderQueue((provider) => catalogue.read().providers[provider]?.maxConcurrent ?? 1),
|
|
270
411
|
};
|
|
271
|
-
|
|
412
|
+
const checkpoints = createCheckpointAuthority({
|
|
413
|
+
decide: (work) => checkpointDecisionForWork(execution, work),
|
|
414
|
+
approve: (projectId, checkpointId, identity) => approveCheckpoint(db, {
|
|
415
|
+
...identity,
|
|
416
|
+
projectId,
|
|
417
|
+
checkpointId,
|
|
418
|
+
approvedAt: clock.now().toISOString(),
|
|
419
|
+
}),
|
|
420
|
+
inTransaction: () => db.isTransaction,
|
|
421
|
+
wake: (projectId) => runner.tick(projectId),
|
|
422
|
+
log,
|
|
423
|
+
});
|
|
424
|
+
const runner = createRunner({
|
|
425
|
+
checkpoints,
|
|
272
426
|
stages: {
|
|
273
|
-
stagesOf: (projectId) =>
|
|
427
|
+
stagesOf: (projectId) => {
|
|
428
|
+
materializeAdmittedWork(execution, projectId);
|
|
429
|
+
projectStandings(execution, projectId);
|
|
430
|
+
return executionStages(execution, projectId);
|
|
431
|
+
},
|
|
432
|
+
standingsOf: (projectId) => executionStandings(execution, projectId),
|
|
433
|
+
ready: (work) => invocationReady(execution, work),
|
|
274
434
|
paused: (projectId) => projectPaused(db, projectId) || queueWaiting(db, projectId),
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
435
|
+
claim: (work) => {
|
|
436
|
+
const claimed = claimWork(db, work);
|
|
437
|
+
projectStandings(execution, work.projectId);
|
|
438
|
+
return claimed;
|
|
278
439
|
},
|
|
279
|
-
|
|
280
|
-
finish: (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
audio: async (context) => {
|
|
288
|
-
const wrapped = stageProviders(providers, context);
|
|
289
|
-
await prepareProvidedArticleSegments(writing, context, wrapped);
|
|
290
|
-
await runNarration({
|
|
291
|
-
...video,
|
|
292
|
-
audioPreviews,
|
|
293
|
-
maxCharacters: (provider, model) => {
|
|
294
|
-
const entry = catalogue.models(provider, "tts").find((m) => m.id === model);
|
|
295
|
-
return entry && "tts" in entry ? entry.tts.maxCharacters : 4000;
|
|
296
|
-
},
|
|
297
|
-
}, context, wrapped);
|
|
440
|
+
maySubmit: (work, pieceId) => maySubmit(db, work, pieceId),
|
|
441
|
+
finish: (work, state, reason) => {
|
|
442
|
+
transact(db, () => {
|
|
443
|
+
finishWork(db, work, state, reason);
|
|
444
|
+
materializeAdmittedWork(execution, work.projectId);
|
|
445
|
+
projectStandings(execution, work.projectId);
|
|
446
|
+
settleReleasedCheckpoints(execution, work.projectId);
|
|
447
|
+
});
|
|
298
448
|
},
|
|
299
|
-
images: (context) => runImages(writing, context, stageProviders(providers, context)),
|
|
300
|
-
thumbnail: (context) => runThumbnail(writing, context, stageProviders(providers, context)),
|
|
301
|
-
video: (context) => renderVideo(video, context),
|
|
302
449
|
},
|
|
450
|
+
runs: Object.fromEntries(["research", "article", "audio", "images", "thumbnail", "video"].map((kind) => [
|
|
451
|
+
kind,
|
|
452
|
+
(context) => runRevisionInvocation(execution, context, stageProviders(providers, context)),
|
|
453
|
+
])),
|
|
303
454
|
// Counting is finer than a stage reaching `done` - each intro and outro text, each
|
|
304
455
|
// narrated segment - and the counters are provider names, token usage and durations
|
|
305
456
|
// only the stage slice ever sees. Each slice records its own units through `count`
|
|
306
457
|
// above, and the runner counts nothing: the kernel may not import a slice.
|
|
307
458
|
emit: (projectId, event) => {
|
|
459
|
+
if (event.type === "stage.progress")
|
|
460
|
+
recordWorkProgress(execution, event);
|
|
308
461
|
hub.emit(projectId, event);
|
|
309
462
|
},
|
|
310
463
|
emitRunningCount: (running) => {
|
|
@@ -312,6 +465,7 @@ function wire({ audioPreviews, db, paths, clock, ids, log, hub, telemetry, flush
|
|
|
312
465
|
},
|
|
313
466
|
log,
|
|
314
467
|
});
|
|
468
|
+
return { ...runner, checkpoints };
|
|
315
469
|
}
|
|
316
470
|
function listen(app, config, log) {
|
|
317
471
|
return new Promise((resolve, reject) => {
|
|
@@ -325,13 +479,10 @@ function listen(app, config, log) {
|
|
|
325
479
|
server.once("error", reject);
|
|
326
480
|
});
|
|
327
481
|
}
|
|
328
|
-
function
|
|
482
|
+
function beginServerClose(server) {
|
|
329
483
|
// An SSE response never ends by itself, and close() waits for every open connection,
|
|
330
|
-
// so
|
|
331
|
-
|
|
332
|
-
server.closeAllConnections();
|
|
333
|
-
}
|
|
334
|
-
return new Promise((resolve, reject) => {
|
|
484
|
+
// so admitted mutations drain first and then the remaining sockets are terminated.
|
|
485
|
+
const closed = new Promise((resolve, reject) => {
|
|
335
486
|
server.close((error) => {
|
|
336
487
|
if (error === undefined || error === null) {
|
|
337
488
|
resolve();
|
|
@@ -340,6 +491,13 @@ function close(server) {
|
|
|
340
491
|
reject(error);
|
|
341
492
|
});
|
|
342
493
|
});
|
|
494
|
+
return {
|
|
495
|
+
closed,
|
|
496
|
+
terminate: () => {
|
|
497
|
+
if ("closeAllConnections" in server)
|
|
498
|
+
server.closeAllConnections();
|
|
499
|
+
},
|
|
500
|
+
};
|
|
343
501
|
}
|
|
344
502
|
function portOf(server) {
|
|
345
503
|
const address = server.address();
|