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/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.