Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/lib/queue-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ beforeEach(() => {
vi.useFakeTimers()
toastShow.mockReset()
// clear any leftover processes between tests
for (const pid of ['p1', 'p2', 'bulk']) queueStore.dismiss(pid)
for (const pid of ['p1', 'p2', 'bulk', 'hints-auto-1', 'hints-1', 'hints-2', 'hints-bg-1', 'hints-bg-2'])
queueStore.dismiss(pid)
})

afterEach(() => {
Expand Down Expand Up @@ -386,6 +387,33 @@ describe('queueStore.attachPhaseHints', () => {
expect(queueStore.getPhaseHints(pid)).toBeUndefined()
})

it('clears hints after auto-dismiss when undo window elapses', async () => {
const pid = 'hints-auto-1'
dispatch({ total: 1, completed: 0, paused: false, processId: pid, label: 'A' })
queueStore.attachPhaseHints(pid, {
reverse: {
messageType: 'bulkUpdate',
data: { reopen: true },
affectedItemIds: ['i1'],
},
})
dispatch({ total: 0, completed: 0, paused: false, status: 'Done!', processId: pid })

await vi.advanceTimersByTimeAsync(10_500)

expect(queueStore.getPhaseHints(pid)).toBeUndefined()

dispatch({ total: 2, completed: 0, paused: false, processId: pid, label: 'Second run' })

const snapshots: any[][] = []
const unsub = queueStore.subscribe((e) => snapshots.push([...e]))
const entry = snapshots[snapshots.length - 1].find((p: any) => p.processId === pid)
expect(entry.phase.kind).toBe('in-flight')
expect(entry.phase.reverse).toBeUndefined()
expect(entry.phase.undoableUntil).toBeUndefined()
unsub()
})

it('§4.9 — consumes reverse hint piggy-backed on Done broadcast and exposes Undo', () => {
const pid = 'hints-bg-1'
const snapshots: any[][] = []
Expand Down
25 changes: 14 additions & 11 deletions src/lib/queue-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ function clearDismissTimer(id: string): void {
}
}

/** Remove process entry and attached phase hints (manual dismiss + auto-dismiss). */
function removeProcess(processId: string, opts?: { skipClearTimer?: boolean }): void {
if (!opts?.skipClearTimer) clearDismissTimer(processId)
phaseHintsMap.delete(processId)
if (!processes.has(processId)) return
const next = new Map(processes)
next.delete(processId)
setState(next)
}

function scheduleDismiss(processId: string, delay: Duration.Duration = DISMISS_DELAY) {
clearDismissTimer(processId)
const fiber = Effect.runFork(
Expand All @@ -166,10 +176,7 @@ function scheduleDismiss(processId: string, delay: Duration.Duration = DISMISS_D
Effect.sync(() => {
if (dismissTimers.get(processId) !== fiber) return
dismissTimers.delete(processId)
if (!processes.has(processId)) return
const next = new Map(processes)
next.delete(processId)
setState(next)
removeProcess(processId, { skipClearTimer: true })
}),
),
),
Expand All @@ -194,16 +201,12 @@ export const queueStore = {
return queueStore.getActiveCount() > 0
},
dismiss(processId: string) {
clearDismissTimer(processId)
phaseHintsMap.delete(processId)
if (!processes.has(processId)) {
const existed = processes.has(processId)
removeProcess(processId)
if (!existed) {
// still notify so callers observing 'after dismiss' state get a tick.
setState(new Map(processes))
return
}
const next = new Map(processes)
next.delete(processId)
setState(next)
},
/**
* Attach reverse / retry hints to a process. Verb handlers call this before
Expand Down
Loading