@try-works/dsh-memory 0.1.11 → 0.1.13
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/package.json +1 -1
- package/src/tasks.js +53 -5
package/package.json
CHANGED
package/src/tasks.js
CHANGED
|
@@ -151,7 +151,7 @@ const STATE_MEANING = {
|
|
|
151
151
|
[TASK_STATES.STOPPED]: 'was armed, and its owning session stopped, so the host deleted it',
|
|
152
152
|
[TASK_STATES.FAILED]: 'was due and has still not delivered; the host records no failure of its own',
|
|
153
153
|
[TASK_STATES.ORPHANED]: 'armed, but the session that owns it cannot be resolved, so it can never fire',
|
|
154
|
-
[TASK_STATES.SKIPPED]: '
|
|
154
|
+
[TASK_STATES.SKIPPED]: 'captured, but its kind is not one we can re-arm; a restore refuses it rather than guessing',
|
|
155
155
|
[TASK_STATES.DONE]: 'delivered, or its recurrence is exhausted',
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -397,12 +397,16 @@ export function readScheduleStore(path) {
|
|
|
397
397
|
* deleted on purpose must stay visible as gone rather than vanish from the record of what this
|
|
398
398
|
* machine used to run.
|
|
399
399
|
*
|
|
400
|
-
* A task whose kind we do not recognise is
|
|
401
|
-
*
|
|
400
|
+
* A task whose kind we do not recognise is stored and flagged SKIPPED: dropping it would mean losing
|
|
401
|
+
* a task from a backup, and flagging it means a restore refuses it rather than guessing.
|
|
402
402
|
*/
|
|
403
|
-
export function backupTasks(root, { schedulePath, now = Date.now(), nowIso } = {}) {
|
|
403
|
+
export function backupTasks(root, { schedulePath, now = Date.now(), nowIso, sessions } = {}) {
|
|
404
404
|
const at = nowIso ?? new Date(now).toISOString()
|
|
405
405
|
const live = readScheduleStore(schedulePath ?? defaultSchedulePath())
|
|
406
|
+
// Read once per pass, not per task: it is a directory walk, and every row asks the same question.
|
|
407
|
+
// Injectable so a test can state the world instead of inheriting this machine's: `null` means
|
|
408
|
+
// the question cannot be answered and no row is called orphaned.
|
|
409
|
+
const sessionSet = sessions === undefined ? sessionIdsOnDisk() : sessions
|
|
406
410
|
const before = listTasks(root)
|
|
407
411
|
if (live.error !== null) {
|
|
408
412
|
// The index is still rewritten, so a page from an earlier pass cannot sit on disk describing a
|
|
@@ -420,7 +424,13 @@ export function backupTasks(root, { schedulePath, now = Date.now(), nowIso } = {
|
|
|
420
424
|
const record = task.record
|
|
421
425
|
if (!record || typeof record !== 'object') { skipped.push({ id: task.id, reason: 'no record' }); continue }
|
|
422
426
|
if (!KNOWN_KINDS.includes(String(record.kind))) {
|
|
427
|
+
// CAPTURED, not dropped. An earlier version skipped these entirely, which meant a task whose
|
|
428
|
+
// kind we did not recognise silently vanished from a BACKUP - the one job this layer has. It
|
|
429
|
+
// is written and flagged 'skipped' instead, and planRestore refuses that state, so it can
|
|
430
|
+
// never be blindly re-armed. Preserved, visible, and unrestorable: all three on purpose.
|
|
423
431
|
skipped.push({ id: task.id, reason: 'unrecognised kind: ' + String(record.kind) })
|
|
432
|
+
writeTask(root, record, TASK_STATES.SKIPPED, byKey.get(contentKey(record))?.backedUpAt ?? at)
|
|
433
|
+
seen.add(contentKey(record))
|
|
424
434
|
continue
|
|
425
435
|
}
|
|
426
436
|
const key = contentKey(record)
|
|
@@ -429,7 +439,11 @@ export function backupTasks(root, { schedulePath, now = Date.now(), nowIso } = {
|
|
|
429
439
|
// A held-back task stays held back across every backup: that state is the user's decision, and
|
|
430
440
|
// a routine pass must not overwrite it with a derived one.
|
|
431
441
|
const held = previous && previous.stateKnown && previous.state === TASK_STATES.PAUSED
|
|
432
|
-
|
|
442
|
+
// Without this the ORPHANED state exists in the vocabulary and is never produced by any real
|
|
443
|
+
// pass - reachable from a unit test and from nothing else. `sessions === null` means the
|
|
444
|
+
// sessions directory is absent and we cannot tell, so the row is left as active.
|
|
445
|
+
const ownerResolvable = sessionSet === null || task.sessionId === undefined ? true : sessionSet.has(task.sessionId)
|
|
446
|
+
const derived = deriveState({ stored: held ? TASK_STATES.PAUSED : null, live: task, now, ownerResolvable })
|
|
433
447
|
writeTask(root, record, derived.state, previous ? previous.backedUpAt : at)
|
|
434
448
|
if (previous) refreshed += 1
|
|
435
449
|
else written += 1
|
|
@@ -542,3 +556,37 @@ export function planRestore(root, { liveTasks = [], now = Date.now() } = {}) {
|
|
|
542
556
|
}
|
|
543
557
|
return { add, present, paused, refused, unreadable }
|
|
544
558
|
}
|
|
559
|
+
|
|
560
|
+
/** The directory DSH keeps session records in, beside the schedule store's home. */
|
|
561
|
+
export function sessionsRoot(home = homedir()) {
|
|
562
|
+
const configured = process.env.DSH_HOME
|
|
563
|
+
const dshHome = configured && configured !== '' ? configured : join(home, '.dsh')
|
|
564
|
+
return join(dshHome, 'sessions')
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Every session id that still has a record on disk.
|
|
569
|
+
*
|
|
570
|
+
* Used to tell ORPHANED from ACTIVE. A live task is armed against the session that created it, and
|
|
571
|
+
* the runtime resolves an agent from that id before every delivery (schedule runtime.ts:101) - so
|
|
572
|
+
* a task whose session is gone is a row that will never fire while still occupying the store. The
|
|
573
|
+
* backup cannot see the runtime, but it can see whether the session still exists.
|
|
574
|
+
*
|
|
575
|
+
* @returns a Set, or null when the sessions directory is absent
|
|
576
|
+
*
|
|
577
|
+
* Null rather than an empty set on purpose: no sessions directory means we cannot tell, and an
|
|
578
|
+
* empty set would mark every task on the machine orphaned. Refusing to guess is the rule here.
|
|
579
|
+
*/
|
|
580
|
+
export function sessionIdsOnDisk(home = homedir()) {
|
|
581
|
+
const root = sessionsRoot(home)
|
|
582
|
+
if (!existsSync(root)) return null
|
|
583
|
+
const ids = new Set()
|
|
584
|
+
let projects
|
|
585
|
+
try { projects = readdirSync(root) } catch { return null }
|
|
586
|
+
for (const project of projects) {
|
|
587
|
+
let sessions
|
|
588
|
+
try { sessions = readdirSync(join(root, project)) } catch { continue }
|
|
589
|
+
for (const id of sessions) ids.add(id)
|
|
590
|
+
}
|
|
591
|
+
return ids
|
|
592
|
+
}
|