@hyperframes/engine 0.6.118 → 0.6.120

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.
Files changed (73) hide show
  1. package/package.json +24 -7
  2. package/scripts/generate-lut-reference.py +0 -168
  3. package/scripts/test-fitTextFontSize-browser.ts +0 -135
  4. package/src/cdp-headless-experimental.d.ts +0 -54
  5. package/src/config.test.ts +0 -213
  6. package/src/config.ts +0 -417
  7. package/src/index.ts +0 -273
  8. package/src/services/audioMixer.test.ts +0 -326
  9. package/src/services/audioMixer.ts +0 -604
  10. package/src/services/audioMixer.types.ts +0 -35
  11. package/src/services/audioVolumeEnvelope.test.ts +0 -176
  12. package/src/services/audioVolumeEnvelope.ts +0 -138
  13. package/src/services/browserManager.test.ts +0 -330
  14. package/src/services/browserManager.ts +0 -670
  15. package/src/services/chunkEncoder.test.ts +0 -1415
  16. package/src/services/chunkEncoder.ts +0 -831
  17. package/src/services/chunkEncoder.types.ts +0 -60
  18. package/src/services/extractionCache.test.ts +0 -199
  19. package/src/services/extractionCache.ts +0 -216
  20. package/src/services/fileServer.ts +0 -110
  21. package/src/services/frameCapture-discardWarmup.test.ts +0 -183
  22. package/src/services/frameCapture-namePolyfill.test.ts +0 -78
  23. package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
  24. package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
  25. package/src/services/frameCapture-warmupTicks.test.ts +0 -174
  26. package/src/services/frameCapture.test.ts +0 -192
  27. package/src/services/frameCapture.ts +0 -1934
  28. package/src/services/hdrCapture.test.ts +0 -159
  29. package/src/services/hdrCapture.ts +0 -315
  30. package/src/services/parallelCoordinator.test.ts +0 -139
  31. package/src/services/parallelCoordinator.ts +0 -437
  32. package/src/services/screenshotService.test.ts +0 -510
  33. package/src/services/screenshotService.ts +0 -615
  34. package/src/services/streamingEncoder.test.ts +0 -832
  35. package/src/services/streamingEncoder.ts +0 -594
  36. package/src/services/systemMemory.test.ts +0 -324
  37. package/src/services/systemMemory.ts +0 -180
  38. package/src/services/videoFrameExtractor.test.ts +0 -1062
  39. package/src/services/videoFrameExtractor.ts +0 -1139
  40. package/src/services/videoFrameInjector.test.ts +0 -300
  41. package/src/services/videoFrameInjector.ts +0 -687
  42. package/src/services/vp9Options.ts +0 -13
  43. package/src/types.ts +0 -191
  44. package/src/utils/alphaBlit.test.ts +0 -1349
  45. package/src/utils/alphaBlit.ts +0 -1015
  46. package/src/utils/assertSwiftShader.test.ts +0 -130
  47. package/src/utils/assertSwiftShader.ts +0 -126
  48. package/src/utils/ffmpegBinaries.test.ts +0 -43
  49. package/src/utils/ffmpegBinaries.ts +0 -63
  50. package/src/utils/ffprobe.test.ts +0 -342
  51. package/src/utils/ffprobe.ts +0 -457
  52. package/src/utils/gpuEncoder.test.ts +0 -140
  53. package/src/utils/gpuEncoder.ts +0 -268
  54. package/src/utils/hdr.test.ts +0 -191
  55. package/src/utils/hdr.ts +0 -137
  56. package/src/utils/hdrCompositing.test.ts +0 -130
  57. package/src/utils/htmlTemplate.test.ts +0 -42
  58. package/src/utils/htmlTemplate.ts +0 -42
  59. package/src/utils/layerCompositor.test.ts +0 -150
  60. package/src/utils/layerCompositor.ts +0 -58
  61. package/src/utils/parityContract.ts +0 -1
  62. package/src/utils/processTracker.test.ts +0 -74
  63. package/src/utils/processTracker.ts +0 -41
  64. package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
  65. package/src/utils/runFfmpeg.test.ts +0 -102
  66. package/src/utils/runFfmpeg.ts +0 -136
  67. package/src/utils/shaderTransitions.test.ts +0 -738
  68. package/src/utils/shaderTransitions.ts +0 -1130
  69. package/src/utils/uint16-alignment-audit.test.ts +0 -125
  70. package/src/utils/urlDownloader.test.ts +0 -65
  71. package/src/utils/urlDownloader.ts +0 -143
  72. package/tsconfig.json +0 -19
  73. package/vitest.config.ts +0 -7
@@ -1,130 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { groupIntoLayers } from "./layerCompositor.js";
3
- import type { ElementStackingInfo } from "../services/videoFrameInjector.js";
4
-
5
- function makeEl(
6
- id: string,
7
- zIndex: number,
8
- isHdr: boolean,
9
- overrides?: Partial<ElementStackingInfo>,
10
- ): ElementStackingInfo {
11
- return {
12
- id,
13
- zIndex,
14
- x: 0,
15
- y: 0,
16
- width: 1920,
17
- height: 1080,
18
- layoutWidth: 1920,
19
- layoutHeight: 1080,
20
- opacity: 1,
21
- visible: true,
22
- isHdr,
23
- transform: "none",
24
- borderRadius: [0, 0, 0, 0],
25
- objectFit: "cover",
26
- objectPosition: "50% 50%",
27
- clipRect: null,
28
- ...overrides,
29
- };
30
- }
31
-
32
- describe("HDR compositing — opacity filtering", () => {
33
- it("zero-opacity elements remain in groupIntoLayers for hide-list correctness", () => {
34
- const elements = [
35
- makeEl("bg", 0, false),
36
- makeEl("v-hdr", 1, true),
37
- makeEl("overlay", 2, false, { opacity: 0 }),
38
- ];
39
- // Elements stay in layers for correct DOM screenshot hide-lists.
40
- // The compositor skips zero-opacity HDR layers during blit.
41
- const layers = groupIntoLayers(elements);
42
- expect(layers).toHaveLength(3);
43
- expect(layers[0]!.type).toBe("dom");
44
- expect(layers[1]!.type).toBe("hdr");
45
- expect(layers[2]!.type).toBe("dom");
46
- });
47
-
48
- it("zero-opacity HDR element should be skipped during blit", () => {
49
- const el = makeEl("v-hdr", 1, true, { opacity: 0 });
50
- // The compositor checks: if (layer.element.opacity <= 0) continue;
51
- expect(el.opacity).toBe(0);
52
- expect(el.opacity <= 0).toBe(true);
53
- });
54
-
55
- it("low but non-zero opacity HDR elements are NOT skipped", () => {
56
- const el = makeEl("v-hdr", 1, true, { opacity: 0.1 });
57
- expect(el.opacity > 0).toBe(true);
58
- });
59
-
60
- it("child data-start element with parent opacity 0 has effective opacity 0", () => {
61
- const childOverlay = makeEl("s6-text-wrap", 10, false, { opacity: 0 });
62
- expect(childOverlay.opacity).toBe(0);
63
- });
64
-
65
- it("DOM overlay above HDR video is in a separate layer when both visible", () => {
66
- const elements = [makeEl("bg", 0, false), makeEl("v-hdr", 1, true), makeEl("badge", 10, false)];
67
- const layers = groupIntoLayers(elements);
68
- expect(layers).toHaveLength(3);
69
- expect(layers[0]!.type).toBe("dom");
70
- expect(layers[1]!.type).toBe("hdr");
71
- expect(layers[2]!.type).toBe("dom");
72
- if (layers[2]!.type === "dom") {
73
- expect(layers[2]!.elementIds).toEqual(["badge"]);
74
- }
75
- });
76
- });
77
-
78
- describe("HDR compositing — clip rect", () => {
79
- it("clipRect is null when no overflow:hidden ancestor", () => {
80
- const el = makeEl("video", 0, true);
81
- expect(el.clipRect).toBeNull();
82
- });
83
-
84
- it("clipRect constrains element bounds for split-screen", () => {
85
- const el = makeEl("video-left", 0, true, {
86
- x: 0,
87
- y: 0,
88
- width: 1920,
89
- height: 1080,
90
- clipRect: { x: 0, y: 0, width: 960, height: 1080 },
91
- });
92
- const cr = el.clipRect!;
93
- const cx1 = Math.max(el.x, cr.x);
94
- const cy1 = Math.max(el.y, cr.y);
95
- const cx2 = Math.min(el.x + el.width, cr.x + cr.width);
96
- const cy2 = Math.min(el.y + el.height, cr.y + cr.height);
97
- expect(cx2 - cx1).toBe(960);
98
- expect(cy2 - cy1).toBe(1080);
99
- });
100
-
101
- it("fully clipped element produces zero-size intersection", () => {
102
- const el = makeEl("offscreen", 0, true, {
103
- x: 1000,
104
- y: 0,
105
- width: 920,
106
- height: 1080,
107
- clipRect: { x: 0, y: 0, width: 960, height: 1080 },
108
- });
109
- const cr = el.clipRect!;
110
- const cx2 = Math.min(el.x + el.width, cr.x + cr.width);
111
- const cx1 = Math.max(el.x, cr.x);
112
- expect(Math.max(0, cx2 - cx1)).toBe(0);
113
- });
114
-
115
- it("right-half clip produces correct source crop offset", () => {
116
- const el = makeEl("video-right", 0, true, {
117
- x: 960,
118
- y: 0,
119
- width: 1920,
120
- height: 1080,
121
- clipRect: { x: 960, y: 0, width: 960, height: 1080 },
122
- });
123
- const cr = el.clipRect!;
124
- const cx1 = Math.max(el.x, cr.x);
125
- const blitSrcX = cx1 - el.x;
126
- expect(blitSrcX).toBe(0);
127
- const blitW = Math.min(el.x + el.width, cr.x + cr.width) - cx1;
128
- expect(blitW).toBe(960);
129
- });
130
- });
@@ -1,42 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { unwrapTemplate } from "./htmlTemplate.js";
3
-
4
- describe("unwrapTemplate", () => {
5
- it("returns the input unchanged when there is no template wrapper", () => {
6
- const html = `<div>hello</div>`;
7
- expect(unwrapTemplate(html)).toBe(html);
8
- });
9
-
10
- it("unwraps a bare top-level template fragment", () => {
11
- const inner = `<span>hi</span>`;
12
- const html = `<template id="t" data-x="1">${inner}</template>`;
13
- expect(unwrapTemplate(html)).toBe(inner);
14
- });
15
-
16
- it("unwraps a full document whose body only contains a template", () => {
17
- const inner = `<div id="root"><audio id="a" src="a.mp3"></audio></div>`;
18
- const html = `<!doctype html><html><body><template>${inner}</template></body></html>`;
19
- expect(unwrapTemplate(html)).toBe(inner);
20
- });
21
-
22
- it("returns the input unchanged when the closing template tag is missing", () => {
23
- const html = `<template><div>broken`;
24
- expect(unwrapTemplate(html)).toBe(html);
25
- });
26
-
27
- it("returns an empty string for an empty template", () => {
28
- const html = `<body><template></template></body>`;
29
- expect(unwrapTemplate(html)).toBe("");
30
- });
31
-
32
- it("preserves nested templates inside the outer wrapper", () => {
33
- const inner = `outer-before<template>inner-content</template>outer-after`;
34
- const html = `<template>${inner}</template>`;
35
- expect(unwrapTemplate(html)).toBe(inner);
36
- });
37
-
38
- it("leaves multiple sibling templates unchanged", () => {
39
- const html = `<template>a</template>middle<template>b</template>`;
40
- expect(unwrapTemplate(html)).toBe(html);
41
- });
42
- });
@@ -1,42 +0,0 @@
1
- import { parseHTMLContent } from "@hyperframes/core/compiler";
2
-
3
- function getSingleMeaningfulChild(container: Element): Element | null {
4
- let child: Element | null = null;
5
- for (const node of Array.from(container.childNodes)) {
6
- if (node.nodeType === 3 && !(node.textContent || "").trim()) continue;
7
- if (node.nodeType === 8) continue;
8
- if (node.nodeType !== 1) return null;
9
- if (child) return null;
10
- child = node as Element;
11
- }
12
- return child;
13
- }
14
-
15
- /**
16
- * Sub-compositions commonly use a single top-level <template> wrapper. Parse
17
- * the HTML and unwrap only that exact shape, rather than pattern-matching the
18
- * raw string. This avoids both regex backtracking risk and accidental rewrites
19
- * of inputs that contain multiple sibling templates or other top-level content.
20
- */
21
- export function unwrapTemplate(html: string): string {
22
- const lowered = html.toLowerCase();
23
- if (!lowered.includes("<template") || !lowered.includes("</template>")) {
24
- return html;
25
- }
26
-
27
- const { body } = parseHTMLContent(html);
28
- if (!body) return html;
29
-
30
- let container: Element = body;
31
- const bodyWrapper = getSingleMeaningfulChild(container);
32
- if (bodyWrapper?.tagName === "BODY") {
33
- container = bodyWrapper;
34
- }
35
-
36
- const template = getSingleMeaningfulChild(container);
37
- if (template?.tagName !== "TEMPLATE") {
38
- return html;
39
- }
40
-
41
- return template.innerHTML ?? html;
42
- }
@@ -1,150 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { groupIntoLayers } from "./layerCompositor.js";
3
- import type { ElementStackingInfo } from "../services/videoFrameInjector.js";
4
-
5
- function makeEl(
6
- id: string,
7
- zIndex: number,
8
- isHdr: boolean,
9
- overrides?: Partial<ElementStackingInfo>,
10
- ): ElementStackingInfo {
11
- return {
12
- id,
13
- zIndex,
14
- x: 0,
15
- y: 0,
16
- width: 1920,
17
- height: 1080,
18
- layoutWidth: 1920,
19
- layoutHeight: 1080,
20
- opacity: 1,
21
- visible: true,
22
- isHdr,
23
- transform: "none",
24
- borderRadius: [0, 0, 0, 0],
25
- objectFit: "cover",
26
- objectPosition: "50% 50%",
27
- clipRect: null,
28
- ...overrides,
29
- };
30
- }
31
-
32
- describe("groupIntoLayers", () => {
33
- it("single DOM element → 1 DOM layer", () => {
34
- const layers = groupIntoLayers([makeEl("text", 0, false)]);
35
- expect(layers).toHaveLength(1);
36
- expect(layers[0]!.type).toBe("dom");
37
- });
38
-
39
- it("single HDR element → 1 HDR layer", () => {
40
- const layers = groupIntoLayers([makeEl("v-hdr", 0, true)]);
41
- expect(layers).toHaveLength(1);
42
- expect(layers[0]!.type).toBe("hdr");
43
- });
44
-
45
- it("merges adjacent DOM elements into one layer", () => {
46
- const elements = [makeEl("bg", 0, false), makeEl("text", 1, false), makeEl("logo", 2, false)];
47
- const layers = groupIntoLayers(elements);
48
- expect(layers).toHaveLength(1);
49
- expect(layers[0]!.type).toBe("dom");
50
- if (layers[0]!.type === "dom") {
51
- expect(layers[0]!.elementIds).toEqual(["bg", "text", "logo"]);
52
- }
53
- });
54
-
55
- it("splits on HDR/DOM boundary: DOM → HDR → DOM = 3 layers", () => {
56
- const elements = [makeEl("bg", 0, false), makeEl("v-hdr", 1, true), makeEl("title", 2, false)];
57
- const layers = groupIntoLayers(elements);
58
- expect(layers).toHaveLength(3);
59
- expect(layers[0]!.type).toBe("dom");
60
- expect(layers[1]!.type).toBe("hdr");
61
- expect(layers[2]!.type).toBe("dom");
62
- });
63
-
64
- it("merges adjacent DOM around multiple HDR: DOM → HDR → HDR → DOM = 4 layers", () => {
65
- const elements = [
66
- makeEl("bg", 0, false),
67
- makeEl("v-hdr1", 1, true),
68
- makeEl("v-hdr2", 2, true),
69
- makeEl("title", 3, false),
70
- ];
71
- const layers = groupIntoLayers(elements);
72
- expect(layers).toHaveLength(4);
73
- expect(layers[0]!.type).toBe("dom");
74
- expect(layers[1]!.type).toBe("hdr");
75
- expect(layers[2]!.type).toBe("hdr");
76
- expect(layers[3]!.type).toBe("dom");
77
- });
78
-
79
- it("complex case: DOM DOM HDR DOM HDR DOM = 5 layers (2 DOM merges)", () => {
80
- const elements = [
81
- makeEl("bg", 0, false),
82
- makeEl("caption", 1, false),
83
- makeEl("v-hdr1", 2, true),
84
- makeEl("text", 3, false),
85
- makeEl("v-hdr2", 4, true),
86
- makeEl("logo", 5, false),
87
- ];
88
- const layers = groupIntoLayers(elements);
89
- expect(layers).toHaveLength(5);
90
- expect(layers.map((l) => l.type)).toEqual(["dom", "hdr", "dom", "hdr", "dom"]);
91
- if (layers[0]!.type === "dom") {
92
- expect(layers[0]!.elementIds).toEqual(["bg", "caption"]);
93
- }
94
- });
95
-
96
- it("sorts by zIndex before grouping", () => {
97
- const elements = [makeEl("title", 5, false), makeEl("v-hdr", 2, true), makeEl("bg", 0, false)];
98
- const layers = groupIntoLayers(elements);
99
- expect(layers).toHaveLength(3);
100
- expect(layers[0]!.type).toBe("dom"); // bg (z=0)
101
- expect(layers[1]!.type).toBe("hdr"); // v-hdr (z=2)
102
- expect(layers[2]!.type).toBe("dom"); // title (z=5)
103
- });
104
-
105
- it("includes invisible elements in correct z-position", () => {
106
- const elements = [
107
- makeEl("bg", 0, false),
108
- { ...makeEl("hidden-sdr", 1, false), visible: false },
109
- { ...makeEl("hidden-hdr", 2, true), visible: false },
110
- makeEl("title", 3, false),
111
- ];
112
- const layers = groupIntoLayers(elements);
113
- // All elements included — invisible SDR videos need their injected
114
- // <img> replacements hidden from other layers' screenshots
115
- expect(layers).toHaveLength(3);
116
- expect(layers[0]!.type).toBe("dom"); // bg + hidden-sdr (merged)
117
- expect(layers[1]!.type).toBe("hdr"); // hidden-hdr
118
- expect(layers[2]!.type).toBe("dom"); // title
119
- if (layers[0]!.type === "dom") {
120
- expect(layers[0]!.elementIds).toEqual(["bg", "hidden-sdr"]);
121
- }
122
- });
123
-
124
- it("returns an empty array for empty input", () => {
125
- expect(groupIntoLayers([])).toEqual([]);
126
- });
127
-
128
- it("handles negative z-index (valid CSS back layers)", () => {
129
- const elements = [makeEl("fg", 1, false), makeEl("bg", -5, false)];
130
- const layers = groupIntoLayers(elements);
131
- expect(layers).toHaveLength(1);
132
- expect(layers[0]!.type).toBe("dom");
133
- if (layers[0]!.type === "dom") {
134
- expect(layers[0]!.elementIds).toEqual(["bg", "fg"]);
135
- }
136
- });
137
-
138
- it("preserves input order for equal z-index (stable tie-break)", () => {
139
- const elements = [
140
- makeEl("first", 0, false),
141
- makeEl("second", 0, false),
142
- makeEl("third", 0, false),
143
- ];
144
- const layers = groupIntoLayers(elements);
145
- expect(layers).toHaveLength(1);
146
- if (layers[0]!.type === "dom") {
147
- expect(layers[0]!.elementIds).toEqual(["first", "second", "third"]);
148
- }
149
- });
150
- });
@@ -1,58 +0,0 @@
1
- /**
2
- * Layer Compositor — z-order analysis for multi-layer HDR compositing.
3
- *
4
- * Groups timed elements into z-ordered layers (DOM or HDR) for the
5
- * per-frame compositing loop. Adjacent DOM elements merge into a single
6
- * layer to minimize Chrome screenshots.
7
- */
8
-
9
- import type { ElementStackingInfo } from "../services/videoFrameInjector.js";
10
-
11
- export type { ElementStackingInfo };
12
-
13
- export type CompositeLayer =
14
- | { type: "dom"; elementIds: string[] }
15
- | { type: "hdr"; element: ElementStackingInfo };
16
-
17
- /**
18
- * Group z-sorted elements into composite layers. Adjacent DOM elements merge
19
- * into a single layer; each HDR video/image is its own layer.
20
- *
21
- * Elements are sorted by \`zIndex\` ascending (back to front). Ties fall
22
- * through to V8's stable sort, which preserves \`querySelectorAll\` DOM order —
23
- * this is the same order Chrome uses for equal-z elements in a stacking
24
- * context, so the blit order matches what the user sees in-browser.
25
- *
26
- * The DOM merge doesn't lose information: DOM layers are rendered via a
27
- * full-page screenshot with non-layer elements hidden, so within-layer
28
- * z-order is handled by Chrome itself.
29
- *
30
- * Invisible elements ARE included (video elements are hidden by the frame
31
- * injector, but their injected \`<img>\` replacements are visible — they must
32
- * stay in the correct z-ordered layer so sibling layers' DOM screenshots
33
- * hide them).
34
- */
35
- export function groupIntoLayers(elements: ElementStackingInfo[]): CompositeLayer[] {
36
- // Include ALL elements regardless of visibility. Video elements are hidden by
37
- // the frame injector (HEVC can't decode in headless Chrome) but their injected
38
- // <img> replacements ARE visible. We need them in the correct z-ordered layer
39
- // so they get hidden from other layers' DOM screenshots.
40
- const sorted = [...elements].sort((a, b) => a.zIndex - b.zIndex);
41
-
42
- const layers: CompositeLayer[] = [];
43
-
44
- for (const el of sorted) {
45
- if (el.isHdr) {
46
- layers.push({ type: "hdr", element: el });
47
- } else {
48
- const last = layers[layers.length - 1];
49
- if (last && last.type === "dom") {
50
- last.elementIds.push(el.id);
51
- } else {
52
- layers.push({ type: "dom", elementIds: [el.id] });
53
- }
54
- }
55
- }
56
-
57
- return layers;
58
- }
@@ -1 +0,0 @@
1
- export { MEDIA_VISUAL_STYLE_PROPERTIES, quantizeTimeToFrame } from "@hyperframes/core";
@@ -1,74 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { spawn } from "node:child_process";
3
- import { trackChildProcess, killTrackedProcesses } from "./processTracker.js";
4
-
5
- // Reset tracked set between tests by killing everything
6
- beforeEach(() => {
7
- killTrackedProcesses();
8
- });
9
-
10
- describe("trackChildProcess", () => {
11
- it("tracks a spawned process and removes it after exit", async () => {
12
- const proc = spawn("echo", ["hello"], { stdio: "ignore" });
13
- trackChildProcess(proc);
14
-
15
- await new Promise<void>((resolve) => proc.on("close", resolve));
16
-
17
- // After exit, killTrackedProcesses should be a no-op (nothing to kill)
18
- killTrackedProcesses();
19
- });
20
-
21
- it("removes the process on spawn error", async () => {
22
- const proc = spawn("/nonexistent-binary-that-does-not-exist", { stdio: "ignore" });
23
- trackChildProcess(proc);
24
-
25
- await new Promise<void>((resolve) => proc.on("error", () => resolve()));
26
-
27
- killTrackedProcesses();
28
- });
29
- });
30
-
31
- describe("killTrackedProcesses", () => {
32
- it("kills a running process", async () => {
33
- const proc = spawn("sleep", ["60"], { stdio: "ignore" });
34
- trackChildProcess(proc);
35
-
36
- const exitPromise = new Promise<number | null>((resolve) => proc.on("close", resolve));
37
- killTrackedProcesses();
38
-
39
- const code = await exitPromise;
40
- // SIGTERM exit: code is null (killed by signal)
41
- expect(code).toBeNull();
42
- });
43
-
44
- it("handles already-exited processes gracefully", async () => {
45
- const proc = spawn("true", { stdio: "ignore" });
46
- trackChildProcess(proc);
47
-
48
- await new Promise<void>((resolve) => proc.on("close", resolve));
49
-
50
- // Should not throw even though process already exited
51
- killTrackedProcesses();
52
- });
53
-
54
- it("escalates to SIGKILL for processes that ignore SIGTERM", async () => {
55
- // Spawn a process that traps SIGTERM (bash ignoring it)
56
- const proc = spawn("bash", ["-c", "trap '' TERM; sleep 60"], { stdio: "ignore" });
57
- trackChildProcess(proc);
58
-
59
- const exitPromise = new Promise<void>((resolve) => proc.on("close", resolve));
60
- killTrackedProcesses();
61
-
62
- // The 500ms SIGKILL escalation should kill it
63
- await exitPromise;
64
- expect(proc.killed).toBe(true);
65
- }, 5000);
66
-
67
- it("is idempotent — second call is a no-op", () => {
68
- const proc = spawn("sleep", ["60"], { stdio: "ignore" });
69
- trackChildProcess(proc);
70
-
71
- killTrackedProcesses();
72
- killTrackedProcesses();
73
- });
74
- });
@@ -1,41 +0,0 @@
1
- import type { ChildProcess } from "node:child_process";
2
-
3
- const tracked = new Set<ChildProcess>();
4
-
5
- export function trackChildProcess(proc: ChildProcess): void {
6
- tracked.add(proc);
7
- const remove = () => tracked.delete(proc);
8
- proc.once("exit", remove);
9
- proc.once("error", remove);
10
- }
11
-
12
- /**
13
- * SIGTERM all tracked child processes, then SIGKILL any that survive
14
- * after a short grace period.
15
- */
16
- export function killTrackedProcesses(): void {
17
- const alive: ChildProcess[] = [];
18
- for (const proc of tracked) {
19
- if (!proc.killed) {
20
- try {
21
- proc.kill("SIGTERM");
22
- alive.push(proc);
23
- } catch {
24
- // Already exited between the check and the kill.
25
- }
26
- }
27
- }
28
- tracked.clear();
29
-
30
- if (alive.length === 0) return;
31
-
32
- setTimeout(() => {
33
- for (const proc of alive) {
34
- try {
35
- proc.kill("SIGKILL");
36
- } catch {
37
- // Already exited.
38
- }
39
- }
40
- }, 500).unref();
41
- }
@@ -1,52 +0,0 @@
1
- /**
2
- * Read SwiftShader vendor/renderer via a 1×1 WebGL canvas + the
3
- * `WEBGL_debug_renderer_info` extension. Used as the `readInfo` override
4
- * for {@link assertSwiftShader} when the worker is running on
5
- * `chrome-headless-shell` — that build serves `chrome://gpu` as an empty
6
- * document so the default `chrome://gpu`-based info reader trips
7
- * `net::ERR_FAILED` even when the GL backend is in fact SwiftShader.
8
- *
9
- * The canvas-based probe runs against whatever page the caller hands in
10
- * (we use a fresh `about:blank` so it doesn't depend on the composition
11
- * URL being navigated yet). The renderer string returned matches the
12
- * format `assertSwiftShader` expects (substring match against
13
- * `"swiftshader"`).
14
- */
15
-
16
- import type { Page } from "puppeteer-core";
17
-
18
- export async function readWebGlVendorInfoFromCanvas(
19
- page: Page,
20
- ): Promise<{ vendor: string; renderer: string }> {
21
- await page.goto("about:blank", { waitUntil: "domcontentloaded", timeout: 30_000 });
22
- return page.evaluate((): { vendor: string; renderer: string } => {
23
- try {
24
- const canvas = document.createElement("canvas");
25
- const gl =
26
- (canvas.getContext("webgl") as WebGLRenderingContext | null) ??
27
- (canvas.getContext("experimental-webgl") as WebGLRenderingContext | null);
28
- if (!gl) {
29
- return { vendor: "", renderer: "" };
30
- }
31
- const ext = gl.getExtension("WEBGL_debug_renderer_info") as {
32
- UNMASKED_VENDOR_WEBGL: number;
33
- UNMASKED_RENDERER_WEBGL: number;
34
- } | null;
35
- if (!ext) {
36
- return {
37
- vendor: String(gl.getParameter(gl.VENDOR) ?? ""),
38
- renderer: String(gl.getParameter(gl.RENDERER) ?? ""),
39
- };
40
- }
41
- // Older Chrome builds expose the unmasked strings under the literal
42
- // numeric constants 0x9245 / 0x9246. The extension surface above is
43
- // identical across builds — read through it.
44
- return {
45
- vendor: String(gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) ?? ""),
46
- renderer: String(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) ?? ""),
47
- };
48
- } catch {
49
- return { vendor: "", renderer: "" };
50
- }
51
- });
52
- }