@wildwinter/app-shell 0.46.2 → 0.46.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/updater.js CHANGED
@@ -1,9 +1,9 @@
1
+ import { frontForModal } from './chunk-CPNTROWD.js';
1
2
  import { app, BrowserWindow, ipcMain } from 'electron';
2
3
  import { appendFileSync } from 'fs';
3
4
  import { join } from 'path';
4
5
  import electronUpdater from 'electron-updater';
5
6
 
6
- // src/updater.ts
7
7
  var UPDATER_CHANNELS = {
8
8
  /** main -> renderer: "are there unsaved edits?"; reply on `dirtyReply`. */
9
9
  checkDirty: "updater:check-dirty",
@@ -153,18 +153,37 @@ function themedPrompt(win, opts) {
153
153
  resolve(fallback);
154
154
  return;
155
155
  }
156
+ const release = frontWhileOpen(win);
157
+ const done = (idx) => {
158
+ release();
159
+ resolve(idx);
160
+ };
156
161
  const onReply = (_e, idx) => {
157
162
  clearTimeout(timeout);
158
- resolve(typeof idx === "number" ? idx : fallback);
163
+ done(typeof idx === "number" ? idx : fallback);
159
164
  };
160
165
  ipcMain.once(UPDATER_CHANNELS.promptReply, onReply);
161
166
  const timeout = setTimeout(() => {
162
167
  ipcMain.removeListener(UPDATER_CHANNELS.promptReply, onReply);
163
- resolve(fallback);
168
+ done(fallback);
164
169
  }, 3e5);
165
170
  win.webContents.send(UPDATER_CHANNELS.prompt, opts);
166
171
  });
167
172
  }
173
+ function frontWhileOpen(win) {
174
+ let release = null;
175
+ let finished = false;
176
+ const front = () => {
177
+ if (!finished && !win.isDestroyed()) release = frontForModal(win);
178
+ };
179
+ if (BrowserWindow.getFocusedWindow()) front();
180
+ else app.once("browser-window-focus", front);
181
+ return () => {
182
+ finished = true;
183
+ app.removeListener("browser-window-focus", front);
184
+ release?.();
185
+ };
186
+ }
168
187
  async function quitAndInstallSafely() {
169
188
  const win = activeWindow();
170
189
  if (!await askRendererIsDirty(win)) {
@@ -204,7 +223,8 @@ autoUpdater.on("update-downloaded", (info) => {
204
223
  defaultId: 0,
205
224
  cancelId: 1,
206
225
  message: "Update ready to install",
207
- detail: `${appName} ${info.version} has been downloaded. Restart now to install it, or it installs the next time you quit.`
226
+ detail: `${appName} ${info.version} has been downloaded. Restart now to install it, or it installs the next time you quit.`,
227
+ unasked: true
208
228
  }).then((response) => {
209
229
  if (response === 0) void quitAndInstallSafely();
210
230
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/updater.ts"],"names":[],"mappings":"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","file":"updater.js","sourcesContent":["// ---------------------------------------------------------------------------\n// In-app auto-update over electron-updater: the family's, lifted from Patterpad\n// where it was hardened in the field.\n//\n// This is not a wrapper anybody should write twice. Every rule in it is a bug\n// that reached a user:\n//\n// - Downloads are driven HERE rather than by electron-updater's autoDownload,\n// because holding the CancellationToken is what lets a stall watchdog kill\n// and retry a download that hangs. A hung stream emits neither progress nor\n// `error`, so anything built on events alone sits \"downloading\" for ever\n// (Patterpad #33: a Windows user's download did exactly that, all day).\n// - Differential download is off. On Windows it stalled silently across an\n// Electron major bump, where almost no blocks match anyway.\n// - There is a PERSISTENT LOG in userData, because electron-updater is silent\n// by default and a stalled download otherwise leaves nothing to diagnose.\n// - Installing asks the renderer whether there is unsaved work, and offers\n// Save / Discard / Cancel. \"Restart Now\" must never silently drop an\n// author's afternoon.\n// - Every prompt is THEMED, in the renderer, never a stock OS dialog.\n//\n// Only runs in a packaged build; in development the manual check says so rather\n// than failing obscurely.\n//\n// THE SEAMS: the app supplies its NAME (it appears in every message), a way to\n// find the ACTIVE WINDOW, and a renderer that answers four channels - the\n// contract below. Everything else is the same in every app.\n// ---------------------------------------------------------------------------\n\nimport { app, BrowserWindow, ipcMain } from \"electron\";\nimport { appendFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ProgressInfo, UpdateDownloadedEvent, UpdateInfo } from \"electron-updater\";\nimport electronUpdater from \"electron-updater\";\n\n/** Download progress, broadcast to every window so a live dialog can show it. */\nexport interface UpdaterDownloadProgress {\n percent: number;\n transferred: number;\n total: number;\n bytesPerSecond: number;\n /** Which version is coming, so a dialog can name it. */\n version: string;\n}\n\n/** A themed prompt, answered by the renderer with the chosen button index -\n * the same contract as `dialog.showMessageBox`'s `response`. */\nexport interface UpdaterPromptOptions {\n message: string;\n detail?: string;\n buttons: string[];\n defaultId?: number;\n cancelId?: number;\n /** Show live download progress in the dialog. */\n progress?: boolean;\n}\n\n/** The channels the app's renderer must answer. Named here so both sides agree. */\nexport const UPDATER_CHANNELS = {\n /** main -> renderer: \"are there unsaved edits?\"; reply on `dirtyReply`. */\n checkDirty: \"updater:check-dirty\",\n dirtyReply: \"updater:dirty-reply\",\n /** main -> renderer: \"save now\"; reply `{ ok }` on `saveDone`. */\n saveBeforeInstall: \"updater:save-before-install\",\n saveDone: \"updater:save-done\",\n /** main -> renderer: show this prompt; reply with the button index. */\n prompt: \"updater:prompt\",\n promptReply: \"updater:prompt-reply\",\n /** main -> renderer: live download progress. */\n progress: \"updater:download-progress\",\n} as const;\n\nexport interface UpdaterOptions {\n /** The product name, as it should read in every message (\"Patterpad\"). */\n appName: string;\n /** Which window the prompts belong to. */\n activeWindow: () => BrowserWindow | null;\n /** The log file inside userData. */\n logFile?: string;\n}\n\nlet appName = \"This app\";\nlet activeWindowFn: () => BrowserWindow | null = () =>\n BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0] ?? null;\nconst activeWindow = (): BrowserWindow | null => activeWindowFn();\n\nconst { autoUpdater, CancellationToken } = electronUpdater;\n\nlet updateDownloaded: UpdateDownloadedEvent | null = null;\n// Remember the last background error so the manual \"Check for Updates\" dialog can\n// surface it - otherwise a broken feed (network policy, expired cert, GitHub outage)\n// stays invisible and the user has no idea why updates never arrive.\nlet lastBackgroundError: string | null = null;\n\n// Persistent updater log (userData/updater.log). electron-updater is silent by default; without a log a\n// Windows download that stalls without emitting `error` (found in 0.1.5) leaves nothing to diagnose.\nlet logPath = join(app.getPath(\"userData\"), \"updater.log\");\nconst writeLog = (level: string, args: unknown[]): void => {\n try { appendFileSync(logPath, `${new Date().toISOString()} [${level}] ${args.map((a) => (a instanceof Error ? a.stack || a.message : String(a))).join(\" \")}\\n`); } catch { /* logging must never throw */ }\n};\nautoUpdater.logger = {\n info: (...a: unknown[]) => writeLog(\"info\", a),\n warn: (...a: unknown[]) => writeLog(\"warn\", a),\n error: (...a: unknown[]) => writeLog(\"error\", a),\n debug: (...a: unknown[]) => writeLog(\"debug\", a),\n};\n\n// Downloads are driven by the download manager below, NOT electron-updater's autoDownload: holding\n// the CancellationToken ourselves is what lets the stall watchdog kill and retry a download that\n// hangs without ever erroring (#33 - a Windows user's download sat \"in the background\" all day; a\n// hung stream emits neither progress nor `error`, so nothing built on events alone can recover it).\nautoUpdater.autoDownload = false;\nautoUpdater.autoInstallOnAppQuit = true;\n// Force a plain full download instead of the block-by-block differential. On Windows the differential\n// path stalled silently (0.1.5): across an Electron-major bump almost no blocks match, so it downloads\n// nearly everything anyway, and it's fragile for unsigned installers. Full download is one reliable\n// stream. No-op on macOS (Squirrel.Mac always fetches the whole zip).\nautoUpdater.disableDifferentialDownload = true;\n\nautoUpdater.on(\"error\", (err) => {\n console.error(\"AutoUpdater error:\", err?.message || err);\n lastBackgroundError = err?.message || String(err);\n});\n\n// ---------------------------------------------------------------------------\n// Download manager: start, watch, retry. Platform-agnostic - it rides\n// electron-updater's shared events, identical for NSIS (win), zip (mac), and\n// AppImage (linux) feeds.\n// ---------------------------------------------------------------------------\n\nconst STALL_MS = 3 * 60 * 1000; // no progress event for this long = the download is hung\nconst WATCH_EVERY_MS = 30 * 1000; // how often the watchdog looks at the clock\nconst RETRY_DELAY_MS = 15 * 1000; // pause before re-attempting a killed/failed download\nconst MAX_ATTEMPTS = 3; // per check cycle; the 6-hourly background check starts a fresh cycle\n\ntype DownloadState = {\n info: UpdateInfo;\n token: InstanceType<typeof CancellationToken>;\n attempts: number; // attempts consumed this cycle, including the running one\n lastProgressAt: number;\n cancelledByWatchdog: boolean;\n watchdog: NodeJS.Timeout;\n};\nlet download: DownloadState | null = null;\n\nfunction broadcastProgress(p: UpdaterDownloadProgress): void {\n for (const w of BrowserWindow.getAllWindows()) {\n if (!w.isDestroyed() && !w.webContents.isDestroyed()) w.webContents.send(UPDATER_CHANNELS.progress, p);\n }\n}\n\nfunction clearDownload(): void {\n if (download) clearInterval(download.watchdog);\n download = null;\n}\n\n/** Begin (or re-attempt) downloading `info`. One state machine for every platform. */\nfunction beginDownload(info: UpdateInfo, attempts: number): void {\n clearDownload();\n const token = new CancellationToken();\n const state: DownloadState = {\n info, token, attempts: attempts + 1, lastProgressAt: Date.now(), cancelledByWatchdog: false,\n watchdog: setInterval(() => {\n if (download !== state) return;\n const quiet = Date.now() - state.lastProgressAt;\n if (quiet < STALL_MS) return;\n // Hung: no progress and no error for STALL_MS. Kill it; the download promise's catch retries.\n writeLog(\"warn\", [`watchdog: no download progress for ${Math.round(quiet / 1000)}s - cancelling (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n state.cancelledByWatchdog = true;\n state.token.cancel();\n }, WATCH_EVERY_MS),\n };\n download = state;\n writeLog(\"info\", [`download: starting ${info.version} (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n\n autoUpdater.downloadUpdate(token).then(() => {\n // Success is reported via the update-downloaded event; it clears the state.\n }).catch((err: unknown) => {\n if (download !== state) return; // superseded by a newer attempt/cycle\n const why = state.cancelledByWatchdog ? \"stalled (killed by the watchdog)\"\n : (err instanceof Error ? err.message : String(err));\n clearDownload();\n if (state.attempts < MAX_ATTEMPTS) {\n writeLog(\"warn\", [`download: attempt ${state.attempts} failed - ${why}; retrying in ${RETRY_DELAY_MS / 1000}s`]);\n setTimeout(() => { if (!download && !updateDownloaded) beginDownload(info, state.attempts); }, RETRY_DELAY_MS);\n } else {\n // Out of attempts for this cycle. Record it where the manual check surfaces it; the next\n // background check (6-hourly, or Help > Check for Updates) starts a fresh cycle.\n lastBackgroundError = `Downloading ${info.version} failed ${MAX_ATTEMPTS} times, most recently with \"${why}\". It will be tried again on the next check.`;\n writeLog(\"error\", [`download: giving up on ${info.version} this cycle - ${why}`]);\n }\n });\n}\n\nautoUpdater.on(\"update-available\", (info: UpdateInfo) => {\n if (download || updateDownloaded) return; // already downloading it, or already have it\n beginDownload(info, 0);\n});\n\nautoUpdater.on(\"download-progress\", (p: ProgressInfo) => {\n if (download) download.lastProgressAt = Date.now();\n broadcastProgress({\n percent: p.percent, transferred: p.transferred, total: p.total,\n bytesPerSecond: p.bytesPerSecond, version: download?.info.version ?? \"\",\n });\n});\n\n/** Ask the renderer (one-shot, timeout-guarded) whether the open scene has unsaved edits.\n * Resolves false on any error/timeout - the renderer's dirty flag is the only truth here. */\nfunction askRendererIsDirty(win: BrowserWindow | null): Promise<boolean> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(false); return; }\n const onReply = (_e: unknown, isDirty: boolean) => { clearTimeout(timeout); resolve(!!isDirty); };\n ipcMain.once(UPDATER_CHANNELS.dirtyReply, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.dirtyReply, onReply);\n console.warn(\"Updater dirty-check timed out; assuming clean.\");\n resolve(false);\n }, 2000);\n win.webContents.send(UPDATER_CHANNELS.checkDirty);\n });\n}\n\n/** Trigger a save in the renderer and wait for completion. Saves run the lock-aware VC write path,\n * so the timeout is generous; if it elapses we abort rather than install over a half-saved project. */\nfunction triggerRendererSave(win: BrowserWindow | null): Promise<{ ok: boolean }> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve({ ok: false }); return; }\n const onReply = (_e: unknown, result: { ok: boolean }) => { clearTimeout(timeout); resolve(result || { ok: false }); };\n ipcMain.once(UPDATER_CHANNELS.saveDone, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.saveDone, onReply);\n console.warn(\"Updater save timed out.\");\n resolve({ ok: false });\n }, 30000);\n win.webContents.send(UPDATER_CHANNELS.saveBeforeInstall);\n });\n}\n\n/** Show a THEMED prompt in the renderer (never a stock OS dialog; design-language \"coherent to the edges\")\n * and resolve the chosen button index - the same contract as dialog.showMessageBox's `response`. Falls\n * back to cancelId/defaultId if there's no live renderer to answer (so an install is never blocked on it). */\nfunction themedPrompt(win: BrowserWindow | null, opts: UpdaterPromptOptions): Promise<number> {\n return new Promise((resolve) => {\n const fallback = opts.cancelId ?? opts.defaultId ?? 0;\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(fallback); return; }\n const onReply = (_e: unknown, idx: number) => { clearTimeout(timeout); resolve(typeof idx === \"number\" ? idx : fallback); };\n ipcMain.once(UPDATER_CHANNELS.promptReply, onReply);\n // Generous: these are user-facing prompts. If the renderer never answers (gone / hung), fall back safely.\n const timeout = setTimeout(() => { ipcMain.removeListener(UPDATER_CHANNELS.promptReply, onReply); resolve(fallback); }, 300000);\n win.webContents.send(UPDATER_CHANNELS.prompt, opts);\n });\n}\n\n/** quitAndInstall, guarded by the unsaved-edits check. autoInstallOnAppQuit routes through the normal\n * window-close save path already, so only this explicit \"Restart Now\" needs the guard. */\nasync function quitAndInstallSafely(): Promise<void> {\n const win = activeWindow();\n if (!(await askRendererIsDirty(win))) { autoUpdater.quitAndInstall(); return; }\n\n const response = await themedPrompt(win, {\n buttons: [\"Save and restart\", \"Discard and restart\", \"Cancel\"],\n defaultId: 0,\n cancelId: 2,\n message: \"You have unsaved changes.\",\n detail: `Save them before restarting to install ${appName} ${updateDownloaded?.version || \"\"}?`,\n });\n\n if (response === 2) return; // Cancel - abort the install\n if (response === 0) {\n const result = await triggerRendererSave(win);\n if (!result.ok) {\n await themedPrompt(win, {\n message: \"Save failed\",\n detail: \"Your changes could not be saved, so the update was not installed. Save manually, then try again.\",\n buttons: [\"OK\"],\n });\n return;\n }\n }\n // response === 1 (Discard) falls through.\n autoUpdater.quitAndInstall();\n}\n\nautoUpdater.on(\"update-downloaded\", (info: UpdateDownloadedEvent) => {\n clearDownload();\n updateDownloaded = info;\n lastBackgroundError = null; // it got here in the end; stale retry noise would only mislead\n writeLog(\"info\", [`download: ${info.version} downloaded and ready to install`]);\n const win = activeWindow();\n if (!win) return;\n void themedPrompt(win, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${info.version} has been downloaded. Restart now to install it, or it installs the next time you quit.`,\n }).then((response) => { if (response === 0) void quitAndInstallSafely(); });\n});\n\n/** Name the app and say how to find its window. Call once, at startup. */\nexport function configureUpdater(opts: UpdaterOptions): void {\n appName = opts.appName;\n activeWindowFn = opts.activeWindow;\n if (opts.logFile !== undefined) logPath = join(app.getPath(\"userData\"), opts.logFile);\n}\n\n/** Fire-and-forget background check; periodic + on launch. No-op in dev. */\nexport function startBackgroundUpdateCheck(): void {\n if (!app.isPackaged) return;\n autoUpdater.checkForUpdates().catch((err) => {\n console.error(\"AutoUpdater background check failed:\", err?.message || err);\n });\n}\n\n/** The Help ▸ Check for Updates… handler: always gives the user feedback (downloading / up to date /\n * error), unlike the silent background check. */\nexport async function manualCheckForUpdates(win?: BrowserWindow | null): Promise<void> {\n const parent = win || activeWindow();\n\n if (!app.isPackaged) {\n await themedPrompt(parent, {\n message: \"Updates unavailable in a development build\",\n detail: `Auto-update only runs in a packaged build. This is version ${app.getVersion()}.`,\n buttons: [\"OK\"],\n });\n return;\n }\n\n if (updateDownloaded) {\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${updateDownloaded.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n return;\n }\n\n if (download) {\n // A download is already in flight: show it, live (the dialog subscribes to progress events).\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${download.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n return;\n }\n\n try {\n // A manual check starts a fresh retry cycle: if the last one gave up, this is the user asking again.\n await autoUpdater.checkForUpdates();\n // With autoDownload off, `update-available` (fired during the await) starts the managed download,\n // so by here `download` is set iff the feed had something newer. (Read through locals: TS narrows the\n // module-level `download` to null after the guard above and cannot see the event handler mutate it.)\n const started = download as DownloadState | null;\n const ready = updateDownloaded as UpdateDownloadedEvent | null;\n if (started) {\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${started.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n lastBackgroundError = null;\n } else if (ready) {\n // The check completed a download between our earlier guard and now (tiny window, but free to handle).\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"], defaultId: 0, cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${ready.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n } else if (lastBackgroundError) {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date. The last update check reported an error:\\n${lastBackgroundError}`,\n buttons: [\"OK\"],\n });\n lastBackgroundError = null;\n } else {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date.`,\n buttons: [\"OK\"],\n });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const detail = lastBackgroundError && lastBackgroundError !== message\n ? `${message}\\n\\nA previous background check also failed with:\\n${lastBackgroundError}`\n : message;\n await themedPrompt(parent, { message: \"Update check failed\", detail, buttons: [\"OK\"] });\n lastBackgroundError = null;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/updater.ts"],"names":[],"mappings":"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","file":"updater.js","sourcesContent":["// ---------------------------------------------------------------------------\n// In-app auto-update over electron-updater: the family's, lifted from Patterpad\n// where it was hardened in the field.\n//\n// This is not a wrapper anybody should write twice. Every rule in it is a bug\n// that reached a user:\n//\n// - Downloads are driven HERE rather than by electron-updater's autoDownload,\n// because holding the CancellationToken is what lets a stall watchdog kill\n// and retry a download that hangs. A hung stream emits neither progress nor\n// `error`, so anything built on events alone sits \"downloading\" for ever\n// (Patterpad #33: a Windows user's download did exactly that, all day).\n// - Differential download is off. On Windows it stalled silently across an\n// Electron major bump, where almost no blocks match anyway.\n// - There is a PERSISTENT LOG in userData, because electron-updater is silent\n// by default and a stalled download otherwise leaves nothing to diagnose.\n// - Installing asks the renderer whether there is unsaved work, and offers\n// Save / Discard / Cancel. \"Restart Now\" must never silently drop an\n// author's afternoon.\n// - Every prompt is THEMED, in the renderer, never a stock OS dialog.\n// - Every prompt brings its window to the FRONT, past pinned tool windows,\n// for as long as it is open. It used to open under the Play window, so\n// the editor blurred behind a prompt nobody could see. A prompt that\n// arrives while the app is in the background waits for the app to come\n// back before it takes the front, and one nobody asked for puts focus on\n// its safe button, so a keystroke meant for the window it covered cannot\n// answer it.\n//\n// Only runs in a packaged build; in development the manual check says so rather\n// than failing obscurely.\n//\n// THE SEAMS: the app supplies its NAME (it appears in every message), a way to\n// find the ACTIVE WINDOW, and a renderer that answers four channels - the\n// contract below. Everything else is the same in every app.\n// ---------------------------------------------------------------------------\n\nimport { app, BrowserWindow, ipcMain } from \"electron\";\nimport { appendFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ProgressInfo, UpdateDownloadedEvent, UpdateInfo } from \"electron-updater\";\nimport electronUpdater from \"electron-updater\";\nimport { frontForModal } from \"./tool-window.js\";\n\n/** Download progress, broadcast to every window so a live dialog can show it. */\nexport interface UpdaterDownloadProgress {\n percent: number;\n transferred: number;\n total: number;\n bytesPerSecond: number;\n /** Which version is coming, so a dialog can name it. */\n version: string;\n}\n\n/** A themed prompt, answered by the renderer with the chosen button index -\n * the same contract as `dialog.showMessageBox`'s `response`. */\nexport interface UpdaterPromptOptions {\n message: string;\n detail?: string;\n buttons: string[];\n defaultId?: number;\n cancelId?: number;\n /** Show live download progress in the dialog. */\n progress?: boolean;\n /** Nobody asked for this prompt: it arrived on its own (an update finished\n * downloading). Focus starts on the cancel button, so a keystroke meant for\n * whatever the prompt covered dismisses it rather than answering yes. */\n unasked?: boolean;\n}\n\n/** The channels the app's renderer must answer. Named here so both sides agree. */\nexport const UPDATER_CHANNELS = {\n /** main -> renderer: \"are there unsaved edits?\"; reply on `dirtyReply`. */\n checkDirty: \"updater:check-dirty\",\n dirtyReply: \"updater:dirty-reply\",\n /** main -> renderer: \"save now\"; reply `{ ok }` on `saveDone`. */\n saveBeforeInstall: \"updater:save-before-install\",\n saveDone: \"updater:save-done\",\n /** main -> renderer: show this prompt; reply with the button index. */\n prompt: \"updater:prompt\",\n promptReply: \"updater:prompt-reply\",\n /** main -> renderer: live download progress. */\n progress: \"updater:download-progress\",\n} as const;\n\nexport interface UpdaterOptions {\n /** The product name, as it should read in every message (\"Patterpad\"). */\n appName: string;\n /** Which window the prompts belong to. */\n activeWindow: () => BrowserWindow | null;\n /** The log file inside userData. */\n logFile?: string;\n}\n\nlet appName = \"This app\";\nlet activeWindowFn: () => BrowserWindow | null = () =>\n BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0] ?? null;\nconst activeWindow = (): BrowserWindow | null => activeWindowFn();\n\nconst { autoUpdater, CancellationToken } = electronUpdater;\n\nlet updateDownloaded: UpdateDownloadedEvent | null = null;\n// Remember the last background error so the manual \"Check for Updates\" dialog can\n// surface it - otherwise a broken feed (network policy, expired cert, GitHub outage)\n// stays invisible and the user has no idea why updates never arrive.\nlet lastBackgroundError: string | null = null;\n\n// Persistent updater log (userData/updater.log). electron-updater is silent by default; without a log a\n// Windows download that stalls without emitting `error` (found in 0.1.5) leaves nothing to diagnose.\nlet logPath = join(app.getPath(\"userData\"), \"updater.log\");\nconst writeLog = (level: string, args: unknown[]): void => {\n try { appendFileSync(logPath, `${new Date().toISOString()} [${level}] ${args.map((a) => (a instanceof Error ? a.stack || a.message : String(a))).join(\" \")}\\n`); } catch { /* logging must never throw */ }\n};\nautoUpdater.logger = {\n info: (...a: unknown[]) => writeLog(\"info\", a),\n warn: (...a: unknown[]) => writeLog(\"warn\", a),\n error: (...a: unknown[]) => writeLog(\"error\", a),\n debug: (...a: unknown[]) => writeLog(\"debug\", a),\n};\n\n// Downloads are driven by the download manager below, NOT electron-updater's autoDownload: holding\n// the CancellationToken ourselves is what lets the stall watchdog kill and retry a download that\n// hangs without ever erroring (#33 - a Windows user's download sat \"in the background\" all day; a\n// hung stream emits neither progress nor `error`, so nothing built on events alone can recover it).\nautoUpdater.autoDownload = false;\nautoUpdater.autoInstallOnAppQuit = true;\n// Force a plain full download instead of the block-by-block differential. On Windows the differential\n// path stalled silently (0.1.5): across an Electron-major bump almost no blocks match, so it downloads\n// nearly everything anyway, and it's fragile for unsigned installers. Full download is one reliable\n// stream. No-op on macOS (Squirrel.Mac always fetches the whole zip).\nautoUpdater.disableDifferentialDownload = true;\n\nautoUpdater.on(\"error\", (err) => {\n console.error(\"AutoUpdater error:\", err?.message || err);\n lastBackgroundError = err?.message || String(err);\n});\n\n// ---------------------------------------------------------------------------\n// Download manager: start, watch, retry. Platform-agnostic - it rides\n// electron-updater's shared events, identical for NSIS (win), zip (mac), and\n// AppImage (linux) feeds.\n// ---------------------------------------------------------------------------\n\nconst STALL_MS = 3 * 60 * 1000; // no progress event for this long = the download is hung\nconst WATCH_EVERY_MS = 30 * 1000; // how often the watchdog looks at the clock\nconst RETRY_DELAY_MS = 15 * 1000; // pause before re-attempting a killed/failed download\nconst MAX_ATTEMPTS = 3; // per check cycle; the 6-hourly background check starts a fresh cycle\n\ntype DownloadState = {\n info: UpdateInfo;\n token: InstanceType<typeof CancellationToken>;\n attempts: number; // attempts consumed this cycle, including the running one\n lastProgressAt: number;\n cancelledByWatchdog: boolean;\n watchdog: NodeJS.Timeout;\n};\nlet download: DownloadState | null = null;\n\nfunction broadcastProgress(p: UpdaterDownloadProgress): void {\n for (const w of BrowserWindow.getAllWindows()) {\n if (!w.isDestroyed() && !w.webContents.isDestroyed()) w.webContents.send(UPDATER_CHANNELS.progress, p);\n }\n}\n\nfunction clearDownload(): void {\n if (download) clearInterval(download.watchdog);\n download = null;\n}\n\n/** Begin (or re-attempt) downloading `info`. One state machine for every platform. */\nfunction beginDownload(info: UpdateInfo, attempts: number): void {\n clearDownload();\n const token = new CancellationToken();\n const state: DownloadState = {\n info, token, attempts: attempts + 1, lastProgressAt: Date.now(), cancelledByWatchdog: false,\n watchdog: setInterval(() => {\n if (download !== state) return;\n const quiet = Date.now() - state.lastProgressAt;\n if (quiet < STALL_MS) return;\n // Hung: no progress and no error for STALL_MS. Kill it; the download promise's catch retries.\n writeLog(\"warn\", [`watchdog: no download progress for ${Math.round(quiet / 1000)}s - cancelling (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n state.cancelledByWatchdog = true;\n state.token.cancel();\n }, WATCH_EVERY_MS),\n };\n download = state;\n writeLog(\"info\", [`download: starting ${info.version} (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n\n autoUpdater.downloadUpdate(token).then(() => {\n // Success is reported via the update-downloaded event; it clears the state.\n }).catch((err: unknown) => {\n if (download !== state) return; // superseded by a newer attempt/cycle\n const why = state.cancelledByWatchdog ? \"stalled (killed by the watchdog)\"\n : (err instanceof Error ? err.message : String(err));\n clearDownload();\n if (state.attempts < MAX_ATTEMPTS) {\n writeLog(\"warn\", [`download: attempt ${state.attempts} failed - ${why}; retrying in ${RETRY_DELAY_MS / 1000}s`]);\n setTimeout(() => { if (!download && !updateDownloaded) beginDownload(info, state.attempts); }, RETRY_DELAY_MS);\n } else {\n // Out of attempts for this cycle. Record it where the manual check surfaces it; the next\n // background check (6-hourly, or Help > Check for Updates) starts a fresh cycle.\n lastBackgroundError = `Downloading ${info.version} failed ${MAX_ATTEMPTS} times, most recently with \"${why}\". It will be tried again on the next check.`;\n writeLog(\"error\", [`download: giving up on ${info.version} this cycle - ${why}`]);\n }\n });\n}\n\nautoUpdater.on(\"update-available\", (info: UpdateInfo) => {\n if (download || updateDownloaded) return; // already downloading it, or already have it\n beginDownload(info, 0);\n});\n\nautoUpdater.on(\"download-progress\", (p: ProgressInfo) => {\n if (download) download.lastProgressAt = Date.now();\n broadcastProgress({\n percent: p.percent, transferred: p.transferred, total: p.total,\n bytesPerSecond: p.bytesPerSecond, version: download?.info.version ?? \"\",\n });\n});\n\n/** Ask the renderer (one-shot, timeout-guarded) whether the open scene has unsaved edits.\n * Resolves false on any error/timeout - the renderer's dirty flag is the only truth here. */\nfunction askRendererIsDirty(win: BrowserWindow | null): Promise<boolean> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(false); return; }\n const onReply = (_e: unknown, isDirty: boolean) => { clearTimeout(timeout); resolve(!!isDirty); };\n ipcMain.once(UPDATER_CHANNELS.dirtyReply, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.dirtyReply, onReply);\n console.warn(\"Updater dirty-check timed out; assuming clean.\");\n resolve(false);\n }, 2000);\n win.webContents.send(UPDATER_CHANNELS.checkDirty);\n });\n}\n\n/** Trigger a save in the renderer and wait for completion. Saves run the lock-aware VC write path,\n * so the timeout is generous; if it elapses we abort rather than install over a half-saved project. */\nfunction triggerRendererSave(win: BrowserWindow | null): Promise<{ ok: boolean }> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve({ ok: false }); return; }\n const onReply = (_e: unknown, result: { ok: boolean }) => { clearTimeout(timeout); resolve(result || { ok: false }); };\n ipcMain.once(UPDATER_CHANNELS.saveDone, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.saveDone, onReply);\n console.warn(\"Updater save timed out.\");\n resolve({ ok: false });\n }, 30000);\n win.webContents.send(UPDATER_CHANNELS.saveBeforeInstall);\n });\n}\n\n/** Show a THEMED prompt in the renderer (never a stock OS dialog; design-language \"coherent to the edges\")\n * and resolve the chosen button index - the same contract as dialog.showMessageBox's `response`. Falls\n * back to cancelId/defaultId if there's no live renderer to answer (so an install is never blocked on it). */\nfunction themedPrompt(win: BrowserWindow | null, opts: UpdaterPromptOptions): Promise<number> {\n return new Promise((resolve) => {\n const fallback = opts.cancelId ?? opts.defaultId ?? 0;\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(fallback); return; }\n const release = frontWhileOpen(win);\n const done = (idx: number): void => { release(); resolve(idx); };\n const onReply = (_e: unknown, idx: number) => { clearTimeout(timeout); done(typeof idx === \"number\" ? idx : fallback); };\n ipcMain.once(UPDATER_CHANNELS.promptReply, onReply);\n // Generous: these are user-facing prompts. If the renderer never answers (gone / hung), fall back safely.\n const timeout = setTimeout(() => { ipcMain.removeListener(UPDATER_CHANNELS.promptReply, onReply); done(fallback); }, 300000);\n win.webContents.send(UPDATER_CHANNELS.prompt, opts);\n });\n}\n\n/** Keep `win` in front of the app's other windows, pinned tool windows included, while a prompt is open\n * on it, and return the release. If the app is in the background the fronting waits until one of its\n * windows takes focus again: a prompt is no reason to pull an app over whatever the user went to. */\nfunction frontWhileOpen(win: BrowserWindow): () => void {\n let release: (() => void) | null = null;\n let finished = false;\n const front = (): void => { if (!finished && !win.isDestroyed()) release = frontForModal(win); };\n if (BrowserWindow.getFocusedWindow()) front();\n else app.once(\"browser-window-focus\", front);\n return () => {\n finished = true;\n app.removeListener(\"browser-window-focus\", front);\n release?.();\n };\n}\n\n/** quitAndInstall, guarded by the unsaved-edits check. autoInstallOnAppQuit routes through the normal\n * window-close save path already, so only this explicit \"Restart Now\" needs the guard. */\nasync function quitAndInstallSafely(): Promise<void> {\n const win = activeWindow();\n if (!(await askRendererIsDirty(win))) { autoUpdater.quitAndInstall(); return; }\n\n const response = await themedPrompt(win, {\n buttons: [\"Save and restart\", \"Discard and restart\", \"Cancel\"],\n defaultId: 0,\n cancelId: 2,\n message: \"You have unsaved changes.\",\n detail: `Save them before restarting to install ${appName} ${updateDownloaded?.version || \"\"}?`,\n });\n\n if (response === 2) return; // Cancel - abort the install\n if (response === 0) {\n const result = await triggerRendererSave(win);\n if (!result.ok) {\n await themedPrompt(win, {\n message: \"Save failed\",\n detail: \"Your changes could not be saved, so the update was not installed. Save manually, then try again.\",\n buttons: [\"OK\"],\n });\n return;\n }\n }\n // response === 1 (Discard) falls through.\n autoUpdater.quitAndInstall();\n}\n\nautoUpdater.on(\"update-downloaded\", (info: UpdateDownloadedEvent) => {\n clearDownload();\n updateDownloaded = info;\n lastBackgroundError = null; // it got here in the end; stale retry noise would only mislead\n writeLog(\"info\", [`download: ${info.version} downloaded and ready to install`]);\n const win = activeWindow();\n if (!win) return;\n void themedPrompt(win, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${info.version} has been downloaded. Restart now to install it, or it installs the next time you quit.`,\n unasked: true,\n }).then((response) => { if (response === 0) void quitAndInstallSafely(); });\n});\n\n/** Name the app and say how to find its window. Call once, at startup. */\nexport function configureUpdater(opts: UpdaterOptions): void {\n appName = opts.appName;\n activeWindowFn = opts.activeWindow;\n if (opts.logFile !== undefined) logPath = join(app.getPath(\"userData\"), opts.logFile);\n}\n\n/** Fire-and-forget background check; periodic + on launch. No-op in dev. */\nexport function startBackgroundUpdateCheck(): void {\n if (!app.isPackaged) return;\n autoUpdater.checkForUpdates().catch((err) => {\n console.error(\"AutoUpdater background check failed:\", err?.message || err);\n });\n}\n\n/** The Help ▸ Check for Updates… handler: always gives the user feedback (downloading / up to date /\n * error), unlike the silent background check. */\nexport async function manualCheckForUpdates(win?: BrowserWindow | null): Promise<void> {\n const parent = win || activeWindow();\n\n if (!app.isPackaged) {\n await themedPrompt(parent, {\n message: \"Updates unavailable in a development build\",\n detail: `Auto-update only runs in a packaged build. This is version ${app.getVersion()}.`,\n buttons: [\"OK\"],\n });\n return;\n }\n\n if (updateDownloaded) {\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${updateDownloaded.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n return;\n }\n\n if (download) {\n // A download is already in flight: show it, live (the dialog subscribes to progress events).\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${download.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n return;\n }\n\n try {\n // A manual check starts a fresh retry cycle: if the last one gave up, this is the user asking again.\n await autoUpdater.checkForUpdates();\n // With autoDownload off, `update-available` (fired during the await) starts the managed download,\n // so by here `download` is set iff the feed had something newer. (Read through locals: TS narrows the\n // module-level `download` to null after the guard above and cannot see the event handler mutate it.)\n const started = download as DownloadState | null;\n const ready = updateDownloaded as UpdateDownloadedEvent | null;\n if (started) {\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${started.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n lastBackgroundError = null;\n } else if (ready) {\n // The check completed a download between our earlier guard and now (tiny window, but free to handle).\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"], defaultId: 0, cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${ready.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n } else if (lastBackgroundError) {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date. The last update check reported an error:\\n${lastBackgroundError}`,\n buttons: [\"OK\"],\n });\n lastBackgroundError = null;\n } else {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date.`,\n buttons: [\"OK\"],\n });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const detail = lastBackgroundError && lastBackgroundError !== message\n ? `${message}\\n\\nA previous background check also failed with:\\n${lastBackgroundError}`\n : message;\n await themedPrompt(parent, { message: \"Update check failed\", detail, buttons: [\"OK\"] });\n lastBackgroundError = null;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildwinter/app-shell",
3
- "version": "0.46.2",
3
+ "version": "0.46.4",
4
4
  "description": "The shared shell for the editor family (Patterpad, Storylet Studio): the app-agnostic design-token grammar, the per-entity colour hash, and the vanilla-TS DOM primitive. Two palettes, one grammar - each app supplies its own palette/fonts over these tokens.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/updater-view.ts"],"names":[],"mappings":"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","file":"chunk-OPZPFOPS.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The updater's VIEW: the themed prompt main summons over `updater:prompt`\n// (updater.ts) and reads the chosen button index back from, the same contract\n// as dialog.showMessageBox's `response`. The renderer half of the updater is\n// not optional: an unanswered prompt does not fall back to a native dialog,\n// the shell waits 300 seconds and resolves its own fallback index. Both apps\n// had written this view by hand (55 of 60 lines identical); this is the one\n// they share, on the family's dialog frame, the way save.ts has save-view.ts.\n//\n// Storyletter's copy, plus Patterpad's `links` row as an option (an About box\n// used to share this dialog there). Styles ship as updater.css.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { dialogFrame } from \"./dialog.js\";\nimport type { UpdaterDownloadProgress, UpdaterPromptOptions } from \"./updater.js\";\n\nexport interface UpdaterLink { label: string; url: string }\n\nexport interface UpdaterViewOptions extends UpdaterPromptOptions {\n /** Web links under the detail. Opened through `openExternal`, never by\n * navigating the window. */\n links?: UpdaterLink[];\n /** How a link opens (the app's allow-listed shell.openExternal bridge).\n * Required for `links` to do anything. */\n openExternal?: (url: string) => void;\n}\n\n// The progress row of the open `progress: true` dialog, if any. These are\n// modal, so one at a time, so one slot is enough. Closing clears it.\nlet liveProgress: { bar: HTMLElement; label: HTMLElement } | null = null;\n\nconst mb = (n: number): string => (n / (1024 * 1024)).toFixed(1);\n\n/** Route a download-progress tick into the open dialog's bar. No-op when none\n * is showing. Registered once at boot on the app's progress channel. */\nexport function feedUpdaterDownloadProgress(p: UpdaterDownloadProgress): void {\n if (!liveProgress) return;\n const pct = Math.max(0, Math.min(100, p.percent));\n liveProgress.bar.style.width = `${pct}%`;\n liveProgress.label.textContent =\n `${pct.toFixed(0)}%, ${mb(p.transferred)} of ${mb(p.total)} MB (${mb(p.bytesPerSecond)} MB/s)`;\n}\n\n/** Show the prompt as a modal themed dialog; resolve the index of the clicked\n * button (Esc resolves `cancelId`, which defaults to the last button). */\nexport function showUpdaterDialog(opts: UpdaterViewOptions): Promise<number> {\n return new Promise((resolve) => {\n const defaultId = opts.defaultId ?? 0;\n const cancelId = opts.cancelId ?? opts.buttons.length - 1;\n\n let done = false;\n const finish = (idx: number): void => {\n if (done) return;\n done = true;\n liveProgress = null; // stop feeding a dialog that is gone\n resolve(idx);\n frame.close();\n };\n const frame = dialogFrame({\n title: opts.message, className: \"updater-dialog\",\n // Esc is the cancel button, not a further answer: main is waiting on an index.\n onClose: () => finish(cancelId),\n });\n if (opts.detail) frame.body.append(el(\"p\", \"updater-detail\", opts.detail));\n\n if (opts.progress) {\n const track = el(\"div\", \"updater-progress-track\");\n const bar = el(\"div\", \"updater-progress-bar\");\n track.append(bar);\n const label = el(\"p\", \"updater-progress-label\", \"Starting download…\");\n frame.body.append(track, label);\n liveProgress = { bar, label };\n }\n\n if (opts.links?.length) {\n const row = el(\"p\", \"updater-links\");\n for (const link of opts.links) {\n const a = el(\"a\", undefined, link.label);\n a.href = link.url;\n a.addEventListener(\"click\", (e) => { e.preventDefault(); opts.openExternal?.(link.url); });\n row.append(a);\n }\n frame.body.append(row);\n }\n\n opts.buttons.forEach((label, i) => {\n // The default is the affirmative and wears the accent; every other\n // button, the way out included, is the plain one.\n const b = el(\"button\", i === defaultId ? \"btn primary\" : \"btn\", label);\n b.type = \"button\";\n b.addEventListener(\"click\", () => finish(i));\n frame.actions.append(b);\n if (i === defaultId) queueMicrotask(() => b.focus());\n });\n\n frame.open();\n });\n}\n"]}