From 2a3d73c25c88f0bccc88f683878dad8bae195893 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Fri, 24 Jul 2026 16:11:45 -0700 Subject: [PATCH 1/4] Downlevel the Babylon.js core bundle to ES5 in the nightly script fetch The nightly pipeline has failed every scheduled run since 2026-07-07. `Run VisualizationTests` stalls on `Glow layer and LODs`, which never satisfies `Scene.executeWhenReady`, burns the 600s readiness budget and aborts the run at the 24th test. A green run executes 294. Babylon.js emits its UMD bundles at an ES2015 target starting with 9.16.0: the TC39 decorator migration relies on the `accessor` keyword, which cannot be expressed in ES5, and esbuild cannot emit ES5 classes. Babylon Native runs those bundles on Chakra, which consumes ES5-level script. Babylon.js already accounts for this. Its monorepo pipeline downloads this repository's nightly artifacts and runs `downlevel:native-scripts` over `babylon.max.js` before it launches either UnitTests.exe or Playground.exe, which is why the Babylon.js side of the same test stays green. `npm run getNightly` had no equivalent step, so the nightly ran the ES2015 bundle directly. The failure mode is silent rather than a parse error. The bundle loads, the engine reports itself launched, meshes and textures become ready, the glow layer's render target and all four blur post-processes report ready, and no exception or shader compilation error is logged - `GlowLayer.isLayerReady()` simply stays false forever. Any scene containing a `GlowLayer` is affected; the LOD and `customEmissiveColorSelector` in the failing test are incidental. It surfaces through this one test only because every other glow-layer scene in config.json is already `excludeFromAutomaticTesting`. `getNightly` now downlevels `node_modules/babylonjs/babylon.max.js` after fetching. Both consumers read that same file - Apps/UnitTests/CMakeLists.txt and Apps/Playground/CMakeLists.txt each copy it out of node_modules - so one transform at the source covers both. TypeScript performs the transform rather than Babel, for the reason Babylon.js documents in its own script: Babel's ES5 class lowering emits `Reflect.construct` / `_wrapNativeSuper` machinery for classes extending native built-ins such as `Error`, and that machinery runs at class-definition time and hard-crashes Chakra as the bundle loads. The downloads in `getNightly` are now awaited and check their status code. They were fire-and-forget, so a failed fetch silently left the previous file in place, and the down-level step that now follows must not race an incomplete write. Verified on Win32 x64 D3D11 against the current preview build: the full visualization suite goes from aborting at test 24 to `ran=296 passed=296 failed=0`. Swapping only `babylon.max.js` in one unchanged Playground build confirms the boundary - 9.15.0 passes, 9.16.0 through 9.18.0 hang, and 9.16.0 passes once downleveled. The same scene is unaffected in a browser on all of those versions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 714b4495-258e-4645-abf7-26c17bc29d5b --- Apps/package-lock.json | 3 ++ Apps/package.json | 6 ++- Apps/scripts/downlevelNativeScripts.mjs | 53 +++++++++++++++++++++++++ Apps/scripts/getNightly.js | 47 ++++++++++++++++------ 4 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 Apps/scripts/downlevelNativeScripts.mjs diff --git a/Apps/package-lock.json b/Apps/package-lock.json index d8adf7009..bada42b2f 100644 --- a/Apps/package-lock.json +++ b/Apps/package-lock.json @@ -20,6 +20,9 @@ "babylonjs-serializers": "^9.15.0", "jsc-android": "^241213.1.0", "v8-android": "^7.8.2" + }, + "devDependencies": { + "typescript": "^5.9.3" } }, "node_modules/@babel/cli": { diff --git a/Apps/package.json b/Apps/package.json index 632285ede..fad341b3f 100644 --- a/Apps/package.json +++ b/Apps/package.json @@ -6,7 +6,11 @@ "UnitTests/JavaScript" ], "scripts": { - "getNightly": "node scripts/getNightly.js" + "getNightly": "node scripts/getNightly.js && npm run downlevel:native-scripts -- node_modules/babylonjs/babylon.max.js", + "downlevel:native-scripts": "node scripts/downlevelNativeScripts.mjs" + }, + "devDependencies": { + "typescript": "^5.9.3" }, "dependencies": { "babylonjs": "^9.15.0", diff --git a/Apps/scripts/downlevelNativeScripts.mjs b/Apps/scripts/downlevelNativeScripts.mjs new file mode 100644 index 000000000..e9c6f7291 --- /dev/null +++ b/Apps/scripts/downlevelNativeScripts.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node + +import { readFile, writeFile } from "fs/promises"; +import { resolve } from "path"; +import ts from "typescript"; + +const targets = process.argv.slice(2); + +if (targets.length === 0) { + process.stderr.write("Usage: node scripts/downlevelNativeScripts.mjs [...]\n"); + process.exit(1); +} + +// Babylon.js emits its UMD bundles at an ES2015 target: the TC39 decorator migration relies on the +// `accessor` keyword, which cannot be expressed in ES5, and esbuild cannot emit ES5 classes. Babylon +// Native runs those bundles on Chakra, which consumes ES5-level script, so the core bundle must be +// down-leveled before it is executed. Babylon.js does the same thing to the artifacts this repo +// publishes (see `downlevel:native-scripts` in the Babylon.js monorepo pipeline); without it the +// bundle still parses and most of the engine works, so the breakage is silent - e.g. GlowLayer never +// reports ready and any scene using it hangs in Scene.executeWhenReady. +// +// TypeScript is used for the transform rather than Babel. Babel's ES5 class lowering emits +// `Reflect.construct` / `_wrapNativeSuper` machinery for classes that extend native built-ins such as +// `Error`, and that machinery runs at class-definition time and hard-crashes Chakra while the bundle +// loads. TypeScript lowers classes with its `__extends` helper - plain prototype assignment, no +// `Reflect.construct` - which is the emit Babylon Native consumed for years when the bundles were +// built at an ES5 target directly. `ts.transpileModule` is a purely syntactic single-file transform, +// so it handles the multi-megabyte bundle without type checking and inlines its own helpers. +const compilerOptions = { + target: ts.ScriptTarget.ES5, + // The bundles are UMD/IIFE scripts with no top-level module syntax; leave module output untouched. + module: ts.ModuleKind.None, + // Lower `for..of`, spread and other iterator protocols correctly for ES5. + downlevelIteration: true, + // Inline the emit helpers so the bundle stays self-contained on Chakra. + importHelpers: false, + newLine: ts.NewLineKind.LineFeed, + sourceMap: false, + ignoreDeprecations: "6.0", +}; + +let count = 0; + +for (const target of targets) { + const filePath = resolve(target); + const source = await readFile(filePath, "utf8"); + const { outputText } = ts.transpileModule(source, { compilerOptions, fileName: filePath }); + await writeFile(filePath, outputText, "utf8"); + process.stdout.write(`Downleveled ${filePath}\n`); + ++count; +} + +process.stdout.write(`Downleveled ${count} Babylon Native script file(s).\n`); diff --git a/Apps/scripts/getNightly.js b/Apps/scripts/getNightly.js index 52d13cb03..0a5f40241 100644 --- a/Apps/scripts/getNightly.js +++ b/Apps/scripts/getNightly.js @@ -2,20 +2,41 @@ var https = require('https'); var fs = require('fs'); function download(filename, url) { - var file = fs.createWriteStream(filename); - var request = https.get(url, function(response) { - response.pipe(file); + return new Promise(function (resolve, reject) { + https.get(url, function (response) { + if (response.statusCode !== 200) { + response.resume(); + reject(new Error('GET ' + url + ' failed with status ' + response.statusCode)); + return; + } + var file = fs.createWriteStream(filename); + file.on('error', reject); + file.on('finish', resolve); + response.pipe(file); + }).on('error', reject); }); } +var files = [ + ['node_modules/babylonjs/babylon.max.js', 'https://preview.babylonjs.com/babylon.max.js'], + ['node_modules/babylonjs/babylon.max.js.map', 'https://preview.babylonjs.com/babylon.max.js.map'], + ['node_modules/babylonjs-materials/babylonjs.materials.js', 'https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.js'], + ['node_modules/babylonjs-materials/babylonjs.materials.js.map', 'https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.js.map'], + ['node_modules/babylonjs-loaders/babylonjs.loaders.js', 'https://preview.babylonjs.com/loaders/babylonjs.loaders.js'], + ['node_modules/babylonjs-loaders/babylonjs.loaders.js.map', 'https://preview.babylonjs.com/loaders/babylonjs.loaders.js.map'], + ['node_modules/babylonjs-gui/babylon.gui.js', 'https://preview.babylonjs.com/gui/babylon.gui.js'], + ['node_modules/babylonjs-gui/babylon.gui.js.map', 'https://preview.babylonjs.com/gui/babylon.gui.js.map'], + ['node_modules/babylonjs-serializers/babylonjs.serializers.js', 'https://preview.babylonjs.com/serializers/babylonjs.serializers.js'], + ['node_modules/babylonjs-serializers/babylonjs.serializers.js.map', 'https://preview.babylonjs.com/serializers/babylonjs.serializers.js.map'], +]; + console.log('Downloading babylon.js nightly'); -download('node_modules/babylonjs/babylon.max.js', 'https://preview.babylonjs.com/babylon.max.js'); -download('node_modules/babylonjs/babylon.max.js.map', 'https://preview.babylonjs.com/babylon.max.js.map'); -download('node_modules/babylonjs-materials/babylonjs.materials.js', 'https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.js'); -download('node_modules/babylonjs-materials/babylonjs.materials.js.map', 'https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.js.map'); -download('node_modules/babylonjs-loaders/babylonjs.loaders.js', 'https://preview.babylonjs.com/loaders/babylonjs.loaders.js'); -download('node_modules/babylonjs-loaders/babylonjs.loaders.js.map', 'https://preview.babylonjs.com/loaders/babylonjs.loaders.js.map'); -download('node_modules/babylonjs-gui/babylon.gui.js', 'https://preview.babylonjs.com/gui/babylon.gui.js'); -download('node_modules/babylonjs-gui/babylon.gui.js.map', 'https://preview.babylonjs.com/gui/babylon.gui.js.map'); -download('node_modules/babylonjs-serializers/babylonjs.serializers.js', 'https://preview.babylonjs.com/serializers/babylonjs.serializers.js'); -download('node_modules/babylonjs-serializers/babylonjs.serializers.js.map', 'https://preview.babylonjs.com/serializers/babylonjs.serializers.js.map'); + +// Awaited so a failed or partial download fails the script instead of silently leaving the previous +// file in place, and so the down-level step that runs next cannot race an incomplete write. +Promise.all(files.map(function (f) { return download(f[0], f[1]); })).then(function () { + console.log('Downloaded ' + files.length + ' file(s)'); +}, function (err) { + console.error(err.message); + process.exit(1); +}); From a30826de5074bf25ae8a3284c0be71d41dcdde61 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Fri, 24 Jul 2026 16:39:21 -0700 Subject: [PATCH 2/4] Fetch addons from preview too, and use Babylon.js's downlevel script verbatim getNightly refreshed babylonjs, -gui, -loaders, -materials and -serializers but not babylonjs-addons, so every nightly ran a preview core against an npm-pinned addons build. The Playground loads addons immediately after the core bundle (PlaygroundScripts.cpp), so the two were free to drift a full release apart. Addons is subject to the same ES2015 target as the core - the preview build has 13 class declarations and 93 arrow functions where the npm 9.15.0 build has none - so it is down-leveled alongside it. UnitTests does not load addons. downlevelNativeScripts.mjs is now a verbatim copy of the Babylon.js script, apart from a provenance header. The earlier adaptation had dropped the diagnostic handling: Babylon.js's version throws on fatal syntactic diagnostics and on empty output, whereas the trimmed version ignored both, so a corrupt download would have silently produced a broken bundle - the same class of silent failure this branch exists to fix. Keeping the two files diffable also stops them drifting. Verified on Win32 x64 D3D11 against the current preview with both bundles down-leveled: ran=296 passed=296 failed=0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 714b4495-258e-4645-abf7-26c17bc29d5b --- Apps/package.json | 2 +- Apps/scripts/downlevelNativeScripts.mjs | 108 ++++++++++++++++++------ Apps/scripts/getNightly.js | 2 + 3 files changed, 84 insertions(+), 28 deletions(-) diff --git a/Apps/package.json b/Apps/package.json index fad341b3f..d28f8ba16 100644 --- a/Apps/package.json +++ b/Apps/package.json @@ -6,7 +6,7 @@ "UnitTests/JavaScript" ], "scripts": { - "getNightly": "node scripts/getNightly.js && npm run downlevel:native-scripts -- node_modules/babylonjs/babylon.max.js", + "getNightly": "node scripts/getNightly.js && npm run downlevel:native-scripts -- node_modules/babylonjs/babylon.max.js node_modules/babylonjs-addons/babylonjs.addons.js", "downlevel:native-scripts": "node scripts/downlevelNativeScripts.mjs" }, "devDependencies": { diff --git a/Apps/scripts/downlevelNativeScripts.mjs b/Apps/scripts/downlevelNativeScripts.mjs index e9c6f7291..68e9a3351 100644 --- a/Apps/scripts/downlevelNativeScripts.mjs +++ b/Apps/scripts/downlevelNativeScripts.mjs @@ -1,53 +1,107 @@ #!/usr/bin/env node -import { readFile, writeFile } from "fs/promises"; -import { resolve } from "path"; +// Kept byte-identical to scripts/downlevelNativeScripts.mjs in the Babylon.js repo, which applies the +// same transform to the artifacts this repo publishes. Diff the two before changing either. + +import { readdir, readFile, stat, writeFile } from "fs/promises"; +import { basename, join, resolve } from "path"; import ts from "typescript"; const targets = process.argv.slice(2); if (targets.length === 0) { - process.stderr.write("Usage: node scripts/downlevelNativeScripts.mjs [...]\n"); + process.stderr.write("Usage: node scripts/downlevelNativeScripts.mjs [...]\n"); process.exit(1); } -// Babylon.js emits its UMD bundles at an ES2015 target: the TC39 decorator migration relies on the -// `accessor` keyword, which cannot be expressed in ES5, and esbuild cannot emit ES5 classes. Babylon -// Native runs those bundles on Chakra, which consumes ES5-level script, so the core bundle must be -// down-leveled before it is executed. Babylon.js does the same thing to the artifacts this repo -// publishes (see `downlevel:native-scripts` in the Babylon.js monorepo pipeline); without it the -// bundle still parses and most of the engine works, so the breakage is silent - e.g. GlowLayer never -// reports ready and any scene using it hangs in Scene.executeWhenReady. +// The TC39 decorator migration forces the Native UMD bundle to be emitted at an ES2015 target (the +// `accessor` keyword requires ES2015+, and esbuild cannot emit ES5 classes). Babylon Native's Chakra +// engine consumes ES5-level script, so the bundle must be down-leveled before it runs. // -// TypeScript is used for the transform rather than Babel. Babel's ES5 class lowering emits -// `Reflect.construct` / `_wrapNativeSuper` machinery for classes that extend native built-ins such as -// `Error`, and that machinery runs at class-definition time and hard-crashes Chakra while the bundle -// loads. TypeScript lowers classes with its `__extends` helper - plain prototype assignment, no -// `Reflect.construct` - which is the emit Babylon Native consumed for years when the bundles were -// built at an ES5 target directly. `ts.transpileModule` is a purely syntactic single-file transform, -// so it handles the multi-megabyte bundle without type checking and inlines its own helpers. +// We use the TypeScript transpiler (not Babel) for this. Babel's ES5 class transform emits +// `Reflect.construct`/`_wrapNativeSuper` machinery for classes that extend native built-ins (e.g. +// `Error`, `Array`); that machinery executes at class-definition time and hard-crashes Chakra when +// the bundle loads. TypeScript instead lowers classes with its `__extends` helper (plain prototype +// assignment, no `Reflect.construct`) - the exact emit Babylon Native ran successfully for years when +// the UMD bundles were built directly at an ES5 target. `ts.transpileModule` performs a purely +// syntactic, single-file transform (no type checking), so it does not choke on the multi-megabyte +// bundle, and it inlines self-contained helpers (no external `tslib`/`regeneratorRuntime` required). const compilerOptions = { target: ts.ScriptTarget.ES5, // The bundles are UMD/IIFE scripts with no top-level module syntax; leave module output untouched. module: ts.ModuleKind.None, // Lower `for..of`, spread and other iterator protocols correctly for ES5. downlevelIteration: true, - // Inline the emit helpers so the bundle stays self-contained on Chakra. + // Inline the emit helpers into each file so the bundle stays self-contained on Chakra. importHelpers: false, newLine: ts.NewLineKind.LineFeed, sourceMap: false, + // The ES5/`module: none`/`downlevelIteration` combination emits TS 7.0 deprecation notices; they + // are informational and do not affect the emitted output. ignoreDeprecations: "6.0", }; -let count = 0; +function isNativeScriptFile(filePath) { + return /^babylon.*\.js$/i.test(basename(filePath)); +} + +async function collectFiles(target) { + const resolvedTarget = resolve(target); + const targetStat = await stat(resolvedTarget); + + if (targetStat.isFile()) { + return isNativeScriptFile(resolvedTarget) ? [resolvedTarget] : []; + } + + if (!targetStat.isDirectory()) { + return []; + } + + const entries = await readdir(resolvedTarget, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const entryPath = join(resolvedTarget, entry.name); + if (entry.isDirectory()) { + files.push(...(await collectFiles(entryPath))); + } else if (entry.isFile() && isNativeScriptFile(entryPath)) { + files.push(entryPath); + } + } + + return files; +} + +const files = [...new Set((await Promise.all(targets.map(collectFiles))).flat())]; + +if (files.length === 0) { + process.stdout.write("No Babylon Native scripts found to downlevel.\n"); + process.exit(0); +} + +for (const file of files) { + const code = await readFile(file, "utf8"); + const result = ts.transpileModule(code, { compilerOptions, fileName: file, reportDiagnostics: true }); + + // `transpileModule` only surfaces syntactic and command-line/config diagnostics (it has no type + // information). Command-line/config diagnostics (code >= 5000) are informational for our + // transpile-only use; a real problem shows up as a syntactic error (code < 5000). + const fatalDiagnostics = (result.diagnostics || []).filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error && diagnostic.code < 5000); + if (fatalDiagnostics.length > 0) { + const formatted = ts.formatDiagnostics(fatalDiagnostics, { + getCanonicalFileName: (fileName) => fileName, + getCurrentDirectory: () => process.cwd(), + getNewLine: () => "\n", + }); + throw new Error(`TypeScript reported errors while down-leveling ${file}:\n${formatted}`); + } + + if (!result.outputText) { + throw new Error(`TypeScript did not produce output for ${file}`); + } -for (const target of targets) { - const filePath = resolve(target); - const source = await readFile(filePath, "utf8"); - const { outputText } = ts.transpileModule(source, { compilerOptions, fileName: filePath }); - await writeFile(filePath, outputText, "utf8"); - process.stdout.write(`Downleveled ${filePath}\n`); - ++count; + await writeFile(file, result.outputText, "utf8"); + process.stdout.write(`Downleveled ${file}\n`); } -process.stdout.write(`Downleveled ${count} Babylon Native script file(s).\n`); +process.stdout.write(`Downleveled ${files.length} Babylon Native script file(s).\n`); diff --git a/Apps/scripts/getNightly.js b/Apps/scripts/getNightly.js index 0a5f40241..b7ee42838 100644 --- a/Apps/scripts/getNightly.js +++ b/Apps/scripts/getNightly.js @@ -20,6 +20,8 @@ function download(filename, url) { var files = [ ['node_modules/babylonjs/babylon.max.js', 'https://preview.babylonjs.com/babylon.max.js'], ['node_modules/babylonjs/babylon.max.js.map', 'https://preview.babylonjs.com/babylon.max.js.map'], + ['node_modules/babylonjs-addons/babylonjs.addons.js', 'https://preview.babylonjs.com/addons/babylonjs.addons.js'], + ['node_modules/babylonjs-addons/babylonjs.addons.js.map', 'https://preview.babylonjs.com/addons/babylonjs.addons.js.map'], ['node_modules/babylonjs-materials/babylonjs.materials.js', 'https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.js'], ['node_modules/babylonjs-materials/babylonjs.materials.js.map', 'https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.js.map'], ['node_modules/babylonjs-loaders/babylonjs.loaders.js', 'https://preview.babylonjs.com/loaders/babylonjs.loaders.js'], From 4801e069a56cf0ef802d6e794e3349f1cf913975 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Fri, 24 Jul 2026 16:56:05 -0700 Subject: [PATCH 3/4] Downlevel only the core bundle, not addons The previous commit down-leveled babylonjs.addons.js alongside babylon.max.js. That was unnecessary. Chakra parses ES2015 classes and arrow functions without trouble; what it cannot take is the Stage 3 decorator emit, and the `accessor` keyword that produces appears twice in the preview core bundle and not at all in preview addons. Babylon.js reaches the same conclusion in its own pipeline - it replaces every babylon* script from its snapshot but down-levels only babylon.max.js. Verified on Win32 x64 D3D11 against the current preview with addons left as the raw ES2015 build: ran=296 passed=296 failed=0. Addons is still fetched from preview, which is the point of the previous commit; only the down-level of it is dropped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 714b4495-258e-4645-abf7-26c17bc29d5b --- Apps/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Apps/package.json b/Apps/package.json index d28f8ba16..fad341b3f 100644 --- a/Apps/package.json +++ b/Apps/package.json @@ -6,7 +6,7 @@ "UnitTests/JavaScript" ], "scripts": { - "getNightly": "node scripts/getNightly.js && npm run downlevel:native-scripts -- node_modules/babylonjs/babylon.max.js node_modules/babylonjs-addons/babylonjs.addons.js", + "getNightly": "node scripts/getNightly.js && npm run downlevel:native-scripts -- node_modules/babylonjs/babylon.max.js", "downlevel:native-scripts": "node scripts/downlevelNativeScripts.mjs" }, "devDependencies": { From 8126e1b5e13fb5907186ce7ac46ec79318912e1a Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Fri, 24 Jul 2026 17:07:48 -0700 Subject: [PATCH 4/4] Settle the nightly downloads on close, abort and timeout Addresses review feedback on the download helper. Resolving on the write stream's `finish` event released the promise once the data was flushed but before the descriptor was closed. The down-level step runs against these files immediately afterwards, so on Windows that is a file-lock race. A server that aborts mid-transfer emitted neither `finish` nor `error` on the file, so the promise never settled and the script hung. Confirmed against the previous implementation with a local server that destroys the response mid-body: it never settles, while the new one rejects in ~60ms. `response` error and abort are now handled, the request has a two minute timeout, and the stream is destroyed on failure. Exercised all three failure paths against a local server - non-200, mid-transfer abort, and connection refused - each rejects promptly rather than hanging. `npm run getNightly` still produces the same bytes as before. The other review comment, about ignoring `ts.transpileModule` diagnostics, was raised against the trimmed adaptation of the down-level script and no longer applies: that file is now a verbatim copy of the Babylon.js script, which passes `reportDiagnostics: true`, throws on fatal diagnostics and throws on empty output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 714b4495-258e-4645-abf7-26c17bc29d5b --- Apps/scripts/getNightly.js | 42 ++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/Apps/scripts/getNightly.js b/Apps/scripts/getNightly.js index b7ee42838..b3c8dc70b 100644 --- a/Apps/scripts/getNightly.js +++ b/Apps/scripts/getNightly.js @@ -3,17 +3,47 @@ var fs = require('fs'); function download(filename, url) { return new Promise(function (resolve, reject) { - https.get(url, function (response) { + var file = fs.createWriteStream(filename); + var settled = false; + + function fail(err) { + if (settled) { + return; + } + settled = true; + file.destroy(); + reject(err); + } + + file.on('error', fail); + // 'close', not 'finish': 'finish' fires once the data is flushed but before the descriptor is + // released, and the down-level step runs against these files immediately afterwards. + file.on('close', function () { + if (!settled) { + settled = true; + resolve(); + } + }); + + var request = https.get(url, function (response) { if (response.statusCode !== 200) { response.resume(); - reject(new Error('GET ' + url + ' failed with status ' + response.statusCode)); + fail(new Error('GET ' + url + ' failed with status ' + response.statusCode)); return; } - var file = fs.createWriteStream(filename); - file.on('error', reject); - file.on('finish', resolve); + response.on('error', fail); + // A server-side abort emits neither 'finish' nor 'error' on the file, so without this the + // promise would never settle. + response.on('aborted', function () { + fail(new Error('GET ' + url + ' aborted by the server')); + }); response.pipe(file); - }).on('error', reject); + }); + + request.on('error', fail); + request.setTimeout(120000, function () { + request.destroy(new Error('GET ' + url + ' timed out')); + }); }); }