mnfst-render 0.5.4 → 0.5.5

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.
@@ -1787,6 +1787,11 @@ async function runPrerender(config) {
1787
1787
  const browserRecycleEvery = Math.max(0, pre.browserRecycleEvery ?? 40);
1788
1788
  let pagesSinceRecycle = 0;
1789
1789
  const recycleLock = { busy: false };
1790
+ // Workers block on this promise before touching `browser`. While a recycle
1791
+ // is in progress it's a pending promise; once the new browser is up it
1792
+ // resolves and workers can proceed. This prevents "browser not ready"
1793
+ // errors from racing retries during recycle.
1794
+ let browserReadyPromise = Promise.resolve();
1790
1795
  const pathTotal = pathList.length;
1791
1796
  const failedPaths = [];
1792
1797
  const debugRows = [];
@@ -1878,11 +1883,11 @@ async function runPrerender(config) {
1878
1883
  : defaultLocale || 'en'
1879
1884
  : defaultLocale || 'en';
1880
1885
 
1881
- // Refuse to newPage() against a closed browser (happens briefly during
1882
- // recycle); the worker loop will retry.
1883
- if (!browser || !browser.connected) {
1884
- throw new Error('browser not ready');
1885
- }
1886
+ // Wait for any in-progress browser recycle to complete before touching
1887
+ // `browser`. This transparently handles the window between the old
1888
+ // browser being closed and the new one being launched — workers block
1889
+ // here instead of throwing "browser not ready".
1890
+ await browserReadyPromise;
1886
1891
  const page = await browser.newPage();
1887
1892
  try {
1888
1893
  // Align <html lang> with the URL being prerendered before any app script runs.
@@ -2816,14 +2821,23 @@ async function runPrerender(config) {
2816
2821
  if (pagesSinceRecycle < browserRecycleEvery) return;
2817
2822
  if (recycleLock.busy) return;
2818
2823
  recycleLock.busy = true;
2824
+ // Wait for all in-flight workers to finish their current page BEFORE
2825
+ // we gate `browserReadyPromise`, so workers already mid-processPath
2826
+ // don't deadlock awaiting a promise we haven't yet started.
2827
+ await waitUntilZero();
2828
+ // Now gate newPage() calls from any worker that enters processPath
2829
+ // after this point.
2830
+ let resolveReady;
2831
+ browserReadyPromise = new Promise((r) => { resolveReady = r; });
2819
2832
  try {
2820
- // Wait for all in-flight workers to finish their current page.
2821
- await waitUntilZero();
2822
2833
  process.stdout.write(`prerender: recycling browser (processed ${pagesSinceRecycle} pages)\n`);
2823
2834
  try { await browser.close(); } catch (_) {}
2824
2835
  browser = await launchBrowser();
2825
2836
  pagesSinceRecycle = 0;
2826
2837
  } finally {
2838
+ // Release the gate first so any waiting workers can proceed, then
2839
+ // clear the recycle lock so the outer while loop stops pausing.
2840
+ try { resolveReady(); } catch (_) {}
2827
2841
  recycleLock.busy = false;
2828
2842
  const r = recycleGate.resume;
2829
2843
  recycleGate.resume = null;
@@ -2835,12 +2849,19 @@ async function runPrerender(config) {
2835
2849
  while (true) {
2836
2850
  // Pause if a recycle is underway.
2837
2851
  if (recycleLock.busy) await waitForResume();
2852
+ // Also wait for any pending browser readiness (e.g. another worker
2853
+ // started a recycle while we were processing).
2854
+ await browserReadyPromise;
2838
2855
 
2839
2856
  const i = index++;
2840
2857
  if (i >= puppeteerPaths.length) return;
2841
2858
  const pathSeg = puppeteerPaths[i];
2842
2859
  let attempt = 0;
2843
2860
  while (true) {
2861
+ // Re-check recycle state at the start of every retry iteration.
2862
+ if (recycleLock.busy) await waitForResume();
2863
+ await browserReadyPromise;
2864
+
2844
2865
  const failureCountBefore = failedPaths.length;
2845
2866
  activeWorkers++;
2846
2867
  try {
@@ -2849,6 +2870,17 @@ async function runPrerender(config) {
2849
2870
  if (seg !== NOT_FOUND_PATH) baseHtmlCache.set(seg || '', html);
2850
2871
  },
2851
2872
  });
2873
+ } catch (err) {
2874
+ // Unexpected exception escaped processPath (e.g. browser died
2875
+ // mid-call). Record as a failure so the retry logic can handle
2876
+ // it gracefully instead of tearing down the whole worker.
2877
+ failedPaths.push({
2878
+ path: pathSeg === '' ? '/' : '/' + pathSeg,
2879
+ message: err && err.message ? err.message : String(err),
2880
+ });
2881
+ if (failedPaths.length <= 10) {
2882
+ process.stderr.write(`prerender: worker exception on ${pathSeg || '/'}: ${failedPaths[failedPaths.length - 1].message}\n`);
2883
+ }
2852
2884
  } finally {
2853
2885
  activeWorkers--;
2854
2886
  if (activeWorkers === 0 && recycleGate.waitForZero) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {