castle-web-cli 0.4.124 → 0.4.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/headlessCover.js
CHANGED
|
@@ -33,6 +33,12 @@ const NAV_TIMEOUT_MS = 20_000;
|
|
|
33
33
|
// of frame zero is a loading screen. Deliberately generous: this runs once per
|
|
34
34
|
// save, never in a player's path.
|
|
35
35
|
const SETTLE_MS = 3_500;
|
|
36
|
+
// A ceiling on the whole capture, because this runs inside somebody's "Push to
|
|
37
|
+
// Castle". The steps below are individually bounded, but a browser is a big
|
|
38
|
+
// dependency and this is a cover -- nothing here is worth making a creator
|
|
39
|
+
// watch a spinner for. Whatever is unfinished at this point is abandoned and
|
|
40
|
+
// the save proceeds without a cover.
|
|
41
|
+
const CAPTURE_BUDGET_MS = 60_000;
|
|
36
42
|
// The card the SDK mounts (`initCard`) or a deck marks itself. Falling back to
|
|
37
43
|
// the whole viewport keeps a deck that does neither from getting no cover at
|
|
38
44
|
// all -- the viewport IS the card in play mode.
|
|
@@ -40,20 +46,59 @@ const CARD_SELECTOR = "#castle-card, [data-castle-card]";
|
|
|
40
46
|
// Grab the deck's cover from `serveUrl` and write it to `<projectDir>/.castle`.
|
|
41
47
|
// Never throws; a failure is reported, not raised.
|
|
42
48
|
export async function captureCoverHeadless(opts) {
|
|
49
|
+
// A manager we made is a manager we must close. `withBrowser` deliberately
|
|
50
|
+
// leaves the browser RUNNING so the next call reuses it -- right for the
|
|
51
|
+
// agent, which holds one manager for its whole session, and fatal here:
|
|
52
|
+
// `save-deck` is one-shot, and a live browser connection keeps node's event
|
|
53
|
+
// loop alive, so the command finishes its work and then never exits. The
|
|
54
|
+
// caller waits out its own timeout on a process that is done. (The manager's
|
|
55
|
+
// idle timer is unref'd precisely so it isn't what holds a one-shot open --
|
|
56
|
+
// the browser connection is.)
|
|
57
|
+
//
|
|
58
|
+
// A manager passed IN belongs to the caller; closing it would kill a browser
|
|
59
|
+
// they are still using.
|
|
60
|
+
const owned = !opts.manager;
|
|
43
61
|
const manager = opts.manager ?? createPlaytestBrowserManager();
|
|
44
|
-
const outcome = await manager.withBrowser((browser) => shoot(browser, opts.serveUrl), { onProgress: opts.onProgress });
|
|
45
|
-
if (!outcome.ok)
|
|
46
|
-
return { ok: false, error: outcome.error };
|
|
47
|
-
if (!outcome.value.png)
|
|
48
|
-
return { ok: false, error: outcome.value.error ?? "capture produced no image" };
|
|
49
62
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
const outcome = await withBudget(manager.withBrowser((browser) => shoot(browser, opts.serveUrl), {
|
|
64
|
+
onProgress: opts.onProgress,
|
|
65
|
+
}), CAPTURE_BUDGET_MS);
|
|
66
|
+
if (!outcome)
|
|
67
|
+
return { ok: false, error: `capture gave up after ${CAPTURE_BUDGET_MS / 1000}s` };
|
|
68
|
+
if (!outcome.ok)
|
|
69
|
+
return { ok: false, error: outcome.error };
|
|
70
|
+
if (!outcome.value.png) {
|
|
71
|
+
return { ok: false, error: outcome.value.error ?? "capture produced no image" };
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
fs.mkdirSync(path.dirname(opts.outPath), { recursive: true });
|
|
75
|
+
fs.writeFileSync(opts.outPath, outcome.value.png);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, outPath: opts.outPath, installedMs: outcome.installedMs };
|
|
52
81
|
}
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
finally {
|
|
83
|
+
if (owned)
|
|
84
|
+
await manager.shutdown().catch(() => undefined);
|
|
55
85
|
}
|
|
56
|
-
|
|
86
|
+
}
|
|
87
|
+
// Resolve with `work`, or with null if the budget runs out first. The loser is
|
|
88
|
+
// abandoned rather than cancelled -- the shutdown in the caller's `finally` is
|
|
89
|
+
// what actually stops the browser it was using.
|
|
90
|
+
function withBudget(work, ms) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
const timer = setTimeout(() => resolve(null), ms);
|
|
93
|
+
timer.unref?.();
|
|
94
|
+
void work.then((v) => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
resolve(v);
|
|
97
|
+
}, () => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
resolve(null);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
57
102
|
}
|
|
58
103
|
async function shoot(browser, serveUrl) {
|
|
59
104
|
const context = await browser.newContext({
|