dsh-autorestart 0.3.6 → 0.6.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 +254 -11
- package/client.js +2500 -147
- package/cordis.patch.yml +24 -0
- package/docs/README.zh.md +160 -15
- package/index.js +1336 -80
- package/lib/checkpoint.js +116 -0
- package/lib/sleep.js +1012 -0
- package/locale/en.json +108 -1
- package/locale/zh.json +108 -1
- package/package.json +1 -1
package/lib/checkpoint.js
CHANGED
|
@@ -105,6 +105,8 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
105
105
|
'sessions',
|
|
106
106
|
'parkedGoals',
|
|
107
107
|
'disarmedGoals',
|
|
108
|
+
'seenFeatureVersions',
|
|
109
|
+
'sleep',
|
|
108
110
|
'updatedAt',
|
|
109
111
|
]);
|
|
110
112
|
|
|
@@ -172,6 +174,8 @@ export function createEmptyState(now = Date.now()) {
|
|
|
172
174
|
phase: 'idle',
|
|
173
175
|
restart: null,
|
|
174
176
|
sessions: {},
|
|
177
|
+
seenFeatureVersions: {},
|
|
178
|
+
sleep: null,
|
|
175
179
|
updatedAt: now,
|
|
176
180
|
};
|
|
177
181
|
}
|
|
@@ -277,6 +281,59 @@ function normalizeDisarmedGoals(raw) {
|
|
|
277
281
|
return { disarmedGoals, changed };
|
|
278
282
|
}
|
|
279
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Normalize the record of the feature announcements the user has already seen.
|
|
286
|
+
*
|
|
287
|
+
* The shape is `{ [feature]: version }`: the id of the feature and the version
|
|
288
|
+
* of the plugin that announced it. A record whose version is not a string is
|
|
289
|
+
* dropped — an unreadable record must never hide an announcement for ever,
|
|
290
|
+
* so a broken entry makes the announcement show up again instead.
|
|
291
|
+
*
|
|
292
|
+
* @param {unknown} raw
|
|
293
|
+
* @returns {{ seenFeatureVersions: Record<string, string>, changed: boolean }}
|
|
294
|
+
*/
|
|
295
|
+
function normalizeSeenFeatureVersions(raw) {
|
|
296
|
+
if (!isObject(raw)) {
|
|
297
|
+
return { seenFeatureVersions: {}, changed: raw !== null && raw !== undefined };
|
|
298
|
+
}
|
|
299
|
+
const seenFeatureVersions = {};
|
|
300
|
+
let changed = false;
|
|
301
|
+
for (const [feature, version] of Object.entries(raw)) {
|
|
302
|
+
if (typeof version !== 'string' || version === '') {
|
|
303
|
+
changed = true;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
seenFeatureVersions[feature] = version;
|
|
307
|
+
}
|
|
308
|
+
return { seenFeatureVersions, changed };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Normalize the recorded shutdown intent.
|
|
313
|
+
*
|
|
314
|
+
* The intent is the user's "shut the machine down when the work is done" — it
|
|
315
|
+
* has to survive a restart, so it lives in the checkpoint file. A record that
|
|
316
|
+
* is broken is DROPPED: asking the user once more is much better than a
|
|
317
|
+
* machine that switches itself off for ever because of a half-written file.
|
|
318
|
+
*
|
|
319
|
+
* @param {unknown} raw
|
|
320
|
+
* @returns {{ sleep: { armed: true, armedAt: number, countdownSeconds: number|null }|null, changed: boolean }}
|
|
321
|
+
*/
|
|
322
|
+
function normalizeSleep(raw) {
|
|
323
|
+
if (raw == null) return { sleep: null, changed: false };
|
|
324
|
+
if (!isObject(raw)) return { sleep: null, changed: true };
|
|
325
|
+
if (raw.armed !== true) return { sleep: null, changed: true };
|
|
326
|
+
const armedAt = isFiniteNumber(raw.armedAt) ? raw.armedAt : null;
|
|
327
|
+
if (armedAt === null) return { sleep: null, changed: true };
|
|
328
|
+
const countdownSeconds = isFiniteNumber(raw.countdownSeconds) && raw.countdownSeconds >= 0
|
|
329
|
+
? Math.trunc(raw.countdownSeconds)
|
|
330
|
+
: null;
|
|
331
|
+
return {
|
|
332
|
+
sleep: { armed: true, armedAt, countdownSeconds },
|
|
333
|
+
changed: raw.countdownSeconds !== countdownSeconds,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
280
337
|
/**
|
|
281
338
|
* Normalize the handoff record of a session: `{ handedOffTo, handoffAt, carriedChars }`.
|
|
282
339
|
* A missing record stays `null`; a corrupt one is dropped instead of being thrown on.
|
|
@@ -536,6 +593,23 @@ export function normalizeState(raw, now = Date.now()) {
|
|
|
536
593
|
if (disarmed.changed) repaired = true;
|
|
537
594
|
}
|
|
538
595
|
|
|
596
|
+
// The record of the feature announcements the user has already seen. It
|
|
597
|
+
// survives a restart of the Host: an announcement that was shown once must
|
|
598
|
+
// not come back with the next boot.
|
|
599
|
+
if (raw.seenFeatureVersions != null) {
|
|
600
|
+
const seen = normalizeSeenFeatureVersions(raw.seenFeatureVersions);
|
|
601
|
+
state.seenFeatureVersions = seen.seenFeatureVersions;
|
|
602
|
+
if (seen.changed) repaired = true;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// The recorded shutdown intent: it has to survive a restart, so the machine
|
|
606
|
+
// goes off AFTER the restart and the work that resumed with it.
|
|
607
|
+
if (raw.sleep != null) {
|
|
608
|
+
const sleep = normalizeSleep(raw.sleep);
|
|
609
|
+
state.sleep = sleep.sleep;
|
|
610
|
+
if (sleep.changed) repaired = true;
|
|
611
|
+
}
|
|
612
|
+
|
|
539
613
|
return { state, repaired };
|
|
540
614
|
}
|
|
541
615
|
|
|
@@ -1197,6 +1271,48 @@ export function cancelRestart(state, { now = Date.now() } = {}) {
|
|
|
1197
1271
|
return state;
|
|
1198
1272
|
}
|
|
1199
1273
|
|
|
1274
|
+
/**
|
|
1275
|
+
* Record the shutdown intent: "switch the machine off when the work is done".
|
|
1276
|
+
*
|
|
1277
|
+
* The intent lives in the checkpoint file, so it survives a restart: a
|
|
1278
|
+
* shutdown that is asked for while a restart is pending waits for that
|
|
1279
|
+
* restart (and for the sessions that resume with it) before the machine goes
|
|
1280
|
+
* off.
|
|
1281
|
+
*
|
|
1282
|
+
* Mutates `state` in place and returns it.
|
|
1283
|
+
*
|
|
1284
|
+
* @param {object} state
|
|
1285
|
+
* @param {{ armedAt?: number, countdownSeconds?: number|null }} [options]
|
|
1286
|
+
* @returns {object} state
|
|
1287
|
+
*/
|
|
1288
|
+
export function armSleep(state, { armedAt = Date.now(), countdownSeconds = null } = {}) {
|
|
1289
|
+
state.sleep = {
|
|
1290
|
+
armed: true,
|
|
1291
|
+
armedAt: isFiniteNumber(armedAt) ? armedAt : Date.now(),
|
|
1292
|
+
countdownSeconds: isFiniteNumber(countdownSeconds) && countdownSeconds >= 0
|
|
1293
|
+
? Math.trunc(countdownSeconds)
|
|
1294
|
+
: null,
|
|
1295
|
+
};
|
|
1296
|
+
state.updatedAt = state.sleep.armedAt;
|
|
1297
|
+
return state;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Forget the shutdown intent: the machine stays on (or it already went off).
|
|
1302
|
+
*
|
|
1303
|
+
* Mutates `state` in place and returns it.
|
|
1304
|
+
*
|
|
1305
|
+
* @param {object} state
|
|
1306
|
+
* @param {{ now?: number }} [options]
|
|
1307
|
+
* @returns {object} state
|
|
1308
|
+
*/
|
|
1309
|
+
export function clearSleep(state, { now = Date.now() } = {}) {
|
|
1310
|
+
if (state.sleep === null || state.sleep === undefined) return state;
|
|
1311
|
+
state.sleep = null;
|
|
1312
|
+
state.updatedAt = now;
|
|
1313
|
+
return state;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1200
1316
|
/**
|
|
1201
1317
|
* Set the lifecycle phase, ignoring unknown values.
|
|
1202
1318
|
* Mutates `state` in place and returns it.
|