Flow
Pieces in the order they lean on each other
1Saves that repeat safely
Check: one window run twice leaves the same rows.
2Success record after rows
Check: a failed save leaves no success record.
3Exact-edge lookup
Check: a receipt with shifted edges is ignored.
4Reuse adds up
Check: only the missing window hits the upstream and totals add.
5One slot in memory
Check: a second click gets a conflict showing the active range.
Nothing was remembered by my discounts-and-returns sync. It pulls credit data for the stores from a slow upstream, a month or a day at a time. If a long run got cut off near the end and I started it again, it went back to the start. And nothing stopped two syncs from running at once.
Both problems waste calls to an upstream I can't speed up. The shared client I'd moved the importer onto that day queues requests, but a queue doesn't know two requests belong to the same job.
Every finished window was already writing a receipt
Each finished window already saved a record: where it pulled from, its exact start and end, whether it worked, and how much it fetched and stored. Now each window checks for a successful record with the same source and the same edges before it calls out. If it finds one, it skips the fetch, adds the old counts to the totals, and posts a progress line saying it reused that window. Resume is on unless you turn it off.
If you build this, three things matter:
- Match exact edges, not overlap. A different slicing then refetches instead of skipping data.
- Make saves safe to repeat. Mine overwrite by source and the upstream's own ID, so a half-done window can just run again.
- Write the success record after the rows, never before.
The test I added seeds a receipt for one of two windows and checks that only the other one hits the upstream, with the totals added up.
Old counts and old data
A reused window is as fresh as its first pull. If the upstream fixes an old month, you need a run with resume off to see it.
The second click gets a conflict
The sync route keeps one slot in memory: a random token, a start time and the range. If the slot is taken, the request gets a conflict answer that shows what's already running. When the run ends, its cleanup frees the slot, but only if the token still matches. The core of the guard is tiny.
If a run is already going, the request gets a conflict answer. Otherwise it takes the slot with its own ticket, start time and range.
One process, one slot
This only works inside one app process. Two instances would each let one sync through. I run one, so it's fine for now, and a crash clears the slot on its own. I haven't checked whether anything between the claim and the stream can throw and leave the slot stuck until a restart. The guard has no test yet, and a test that fires two claims at once is the next thing I'd add.