beauticode-dsh 1.0.7 → 1.0.13
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.zh-CN.md +2 -2
- package/agent.mjs +69 -52
- package/client.js +102 -22
- package/console.js +166 -46
- package/control-client.mjs +6 -4
- package/index.mjs +22 -8
- package/package.json +1 -1
- package/themes/internal-beyond/bg-canvas.png +0 -0
- package/ui-host.mjs +459 -31
- package/vendor/adapter-dsh/bridge.js +2 -4
- package/vendor/adapter-dsh/session.js +61 -27
- package/vendor/adapter-dsh/tray-handoff.js +13 -11
- package/vendor/core/apply-transaction.js +87 -45
- package/vendor/core/background-store.js +211 -351
- package/vendor/core/error-message.js +0 -8
- package/vendor/core/file-lock.js +13 -34
- package/vendor/core/index.js +1 -2
- package/vendor/core/media-server.js +22 -40
- package/vendor/core/media-source.js +36 -0
- package/vendor/core/media-validation.js +26 -19
- package/vendor/core/paths.js +0 -1
- package/vendor/core/process-liveness.js +0 -205
- package/vendor/core/skin-pack.js +0 -64
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { ApplyTransaction, BackgroundStore, MediaServerController, buildHostApplyPayload, defaultDataRoot,
|
|
3
|
+
import { ApplyTransaction, BackgroundStore, MediaServerController, buildHostApplyPayload, defaultDataRoot, isLocalBackgroundSource, resolveSessionBundledThemes, resolveBackgroundImagePath, } from "../core/index.js";
|
|
4
4
|
import { DshHostApplier, normalizeDshBaseUrl } from "./bridge.js";
|
|
5
5
|
import { DSH_HOST_DESCRIPTOR } from "./host-descriptor.js";
|
|
6
6
|
import { acquireDshInjectorLock } from "./injector-lock.js";
|
|
@@ -93,16 +93,26 @@ export class DshSession {
|
|
|
93
93
|
this.startHandoffLoop();
|
|
94
94
|
void this.yieldToTrayIfRequested();
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
// Otherwise a click immediately after lazy session startup can race the
|
|
98
|
-
// background reapply and be rejected as a concurrent operation.
|
|
99
|
-
await this.watchOnce();
|
|
96
|
+
void this.watchOnce();
|
|
100
97
|
return { port: this.bridgePort() };
|
|
101
98
|
}
|
|
102
99
|
async apply(input) {
|
|
103
100
|
return this.trackOperation(this.applyInternal(input));
|
|
104
101
|
}
|
|
105
|
-
async
|
|
102
|
+
async applyAndSaveTheme(input, name) {
|
|
103
|
+
const result = await this.trackOperation(this.applyInternal(input, name));
|
|
104
|
+
if (!result.ok)
|
|
105
|
+
return result;
|
|
106
|
+
if (!result.theme) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: "Theme apply completed without a saved theme.",
|
|
110
|
+
rolledBack: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { ...result, theme: result.theme };
|
|
114
|
+
}
|
|
115
|
+
async applyInternal(input, themeName) {
|
|
106
116
|
if (!this.releaseLock || this.closed || !this.host) {
|
|
107
117
|
throw new Error("Session is not started");
|
|
108
118
|
}
|
|
@@ -115,16 +125,37 @@ export class DshSession {
|
|
|
115
125
|
}
|
|
116
126
|
this.userBusy = true;
|
|
117
127
|
try {
|
|
128
|
+
// The startup/watch probe may already have opened active/background.json
|
|
129
|
+
// before userBusy became true. On Windows that read can overlap the
|
|
130
|
+
// transaction's atomic active-directory rename and cause EPERM. Drain
|
|
131
|
+
// that one read before beginning any apply mutation; later watch ticks
|
|
132
|
+
// observe userBusy and do not reapply.
|
|
133
|
+
const inFlightWatch = this.watchTask;
|
|
134
|
+
if (inFlightWatch)
|
|
135
|
+
await inFlightWatch;
|
|
118
136
|
const tx = new ApplyTransaction({
|
|
119
137
|
store: this.store,
|
|
120
138
|
media: this.media,
|
|
121
139
|
host: this.host,
|
|
140
|
+
includeImageDataUrl: false,
|
|
122
141
|
verifyDeadlineMs: this.verifyDeadlineMs,
|
|
123
142
|
offline: false,
|
|
124
143
|
});
|
|
125
|
-
const
|
|
144
|
+
const saved = { theme: null };
|
|
145
|
+
const result = await tx.run(input, themeName
|
|
146
|
+
? {
|
|
147
|
+
beforeFinalize: async () => {
|
|
148
|
+
saved.theme = await this.store.saveCurrentTheme(themeName);
|
|
149
|
+
},
|
|
150
|
+
onRollback: async () => {
|
|
151
|
+
if (saved.theme) {
|
|
152
|
+
await this.store.deleteSavedTheme(saved.theme.id);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
: undefined);
|
|
126
157
|
if (result.ok) {
|
|
127
|
-
this.activeThemeId = null;
|
|
158
|
+
this.activeThemeId = saved.theme?.type === "video" ? saved.theme.id : null;
|
|
128
159
|
this.lastProgressWriteSec = -1;
|
|
129
160
|
if (input.type === "clear") {
|
|
130
161
|
this.fishMode = false;
|
|
@@ -143,7 +174,7 @@ export class DshSession {
|
|
|
143
174
|
}
|
|
144
175
|
await this.host.setBackgroundTone(this.backgroundTone).catch(() => null);
|
|
145
176
|
}
|
|
146
|
-
return result;
|
|
177
|
+
return result.ok && saved.theme ? { ...result, theme: saved.theme } : result;
|
|
147
178
|
}
|
|
148
179
|
finally {
|
|
149
180
|
this.userBusy = false;
|
|
@@ -184,21 +215,26 @@ export class DshSession {
|
|
|
184
215
|
}
|
|
185
216
|
}
|
|
186
217
|
if (manifest.background) {
|
|
187
|
-
const
|
|
188
|
-
if (!
|
|
189
|
-
throw new Error("
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
218
|
+
const imagePath = resolveBackgroundImagePath(this.store.paths.activeDir, manifest.background);
|
|
219
|
+
if (!imagePath)
|
|
220
|
+
throw new Error("Background has no image source.");
|
|
221
|
+
stagedImage = await this.media.stage(imagePath, {
|
|
222
|
+
validation: manifest.background.type === "image" && isLocalBackgroundSource(manifest.background)
|
|
223
|
+
? "fast"
|
|
224
|
+
: "full",
|
|
225
|
+
});
|
|
226
|
+
if (manifest.background.type === "video") {
|
|
193
227
|
const runtimeVideoPath = await this.store.prepareRuntimeVideo(manifest);
|
|
194
228
|
if (!runtimeVideoPath) {
|
|
195
229
|
throw new Error("DSH video reapply requires a detached runtime copy.");
|
|
196
230
|
}
|
|
197
|
-
stagedVideo = await this.media.stage(runtimeVideoPath
|
|
231
|
+
stagedVideo = await this.media.stage(runtimeVideoPath, {
|
|
232
|
+
validation: isLocalBackgroundSource(manifest.background) ? "fast" : "full",
|
|
233
|
+
});
|
|
198
234
|
}
|
|
199
235
|
}
|
|
200
236
|
const staged = { image: stagedImage, video: stagedVideo };
|
|
201
|
-
const payload = await buildHostApplyPayload(this.store, manifest, staged, "");
|
|
237
|
+
const payload = await buildHostApplyPayload(this.store, manifest, staged, "", undefined, { includeImageDataUrl: false });
|
|
202
238
|
if (payload.video && resumeAt != null)
|
|
203
239
|
payload.video.startAt = resumeAt;
|
|
204
240
|
await this.host.apply(payload);
|
|
@@ -261,7 +297,6 @@ export class DshSession {
|
|
|
261
297
|
fish: this.fishMode,
|
|
262
298
|
muted: this.videoMuted,
|
|
263
299
|
tone: this.backgroundTone,
|
|
264
|
-
themeId: this.activeThemeId,
|
|
265
300
|
};
|
|
266
301
|
}
|
|
267
302
|
async saveCurrentTheme(name) {
|
|
@@ -278,16 +313,15 @@ export class DshSession {
|
|
|
278
313
|
}
|
|
279
314
|
}
|
|
280
315
|
const theme = await this.store.saveCurrentTheme(name, { videoPositionSec });
|
|
281
|
-
|
|
282
|
-
|
|
316
|
+
if (theme.type === "video") {
|
|
317
|
+
this.activeThemeId = theme.id;
|
|
318
|
+
this.lastProgressWriteSec = -1;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
this.activeThemeId = null;
|
|
322
|
+
}
|
|
283
323
|
return theme;
|
|
284
324
|
}
|
|
285
|
-
async importSavedTheme(input) {
|
|
286
|
-
return this.store.importSavedThemeFromMedia(input);
|
|
287
|
-
}
|
|
288
|
-
async applyImportedMedia(input, opts = {}) {
|
|
289
|
-
return this.trackOperation(persistAndApplyImportedMedia(this, input, opts));
|
|
290
|
-
}
|
|
291
325
|
async listSavedThemes() {
|
|
292
326
|
return this.store.listSavedThemes();
|
|
293
327
|
}
|
|
@@ -304,7 +338,7 @@ export class DshSession {
|
|
|
304
338
|
const saved = await this.store.loadSavedTheme(themeId);
|
|
305
339
|
const result = await this.apply(saved.input);
|
|
306
340
|
if (result.ok) {
|
|
307
|
-
this.activeThemeId = saved.themeId;
|
|
341
|
+
this.activeThemeId = saved.input.type === "video" ? saved.themeId : null;
|
|
308
342
|
this.lastProgressWriteSec = -1;
|
|
309
343
|
}
|
|
310
344
|
return result;
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { isRecordedPidLive } from "../core/index.js";
|
|
4
3
|
export const TRAY_CLAIM_FILE = "tray-claim.json";
|
|
5
4
|
export const TRAY_CLAIM_SCHEMA = "beauticode.tray-claim/v1";
|
|
6
5
|
export const SESSION_HOST_FILE = "session-host.json";
|
|
7
6
|
export const SESSION_HOST_SCHEMA = "beauticode.session-host/v1";
|
|
8
7
|
export const DSH_CONTROL_FILE = "dsh-control.json";
|
|
9
8
|
export const DSH_CONTROL_SCHEMA = "beauticode.dsh-control/v1";
|
|
9
|
+
function isPidAlive(pid) {
|
|
10
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
11
|
+
return false;
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, 0);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
async function readLivePid(filePath, schema) {
|
|
11
21
|
let raw;
|
|
12
|
-
let mtimeMs = 0;
|
|
13
22
|
try {
|
|
14
|
-
|
|
15
|
-
raw = text;
|
|
16
|
-
mtimeMs = stat.mtimeMs;
|
|
23
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
17
24
|
}
|
|
18
25
|
catch {
|
|
19
26
|
return null;
|
|
@@ -23,12 +30,7 @@ async function readLivePid(filePath, schema) {
|
|
|
23
30
|
if (schema && parsed.schema !== schema)
|
|
24
31
|
return null;
|
|
25
32
|
const pid = Number(parsed.pid);
|
|
26
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
27
|
-
return null;
|
|
28
|
-
const startedAt = typeof parsed.startedAt === "string" && parsed.startedAt
|
|
29
|
-
? parsed.startedAt
|
|
30
|
-
: new Date(mtimeMs).toISOString();
|
|
31
|
-
if (!(await isRecordedPidLive(pid, startedAt)))
|
|
33
|
+
if (!Number.isInteger(pid) || pid <= 0 || !isPidAlive(pid))
|
|
32
34
|
return null;
|
|
33
35
|
return pid;
|
|
34
36
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { MAX_INLINE_DATA_URL_BYTES } from "./constants.js";
|
|
4
|
+
import { isLocalBackgroundSource, resolveBackgroundImagePath, resolveBackgroundVideoPath, } from "./media-source.js";
|
|
4
5
|
import { detectImageMime } from "./media-validation.js";
|
|
5
6
|
/**
|
|
6
7
|
* Minimal baseline only — live Codex path injects packages/adapter-codex
|
|
@@ -39,6 +40,7 @@ export class ApplyTransaction {
|
|
|
39
40
|
cssText;
|
|
40
41
|
verifyDeadlineMs;
|
|
41
42
|
offline;
|
|
43
|
+
includeImageDataUrl;
|
|
42
44
|
#busy = false;
|
|
43
45
|
constructor(opts) {
|
|
44
46
|
this.store = opts.store;
|
|
@@ -47,11 +49,12 @@ export class ApplyTransaction {
|
|
|
47
49
|
this.cssText = opts.cssText ?? DEFAULT_CSS;
|
|
48
50
|
this.verifyDeadlineMs = opts.verifyDeadlineMs ?? 30_000;
|
|
49
51
|
this.offline = opts.offline ?? false;
|
|
52
|
+
this.includeImageDataUrl = opts.includeImageDataUrl ?? true;
|
|
50
53
|
}
|
|
51
54
|
get busy() {
|
|
52
55
|
return this.#busy;
|
|
53
56
|
}
|
|
54
|
-
async run(input) {
|
|
57
|
+
async run(input, hooks = {}) {
|
|
55
58
|
if (this.#busy) {
|
|
56
59
|
return {
|
|
57
60
|
ok: false,
|
|
@@ -61,7 +64,7 @@ export class ApplyTransaction {
|
|
|
61
64
|
}
|
|
62
65
|
this.#busy = true;
|
|
63
66
|
try {
|
|
64
|
-
return await this.store.withExclusiveMutation(() => this.#runExclusive(input));
|
|
67
|
+
return await this.store.withExclusiveMutation(() => this.#runExclusive(input, hooks));
|
|
65
68
|
}
|
|
66
69
|
catch (error) {
|
|
67
70
|
return {
|
|
@@ -74,45 +77,78 @@ export class ApplyTransaction {
|
|
|
74
77
|
this.#busy = false;
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
|
-
async #runExclusive(input) {
|
|
80
|
+
async #runExclusive(input, hooks) {
|
|
81
|
+
const startedAt = performance.now();
|
|
82
|
+
const phases = {};
|
|
83
|
+
const measure = async (name, work) => {
|
|
84
|
+
const phaseStartedAt = performance.now();
|
|
85
|
+
try {
|
|
86
|
+
return await work();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
phases[name] = Math.round((performance.now() - phaseStartedAt) * 10) / 10;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const requestedSourceMode = input.type === "clear" ? "clear" : input.source ?? "managed";
|
|
93
|
+
const timings = () => ({
|
|
94
|
+
totalMs: Math.round((performance.now() - startedAt) * 10) / 10,
|
|
95
|
+
phases: { ...phases },
|
|
96
|
+
});
|
|
78
97
|
let snapshot = null;
|
|
79
98
|
let staged = null;
|
|
80
99
|
let runtimeVideoPath = null;
|
|
100
|
+
let finalizeStarted = false;
|
|
81
101
|
try {
|
|
82
|
-
await this.store.init();
|
|
83
|
-
snapshot = await this.store.snapshot();
|
|
84
|
-
const manifest = await this.store.commitImport(input);
|
|
85
|
-
|
|
102
|
+
await measure("initialize", () => this.store.init());
|
|
103
|
+
snapshot = await measure("snapshot", () => this.store.snapshot());
|
|
104
|
+
const manifest = await measure("commitImport", () => this.store.commitImport(input));
|
|
105
|
+
const sourceMode = !manifest.background
|
|
106
|
+
? "clear"
|
|
107
|
+
: isLocalBackgroundSource(manifest.background)
|
|
108
|
+
? "local"
|
|
109
|
+
: "managed";
|
|
110
|
+
if (requestedSourceMode === "local" && sourceMode !== "local") {
|
|
111
|
+
throw new Error("Local import contract was not preserved by the media store.");
|
|
112
|
+
}
|
|
113
|
+
staged = await measure("stageMedia", () => this.#stageMediaFor(manifest));
|
|
86
114
|
if (!this.offline && this.host) {
|
|
87
|
-
const payload = await this.#buildPayload(manifest, staged, input.type === "video" ? input.startAt : undefined);
|
|
115
|
+
const payload = await measure("buildPayload", () => this.#buildPayload(manifest, staged, input.type === "video" ? input.startAt : undefined));
|
|
88
116
|
runtimeVideoPath = payload.video?.localPath ?? null;
|
|
89
|
-
await this.host.apply(payload);
|
|
90
|
-
const verify = await this.host.verify({
|
|
117
|
+
await measure("hostApply", () => this.host.apply(payload));
|
|
118
|
+
const verify = await measure("rendererVerify", () => this.host.verify({
|
|
91
119
|
generation: manifest.generation,
|
|
92
120
|
media: manifest.background?.type ?? "clear",
|
|
93
|
-
}, { deadlineMs: this.verifyDeadlineMs });
|
|
121
|
+
}, { deadlineMs: this.verifyDeadlineMs }));
|
|
94
122
|
if (verify.status !== "pass") {
|
|
95
|
-
await this.#rollback(snapshot, staged);
|
|
123
|
+
await measure("rollback", () => this.#rollback(snapshot, staged));
|
|
96
124
|
staged = null;
|
|
97
125
|
snapshot = null;
|
|
98
126
|
return {
|
|
99
127
|
ok: false,
|
|
100
128
|
error: `Live verify did not pass (${verify.status}): ${verify.reason}`,
|
|
101
129
|
rolledBack: true,
|
|
130
|
+
sourceMode,
|
|
131
|
+
timings: timings(),
|
|
102
132
|
};
|
|
103
133
|
}
|
|
104
134
|
}
|
|
105
|
-
await this.media.commit(staged);
|
|
106
|
-
await this.store.pruneRuntimeMedia(
|
|
135
|
+
await measure("mediaCommit", () => this.media.commit(staged));
|
|
136
|
+
await measure("pruneRuntimeMedia", () => this.store.pruneRuntimeMedia(runtimeVideoPath));
|
|
107
137
|
staged = null;
|
|
138
|
+
if (hooks.beforeFinalize) {
|
|
139
|
+
finalizeStarted = true;
|
|
140
|
+
await measure("saveTheme", () => hooks.beforeFinalize(manifest));
|
|
141
|
+
}
|
|
108
142
|
if (snapshot) {
|
|
109
|
-
await this.store.clearSnapshot(snapshot);
|
|
143
|
+
await measure("clearSnapshot", () => this.store.clearSnapshot(snapshot));
|
|
110
144
|
snapshot = null;
|
|
111
145
|
}
|
|
112
146
|
return {
|
|
113
147
|
ok: true,
|
|
114
148
|
generation: manifest.generation,
|
|
115
149
|
mode: manifest.background?.type ?? "clear",
|
|
150
|
+
sourceMode,
|
|
151
|
+
timings: timings(),
|
|
116
152
|
};
|
|
117
153
|
}
|
|
118
154
|
catch (err) {
|
|
@@ -120,7 +156,7 @@ export class ApplyTransaction {
|
|
|
120
156
|
let rolledBack = false;
|
|
121
157
|
if (snapshot) {
|
|
122
158
|
try {
|
|
123
|
-
await this.#rollback(snapshot, staged);
|
|
159
|
+
await measure("rollback", () => this.#rollback(snapshot, staged));
|
|
124
160
|
staged = null;
|
|
125
161
|
snapshot = null;
|
|
126
162
|
rolledBack = true;
|
|
@@ -134,32 +170,47 @@ export class ApplyTransaction {
|
|
|
134
170
|
await this.#abortPair(staged);
|
|
135
171
|
staged = null;
|
|
136
172
|
}
|
|
137
|
-
|
|
173
|
+
if (finalizeStarted && hooks.onRollback) {
|
|
174
|
+
await measure("finalizeRollback", () => hooks.onRollback()).catch(() => { });
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
error: message,
|
|
179
|
+
rolledBack,
|
|
180
|
+
sourceMode: requestedSourceMode,
|
|
181
|
+
timings: timings(),
|
|
182
|
+
};
|
|
138
183
|
}
|
|
139
184
|
}
|
|
140
185
|
async #stageMediaFor(manifest) {
|
|
141
186
|
if (!manifest.background) {
|
|
142
187
|
return { image: null, video: null };
|
|
143
188
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
189
|
+
const imagePath = resolveBackgroundImagePath(this.store.paths.activeDir, manifest.background);
|
|
190
|
+
if (!imagePath)
|
|
191
|
+
throw new Error("Background has no image source.");
|
|
192
|
+
const image = await this.media.stage(imagePath, {
|
|
193
|
+
validation: manifest.background.type === "image" && isLocalBackgroundSource(manifest.background)
|
|
194
|
+
? "fast"
|
|
195
|
+
: "full",
|
|
196
|
+
});
|
|
151
197
|
let video = null;
|
|
152
|
-
if (manifest.background.type === "video"
|
|
198
|
+
if (manifest.background.type === "video") {
|
|
199
|
+
// DSH streams this handle for the lifetime of the active <video>. On
|
|
200
|
+
// Windows, serving active/background.mp4 directly keeps the active
|
|
201
|
+
// directory open and the next atomic directory swap fails with EPERM.
|
|
153
202
|
const runtimeVideoPath = await this.store.prepareRuntimeVideo(manifest);
|
|
154
203
|
if (!runtimeVideoPath) {
|
|
155
204
|
throw new Error("Video media staging requires a detached runtime copy.");
|
|
156
205
|
}
|
|
157
|
-
video = await this.media.stage(runtimeVideoPath
|
|
206
|
+
video = await this.media.stage(runtimeVideoPath, {
|
|
207
|
+
validation: isLocalBackgroundSource(manifest.background) ? "fast" : "full",
|
|
208
|
+
});
|
|
158
209
|
}
|
|
159
210
|
return { image, video };
|
|
160
211
|
}
|
|
161
212
|
async #buildPayload(manifest, staged, videoStartAt) {
|
|
162
|
-
return buildHostApplyPayload(this.store, manifest, staged, this.cssText, videoStartAt);
|
|
213
|
+
return buildHostApplyPayload(this.store, manifest, staged, this.cssText, videoStartAt, { includeImageDataUrl: this.includeImageDataUrl });
|
|
163
214
|
}
|
|
164
215
|
async #abortPair(staged) {
|
|
165
216
|
if (!staged)
|
|
@@ -190,7 +241,7 @@ export class ApplyTransaction {
|
|
|
190
241
|
}
|
|
191
242
|
}
|
|
192
243
|
await this.media.commit(restoredMedia);
|
|
193
|
-
await this.store.pruneRuntimeMedia(
|
|
244
|
+
await this.store.pruneRuntimeMedia(runtimeVideoPath);
|
|
194
245
|
restoredMedia = null;
|
|
195
246
|
await this.store.clearSnapshot(snapshot);
|
|
196
247
|
}
|
|
@@ -199,17 +250,7 @@ export class ApplyTransaction {
|
|
|
199
250
|
}
|
|
200
251
|
}
|
|
201
252
|
}
|
|
202
|
-
function
|
|
203
|
-
const keep = [];
|
|
204
|
-
if (staged?.image?.filePath)
|
|
205
|
-
keep.push(staged.image.filePath);
|
|
206
|
-
if (staged?.video?.filePath)
|
|
207
|
-
keep.push(staged.video.filePath);
|
|
208
|
-
if (extra)
|
|
209
|
-
keep.push(extra);
|
|
210
|
-
return keep;
|
|
211
|
-
}
|
|
212
|
-
export async function buildHostApplyPayload(store, manifest, staged, cssText, videoStartAt) {
|
|
253
|
+
export async function buildHostApplyPayload(store, manifest, staged, cssText, videoStartAt, opts = {}) {
|
|
213
254
|
if (!manifest.background) {
|
|
214
255
|
return {
|
|
215
256
|
generation: manifest.generation,
|
|
@@ -220,8 +261,10 @@ export async function buildHostApplyPayload(store, manifest, staged, cssText, vi
|
|
|
220
261
|
cssText,
|
|
221
262
|
};
|
|
222
263
|
}
|
|
223
|
-
const imagePath =
|
|
224
|
-
|
|
264
|
+
const imagePath = resolveBackgroundImagePath(store.paths.activeDir, manifest.background);
|
|
265
|
+
if (!imagePath)
|
|
266
|
+
throw new Error("Background has no image source.");
|
|
267
|
+
const imageDataUrl = opts.includeImageDataUrl === false ? null : await fileToDataUrl(imagePath);
|
|
225
268
|
const imageUrl = staged?.image?.srcUrl ?? null;
|
|
226
269
|
if (manifest.background.type === "image") {
|
|
227
270
|
return {
|
|
@@ -234,10 +277,9 @@ export async function buildHostApplyPayload(store, manifest, staged, cssText, vi
|
|
|
234
277
|
atmosphere: manifest.background.effects ?? null,
|
|
235
278
|
};
|
|
236
279
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const videoPath = path.join(store.paths.activeDir, manifest.background.video);
|
|
280
|
+
const videoPath = resolveBackgroundVideoPath(store.paths.activeDir, manifest.background);
|
|
281
|
+
if (!videoPath)
|
|
282
|
+
throw new Error("Video background has no video source.");
|
|
241
283
|
const runtimeVideoPath = await store.prepareRuntimeVideo(manifest);
|
|
242
284
|
if (!runtimeVideoPath) {
|
|
243
285
|
throw new Error("Video payload requires a detached runtime copy.");
|