@rehearsal-labs/retest 0.1.0 → 0.2.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.
Files changed (315) hide show
  1. package/README.md +6 -0
  2. package/dist/agent/session.d.ts +21 -2
  3. package/dist/agent/session.d.ts.map +1 -1
  4. package/dist/agent/session.js +73 -11
  5. package/dist/api/app-page.d.ts +39 -3
  6. package/dist/api/app-page.d.ts.map +1 -1
  7. package/dist/api/app-page.js +108 -0
  8. package/dist/api/dialog.d.ts +38 -0
  9. package/dist/api/dialog.d.ts.map +1 -0
  10. package/dist/api/dialog.js +78 -0
  11. package/dist/api/drag.d.ts +32 -0
  12. package/dist/api/drag.d.ts.map +1 -0
  13. package/dist/api/drag.js +26 -0
  14. package/dist/api/files.d.ts +49 -0
  15. package/dist/api/files.d.ts.map +1 -0
  16. package/dist/api/files.js +168 -0
  17. package/dist/api/inbox.d.ts +97 -0
  18. package/dist/api/inbox.d.ts.map +1 -0
  19. package/dist/api/inbox.js +191 -0
  20. package/dist/api/locator-recipes.d.ts +1 -1
  21. package/dist/api/locator-recipes.d.ts.map +1 -1
  22. package/dist/api/locator-recipes.js +4 -3
  23. package/dist/api/page.d.ts +214 -4
  24. package/dist/api/page.d.ts.map +1 -1
  25. package/dist/api/route.d.ts +21 -0
  26. package/dist/api/route.d.ts.map +1 -0
  27. package/dist/api/route.js +99 -0
  28. package/dist/api/test-run.d.ts +127 -1
  29. package/dist/api/test-run.d.ts.map +1 -1
  30. package/dist/api/test-run.js +293 -0
  31. package/dist/assertions/expect.d.ts +91 -2
  32. package/dist/assertions/expect.d.ts.map +1 -1
  33. package/dist/assertions/expect.js +98 -0
  34. package/dist/assertions/look-until.d.ts +5 -0
  35. package/dist/assertions/look-until.d.ts.map +1 -1
  36. package/dist/assertions/look-until.js +1 -1
  37. package/dist/assertions/poll-layout.d.ts +24 -0
  38. package/dist/assertions/poll-layout.d.ts.map +1 -0
  39. package/dist/assertions/poll-layout.js +63 -0
  40. package/dist/assertions/screenshot.d.ts +36 -0
  41. package/dist/assertions/screenshot.d.ts.map +1 -0
  42. package/dist/assertions/screenshot.js +125 -0
  43. package/dist/browser/accessibility.d.ts.map +1 -1
  44. package/dist/browser/accessibility.js +22 -1
  45. package/dist/browser/actionability.d.ts +9 -6
  46. package/dist/browser/actionability.d.ts.map +1 -1
  47. package/dist/browser/actionability.js +17 -8
  48. package/dist/browser/browser.d.ts.map +1 -1
  49. package/dist/browser/browser.js +15 -4
  50. package/dist/browser/cdp/connection.d.ts +6 -0
  51. package/dist/browser/cdp/connection.d.ts.map +1 -1
  52. package/dist/browser/cdp/connection.js +8 -0
  53. package/dist/browser/checked-state.d.ts +2 -2
  54. package/dist/browser/checked-state.d.ts.map +1 -1
  55. package/dist/browser/chromium-process.d.ts +9 -2
  56. package/dist/browser/chromium-process.d.ts.map +1 -1
  57. package/dist/browser/chromium-process.js +17 -4
  58. package/dist/browser/contract.d.ts +119 -5
  59. package/dist/browser/contract.d.ts.map +1 -1
  60. package/dist/browser/dialogs.d.ts +157 -0
  61. package/dist/browser/dialogs.d.ts.map +1 -0
  62. package/dist/browser/dialogs.js +271 -0
  63. package/dist/browser/downloads.d.ts +71 -0
  64. package/dist/browser/downloads.d.ts.map +1 -0
  65. package/dist/browser/downloads.js +253 -0
  66. package/dist/browser/drag.d.ts +50 -0
  67. package/dist/browser/drag.d.ts.map +1 -0
  68. package/dist/browser/drag.js +584 -0
  69. package/dist/browser/electron-window-watch.d.ts +32 -0
  70. package/dist/browser/electron-window-watch.d.ts.map +1 -0
  71. package/dist/browser/electron-window-watch.js +93 -0
  72. package/dist/browser/electron.d.ts +13 -2
  73. package/dist/browser/electron.d.ts.map +1 -1
  74. package/dist/browser/electron.js +53 -8
  75. package/dist/browser/element-queries.d.ts +51 -15
  76. package/dist/browser/element-queries.d.ts.map +1 -1
  77. package/dist/browser/element-queries.js +92 -13
  78. package/dist/browser/emulation.d.ts.map +1 -1
  79. package/dist/browser/emulation.js +7 -5
  80. package/dist/browser/file-chooser.d.ts +75 -0
  81. package/dist/browser/file-chooser.d.ts.map +1 -0
  82. package/dist/browser/file-chooser.js +449 -0
  83. package/dist/browser/firefox/input.d.ts +5 -0
  84. package/dist/browser/firefox/input.d.ts.map +1 -1
  85. package/dist/browser/firefox/input.js +6 -3
  86. package/dist/browser/firefox/page.d.ts +11 -1
  87. package/dist/browser/firefox/page.d.ts.map +1 -1
  88. package/dist/browser/firefox/page.js +66 -7
  89. package/dist/browser/frames.d.ts +205 -0
  90. package/dist/browser/frames.d.ts.map +1 -0
  91. package/dist/browser/frames.js +657 -0
  92. package/dist/browser/input-guard.d.ts +18 -5
  93. package/dist/browser/input-guard.d.ts.map +1 -1
  94. package/dist/browser/input-guard.js +65 -6
  95. package/dist/browser/isolated-world.d.ts +8 -1
  96. package/dist/browser/isolated-world.d.ts.map +1 -1
  97. package/dist/browser/layout.d.ts +120 -0
  98. package/dist/browser/layout.d.ts.map +1 -0
  99. package/dist/browser/layout.js +213 -0
  100. package/dist/browser/locate.d.ts +10 -0
  101. package/dist/browser/locate.d.ts.map +1 -1
  102. package/dist/browser/locate.js +24 -6
  103. package/dist/browser/origin-document.d.ts +4 -0
  104. package/dist/browser/origin-document.d.ts.map +1 -1
  105. package/dist/browser/origin-document.js +9 -1
  106. package/dist/browser/page-scripts.d.ts +48 -0
  107. package/dist/browser/page-scripts.d.ts.map +1 -1
  108. package/dist/browser/page-scripts.js +515 -39
  109. package/dist/browser/page.d.ts +60 -1
  110. package/dist/browser/page.d.ts.map +1 -1
  111. package/dist/browser/page.js +275 -32
  112. package/dist/browser/popups.d.ts +166 -0
  113. package/dist/browser/popups.d.ts.map +1 -0
  114. package/dist/browser/popups.js +317 -0
  115. package/dist/browser/routes.d.ts +97 -0
  116. package/dist/browser/routes.d.ts.map +1 -0
  117. package/dist/browser/routes.js +226 -0
  118. package/dist/browser/screenshot.d.ts +44 -0
  119. package/dist/browser/screenshot.d.ts.map +1 -0
  120. package/dist/browser/screenshot.js +51 -0
  121. package/dist/browser/shadow-candidates.d.ts +20 -0
  122. package/dist/browser/shadow-candidates.d.ts.map +1 -0
  123. package/dist/browser/shadow-candidates.js +77 -0
  124. package/dist/browser/webkit/page.d.ts +11 -1
  125. package/dist/browser/webkit/page.d.ts.map +1 -1
  126. package/dist/browser/webkit/page.js +53 -2
  127. package/dist/browser/webkit/relay.d.ts.map +1 -1
  128. package/dist/browser/webkit/relay.js +5 -1
  129. package/dist/cli/commands/run.d.ts.map +1 -1
  130. package/dist/cli/commands/run.js +2 -0
  131. package/dist/cli/inspect/test-timeline.d.ts.map +1 -1
  132. package/dist/cli/inspect/test-timeline.js +38 -4
  133. package/dist/config/loaded.d.ts +12 -2
  134. package/dist/config/loaded.d.ts.map +1 -1
  135. package/dist/config/read-inboxes.d.ts +10 -0
  136. package/dist/config/read-inboxes.d.ts.map +1 -0
  137. package/dist/config/read-inboxes.js +78 -0
  138. package/dist/config/register.d.ts +5 -1
  139. package/dist/config/register.d.ts.map +1 -1
  140. package/dist/config/types.d.ts +36 -0
  141. package/dist/config/types.d.ts.map +1 -1
  142. package/dist/config/validate.d.ts.map +1 -1
  143. package/dist/config/validate.js +4 -1
  144. package/dist/diagnostics/artifact.d.ts.map +1 -1
  145. package/dist/diagnostics/artifact.js +10 -2
  146. package/dist/diagnostics/attempt.d.ts +5 -0
  147. package/dist/diagnostics/attempt.d.ts.map +1 -1
  148. package/dist/diagnostics/attempt.js +10 -0
  149. package/dist/diagnostics/chromium-collector.d.ts +9 -1
  150. package/dist/diagnostics/chromium-collector.d.ts.map +1 -1
  151. package/dist/diagnostics/chromium-collector.js +19 -4
  152. package/dist/diagnostics/observations.d.ts +8 -0
  153. package/dist/diagnostics/observations.d.ts.map +1 -1
  154. package/dist/diagnostics/session-capture.d.ts.map +1 -1
  155. package/dist/diagnostics/session-capture.js +8 -0
  156. package/dist/index.d.ts +9 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +1 -0
  159. package/dist/media/policy.d.ts +2 -2
  160. package/dist/media/policy.d.ts.map +1 -1
  161. package/dist/media/policy.js +1 -1
  162. package/dist/playwright/not-yet.d.ts.map +1 -1
  163. package/dist/playwright/not-yet.js +5 -2
  164. package/dist/protocol/commands.d.ts +122 -13
  165. package/dist/protocol/commands.d.ts.map +1 -1
  166. package/dist/protocol/commands.js +85 -6
  167. package/dist/protocol/diagnostics.d.ts +10 -0
  168. package/dist/protocol/diagnostics.d.ts.map +1 -1
  169. package/dist/protocol/diagnostics.js +5 -0
  170. package/dist/protocol/dialogs.d.ts +104 -0
  171. package/dist/protocol/dialogs.d.ts.map +1 -0
  172. package/dist/protocol/dialogs.js +101 -0
  173. package/dist/protocol/drag.d.ts +45 -0
  174. package/dist/protocol/drag.d.ts.map +1 -0
  175. package/dist/protocol/drag.js +31 -0
  176. package/dist/protocol/events.d.ts +153 -1
  177. package/dist/protocol/events.d.ts.map +1 -1
  178. package/dist/protocol/events.js +97 -0
  179. package/dist/protocol/files.d.ts +83 -0
  180. package/dist/protocol/files.d.ts.map +1 -0
  181. package/dist/protocol/files.js +68 -0
  182. package/dist/protocol/inbox.d.ts +87 -0
  183. package/dist/protocol/inbox.d.ts.map +1 -0
  184. package/dist/protocol/inbox.js +51 -0
  185. package/dist/protocol/index.d.ts +4 -1
  186. package/dist/protocol/index.d.ts.map +1 -1
  187. package/dist/protocol/layout-checks.d.ts +83 -0
  188. package/dist/protocol/layout-checks.d.ts.map +1 -0
  189. package/dist/protocol/layout-checks.js +214 -0
  190. package/dist/protocol/layout.d.ts +135 -0
  191. package/dist/protocol/layout.d.ts.map +1 -0
  192. package/dist/protocol/layout.js +124 -0
  193. package/dist/protocol/locator-checks.d.ts +19 -2
  194. package/dist/protocol/locator-checks.d.ts.map +1 -1
  195. package/dist/protocol/locator-checks.js +23 -1
  196. package/dist/protocol/locator.d.ts +55 -7
  197. package/dist/protocol/locator.d.ts.map +1 -1
  198. package/dist/protocol/locator.js +99 -18
  199. package/dist/protocol/messages.d.ts +95 -0
  200. package/dist/protocol/messages.d.ts.map +1 -1
  201. package/dist/protocol/messages.js +63 -0
  202. package/dist/protocol/popups.d.ts +43 -0
  203. package/dist/protocol/popups.d.ts.map +1 -0
  204. package/dist/protocol/popups.js +27 -0
  205. package/dist/protocol/result.d.ts +5 -0
  206. package/dist/protocol/result.d.ts.map +1 -1
  207. package/dist/protocol/result.js +1 -0
  208. package/dist/protocol/routes.d.ts +136 -0
  209. package/dist/protocol/routes.d.ts.map +1 -0
  210. package/dist/protocol/routes.js +191 -0
  211. package/dist/protocol/screenshot.d.ts +219 -0
  212. package/dist/protocol/screenshot.d.ts.map +1 -0
  213. package/dist/protocol/screenshot.js +335 -0
  214. package/dist/reporters/actions.d.ts +6 -4
  215. package/dist/reporters/actions.d.ts.map +1 -1
  216. package/dist/reporters/actions.js +24 -6
  217. package/dist/reporters/agent.d.ts.map +1 -1
  218. package/dist/reporters/agent.js +4 -2
  219. package/dist/reporters/dialogs-popups.d.ts +22 -0
  220. package/dist/reporters/dialogs-popups.d.ts.map +1 -0
  221. package/dist/reporters/dialogs-popups.js +30 -0
  222. package/dist/reporters/failure-card.d.ts +10 -1
  223. package/dist/reporters/failure-card.d.ts.map +1 -1
  224. package/dist/reporters/failure-card.js +5 -1
  225. package/dist/reporters/format.d.ts +33 -0
  226. package/dist/reporters/format.d.ts.map +1 -1
  227. package/dist/reporters/format.js +42 -0
  228. package/dist/reporters/html/comparison-view.d.ts +12 -0
  229. package/dist/reporters/html/comparison-view.d.ts.map +1 -0
  230. package/dist/reporters/html/comparison-view.js +21 -0
  231. package/dist/reporters/html/diagnostics-view.js +8 -0
  232. package/dist/reporters/html/downloads-view.d.ts +10 -0
  233. package/dist/reporters/html/downloads-view.d.ts.map +1 -0
  234. package/dist/reporters/html/downloads-view.js +29 -0
  235. package/dist/reporters/html/style.d.ts.map +1 -1
  236. package/dist/reporters/html/style.js +3 -0
  237. package/dist/reporters/html/summary-view.d.ts.map +1 -1
  238. package/dist/reporters/html/summary-view.js +3 -2
  239. package/dist/reporters/html/test-view.d.ts.map +1 -1
  240. package/dist/reporters/html/test-view.js +6 -3
  241. package/dist/reporters/html/timeline-view.d.ts.map +1 -1
  242. package/dist/reporters/html/timeline-view.js +26 -2
  243. package/dist/reporters/human-card.js +10 -0
  244. package/dist/reporters/human.d.ts.map +1 -1
  245. package/dist/reporters/human.js +3 -1
  246. package/dist/reporters/routes.d.ts +9 -0
  247. package/dist/reporters/routes.d.ts.map +1 -0
  248. package/dist/reporters/routes.js +17 -0
  249. package/dist/runner/child-program.js +10 -0
  250. package/dist/runner/contract.d.ts +5 -0
  251. package/dist/runner/contract.d.ts.map +1 -1
  252. package/dist/runner/dialogs.d.ts +71 -0
  253. package/dist/runner/dialogs.d.ts.map +1 -0
  254. package/dist/runner/dialogs.js +189 -0
  255. package/dist/runner/files.d.ts +79 -0
  256. package/dist/runner/files.d.ts.map +1 -0
  257. package/dist/runner/files.js +312 -0
  258. package/dist/runner/inbox-links.d.ts +19 -0
  259. package/dist/runner/inbox-links.d.ts.map +1 -0
  260. package/dist/runner/inbox-links.js +63 -0
  261. package/dist/runner/inbox.d.ts +87 -0
  262. package/dist/runner/inbox.d.ts.map +1 -0
  263. package/dist/runner/inbox.js +340 -0
  264. package/dist/runner/index.d.ts +2 -1
  265. package/dist/runner/index.d.ts.map +1 -1
  266. package/dist/runner/native-pool.d.ts.map +1 -1
  267. package/dist/runner/native-pool.js +2 -0
  268. package/dist/runner/observations.d.ts +15 -2
  269. package/dist/runner/observations.d.ts.map +1 -1
  270. package/dist/runner/observations.js +109 -6
  271. package/dist/runner/popups.d.ts +82 -0
  272. package/dist/runner/popups.d.ts.map +1 -0
  273. package/dist/runner/popups.js +236 -0
  274. package/dist/runner/redactor.d.ts.map +1 -1
  275. package/dist/runner/redactor.js +10 -6
  276. package/dist/runner/routes.d.ts +23 -0
  277. package/dist/runner/routes.d.ts.map +1 -0
  278. package/dist/runner/routes.js +128 -0
  279. package/dist/runner/run-media.d.ts +6 -0
  280. package/dist/runner/run-media.d.ts.map +1 -1
  281. package/dist/runner/run-media.js +21 -0
  282. package/dist/runner/run-session.d.ts.map +1 -1
  283. package/dist/runner/run-session.js +93 -8
  284. package/dist/runner/running-test.d.ts +35 -0
  285. package/dist/runner/running-test.d.ts.map +1 -1
  286. package/dist/runner/running-test.js +333 -22
  287. package/dist/runner/screenshot-baselines.d.ts +159 -0
  288. package/dist/runner/screenshot-baselines.d.ts.map +1 -0
  289. package/dist/runner/screenshot-baselines.js +357 -0
  290. package/dist/runner/secrets.d.ts +13 -0
  291. package/dist/runner/secrets.d.ts.map +1 -1
  292. package/dist/runner/secrets.js +6 -4
  293. package/dist/runner/test-pages.d.ts +3 -1
  294. package/dist/runner/test-pages.d.ts.map +1 -1
  295. package/dist/runner/test-pages.js +4 -3
  296. package/dist/schemas/event-v1.schema.json +118873 -6311
  297. package/dist/schemas/result-v1.schema.json +4 -0
  298. package/dist/shared/image-diff.d.ts +36 -0
  299. package/dist/shared/image-diff.d.ts.map +1 -0
  300. package/dist/shared/image-diff.js +242 -0
  301. package/dist/shared/metadata-process.d.ts +4 -1
  302. package/dist/shared/metadata-process.d.ts.map +1 -1
  303. package/dist/shared/metadata-process.js +51 -9
  304. package/dist/shared/png.d.ts +35 -0
  305. package/dist/shared/png.d.ts.map +1 -0
  306. package/dist/shared/png.js +251 -0
  307. package/dist/shared/process-ownership.d.ts +15 -4
  308. package/dist/shared/process-ownership.d.ts.map +1 -1
  309. package/dist/shared/process-ownership.js +123 -34
  310. package/dist/store/artifacts.d.ts +23 -2
  311. package/dist/store/artifacts.d.ts.map +1 -1
  312. package/dist/store/artifacts.js +40 -1
  313. package/dist/store/rebuild-result.d.ts.map +1 -1
  314. package/dist/store/rebuild-result.js +6 -0
  315. package/package.json +1 -1
@@ -0,0 +1,251 @@
1
+ import { crc32, deflateSync, inflateSync } from 'node:zlib';
2
+ /** Thrown for bytes that are not a PNG Retest reads. The message says what the bytes were. */
3
+ export class PngError extends Error {
4
+ name = 'PngError';
5
+ }
6
+ /** The most bytes a decoded picture may take, filter bytes included, so a damaged file cannot ask for all memory. */
7
+ export const maxPictureBytes = 512 * 1024 * 1024;
8
+ const signature = Uint8Array.of(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a);
9
+ const readable = 'Retest reads 8-bit RGB and RGBA PNGs without interlacing, as browsers write them';
10
+ // Bytes per pixel by colour type, for the two types Retest reads: RGB and RGBA.
11
+ const channelsByType = { 2: 3, 6: 4 };
12
+ const typeNames = { 0: 'greyscale', 3: 'a palette of colours', 4: 'greyscale with alpha' };
13
+ /**
14
+ * Decodes a PNG into RGBA pixels: 8-bit RGB or RGBA, not interlaced, its rows filtered with any of the five filters.
15
+ * Every chunk's checksum is checked. RGB pixels get an alpha of 255. Throws `PngError` for anything else.
16
+ *
17
+ * @example readPng(bytes).width // 1280
18
+ */
19
+ export function readPng(bytes) {
20
+ if (bytes.length < signature.length || signature.some((byte, index) => bytes[index] !== byte))
21
+ throw new PngError('The file is not a PNG: it does not start with the PNG signature.');
22
+ const chunks = readChunks(bytes);
23
+ const [header] = chunks;
24
+ if (header === undefined || header.type !== 'IHDR' || header.data.length !== 13)
25
+ throw new PngError('The PNG does not start with its header chunk.');
26
+ const view = new DataView(header.data.buffer, header.data.byteOffset, header.data.byteLength);
27
+ const width = view.getUint32(0);
28
+ const height = view.getUint32(4);
29
+ const [bitDepth, colourType, compression, filterMethod, interlace] = header.data.subarray(8);
30
+ if (width === 0 || height === 0)
31
+ throw new PngError(`The PNG is ${width} by ${height} pixels, and a picture needs at least one pixel each way.`);
32
+ if (bitDepth !== 8)
33
+ throw new PngError(`The PNG has ${String(bitDepth)}-bit channels. ${readable}.`);
34
+ const channels = channelsByType[colourType ?? -1];
35
+ if (channels === undefined)
36
+ throw new PngError(`The PNG holds ${typeNames[colourType ?? -1] ?? `colour type ${String(colourType)}`}. ${readable}.`);
37
+ if (interlace !== 0)
38
+ throw new PngError(`The PNG is interlaced. ${readable}.`);
39
+ if (compression !== 0 || filterMethod !== 0)
40
+ throw new PngError('The PNG names a compression or filter method the format does not define.');
41
+ const stride = width * channels;
42
+ const filtered = height * (stride + 1);
43
+ if (!Number.isSafeInteger(filtered) || filtered > maxPictureBytes || width * height * 4 > maxPictureBytes)
44
+ throw new PngError(`The PNG is ${width} by ${height} pixels, more than Retest decodes.`);
45
+ const text = {};
46
+ const compressed = [];
47
+ let ended = false;
48
+ for (const chunk of chunks.slice(1)) {
49
+ if (chunk.type === 'IEND') {
50
+ ended = true;
51
+ break;
52
+ }
53
+ if (chunk.type === 'IHDR')
54
+ throw new PngError('The PNG has a second header chunk.');
55
+ if (chunk.type === 'IDAT')
56
+ compressed.push(chunk.data);
57
+ else if (chunk.type === 'tEXt')
58
+ readText(chunk.data, text);
59
+ // A chunk whose type starts with a capital letter is critical: a reader that does not know it must not show the picture.
60
+ else if (chunk.type !== 'PLTE' && isCritical(chunk.type))
61
+ throw new PngError(`The PNG holds a ${chunk.type} chunk, which Retest does not know and must not skip.`);
62
+ }
63
+ if (!ended)
64
+ throw new PngError('The PNG ends before its end chunk, so it is cut short.');
65
+ if (compressed.length === 0)
66
+ throw new PngError('The PNG holds no image data.');
67
+ let raw;
68
+ try {
69
+ raw = inflateSync(Buffer.concat(compressed), { maxOutputLength: filtered });
70
+ }
71
+ catch {
72
+ throw new PngError(`The PNG's image data does not inflate to the ${width} by ${height} pixels its header states.`);
73
+ }
74
+ if (raw.length !== filtered)
75
+ throw new PngError(`The PNG's image data holds ${raw.length} bytes, and ${width} by ${height} pixels need ${filtered}.`);
76
+ return { width, height, data: toRgba(unfilter(raw, height, stride, channels), channels), text };
77
+ }
78
+ /**
79
+ * Encodes RGBA pixels as a PNG: RGB when every pixel is opaque, RGBA otherwise, each row with the filter that leaves the
80
+ * smallest sum, as libpng chooses, and any `tEXt` chunks the options name.
81
+ *
82
+ * @example writePng({ width: 1, height: 1, data: Uint8Array.of(255, 0, 0, 255) }, { text: { 'retest:scale': '2' } })
83
+ */
84
+ export function writePng(image, options = {}) {
85
+ const { width, height, data } = image;
86
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 1 || height < 1)
87
+ throw new RangeError(`A picture is at least 1 by 1 pixel, not ${width} by ${height}.`);
88
+ if (data.length !== width * height * 4)
89
+ throw new RangeError(`${width} by ${height} RGBA pixels take ${width * height * 4} bytes, not ${data.length}.`);
90
+ const opaque = isOpaque(data);
91
+ const channels = opaque ? 3 : 4;
92
+ const pixels = opaque ? toRgb(data) : data;
93
+ const header = new Uint8Array(13);
94
+ const view = new DataView(header.buffer);
95
+ view.setUint32(0, width);
96
+ view.setUint32(4, height);
97
+ header.set([8, opaque ? 2 : 6, 0, 0, 0], 8);
98
+ const texts = Object.entries(options.text ?? {}).map(([keyword, value]) => chunk('tEXt', textData(keyword, value)));
99
+ const compressed = deflateSync(filterRows(pixels, height, width * channels, channels));
100
+ return Buffer.concat([signature, chunk('IHDR', header), ...texts, chunk('IDAT', compressed), chunk('IEND', new Uint8Array(0))]);
101
+ }
102
+ function readChunks(bytes) {
103
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
104
+ const chunks = [];
105
+ let offset = signature.length;
106
+ while (offset < bytes.length) {
107
+ if (offset + 12 > bytes.length)
108
+ throw new PngError('The PNG is cut short inside a chunk.');
109
+ const length = view.getUint32(offset);
110
+ const end = offset + 12 + length;
111
+ if (end > bytes.length)
112
+ throw new PngError('The PNG is cut short inside a chunk.');
113
+ const typeBytes = bytes.subarray(offset + 4, offset + 8);
114
+ const type = String.fromCharCode(...typeBytes);
115
+ if (!/^[A-Za-z]{4}$/.test(type))
116
+ throw new PngError('The PNG holds a chunk whose type is not four letters, so it is damaged.');
117
+ const data = bytes.subarray(offset + 8, offset + 8 + length);
118
+ const stated = view.getUint32(offset + 8 + length);
119
+ if (crc32(bytes.subarray(offset + 4, offset + 8 + length)) !== stated)
120
+ throw new PngError(`The PNG's ${type} chunk fails its checksum, so the file is damaged.`);
121
+ chunks.push({ type, data });
122
+ offset = end;
123
+ if (type === 'IEND')
124
+ break;
125
+ }
126
+ return chunks;
127
+ }
128
+ function isCritical(type) {
129
+ const first = type.charCodeAt(0);
130
+ return first >= 0x41 && first <= 0x5a;
131
+ }
132
+ function readText(data, text) {
133
+ const separator = data.indexOf(0);
134
+ if (separator < 1 || separator > 79)
135
+ return;
136
+ text[Buffer.from(data.subarray(0, separator)).toString('latin1')] = Buffer.from(data.subarray(separator + 1)).toString('latin1');
137
+ }
138
+ function textData(keyword, value) {
139
+ if (!/^[\x20-\x7e\xa1-\xff]{1,79}$/.test(keyword) || keyword !== keyword.trim())
140
+ throw new RangeError(`A PNG text keyword is 1 to 79 Latin-1 letters, not ${JSON.stringify(keyword)}.`);
141
+ if (!/^[\n\x20-\x7e\xa1-\xff]*$/.test(value))
142
+ throw new RangeError('A PNG text holds Latin-1 letters and line feeds only.');
143
+ return Buffer.concat([Buffer.from(keyword, 'latin1'), Uint8Array.of(0), Buffer.from(value, 'latin1')]);
144
+ }
145
+ function chunk(type, data) {
146
+ const head = new Uint8Array(8);
147
+ const view = new DataView(head.buffer);
148
+ view.setUint32(0, data.length);
149
+ head.set(Buffer.from(type, 'latin1'), 4);
150
+ const tail = new Uint8Array(4);
151
+ new DataView(tail.buffer).setUint32(0, crc32(Buffer.concat([head.subarray(4), data])));
152
+ return Buffer.concat([head, data, tail]);
153
+ }
154
+ // Each row is stored as a filter byte and the row's bytes, each the difference from a prediction made from the byte to
155
+ // its left, the one above it and the one above and to the left. Undoing it adds the prediction back, row by row.
156
+ function unfilter(raw, height, stride, channels) {
157
+ const out = new Uint8Array(height * stride);
158
+ for (let row = 0; row < height; row++) {
159
+ const filter = raw[row * (stride + 1)];
160
+ const source = row * (stride + 1) + 1;
161
+ const target = row * stride;
162
+ const above = target - stride;
163
+ if (filter === undefined || filter > 4)
164
+ throw new PngError(`The PNG's row ${row + 1} names filter ${String(filter)}, and the format has filters 0 to 4.`);
165
+ for (let column = 0; column < stride; column++) {
166
+ const left = column >= channels ? (out[target + column - channels] ?? 0) : 0;
167
+ const up = row > 0 ? (out[above + column] ?? 0) : 0;
168
+ const upLeft = row > 0 && column >= channels ? (out[above + column - channels] ?? 0) : 0;
169
+ out[target + column] = ((raw[source + column] ?? 0) + predict(filter, left, up, upLeft)) & 0xff;
170
+ }
171
+ }
172
+ return out;
173
+ }
174
+ function predict(filter, left, up, upLeft) {
175
+ switch (filter) {
176
+ case 1:
177
+ return left;
178
+ case 2:
179
+ return up;
180
+ case 3:
181
+ return (left + up) >> 1;
182
+ case 4:
183
+ return paeth(left, up, upLeft);
184
+ default:
185
+ return 0;
186
+ }
187
+ }
188
+ function paeth(left, up, upLeft) {
189
+ const estimate = left + up - upLeft;
190
+ const toLeft = Math.abs(estimate - left);
191
+ const toUp = Math.abs(estimate - up);
192
+ const toUpLeft = Math.abs(estimate - upLeft);
193
+ if (toLeft <= toUp && toLeft <= toUpLeft)
194
+ return left;
195
+ return toUp <= toUpLeft ? up : upLeft;
196
+ }
197
+ // Each row gets the filter whose output, read as signed bytes, has the smallest sum of magnitudes: the heuristic the
198
+ // specification suggests, which keeps flat colour and gradients small for deflate.
199
+ function filterRows(pixels, height, stride, channels) {
200
+ const out = new Uint8Array(height * (stride + 1));
201
+ const candidate = new Uint8Array(stride);
202
+ for (let row = 0; row < height; row++) {
203
+ const target = row * (stride + 1);
204
+ let best = Number.POSITIVE_INFINITY;
205
+ for (let filter = 0; filter <= 4; filter++) {
206
+ let sum = 0;
207
+ for (let column = 0; column < stride; column++) {
208
+ const at = row * stride + column;
209
+ const left = column >= channels ? (pixels[at - channels] ?? 0) : 0;
210
+ const up = row > 0 ? (pixels[at - stride] ?? 0) : 0;
211
+ const upLeft = row > 0 && column >= channels ? (pixels[at - stride - channels] ?? 0) : 0;
212
+ const value = ((pixels[at] ?? 0) - predict(filter, left, up, upLeft)) & 0xff;
213
+ candidate[column] = value;
214
+ sum += value < 128 ? value : 256 - value;
215
+ }
216
+ if (sum >= best)
217
+ continue;
218
+ best = sum;
219
+ out[target] = filter;
220
+ out.set(candidate, target + 1);
221
+ }
222
+ }
223
+ return out;
224
+ }
225
+ function isOpaque(data) {
226
+ for (let index = 3; index < data.length; index += 4)
227
+ if (data[index] !== 255)
228
+ return false;
229
+ return true;
230
+ }
231
+ function toRgb(data) {
232
+ const out = new Uint8Array((data.length / 4) * 3);
233
+ for (let from = 0, to = 0; from < data.length; from += 4, to += 3) {
234
+ out[to] = data[from] ?? 0;
235
+ out[to + 1] = data[from + 1] ?? 0;
236
+ out[to + 2] = data[from + 2] ?? 0;
237
+ }
238
+ return out;
239
+ }
240
+ function toRgba(pixels, channels) {
241
+ if (channels === 4)
242
+ return pixels;
243
+ const out = new Uint8Array((pixels.length / 3) * 4);
244
+ for (let from = 0, to = 0; from < pixels.length; from += 3, to += 4) {
245
+ out[to] = pixels[from] ?? 0;
246
+ out[to + 1] = pixels[from + 1] ?? 0;
247
+ out[to + 2] = pixels[from + 2] ?? 0;
248
+ out[to + 3] = 255;
249
+ }
250
+ return out;
251
+ }
@@ -39,6 +39,8 @@ export declare class OwnedProcessGroup {
39
39
  get initialProcessAbsent(): boolean;
40
40
  /** Failed metadata readings remain visible even when a later reading proves the launch ended. */
41
41
  get readProblems(): readonly string[];
42
+ /** Whether the launch has been read once, whatever the reading found or however it failed. */
43
+ get captured(): boolean;
42
44
  /** Records descendants only while their chain reaches an already verified process this owner launched. */
43
45
  capture(deadline?: Deadline): string[];
44
46
  /**
@@ -46,8 +48,9 @@ export declare class OwnedProcessGroup {
46
48
  * The reading is a moment old when it is applied, which only adds records with their start times; every signal still
47
49
  * reads its pid fresh. A reading that fails records nothing and is not kept as a read problem, since it decides
48
50
  * nothing and the next `capture` reads again. With a cleanup deadline, failures are retained. A newer applied
49
- * snapshot wins over an outstanding background reading. Before the first `capture`, or on a system without
50
- * `readAsync`, it is `capture` itself.
51
+ * snapshot wins over an outstanding background reading. It reads the host as it is when asked: a reading begun
52
+ * before, which cannot hold a process born since, is never taken, though later callers may share this one. Before
53
+ * the first `capture`, or on a system without `readAsync`, it is `capture` itself.
51
54
  */
52
55
  captureAsync(deadline?: Deadline): Promise<string[]>;
53
56
  /**
@@ -64,7 +67,10 @@ export declare class OwnedProcessGroup {
64
67
  * reuse inside it would also need the same start second and an unreadable or identical command.
65
68
  */
66
69
  signalReport(signal: NodeJS.Signals, deadline?: Deadline): ProcessSignalReport;
67
- /** Ordinary cleanup uses asynchronous readings; each signal still immediately follows its own fresh identity. */
70
+ /**
71
+ * Ordinary cleanup uses asynchronous readings; each signal still immediately follows its own fresh identity. The
72
+ * table it records descendants from is begun after it was asked, so a helper born just before is recorded and ended.
73
+ */
68
74
  signalReportAsync(signal: NodeJS.Signals, deadline?: Deadline): Promise<ProcessSignalReport>;
69
75
  /** Keeps every failure and identity refusal visible to callers that do not reconcile a later full absence. */
70
76
  signal(signal: NodeJS.Signals, deadline?: Deadline): string[];
@@ -72,7 +78,12 @@ export declare class OwnedProcessGroup {
72
78
  signalNow(signal: NodeJS.Signals, deadline?: Deadline): string[];
73
79
  /** A failed liveness reading holds the resource. Once observed gone, this owner never names these pids again. */
74
80
  remains(deadline?: Deadline): boolean;
75
- /** A failed or deadline-limited reading retains the resource, just as the synchronous exit-hook reading does. */
81
+ /**
82
+ * A failed or deadline-limited reading retains the resource, just as the synchronous exit-hook reading does. It may
83
+ * take a reading begun before it was asked: a group with no process left when the table was read has none left to
84
+ * start another. Callers asking at once share one reading, and once one of them sees the group gone the others stop
85
+ * reading.
86
+ */
76
87
  remainsAsync(deadline?: Deadline): Promise<boolean>;
77
88
  }
78
89
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"process-ownership.d.ts","sourceRoot":"","sources":["../../src/shared/process-ownership.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"process-ownership.d.ts","sourceRoot":"","sources":["../../src/shared/process-ownership.ts"],"names":[],"mappings":"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"}
@@ -24,7 +24,14 @@ export class OwnedProcessGroup {
24
24
  #initialProcessAbsent = false;
25
25
  // Keep one launch birth after pruning its pid, to distinguish a later group reusing that numeric id.
26
26
  #launchIdentity;
27
- #revision = 0;
27
+ // Each reading, and each change made from a single process's own reading, takes the next number as it begins.
28
+ // `#newest` is the number of the newest one applied: a reading that began before it is stale.
29
+ #sequence = 0;
30
+ #newest = 0;
31
+ // Cleanup runs several waits on one group at once: its end, its output and the stop itself. Each starting a `ps` of
32
+ // its own, with each applied snapshot discarding the others' as stale, multiplied the readings on a loaded host until
33
+ // none was confirmed within the cleanup bound. They share the reading in flight instead.
34
+ #tableReading;
28
35
  constructor(pid, parentPid = process.pid, system = host) {
29
36
  if (!Number.isSafeInteger(pid) || pid < 2 || !Number.isSafeInteger(parentPid) || parentPid < 1)
30
37
  throw new TypeError('A launched process needs a valid pid and parent pid.');
@@ -40,6 +47,10 @@ export class OwnedProcessGroup {
40
47
  get readProblems() {
41
48
  return [...this.#readProblems];
42
49
  }
50
+ /** Whether the launch has been read once, whatever the reading found or however it failed. */
51
+ get captured() {
52
+ return this.#initialized;
53
+ }
43
54
  #failedRead(error) {
44
55
  const problem = `Could not read ownership of process ${this.#pid}: ${errorMessage(error)}`;
45
56
  this.#readProblems.add(problem);
@@ -59,36 +70,38 @@ export class OwnedProcessGroup {
59
70
  this.#initialized = true;
60
71
  return [this.#failedRead(error)];
61
72
  }
62
- return this.#captureFrom(processes);
73
+ return this.#captureFrom(processes, this.#next());
63
74
  }
64
75
  /**
65
76
  * `capture` from a reading taken without holding the thread, for recording a new process while other work goes on.
66
77
  * The reading is a moment old when it is applied, which only adds records with their start times; every signal still
67
78
  * reads its pid fresh. A reading that fails records nothing and is not kept as a read problem, since it decides
68
79
  * nothing and the next `capture` reads again. With a cleanup deadline, failures are retained. A newer applied
69
- * snapshot wins over an outstanding background reading. Before the first `capture`, or on a system without
70
- * `readAsync`, it is `capture` itself.
80
+ * snapshot wins over an outstanding background reading. It reads the host as it is when asked: a reading begun
81
+ * before, which cannot hold a process born since, is never taken, though later callers may share this one. Before
82
+ * the first `capture`, or on a system without `readAsync`, it is `capture` itself.
71
83
  */
72
84
  async captureAsync(deadline) {
73
85
  if (this.#retired)
74
86
  return [];
75
- const readAsync = this.#system.readAsync;
76
- if (!this.#initialized || readAsync === undefined)
87
+ if (!this.#initialized || this.#system.readAsync === undefined)
77
88
  return this.capture(deadline);
78
- const revision = this.#revision;
79
- let processes;
89
+ const asked = this.#sequence;
90
+ let read;
80
91
  try {
81
92
  checkDeadline(deadline);
82
- processes = await readAsync.call(this.#system, deadline);
93
+ read = await this.#sharedTable(deadline, asked);
83
94
  checkDeadline(deadline);
84
95
  }
85
96
  catch (error) {
86
97
  return deadline === undefined ? [] : [this.#failedRead(error)];
87
98
  }
88
- return this.#retired || revision !== this.#revision ? [] : this.#captureFrom(processes);
99
+ if (this.#retired || !this.#fresh(read.reading))
100
+ return [];
101
+ return this.#captureFrom(read.processes, read.reading.sequence);
89
102
  }
90
- #captureFrom(processes) {
91
- this.#revision += 1;
103
+ #captureFrom(processes, sequence) {
104
+ this.#newest = Math.max(this.#newest, sequence);
92
105
  const initial = !this.#initialized;
93
106
  this.#initialized = true;
94
107
  const root = processes.find((entry) => entry.pid === this.#pid);
@@ -153,7 +166,7 @@ export class OwnedProcessGroup {
153
166
  if (current === undefined || !running(current)) {
154
167
  this.#records.delete(pid);
155
168
  this.#unowned.delete(pid);
156
- this.#revision += 1;
169
+ this.#newest = this.#next();
157
170
  return undefined;
158
171
  }
159
172
  if (!this.#sameRecorded(record, current))
@@ -214,7 +227,7 @@ export class OwnedProcessGroup {
214
227
  this.#initialized = true;
215
228
  return { problems: [this.#failedRead(error)], identityRefusals: [] };
216
229
  }
217
- const problems = this.#captureFrom(processes);
230
+ const problems = this.#captureFrom(processes, this.#next());
218
231
  const identityRefusals = [];
219
232
  for (const record of [...this.#records.values()].reverse()) {
220
233
  let current;
@@ -232,23 +245,27 @@ export class OwnedProcessGroup {
232
245
  }
233
246
  return { problems, identityRefusals };
234
247
  }
235
- /** Ordinary cleanup uses asynchronous readings; each signal still immediately follows its own fresh identity. */
248
+ /**
249
+ * Ordinary cleanup uses asynchronous readings; each signal still immediately follows its own fresh identity. The
250
+ * table it records descendants from is begun after it was asked, so a helper born just before is recorded and ended.
251
+ */
236
252
  async signalReportAsync(signal, deadline) {
237
253
  if (this.#retired)
238
254
  return { problems: [], identityRefusals: [] };
239
- let processes;
255
+ const asked = this.#sequence;
256
+ let read;
240
257
  try {
241
258
  checkDeadline(deadline);
242
- processes = await this.#readFreshTableAsync(deadline);
259
+ read = await this.#readFreshTableAsync(deadline, asked);
243
260
  checkDeadline(deadline);
244
261
  }
245
262
  catch (error) {
246
263
  this.#initialized = true;
247
264
  return { problems: [this.#failedRead(error)], identityRefusals: [] };
248
265
  }
249
- if (this.#retired)
266
+ if (this.#retired || read === undefined)
250
267
  return { problems: [], identityRefusals: [] };
251
- const problems = this.#captureFrom(processes);
268
+ const problems = this.#captureFrom(read.processes, read.reading.sequence);
252
269
  const identityRefusals = [];
253
270
  for (const record of [...this.#records.values()].reverse()) {
254
271
  let current;
@@ -275,7 +292,7 @@ export class OwnedProcessGroup {
275
292
  if (current === undefined || !running(current)) {
276
293
  this.#records.delete(record.pid);
277
294
  this.#unowned.delete(record.pid);
278
- this.#revision += 1;
295
+ this.#newest = this.#next();
279
296
  return;
280
297
  }
281
298
  if (!this.#sameRecorded(record, current)) {
@@ -320,17 +337,58 @@ export class OwnedProcessGroup {
320
337
  return this.#system.readProcess(pid, deadline);
321
338
  return this.#system.read(deadline).find((entry) => entry.pid === pid);
322
339
  }
323
- #readTableAsync(deadline) {
324
- return this.#system.readAsync === undefined ? Promise.resolve(this.#system.read(deadline)) : this.#system.readAsync(deadline);
340
+ async #readTableAsync(deadline) {
341
+ return this.#system.readAsync === undefined ? this.#system.read(deadline) : this.#system.readAsync(deadline);
325
342
  }
326
- async #readFreshTableAsync(deadline) {
343
+ #next() {
344
+ this.#sequence += 1;
345
+ return this.#sequence;
346
+ }
347
+ // The reading running now while nothing newer was applied since it began, which a caller waits for rather than start
348
+ // another `ps`; otherwise a new one under this caller's deadline. A caller that gives `asked`, the number current when
349
+ // it asked, takes only a reading begun after it.
350
+ #tableReadingFor(deadline, asked) {
351
+ const running = this.#tableReading;
352
+ if (running !== undefined && this.#fresh(running) && (asked === undefined || running.sequence > asked))
353
+ return running;
354
+ const reading = { processes: this.#readTableAsync(deadline), deadline, sequence: this.#next() };
355
+ this.#tableReading = reading;
356
+ const finished = () => { if (this.#tableReading === reading)
357
+ this.#tableReading = undefined; };
358
+ reading.processes.then(finished, finished);
359
+ return reading;
360
+ }
361
+ // One completed table reading. A caller waits for a reading another caller started only within its own deadline. When
362
+ // that reading fails, as one the other caller's shorter deadline ended does, it moves to a fresh one within its own.
363
+ async #sharedTable(deadline, asked) {
327
364
  for (;;) {
328
365
  checkDeadline(deadline);
329
- const revision = this.#revision;
330
- const processes = await this.#readTableAsync(deadline);
366
+ const reading = this.#tableReadingFor(deadline, asked);
367
+ if (reading.deadline === deadline)
368
+ return { processes: await reading.processes, reading };
369
+ try {
370
+ return { processes: await withinDeadline(reading.processes, deadline), reading };
371
+ }
372
+ catch {
373
+ checkDeadline(deadline);
374
+ }
375
+ }
376
+ }
377
+ // A snapshot may be applied while nothing newer was. Its own callers apply it after one another, which ages nothing.
378
+ #fresh(reading) {
379
+ return reading.sequence >= this.#newest;
380
+ }
381
+ // Undefined once the group has been shown gone, which no later reading can change.
382
+ async #readFreshTableAsync(deadline, asked) {
383
+ for (;;) {
384
+ if (this.#retired)
385
+ return undefined;
386
+ const read = await this.#sharedTable(deadline, asked);
331
387
  checkDeadline(deadline);
332
- if (revision === this.#revision)
333
- return processes;
388
+ if (this.#retired)
389
+ return undefined;
390
+ if (this.#fresh(read.reading))
391
+ return read;
334
392
  }
335
393
  }
336
394
  /** Keeps every failure and identity refusal visible to callers that do not reconcile a later full absence. */
@@ -356,26 +414,33 @@ export class OwnedProcessGroup {
356
414
  this.#failedRead(error);
357
415
  return true;
358
416
  }
359
- return this.#remainsFrom(processes);
417
+ return this.#remainsFrom(processes, this.#next());
360
418
  }
361
- /** A failed or deadline-limited reading retains the resource, just as the synchronous exit-hook reading does. */
419
+ /**
420
+ * A failed or deadline-limited reading retains the resource, just as the synchronous exit-hook reading does. It may
421
+ * take a reading begun before it was asked: a group with no process left when the table was read has none left to
422
+ * start another. Callers asking at once share one reading, and once one of them sees the group gone the others stop
423
+ * reading.
424
+ */
362
425
  async remainsAsync(deadline) {
363
426
  if (this.#retired)
364
427
  return false;
365
- let processes;
428
+ let read;
366
429
  try {
367
430
  checkDeadline(deadline);
368
- processes = await this.#readFreshTableAsync(deadline);
431
+ read = await this.#readFreshTableAsync(deadline);
369
432
  checkDeadline(deadline);
370
433
  }
371
434
  catch (error) {
372
435
  this.#failedRead(error);
373
436
  return true;
374
437
  }
375
- return this.#retired ? false : this.#remainsFrom(processes);
438
+ if (this.#retired || read === undefined)
439
+ return false;
440
+ return this.#remainsFrom(read.processes, read.reading.sequence);
376
441
  }
377
- #remainsFrom(processes) {
378
- this.#revision += 1;
442
+ #remainsFrom(processes, sequence) {
443
+ this.#newest = Math.max(this.#newest, sequence);
379
444
  // A failed first reading never grants signal authority, and an empty ledger is no proof that the launched
380
445
  // child ended. Until its pid and group are absent, that unresolved launch continues to hold its resource.
381
446
  if (this.#launchIdentity === undefined && processes.some((entry) => running(entry) && (entry.pid === this.#pid || entry.groupId === this.#pid)))
@@ -501,6 +566,30 @@ function checkDeadline(deadline) {
501
566
  if (deadline?.signal?.aborted === true || deadline?.reached === true)
502
567
  throw new Error('The ownership reading was not confirmed before its deadline ended or was cancelled.');
503
568
  }
569
+ // Waits for a reading another caller started until this caller's deadline ends or is cancelled, and leaves the reading
570
+ // running for whoever else waits on it. Its timer holds no process open; the reading itself does while it runs.
571
+ function withinDeadline(work, deadline) {
572
+ if (deadline === undefined)
573
+ return work;
574
+ const { signal } = deadline;
575
+ if (signal?.aborted === true)
576
+ return Promise.reject(new Error('The ownership reading was not confirmed before its deadline ended or was cancelled.'));
577
+ return new Promise((resolve, reject) => {
578
+ let timer;
579
+ const settle = () => {
580
+ clearTimeout(timer);
581
+ signal?.removeEventListener('abort', ended);
582
+ };
583
+ function ended() {
584
+ settle();
585
+ reject(new Error('The ownership reading was not confirmed before its deadline ended or was cancelled.'));
586
+ }
587
+ timer = setTimeout(ended, deadline.waitToEndMs);
588
+ timer.unref();
589
+ signal?.addEventListener('abort', ended, { once: true });
590
+ work.then((value) => { settle(); resolve(value); }, (error) => { settle(); reject(error); });
591
+ });
592
+ }
504
593
  function parseProcessTable(text) {
505
594
  const records = [];
506
595
  for (const line of text.split('\n')) {
@@ -48,6 +48,10 @@ export type ArtifactSpec = {
48
48
  } | {
49
49
  readonly kind: 'diagnostics';
50
50
  readonly identity: RecordIdentity;
51
+ } | {
52
+ readonly kind: 'download';
53
+ readonly identity: RecordIdentity;
54
+ readonly name: string;
51
55
  } | {
52
56
  readonly kind: 'report';
53
57
  };
@@ -65,6 +69,21 @@ export declare const partialSuffix = ".partial";
65
69
  * @example artifactPath({ kind: 'screenshot', identity, purpose: 'failure', sequence: 1, format: 'png' }) // 'artifacts/k3v9q0x2mb/web-<hash>/screenshot-failure-1.png'
66
70
  */
67
71
  export declare function artifactPath(spec: ArtifactSpec): string;
72
+ /**
73
+ * The folder a session's downloads are saved in, relative to the run folder: the browser writes each one there by its
74
+ * own id, and Retest renames it to the name `downloadFileName` makes of the name the page suggested.
75
+ *
76
+ * @example downloadsFolder(identity) // 'artifacts/k3v9q0x2mb/web-<hash>/downloads'
77
+ */
78
+ export declare function downloadsFolder(identity: RecordIdentity): string;
79
+ /**
80
+ * A name a reference may hold, made of the name a page suggested for a download: every run of other characters than
81
+ * letters, digits, `.`, `_` and `-` becomes one `-`, a leading `.` or `-` goes, and a long name keeps its end, where
82
+ * its extension is. A name with nothing left is `download`.
83
+ *
84
+ * @example downloadFileName('Tasks (May).csv') // 'Tasks-May-.csv'
85
+ */
86
+ export declare function downloadFileName(suggested: string): string;
68
87
  /**
69
88
  * The path the media process is given as a recording's `output`, relative to the run folder: the recording's path
70
89
  * without its extension, which the process adds once it knows the container. Join it to the run folder with
@@ -229,7 +248,8 @@ export declare function readArtifactFile(runFolder: string, reference: string, l
229
248
  */
230
249
  export type ArtifactReference = {
231
250
  readonly path: string;
232
- readonly kind: ArtifactKind;
251
+ /** What the file is; `download` is a file a page downloaded, which retention never removes. */
252
+ readonly kind: ArtifactKind | 'download';
233
253
  readonly namedBy: string;
234
254
  readonly testId?: string;
235
255
  readonly attemptId?: string;
@@ -242,7 +262,8 @@ export type ArtifactReference = {
242
262
  export declare function recordingArtifactReferences(recording: RecordingRecord, namedBy: string): ArtifactReference[];
243
263
  /**
244
264
  * The artifacts the events of a run name: failure screenshots (`evidence.captured`), diagnostics artifacts
245
- * (`diagnostics.finished`) and the screenshots AI checks sent their judges (`evaluation.finished`). Event types this
265
+ * (`diagnostics.finished`), the screenshots AI checks sent their judges (`evaluation.finished`) and the pictures a failed
266
+ * screenshot comparison left (`assertion.failed` with `attachments`). Event types this
246
267
  * reader does not know name nothing here, so an event type added later must be added to this function. Recordings name
247
268
  * videos and partial files; retention removes references unless a later removal failure preserves the file.
248
269
  *