@try-works/dsh-memory 0.1.12 → 0.1.14
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 +36 -10
package/package.json
CHANGED
package/src/tasks.js
CHANGED
|
@@ -56,14 +56,22 @@ export const TASK_STATES = {
|
|
|
56
56
|
* without one.
|
|
57
57
|
*/
|
|
58
58
|
export const TASK_STATE_EXITS = {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// Two entries were missing, and NOTHING noticed until writeTask began reading this table.
|
|
60
|
+
//
|
|
61
|
+
// 'orphaned': the session holding a live task can disappear while the task itself does not
|
|
62
|
+
// change, so an armed task can become one that can never fire.
|
|
63
|
+
//
|
|
64
|
+
// 'paused' in every row: holding a task back is a DECISION the user makes about a task in any
|
|
65
|
+
// condition, not a lifecycle step. Writing the table without it left the entire pause path
|
|
66
|
+
// undeclared - the feature worked, and the declaration that was supposed to describe it did not.
|
|
67
|
+
active: ['stopped', 'done', 'failed', 'orphaned', 'paused'],
|
|
68
|
+
pending: ['active', 'paused'],
|
|
61
69
|
paused: ['pending'],
|
|
62
|
-
stopped: ['pending'],
|
|
63
|
-
failed: ['active'],
|
|
64
|
-
orphaned: ['pending'],
|
|
65
|
-
skipped: ['active'],
|
|
66
|
-
done: ['active'],
|
|
70
|
+
stopped: ['pending', 'paused'],
|
|
71
|
+
failed: ['active', 'paused'],
|
|
72
|
+
orphaned: ['pending', 'paused'],
|
|
73
|
+
skipped: ['active', 'paused'],
|
|
74
|
+
done: ['active', 'paused'],
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
const STATES = Object.values(TASK_STATES)
|
|
@@ -151,7 +159,7 @@ const STATE_MEANING = {
|
|
|
151
159
|
[TASK_STATES.STOPPED]: 'was armed, and its owning session stopped, so the host deleted it',
|
|
152
160
|
[TASK_STATES.FAILED]: 'was due and has still not delivered; the host records no failure of its own',
|
|
153
161
|
[TASK_STATES.ORPHANED]: 'armed, but the session that owns it cannot be resolved, so it can never fire',
|
|
154
|
-
[TASK_STATES.SKIPPED]: '
|
|
162
|
+
[TASK_STATES.SKIPPED]: 'captured, but its kind is not one we can re-arm; a restore refuses it rather than guessing',
|
|
155
163
|
[TASK_STATES.DONE]: 'delivered, or its recurrence is exhausted',
|
|
156
164
|
}
|
|
157
165
|
|
|
@@ -183,6 +191,18 @@ function ensureTasksDir(root) {
|
|
|
183
191
|
export function writeTask(root, record, state, nowIso) {
|
|
184
192
|
ensureTasksDir(root)
|
|
185
193
|
const file = taskFilePath(root, record)
|
|
194
|
+
// ENFORCED, not documented. This table sat in the file unread since it was written, so a state
|
|
195
|
+
// could move anywhere and nothing objected - the same shape as the vocabulary states that had a
|
|
196
|
+
// test and no producer. Refusing here means a task cannot silently leave a state by a route
|
|
197
|
+
// nobody declared, which is what makes 'every resting state has a working exit' checkable
|
|
198
|
+
// rather than aspirational.
|
|
199
|
+
const previous = readTaskFile(file)
|
|
200
|
+
if (previous && previous.stateKnown && previous.state !== state) {
|
|
201
|
+
const allowed = TASK_STATE_EXITS[previous.state] ?? []
|
|
202
|
+
if (!allowed.includes(state)) {
|
|
203
|
+
throw new Error('tasks: refusing the transition ' + previous.state + ' -> ' + state + ' for "' + String(record.title ?? '') + '"; allowed from ' + previous.state + ': ' + allowed.join(', '))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
186
206
|
writeFileSync(file, JSON.stringify({ backedUpAt: nowIso, state, record }, null, 2) + '\n', 'utf8')
|
|
187
207
|
return file;
|
|
188
208
|
}
|
|
@@ -397,8 +417,8 @@ export function readScheduleStore(path) {
|
|
|
397
417
|
* deleted on purpose must stay visible as gone rather than vanish from the record of what this
|
|
398
418
|
* machine used to run.
|
|
399
419
|
*
|
|
400
|
-
* A task whose kind we do not recognise is
|
|
401
|
-
*
|
|
420
|
+
* A task whose kind we do not recognise is stored and flagged SKIPPED: dropping it would mean losing
|
|
421
|
+
* a task from a backup, and flagging it means a restore refuses it rather than guessing.
|
|
402
422
|
*/
|
|
403
423
|
export function backupTasks(root, { schedulePath, now = Date.now(), nowIso, sessions } = {}) {
|
|
404
424
|
const at = nowIso ?? new Date(now).toISOString()
|
|
@@ -424,7 +444,13 @@ export function backupTasks(root, { schedulePath, now = Date.now(), nowIso, sess
|
|
|
424
444
|
const record = task.record
|
|
425
445
|
if (!record || typeof record !== 'object') { skipped.push({ id: task.id, reason: 'no record' }); continue }
|
|
426
446
|
if (!KNOWN_KINDS.includes(String(record.kind))) {
|
|
447
|
+
// CAPTURED, not dropped. An earlier version skipped these entirely, which meant a task whose
|
|
448
|
+
// kind we did not recognise silently vanished from a BACKUP - the one job this layer has. It
|
|
449
|
+
// is written and flagged 'skipped' instead, and planRestore refuses that state, so it can
|
|
450
|
+
// never be blindly re-armed. Preserved, visible, and unrestorable: all three on purpose.
|
|
427
451
|
skipped.push({ id: task.id, reason: 'unrecognised kind: ' + String(record.kind) })
|
|
452
|
+
writeTask(root, record, TASK_STATES.SKIPPED, byKey.get(contentKey(record))?.backedUpAt ?? at)
|
|
453
|
+
seen.add(contentKey(record))
|
|
428
454
|
continue
|
|
429
455
|
}
|
|
430
456
|
const key = contentKey(record)
|