DEV Community

Renga
Renga

Posted on Originally published at wisp.rengaworks.com

A CSV comparison result expires when its inputs change

A CSV comparison has more inputs than two files. It also depends on the encoding, the key columns, the column mappings, and the fields the user wants to protect.

After a comparison finishes, the user can change any of those inputs. If the old result remains visible until the next comparison finishes, the UI can show a new filename next to an old diff. An enabled save button can turn that mismatch into a report.

A private browser-local CSV prototype handles this by expiring results as soon as their inputs change. This article describes its implementation and stored test records. It does not describe a customer incident or a public product launch.

The prototype runs previews, comparisons, and HTML report generation in Workers. A controller on the UI side owns result state.

Write the expiration rules first

Consider this illustrative sequence:

  1. Start a comparison using configuration A.
  2. Change the key column to configuration B before A finishes.
  3. Receive A's result.

The result can be internally correct and still be unusable in the current UI. The relevant question is which state changes invalidate it.

The prototype uses these rules:

Action Completed comparison Unsaved report
Change a file, encoding, key, mapping, or protected field Clear Clear
Change only the case name Keep Clear and regenerate with the new name
Start another comparison Clear Clear
Cancel or reset Clear Clear

The case name is report metadata. It does not participate in the diff calculation, so a completed comparison can survive a name change. Cancel keeps the selected files; reset removes their references too.

These transitions clear data from application state. The result display and save button derive their availability from that same state. Hiding a panel while keeping an old report available elsewhere would leave the expiration rule incomplete.

An input epoch and a job token answer different questions

Each input change increments an epoch. A job receives the current epoch when it starts, and the state reducer rejects an event from another epoch:

This epoch tracks input and state generations. Changing the case name, canceling, or resetting also advances it. Rerunning a comparison without changing the inputs does not.

// State-update excerpt: events carry the epoch used to start the job.
if (event.epoch !== state.epoch) return state;
Enter fullscreen mode Exit fullscreen mode

That rejects configuration A's result after the user moves to B. It does not distinguish two runs started with identical inputs: both can have the same epoch.

The controller therefore assigns each job a separate token. The left preview, right preview, comparison, and report have separate slots. A slot accepts only the token it currently owns.

The controller captures the token in the callback; it is not a field returned by the Worker. With an input containing the current epoch prepared, the comparison-start code registers the job in its slot:

const token = nextToken++;
const handle = runJob(input, event => onCompareEvent(token, event));
handles.compare = { token, handle };
Enter fullscreen mode Exit fullscreen mode

The callback is expected to run asynchronously after slot registration. In this implementation, a Worker response triggers it; even Worker-construction failure is deferred with queueMicrotask. MDN's Worker.postMessage reference describes the message mechanism.

The response callback then checks its captured token against the current slot. These are excerpts, with input validation, cancellation of old jobs, and Worker creation omitted.

function onCompareEvent(token: number, event: JobEvent): void {
  if (disposed) return;
  if (handles.compare?.token !== token) return;
  handles.compare = null;
  setState(reduceState(state, { type: "event", event }));
}
Enter fullscreen mode Exit fullscreen mode

The reducer still checks the epoch after the token check. The epoch identifies the input generation. The token identifies the particular operation the application currently accepts.

Reports need the same ownership rule because accepting a report can trigger a side effect. Starting another comparison cancels an in-flight report. An old report callback must not reach the function that initiates saving, even when the inputs and epoch have not changed.

Cancellation and acceptance have separate checks

The implementation terminates a Worker when its job is canceled. Worker.terminate stops the Worker immediately, without giving the running operation a chance to finish.

The controller separately specifies which callbacks it accepts. That rule can be tested without depending on a real Worker's termination behavior. This is not evidence that a browser delivers messages after Worker.terminate() has been called; the test uses a fake job deliberately capable of producing a canceled job's response.

Test the event order directly

The unit-test harness records cancellation but still lets a test emit a response afterward. This makes the order deterministic; it does not require a large CSV or a carefully chosen delay.

The stored tests cover these sequences:

Sequence supplied by the test Required outcome
Start comparison A → change to B → emit A's response No comparison or report reappears
Start a preview → reset → emit the old preview Initial state remains free of that preview
Start a report → cancel → emit its response No comparison or generated report remains
Start a report → rerun comparison with the same inputs → emit the old report The save callback is not called despite the matching epoch

The cancel case checks result state; it does not directly monitor calls to the save callback. The last sequence exercises operation ownership within one epoch. The test checks that the array recording calls to the save callback stays empty after the old report responds.

Stored browser tests check the visible contract too: changing the file, encoding, key, mapping, or protected fields, and resetting, removes old output and the save button. A separate case verifies that changing only the case name keeps the completed comparison and puts the new name into the generated report.

These checks cover specific transitions. They do not prove every possible scheduling race or confirm that the browser finished writing a download to disk. For this article, I reviewed the implementation and existing logs; I did not rerun the tests.

Treat an input change as one operation that clears expired output, invalidates stored result data, stops obsolete work, and rejects obsolete responses. The comparison then stays attached to the inputs it actually describes.

AI assisted development, verification, and the organization and writing of this article. The claims above are grounded in implementation and test records; they do not imply customer adoption or commercial results.

Top comments (0)