feat: add pure task operations and bounded undo stack

Task operations become pure functions over arrays.

Ids move from Date.now() to crypto.randomUUID(), with a fallback for
non-secure contexts, since people do self-host this over plain HTTP on a
LAN.

Undo gets its own module so tasks.js stays pure. It covers single deletes
and clear-completed batches, and clamps indices because the list can
change between recording an entry and undoing it.
This commit is contained in:
Aculix Technologies
2026-08-15 23:52:33 +05:30
parent 5ce5b13ecd
commit f0fd6c1a65
4 changed files with 320 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
/** Bounded stack of reversible operations. Kept separate from tasks.js so that
* module stays purely functional while this one holds the state. */
export function createUndoStack(limit = 10) {
const entries = []
return {
push(entry) {
entries.push(entry)
if (entries.length > limit) entries.shift()
},
pop() {
return entries.length > 0 ? entries.pop() : null
},
get size() {
return entries.length
},
clear() {
entries.length = 0
},
}
}
/**
* Reverses one entry against the current task list.
*
* Indices are clamped because the list may have changed since the entry was
* recorded — a task deleted from position 5 can be restored into a list that
* has since shrunk to two items, and landing at the end beats throwing.
*
* `clearCompleted` entries must record `removed` in ascending index order, so
* re-inserting front to back puts each task back where it was.
*/
export function applyUndo(tasks, entry) {
if (!entry) return tasks
if (entry.type === 'delete') {
const next = [...tasks]
next.splice(Math.min(entry.index, next.length), 0, entry.task)
return next
}
if (entry.type === 'clearCompleted') {
const next = [...tasks]
for (const { task, index } of entry.removed) {
next.splice(Math.min(index, next.length), 0, task)
}
return next
}
return tasks
}