@wildwinter/app-shell 0.1.0 → 0.14.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.
- package/README.md +35 -0
- package/dist/about.css +76 -0
- package/dist/about.css.map +1 -0
- package/dist/anchored.css +50 -0
- package/dist/anchored.css.map +1 -0
- package/dist/app-store.cjs +74 -0
- package/dist/app-store.cjs.map +1 -0
- package/dist/app-store.d.cts +75 -0
- package/dist/app-store.d.ts +75 -0
- package/dist/app-store.js +72 -0
- package/dist/app-store.js.map +1 -0
- package/dist/chunk-MDGOIT3O.js +58 -0
- package/dist/chunk-MDGOIT3O.js.map +1 -0
- package/dist/chunk-R6RO5TXB.js +16 -0
- package/dist/chunk-R6RO5TXB.js.map +1 -0
- package/dist/comments.css +109 -0
- package/dist/comments.css.map +1 -0
- package/dist/confirm.css +62 -0
- package/dist/confirm.css.map +1 -0
- package/dist/identity.css +62 -0
- package/dist/identity.css.map +1 -0
- package/dist/index.cjs +1287 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +488 -1
- package/dist/index.d.ts +488 -1
- package/dist/index.js +1241 -1
- package/dist/index.js.map +1 -1
- package/dist/job.cjs +61 -0
- package/dist/job.cjs.map +1 -0
- package/dist/job.css +70 -0
- package/dist/job.css.map +1 -0
- package/dist/job.d.cts +56 -0
- package/dist/job.d.ts +56 -0
- package/dist/job.js +3 -0
- package/dist/job.js.map +1 -0
- package/dist/menu.cjs +19 -0
- package/dist/menu.cjs.map +1 -0
- package/dist/menu.d.cts +32 -0
- package/dist/menu.d.ts +32 -0
- package/dist/menu.js +3 -0
- package/dist/menu.js.map +1 -0
- package/dist/notes-editor.css +107 -0
- package/dist/notes-editor.css.map +1 -0
- package/dist/pane-shell.css +114 -0
- package/dist/pane-shell.css.map +1 -0
- package/dist/session.cjs +52 -0
- package/dist/session.cjs.map +1 -0
- package/dist/session.d.cts +79 -0
- package/dist/session.d.ts +79 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/settings.css +266 -0
- package/dist/settings.css.map +1 -0
- package/dist/tool-window.cjs +79 -0
- package/dist/tool-window.cjs.map +1 -0
- package/dist/tool-window.css +60 -0
- package/dist/tool-window.css.map +1 -0
- package/dist/tool-window.d.cts +63 -0
- package/dist/tool-window.d.ts +63 -0
- package/dist/tool-window.js +73 -0
- package/dist/tool-window.js.map +1 -0
- package/dist/tooltip.css +22 -0
- package/dist/tooltip.css.map +1 -0
- package/dist/updater.cjs +315 -0
- package/dist/updater.cjs.map +1 -0
- package/dist/updater.d.cts +53 -0
- package/dist/updater.d.ts +53 -0
- package/dist/updater.js +306 -0
- package/dist/updater.js.map +1 -0
- package/dist/vc-status.cjs +77 -0
- package/dist/vc-status.cjs.map +1 -0
- package/dist/vc-status.d.cts +40 -0
- package/dist/vc-status.d.ts +40 -0
- package/dist/vc-status.js +71 -0
- package/dist/vc-status.js.map +1 -0
- package/dist/vc.css +41 -0
- package/dist/vc.css.map +1 -0
- package/package.json +101 -2
- package/dist/tokens.d.cts +0 -2
- package/dist/tokens.d.ts +0 -2
package/dist/updater.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { app, BrowserWindow, ipcMain } from 'electron';
|
|
2
|
+
import { appendFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import electronUpdater from 'electron-updater';
|
|
5
|
+
|
|
6
|
+
// src/updater.ts
|
|
7
|
+
var UPDATER_CHANNELS = {
|
|
8
|
+
/** main -> renderer: "are there unsaved edits?"; reply on `dirtyReply`. */
|
|
9
|
+
checkDirty: "updater:check-dirty",
|
|
10
|
+
dirtyReply: "updater:dirty-reply",
|
|
11
|
+
/** main -> renderer: "save now"; reply `{ ok }` on `saveDone`. */
|
|
12
|
+
saveBeforeInstall: "updater:save-before-install",
|
|
13
|
+
saveDone: "updater:save-done",
|
|
14
|
+
/** main -> renderer: show this prompt; reply with the button index. */
|
|
15
|
+
prompt: "updater:prompt",
|
|
16
|
+
promptReply: "updater:prompt-reply",
|
|
17
|
+
/** main -> renderer: live download progress. */
|
|
18
|
+
progress: "updater:download-progress"
|
|
19
|
+
};
|
|
20
|
+
var appName = "This app";
|
|
21
|
+
var activeWindowFn = () => BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0] ?? null;
|
|
22
|
+
var activeWindow = () => activeWindowFn();
|
|
23
|
+
var { autoUpdater, CancellationToken } = electronUpdater;
|
|
24
|
+
var updateDownloaded = null;
|
|
25
|
+
var lastBackgroundError = null;
|
|
26
|
+
var logPath = join(app.getPath("userData"), "updater.log");
|
|
27
|
+
var writeLog = (level, args) => {
|
|
28
|
+
try {
|
|
29
|
+
appendFileSync(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${args.map((a) => a instanceof Error ? a.stack || a.message : String(a)).join(" ")}
|
|
30
|
+
`);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
autoUpdater.logger = {
|
|
35
|
+
info: (...a) => writeLog("info", a),
|
|
36
|
+
warn: (...a) => writeLog("warn", a),
|
|
37
|
+
error: (...a) => writeLog("error", a),
|
|
38
|
+
debug: (...a) => writeLog("debug", a)
|
|
39
|
+
};
|
|
40
|
+
autoUpdater.autoDownload = false;
|
|
41
|
+
autoUpdater.autoInstallOnAppQuit = true;
|
|
42
|
+
autoUpdater.disableDifferentialDownload = true;
|
|
43
|
+
autoUpdater.on("error", (err) => {
|
|
44
|
+
console.error("AutoUpdater error:", err?.message || err);
|
|
45
|
+
lastBackgroundError = err?.message || String(err);
|
|
46
|
+
});
|
|
47
|
+
var STALL_MS = 3 * 60 * 1e3;
|
|
48
|
+
var WATCH_EVERY_MS = 30 * 1e3;
|
|
49
|
+
var RETRY_DELAY_MS = 15 * 1e3;
|
|
50
|
+
var MAX_ATTEMPTS = 3;
|
|
51
|
+
var download = null;
|
|
52
|
+
function broadcastProgress(p) {
|
|
53
|
+
for (const w of BrowserWindow.getAllWindows()) {
|
|
54
|
+
if (!w.isDestroyed() && !w.webContents.isDestroyed()) w.webContents.send(UPDATER_CHANNELS.progress, p);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function clearDownload() {
|
|
58
|
+
if (download) clearInterval(download.watchdog);
|
|
59
|
+
download = null;
|
|
60
|
+
}
|
|
61
|
+
function beginDownload(info, attempts) {
|
|
62
|
+
clearDownload();
|
|
63
|
+
const token = new CancellationToken();
|
|
64
|
+
const state = {
|
|
65
|
+
info,
|
|
66
|
+
token,
|
|
67
|
+
attempts: attempts + 1,
|
|
68
|
+
lastProgressAt: Date.now(),
|
|
69
|
+
cancelledByWatchdog: false,
|
|
70
|
+
watchdog: setInterval(() => {
|
|
71
|
+
if (download !== state) return;
|
|
72
|
+
const quiet = Date.now() - state.lastProgressAt;
|
|
73
|
+
if (quiet < STALL_MS) return;
|
|
74
|
+
writeLog("warn", [`watchdog: no download progress for ${Math.round(quiet / 1e3)}s - cancelling (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);
|
|
75
|
+
state.cancelledByWatchdog = true;
|
|
76
|
+
state.token.cancel();
|
|
77
|
+
}, WATCH_EVERY_MS)
|
|
78
|
+
};
|
|
79
|
+
download = state;
|
|
80
|
+
writeLog("info", [`download: starting ${info.version} (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);
|
|
81
|
+
autoUpdater.downloadUpdate(token).then(() => {
|
|
82
|
+
}).catch((err) => {
|
|
83
|
+
if (download !== state) return;
|
|
84
|
+
const why = state.cancelledByWatchdog ? "stalled (killed by the watchdog)" : err instanceof Error ? err.message : String(err);
|
|
85
|
+
clearDownload();
|
|
86
|
+
if (state.attempts < MAX_ATTEMPTS) {
|
|
87
|
+
writeLog("warn", [`download: attempt ${state.attempts} failed - ${why}; retrying in ${RETRY_DELAY_MS / 1e3}s`]);
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
if (!download && !updateDownloaded) beginDownload(info, state.attempts);
|
|
90
|
+
}, RETRY_DELAY_MS);
|
|
91
|
+
} else {
|
|
92
|
+
lastBackgroundError = `Downloading ${info.version} failed ${MAX_ATTEMPTS} times (last: ${why}). Will retry on the next check.`;
|
|
93
|
+
writeLog("error", [`download: giving up on ${info.version} this cycle - ${why}`]);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
autoUpdater.on("update-available", (info) => {
|
|
98
|
+
if (download || updateDownloaded) return;
|
|
99
|
+
beginDownload(info, 0);
|
|
100
|
+
});
|
|
101
|
+
autoUpdater.on("download-progress", (p) => {
|
|
102
|
+
if (download) download.lastProgressAt = Date.now();
|
|
103
|
+
broadcastProgress({
|
|
104
|
+
percent: p.percent,
|
|
105
|
+
transferred: p.transferred,
|
|
106
|
+
total: p.total,
|
|
107
|
+
bytesPerSecond: p.bytesPerSecond,
|
|
108
|
+
version: download?.info.version ?? ""
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
function askRendererIsDirty(win) {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
if (!win || win.isDestroyed() || win.webContents.isDestroyed()) {
|
|
114
|
+
resolve(false);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const onReply = (_e, isDirty) => {
|
|
118
|
+
clearTimeout(timeout);
|
|
119
|
+
resolve(!!isDirty);
|
|
120
|
+
};
|
|
121
|
+
ipcMain.once(UPDATER_CHANNELS.dirtyReply, onReply);
|
|
122
|
+
const timeout = setTimeout(() => {
|
|
123
|
+
ipcMain.removeListener(UPDATER_CHANNELS.dirtyReply, onReply);
|
|
124
|
+
console.warn("Updater dirty-check timed out; assuming clean.");
|
|
125
|
+
resolve(false);
|
|
126
|
+
}, 2e3);
|
|
127
|
+
win.webContents.send(UPDATER_CHANNELS.checkDirty);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function triggerRendererSave(win) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
if (!win || win.isDestroyed() || win.webContents.isDestroyed()) {
|
|
133
|
+
resolve({ ok: false });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const onReply = (_e, result) => {
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
resolve(result || { ok: false });
|
|
139
|
+
};
|
|
140
|
+
ipcMain.once(UPDATER_CHANNELS.saveDone, onReply);
|
|
141
|
+
const timeout = setTimeout(() => {
|
|
142
|
+
ipcMain.removeListener(UPDATER_CHANNELS.saveDone, onReply);
|
|
143
|
+
console.warn("Updater save timed out.");
|
|
144
|
+
resolve({ ok: false });
|
|
145
|
+
}, 3e4);
|
|
146
|
+
win.webContents.send(UPDATER_CHANNELS.saveBeforeInstall);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function themedPrompt(win, opts) {
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
const fallback = opts.cancelId ?? opts.defaultId ?? 0;
|
|
152
|
+
if (!win || win.isDestroyed() || win.webContents.isDestroyed()) {
|
|
153
|
+
resolve(fallback);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const onReply = (_e, idx) => {
|
|
157
|
+
clearTimeout(timeout);
|
|
158
|
+
resolve(typeof idx === "number" ? idx : fallback);
|
|
159
|
+
};
|
|
160
|
+
ipcMain.once(UPDATER_CHANNELS.promptReply, onReply);
|
|
161
|
+
const timeout = setTimeout(() => {
|
|
162
|
+
ipcMain.removeListener(UPDATER_CHANNELS.promptReply, onReply);
|
|
163
|
+
resolve(fallback);
|
|
164
|
+
}, 3e5);
|
|
165
|
+
win.webContents.send(UPDATER_CHANNELS.prompt, opts);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async function quitAndInstallSafely() {
|
|
169
|
+
const win = activeWindow();
|
|
170
|
+
if (!await askRendererIsDirty(win)) {
|
|
171
|
+
autoUpdater.quitAndInstall();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const response = await themedPrompt(win, {
|
|
175
|
+
buttons: ["Save and Restart", "Discard and Restart", "Cancel"],
|
|
176
|
+
defaultId: 0,
|
|
177
|
+
cancelId: 2,
|
|
178
|
+
message: "You have unsaved changes.",
|
|
179
|
+
detail: `Save them before restarting to install ${appName} ${updateDownloaded?.version || ""}?`
|
|
180
|
+
});
|
|
181
|
+
if (response === 2) return;
|
|
182
|
+
if (response === 0) {
|
|
183
|
+
const result = await triggerRendererSave(win);
|
|
184
|
+
if (!result.ok) {
|
|
185
|
+
await themedPrompt(win, {
|
|
186
|
+
message: "Save failed",
|
|
187
|
+
detail: "Your changes could not be saved, so the update was not installed. Save manually, then try again.",
|
|
188
|
+
buttons: ["OK"]
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
autoUpdater.quitAndInstall();
|
|
194
|
+
}
|
|
195
|
+
autoUpdater.on("update-downloaded", (info) => {
|
|
196
|
+
clearDownload();
|
|
197
|
+
updateDownloaded = info;
|
|
198
|
+
lastBackgroundError = null;
|
|
199
|
+
writeLog("info", [`download: ${info.version} downloaded and ready to install`]);
|
|
200
|
+
const win = activeWindow();
|
|
201
|
+
if (!win) return;
|
|
202
|
+
void themedPrompt(win, {
|
|
203
|
+
buttons: ["Restart Now", "Later"],
|
|
204
|
+
defaultId: 0,
|
|
205
|
+
cancelId: 1,
|
|
206
|
+
message: "Update ready to install",
|
|
207
|
+
detail: `${appName} ${info.version} has been downloaded. Restart now to apply, or it will install automatically next time you quit.`
|
|
208
|
+
}).then((response) => {
|
|
209
|
+
if (response === 0) void quitAndInstallSafely();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
function configureUpdater(opts) {
|
|
213
|
+
appName = opts.appName;
|
|
214
|
+
activeWindowFn = opts.activeWindow;
|
|
215
|
+
if (opts.logFile !== void 0) logPath = join(app.getPath("userData"), opts.logFile);
|
|
216
|
+
}
|
|
217
|
+
function startBackgroundUpdateCheck() {
|
|
218
|
+
if (!app.isPackaged) return;
|
|
219
|
+
autoUpdater.checkForUpdates().catch((err) => {
|
|
220
|
+
console.error("AutoUpdater background check failed:", err?.message || err);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async function manualCheckForUpdates(win) {
|
|
224
|
+
const parent = win || activeWindow();
|
|
225
|
+
if (!app.isPackaged) {
|
|
226
|
+
await themedPrompt(parent, {
|
|
227
|
+
message: "Updates unavailable in development build",
|
|
228
|
+
detail: `Auto-update only runs in packaged builds.
|
|
229
|
+
|
|
230
|
+
Current version: ${app.getVersion()}`,
|
|
231
|
+
buttons: ["OK"]
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (updateDownloaded) {
|
|
236
|
+
const response = await themedPrompt(parent, {
|
|
237
|
+
buttons: ["Restart Now", "Later"],
|
|
238
|
+
defaultId: 0,
|
|
239
|
+
cancelId: 1,
|
|
240
|
+
message: "Update ready to install",
|
|
241
|
+
detail: `${appName} ${updateDownloaded.version} has been downloaded. Restart now to apply.`
|
|
242
|
+
});
|
|
243
|
+
if (response === 0) await quitAndInstallSafely();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (download) {
|
|
247
|
+
await themedPrompt(parent, {
|
|
248
|
+
message: "Update available",
|
|
249
|
+
detail: `${appName} ${download.info.version} is downloading. You'll be prompted to restart when it's ready.`,
|
|
250
|
+
buttons: ["OK"],
|
|
251
|
+
progress: true
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
await autoUpdater.checkForUpdates();
|
|
257
|
+
const started = download;
|
|
258
|
+
const ready = updateDownloaded;
|
|
259
|
+
if (started) {
|
|
260
|
+
await themedPrompt(parent, {
|
|
261
|
+
message: "Update available",
|
|
262
|
+
detail: `${appName} ${started.info.version} is downloading. You'll be prompted to restart when it's ready.`,
|
|
263
|
+
buttons: ["OK"],
|
|
264
|
+
progress: true
|
|
265
|
+
});
|
|
266
|
+
lastBackgroundError = null;
|
|
267
|
+
} else if (ready) {
|
|
268
|
+
const response = await themedPrompt(parent, {
|
|
269
|
+
buttons: ["Restart Now", "Later"],
|
|
270
|
+
defaultId: 0,
|
|
271
|
+
cancelId: 1,
|
|
272
|
+
message: "Update ready to install",
|
|
273
|
+
detail: `${appName} ${ready.version} has been downloaded. Restart now to apply.`
|
|
274
|
+
});
|
|
275
|
+
if (response === 0) await quitAndInstallSafely();
|
|
276
|
+
} else if (lastBackgroundError) {
|
|
277
|
+
await themedPrompt(parent, {
|
|
278
|
+
message: "You're on the latest version, but updates have had errors.",
|
|
279
|
+
detail: `Current version: ${appName} ${app.getVersion()}
|
|
280
|
+
|
|
281
|
+
Last update error:
|
|
282
|
+
${lastBackgroundError}`,
|
|
283
|
+
buttons: ["OK"]
|
|
284
|
+
});
|
|
285
|
+
lastBackgroundError = null;
|
|
286
|
+
} else {
|
|
287
|
+
await themedPrompt(parent, {
|
|
288
|
+
message: "You're on the latest version.",
|
|
289
|
+
detail: `${appName} ${app.getVersion()} is up to date.`,
|
|
290
|
+
buttons: ["OK"]
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
295
|
+
const detail = lastBackgroundError && lastBackgroundError !== message ? `${message}
|
|
296
|
+
|
|
297
|
+
A previous background check also failed with:
|
|
298
|
+
${lastBackgroundError}` : message;
|
|
299
|
+
await themedPrompt(parent, { message: "Update check failed", detail, buttons: ["OK"] });
|
|
300
|
+
lastBackgroundError = null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export { UPDATER_CHANNELS, configureUpdater, manualCheckForUpdates, startBackgroundUpdateCheck };
|
|
305
|
+
//# sourceMappingURL=updater.js.map
|
|
306
|
+
//# sourceMappingURL=updater.js.map
|
|
@@ -0,0 +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 (last: ${why}). Will retry 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 apply, or it will install automatically 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 development build\",\n detail: `Auto-update only runs in packaged builds.\\n\\nCurrent 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 apply.`,\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 apply.`,\n });\n if (response === 0) await quitAndInstallSafely();\n } else if (lastBackgroundError) {\n await themedPrompt(parent, {\n message: \"You're on the latest version, but updates have had errors.\",\n detail: `Current version: ${appName} ${app.getVersion()}\\n\\nLast update 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"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var simpleVcLib = require('@wildwinter/simple-vc-lib');
|
|
4
|
+
|
|
5
|
+
// src/vc-status.ts
|
|
6
|
+
var REMOTE_STATUS_THROTTLE_MS = 15e3;
|
|
7
|
+
var read = simpleVcLib.fileStatusAsync;
|
|
8
|
+
var logPrefix = "app-shell";
|
|
9
|
+
function setVcLogPrefix(name) {
|
|
10
|
+
logPrefix = name;
|
|
11
|
+
}
|
|
12
|
+
var lastRemoteAt = 0;
|
|
13
|
+
var cachedRemoteBits = /* @__PURE__ */ new Map();
|
|
14
|
+
var inFlight;
|
|
15
|
+
function setStatusReader(reader) {
|
|
16
|
+
read = reader ?? simpleVcLib.fileStatusAsync;
|
|
17
|
+
}
|
|
18
|
+
function resetShardStatus() {
|
|
19
|
+
lastRemoteAt = 0;
|
|
20
|
+
cachedRemoteBits = /* @__PURE__ */ new Map();
|
|
21
|
+
}
|
|
22
|
+
async function query(shards) {
|
|
23
|
+
const states = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const s of shards) states.set(s.key, { writable: true });
|
|
25
|
+
const keyOf = new Map(shards.map((s) => [s.path, s.key]));
|
|
26
|
+
const doRemote = Date.now() - lastRemoteAt >= REMOTE_STATUS_THROTTLE_MS;
|
|
27
|
+
let system = "filesystem";
|
|
28
|
+
try {
|
|
29
|
+
for (const st of await read(shards.map((s) => s.path), { remote: doRemote })) {
|
|
30
|
+
const key = keyOf.get(st.filePath);
|
|
31
|
+
if (key === void 0) continue;
|
|
32
|
+
system = st.system;
|
|
33
|
+
const acc = states.get(key);
|
|
34
|
+
if (!st.writable) acc.writable = false;
|
|
35
|
+
if (doRemote) {
|
|
36
|
+
if (st.lockedBy?.length) acc.lockedBy = [.../* @__PURE__ */ new Set([...acc.lockedBy ?? [], ...st.lockedBy])];
|
|
37
|
+
if (st.outOfDate) acc.outOfDate = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (doRemote) {
|
|
41
|
+
lastRemoteAt = Date.now();
|
|
42
|
+
cachedRemoteBits = new Map([...states].map(([key, s]) => [key, {
|
|
43
|
+
...s.lockedBy !== void 0 ? { lockedBy: s.lockedBy } : {},
|
|
44
|
+
...s.outOfDate !== void 0 ? { outOfDate: s.outOfDate } : {}
|
|
45
|
+
}]));
|
|
46
|
+
} else {
|
|
47
|
+
for (const [key, acc] of states) {
|
|
48
|
+
const cached = cachedRemoteBits.get(key);
|
|
49
|
+
if (cached?.lockedBy?.length) acc.lockedBy = cached.lockedBy;
|
|
50
|
+
if (cached?.outOfDate) acc.outOfDate = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.warn(`${logPrefix}: the version-control status query failed - treating every shard as writable:`, e);
|
|
55
|
+
}
|
|
56
|
+
return { system, states };
|
|
57
|
+
}
|
|
58
|
+
function shardStatus(shards) {
|
|
59
|
+
if (!inFlight) {
|
|
60
|
+
inFlight = (async () => {
|
|
61
|
+
try {
|
|
62
|
+
return await query(shards);
|
|
63
|
+
} finally {
|
|
64
|
+
inFlight = void 0;
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
67
|
+
}
|
|
68
|
+
return inFlight;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
exports.REMOTE_STATUS_THROTTLE_MS = REMOTE_STATUS_THROTTLE_MS;
|
|
72
|
+
exports.resetShardStatus = resetShardStatus;
|
|
73
|
+
exports.setStatusReader = setStatusReader;
|
|
74
|
+
exports.setVcLogPrefix = setVcLogPrefix;
|
|
75
|
+
exports.shardStatus = shardStatus;
|
|
76
|
+
//# sourceMappingURL=vc-status.cjs.map
|
|
77
|
+
//# sourceMappingURL=vc-status.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/vc-status.ts"],"names":["fileStatusAsync"],"mappings":"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","file":"vc-status.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// Per-shard version-control state, over @wildwinter/simple-vc-lib.\n//\n// The family's, because it follows from the SHAPE rather than from any app's\n// subject: these are all Electron apps editing JSON shards in a working copy,\n// so they all want to say \"locked by Bo\" before a save is refused rather than\n// after. Patterpad wrote it, Storyletter hand-ported it, and this is the third\n// copy becoming the only one.\n//\n// A SHARD here is just a key and a path: what the key means is the app's\n// business (a scene, a deck, a box's three files folded into one row).\n//\n// THROTTLING, which is the reason this is a module rather than a function call:\n// the local bits (`writable` - the on-disk read-only bit a lock VCS keys off)\n// are cheap and refresh on every call, so a save re-badges at once. The remote\n// bits (`lockedBy` / `outOfDate`) cost a SERVER round-trip under SVN and\n// Plastic, so they are re-queried at most once per window and otherwise reused\n// from the cache. The query is async (never blocks the main-process event loop)\n// and coalesced (a save, a focus and the poll timer arriving together make ONE\n// query), so a lock badge can never make typing stutter.\n//\n// Best-effort throughout: if the query throws (no tooling, broken repo) every\n// shard reports clean and writable, and the editor carries on.\n// ---------------------------------------------------------------------------\n\nimport { fileStatusAsync } from \"@wildwinter/simple-vc-lib\";\nimport type { VCFileStatus, VCStatusOptions } from \"@wildwinter/simple-vc-lib\";\n\n/** One shard to ask about: the key the renderer looks state up by, and its\n * absolute path on disk. */\nexport interface ShardRef {\n key: string;\n path: string;\n}\n\n/** One shard's version-control state. All-clean is `{ writable: true }`. */\nexport interface ShardState {\n /** Writable on disk right now. `false` = the read-only bit is set (a lock\n * VCS has not checked it out yet, or someone else holds it). */\n writable: boolean;\n /** Who else holds it open / locked (e.g. \"bo@bo-ws\") - we can read it, not\n * write it. */\n lockedBy?: string[];\n /** A newer revision exists on the server (get latest before editing). */\n outOfDate?: boolean;\n}\n\nexport interface ShardStatus {\n /** The backend simple-vc-lib actually detected (\"git\" / \"perforce\" / ...). */\n system: string;\n states: Map<string, ShardState>;\n}\n\n/** How long a remote (server round-trip) answer stays good for. */\nexport const REMOTE_STATUS_THROTTLE_MS = 15_000;\n\ntype StatusReader = (paths: string[], options?: VCStatusOptions) => Promise<VCFileStatus[]>;\n\nlet read: StatusReader = fileStatusAsync;\n/** What the app calls itself in the one warning this module can emit. */\nlet logPrefix = \"app-shell\";\n/** Name the app, for that warning. */\nexport function setVcLogPrefix(name: string): void { logPrefix = name; }\nlet lastRemoteAt = 0;\nlet cachedRemoteBits = new Map<string, { lockedBy?: string[]; outOfDate?: boolean }>();\nlet inFlight: Promise<ShardStatus> | undefined;\n\n/** Test seam, mirroring simple-vc-lib's own `setCommandRunner`: swap the status\n * reader for canned answers. Pass null to restore the real one. */\nexport function setStatusReader(reader: StatusReader | null): void {\n read = reader ?? fileStatusAsync;\n}\n\n/** Drop the throttle window and the cached remote bits - called when a project\n * opens, so a new project never inherits the last one's answers. */\nexport function resetShardStatus(): void {\n lastRemoteAt = 0;\n cachedRemoteBits = new Map();\n}\n\nasync function query(shards: ShardRef[]): Promise<ShardStatus> {\n const states = new Map<string, ShardState>();\n for (const s of shards) states.set(s.key, { writable: true });\n const keyOf = new Map(shards.map((s) => [s.path, s.key]));\n // Hit the server only when the window has elapsed; otherwise a cheap local read.\n const doRemote = Date.now() - lastRemoteAt >= REMOTE_STATUS_THROTTLE_MS;\n let system = \"filesystem\";\n try {\n for (const st of await read(shards.map((s) => s.path), { remote: doRemote })) {\n const key = keyOf.get(st.filePath);\n if (key === undefined) continue;\n system = st.system;\n const acc = states.get(key)!;\n if (!st.writable) acc.writable = false;\n if (doRemote) { // remote bits are authoritative only on a fresh server query\n if (st.lockedBy?.length) acc.lockedBy = [...new Set([...(acc.lockedBy ?? []), ...st.lockedBy])];\n if (st.outOfDate) acc.outOfDate = true;\n }\n }\n if (doRemote) { // snapshot the fresh remote bits for the throttled calls that follow\n lastRemoteAt = Date.now();\n cachedRemoteBits = new Map([...states].map(([key, s]) => [key, {\n ...(s.lockedBy !== undefined ? { lockedBy: s.lockedBy } : {}),\n ...(s.outOfDate !== undefined ? { outOfDate: s.outOfDate } : {}),\n }]));\n } else { // overlay the last known remote bits onto the fresh local snapshot\n for (const [key, acc] of states) {\n const cached = cachedRemoteBits.get(key);\n if (cached?.lockedBy?.length) acc.lockedBy = cached.lockedBy;\n if (cached?.outOfDate) acc.outOfDate = true;\n }\n }\n } catch (e) {\n // Tooling missing, repo error, timeout: everything reads clean + writable.\n console.warn(`${logPrefix}: the version-control status query failed - treating every shard as writable:`, e);\n }\n return { system, states };\n}\n\n/** The current state of every shard, throttled + coalesced as above. Never\n * throws: a failed query yields a clean, writable snapshot. */\nexport function shardStatus(shards: ShardRef[]): Promise<ShardStatus> {\n if (!inFlight) {\n inFlight = (async () => {\n try { return await query(shards); } finally { inFlight = undefined; }\n })();\n }\n return inFlight;\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { VCStatusOptions, VCFileStatus } from '@wildwinter/simple-vc-lib';
|
|
2
|
+
|
|
3
|
+
/** One shard to ask about: the key the renderer looks state up by, and its
|
|
4
|
+
* absolute path on disk. */
|
|
5
|
+
interface ShardRef {
|
|
6
|
+
key: string;
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
/** One shard's version-control state. All-clean is `{ writable: true }`. */
|
|
10
|
+
interface ShardState {
|
|
11
|
+
/** Writable on disk right now. `false` = the read-only bit is set (a lock
|
|
12
|
+
* VCS has not checked it out yet, or someone else holds it). */
|
|
13
|
+
writable: boolean;
|
|
14
|
+
/** Who else holds it open / locked (e.g. "bo@bo-ws") - we can read it, not
|
|
15
|
+
* write it. */
|
|
16
|
+
lockedBy?: string[];
|
|
17
|
+
/** A newer revision exists on the server (get latest before editing). */
|
|
18
|
+
outOfDate?: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface ShardStatus {
|
|
21
|
+
/** The backend simple-vc-lib actually detected ("git" / "perforce" / ...). */
|
|
22
|
+
system: string;
|
|
23
|
+
states: Map<string, ShardState>;
|
|
24
|
+
}
|
|
25
|
+
/** How long a remote (server round-trip) answer stays good for. */
|
|
26
|
+
declare const REMOTE_STATUS_THROTTLE_MS = 15000;
|
|
27
|
+
type StatusReader = (paths: string[], options?: VCStatusOptions) => Promise<VCFileStatus[]>;
|
|
28
|
+
/** Name the app, for that warning. */
|
|
29
|
+
declare function setVcLogPrefix(name: string): void;
|
|
30
|
+
/** Test seam, mirroring simple-vc-lib's own `setCommandRunner`: swap the status
|
|
31
|
+
* reader for canned answers. Pass null to restore the real one. */
|
|
32
|
+
declare function setStatusReader(reader: StatusReader | null): void;
|
|
33
|
+
/** Drop the throttle window and the cached remote bits - called when a project
|
|
34
|
+
* opens, so a new project never inherits the last one's answers. */
|
|
35
|
+
declare function resetShardStatus(): void;
|
|
36
|
+
/** The current state of every shard, throttled + coalesced as above. Never
|
|
37
|
+
* throws: a failed query yields a clean, writable snapshot. */
|
|
38
|
+
declare function shardStatus(shards: ShardRef[]): Promise<ShardStatus>;
|
|
39
|
+
|
|
40
|
+
export { REMOTE_STATUS_THROTTLE_MS, type ShardRef, type ShardState, type ShardStatus, resetShardStatus, setStatusReader, setVcLogPrefix, shardStatus };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { VCStatusOptions, VCFileStatus } from '@wildwinter/simple-vc-lib';
|
|
2
|
+
|
|
3
|
+
/** One shard to ask about: the key the renderer looks state up by, and its
|
|
4
|
+
* absolute path on disk. */
|
|
5
|
+
interface ShardRef {
|
|
6
|
+
key: string;
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
/** One shard's version-control state. All-clean is `{ writable: true }`. */
|
|
10
|
+
interface ShardState {
|
|
11
|
+
/** Writable on disk right now. `false` = the read-only bit is set (a lock
|
|
12
|
+
* VCS has not checked it out yet, or someone else holds it). */
|
|
13
|
+
writable: boolean;
|
|
14
|
+
/** Who else holds it open / locked (e.g. "bo@bo-ws") - we can read it, not
|
|
15
|
+
* write it. */
|
|
16
|
+
lockedBy?: string[];
|
|
17
|
+
/** A newer revision exists on the server (get latest before editing). */
|
|
18
|
+
outOfDate?: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface ShardStatus {
|
|
21
|
+
/** The backend simple-vc-lib actually detected ("git" / "perforce" / ...). */
|
|
22
|
+
system: string;
|
|
23
|
+
states: Map<string, ShardState>;
|
|
24
|
+
}
|
|
25
|
+
/** How long a remote (server round-trip) answer stays good for. */
|
|
26
|
+
declare const REMOTE_STATUS_THROTTLE_MS = 15000;
|
|
27
|
+
type StatusReader = (paths: string[], options?: VCStatusOptions) => Promise<VCFileStatus[]>;
|
|
28
|
+
/** Name the app, for that warning. */
|
|
29
|
+
declare function setVcLogPrefix(name: string): void;
|
|
30
|
+
/** Test seam, mirroring simple-vc-lib's own `setCommandRunner`: swap the status
|
|
31
|
+
* reader for canned answers. Pass null to restore the real one. */
|
|
32
|
+
declare function setStatusReader(reader: StatusReader | null): void;
|
|
33
|
+
/** Drop the throttle window and the cached remote bits - called when a project
|
|
34
|
+
* opens, so a new project never inherits the last one's answers. */
|
|
35
|
+
declare function resetShardStatus(): void;
|
|
36
|
+
/** The current state of every shard, throttled + coalesced as above. Never
|
|
37
|
+
* throws: a failed query yields a clean, writable snapshot. */
|
|
38
|
+
declare function shardStatus(shards: ShardRef[]): Promise<ShardStatus>;
|
|
39
|
+
|
|
40
|
+
export { REMOTE_STATUS_THROTTLE_MS, type ShardRef, type ShardState, type ShardStatus, resetShardStatus, setStatusReader, setVcLogPrefix, shardStatus };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { fileStatusAsync } from '@wildwinter/simple-vc-lib';
|
|
2
|
+
|
|
3
|
+
// src/vc-status.ts
|
|
4
|
+
var REMOTE_STATUS_THROTTLE_MS = 15e3;
|
|
5
|
+
var read = fileStatusAsync;
|
|
6
|
+
var logPrefix = "app-shell";
|
|
7
|
+
function setVcLogPrefix(name) {
|
|
8
|
+
logPrefix = name;
|
|
9
|
+
}
|
|
10
|
+
var lastRemoteAt = 0;
|
|
11
|
+
var cachedRemoteBits = /* @__PURE__ */ new Map();
|
|
12
|
+
var inFlight;
|
|
13
|
+
function setStatusReader(reader) {
|
|
14
|
+
read = reader ?? fileStatusAsync;
|
|
15
|
+
}
|
|
16
|
+
function resetShardStatus() {
|
|
17
|
+
lastRemoteAt = 0;
|
|
18
|
+
cachedRemoteBits = /* @__PURE__ */ new Map();
|
|
19
|
+
}
|
|
20
|
+
async function query(shards) {
|
|
21
|
+
const states = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const s of shards) states.set(s.key, { writable: true });
|
|
23
|
+
const keyOf = new Map(shards.map((s) => [s.path, s.key]));
|
|
24
|
+
const doRemote = Date.now() - lastRemoteAt >= REMOTE_STATUS_THROTTLE_MS;
|
|
25
|
+
let system = "filesystem";
|
|
26
|
+
try {
|
|
27
|
+
for (const st of await read(shards.map((s) => s.path), { remote: doRemote })) {
|
|
28
|
+
const key = keyOf.get(st.filePath);
|
|
29
|
+
if (key === void 0) continue;
|
|
30
|
+
system = st.system;
|
|
31
|
+
const acc = states.get(key);
|
|
32
|
+
if (!st.writable) acc.writable = false;
|
|
33
|
+
if (doRemote) {
|
|
34
|
+
if (st.lockedBy?.length) acc.lockedBy = [.../* @__PURE__ */ new Set([...acc.lockedBy ?? [], ...st.lockedBy])];
|
|
35
|
+
if (st.outOfDate) acc.outOfDate = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (doRemote) {
|
|
39
|
+
lastRemoteAt = Date.now();
|
|
40
|
+
cachedRemoteBits = new Map([...states].map(([key, s]) => [key, {
|
|
41
|
+
...s.lockedBy !== void 0 ? { lockedBy: s.lockedBy } : {},
|
|
42
|
+
...s.outOfDate !== void 0 ? { outOfDate: s.outOfDate } : {}
|
|
43
|
+
}]));
|
|
44
|
+
} else {
|
|
45
|
+
for (const [key, acc] of states) {
|
|
46
|
+
const cached = cachedRemoteBits.get(key);
|
|
47
|
+
if (cached?.lockedBy?.length) acc.lockedBy = cached.lockedBy;
|
|
48
|
+
if (cached?.outOfDate) acc.outOfDate = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.warn(`${logPrefix}: the version-control status query failed - treating every shard as writable:`, e);
|
|
53
|
+
}
|
|
54
|
+
return { system, states };
|
|
55
|
+
}
|
|
56
|
+
function shardStatus(shards) {
|
|
57
|
+
if (!inFlight) {
|
|
58
|
+
inFlight = (async () => {
|
|
59
|
+
try {
|
|
60
|
+
return await query(shards);
|
|
61
|
+
} finally {
|
|
62
|
+
inFlight = void 0;
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
}
|
|
66
|
+
return inFlight;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { REMOTE_STATUS_THROTTLE_MS, resetShardStatus, setStatusReader, setVcLogPrefix, shardStatus };
|
|
70
|
+
//# sourceMappingURL=vc-status.js.map
|
|
71
|
+
//# sourceMappingURL=vc-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/vc-status.ts"],"names":[],"mappings":"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","file":"vc-status.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Per-shard version-control state, over @wildwinter/simple-vc-lib.\n//\n// The family's, because it follows from the SHAPE rather than from any app's\n// subject: these are all Electron apps editing JSON shards in a working copy,\n// so they all want to say \"locked by Bo\" before a save is refused rather than\n// after. Patterpad wrote it, Storyletter hand-ported it, and this is the third\n// copy becoming the only one.\n//\n// A SHARD here is just a key and a path: what the key means is the app's\n// business (a scene, a deck, a box's three files folded into one row).\n//\n// THROTTLING, which is the reason this is a module rather than a function call:\n// the local bits (`writable` - the on-disk read-only bit a lock VCS keys off)\n// are cheap and refresh on every call, so a save re-badges at once. The remote\n// bits (`lockedBy` / `outOfDate`) cost a SERVER round-trip under SVN and\n// Plastic, so they are re-queried at most once per window and otherwise reused\n// from the cache. The query is async (never blocks the main-process event loop)\n// and coalesced (a save, a focus and the poll timer arriving together make ONE\n// query), so a lock badge can never make typing stutter.\n//\n// Best-effort throughout: if the query throws (no tooling, broken repo) every\n// shard reports clean and writable, and the editor carries on.\n// ---------------------------------------------------------------------------\n\nimport { fileStatusAsync } from \"@wildwinter/simple-vc-lib\";\nimport type { VCFileStatus, VCStatusOptions } from \"@wildwinter/simple-vc-lib\";\n\n/** One shard to ask about: the key the renderer looks state up by, and its\n * absolute path on disk. */\nexport interface ShardRef {\n key: string;\n path: string;\n}\n\n/** One shard's version-control state. All-clean is `{ writable: true }`. */\nexport interface ShardState {\n /** Writable on disk right now. `false` = the read-only bit is set (a lock\n * VCS has not checked it out yet, or someone else holds it). */\n writable: boolean;\n /** Who else holds it open / locked (e.g. \"bo@bo-ws\") - we can read it, not\n * write it. */\n lockedBy?: string[];\n /** A newer revision exists on the server (get latest before editing). */\n outOfDate?: boolean;\n}\n\nexport interface ShardStatus {\n /** The backend simple-vc-lib actually detected (\"git\" / \"perforce\" / ...). */\n system: string;\n states: Map<string, ShardState>;\n}\n\n/** How long a remote (server round-trip) answer stays good for. */\nexport const REMOTE_STATUS_THROTTLE_MS = 15_000;\n\ntype StatusReader = (paths: string[], options?: VCStatusOptions) => Promise<VCFileStatus[]>;\n\nlet read: StatusReader = fileStatusAsync;\n/** What the app calls itself in the one warning this module can emit. */\nlet logPrefix = \"app-shell\";\n/** Name the app, for that warning. */\nexport function setVcLogPrefix(name: string): void { logPrefix = name; }\nlet lastRemoteAt = 0;\nlet cachedRemoteBits = new Map<string, { lockedBy?: string[]; outOfDate?: boolean }>();\nlet inFlight: Promise<ShardStatus> | undefined;\n\n/** Test seam, mirroring simple-vc-lib's own `setCommandRunner`: swap the status\n * reader for canned answers. Pass null to restore the real one. */\nexport function setStatusReader(reader: StatusReader | null): void {\n read = reader ?? fileStatusAsync;\n}\n\n/** Drop the throttle window and the cached remote bits - called when a project\n * opens, so a new project never inherits the last one's answers. */\nexport function resetShardStatus(): void {\n lastRemoteAt = 0;\n cachedRemoteBits = new Map();\n}\n\nasync function query(shards: ShardRef[]): Promise<ShardStatus> {\n const states = new Map<string, ShardState>();\n for (const s of shards) states.set(s.key, { writable: true });\n const keyOf = new Map(shards.map((s) => [s.path, s.key]));\n // Hit the server only when the window has elapsed; otherwise a cheap local read.\n const doRemote = Date.now() - lastRemoteAt >= REMOTE_STATUS_THROTTLE_MS;\n let system = \"filesystem\";\n try {\n for (const st of await read(shards.map((s) => s.path), { remote: doRemote })) {\n const key = keyOf.get(st.filePath);\n if (key === undefined) continue;\n system = st.system;\n const acc = states.get(key)!;\n if (!st.writable) acc.writable = false;\n if (doRemote) { // remote bits are authoritative only on a fresh server query\n if (st.lockedBy?.length) acc.lockedBy = [...new Set([...(acc.lockedBy ?? []), ...st.lockedBy])];\n if (st.outOfDate) acc.outOfDate = true;\n }\n }\n if (doRemote) { // snapshot the fresh remote bits for the throttled calls that follow\n lastRemoteAt = Date.now();\n cachedRemoteBits = new Map([...states].map(([key, s]) => [key, {\n ...(s.lockedBy !== undefined ? { lockedBy: s.lockedBy } : {}),\n ...(s.outOfDate !== undefined ? { outOfDate: s.outOfDate } : {}),\n }]));\n } else { // overlay the last known remote bits onto the fresh local snapshot\n for (const [key, acc] of states) {\n const cached = cachedRemoteBits.get(key);\n if (cached?.lockedBy?.length) acc.lockedBy = cached.lockedBy;\n if (cached?.outOfDate) acc.outOfDate = true;\n }\n }\n } catch (e) {\n // Tooling missing, repo error, timeout: everything reads clean + writable.\n console.warn(`${logPrefix}: the version-control status query failed - treating every shard as writable:`, e);\n }\n return { system, states };\n}\n\n/** The current state of every shard, throttled + coalesced as above. Never\n * throws: a failed query yields a clean, writable snapshot. */\nexport function shardStatus(shards: ShardRef[]): Promise<ShardStatus> {\n if (!inFlight) {\n inFlight = (async () => {\n try { return await query(shards); } finally { inFlight = undefined; }\n })();\n }\n return inFlight;\n}\n"]}
|