Files
Triple-C/app/src/viewer/viewerState.ts
T

106 lines
4.2 KiB
TypeScript
Raw Normal View History

/**
* The viewer's reload/dirty/conflict rules as a pure reducer (spec §5).
*
* Two hashes, deliberately: `baseHash` is what the buffer was loaded from or
* last saved as -- the save's precondition. `diskHash` is the last full-file
* hash the poll reported. They differ only for a truncated (read-only) load,
* where the read's hash covers a prefix and can never equal `sha256sum`; the
* poll then seeds `diskHash` without triggering a reload.
*
* The same prefix-vs-full-file split applies to a poll-driven reload of a
* truncated file: the fresh read's hash is still only a prefix hash, so a
* `reloaded` action for a truncated file adopts the *polled* hash as the new
* `diskHash` rather than the read's own hash. Without this, a large file
* would re-download on every poll tick forever (spec Decision 2).
*/
import type { ViewerPoll } from "../lib/types";
export type DocStatus = "clean" | "dirty";
export type DiskStatus = "same" | "changed" | "gone";
export interface ViewerDocState {
doc: DocStatus;
disk: DiskStatus;
/** Hash the buffer was loaded from / last saved as. */
baseHash: string | null;
/** Last known full-file hash on disk (null until known). */
diskHash: string | null;
containerDown: boolean;
/** Set for one render after a clean reload; UI shows "Reloaded". */
justReloaded: boolean;
/** True when the user chose "Overwrite on save" after a disk change. */
overwrite: boolean;
}
export type ViewerAction =
| { type: "loaded"; hash: string; truncated: boolean }
| { type: "edited" }
| { type: "polled"; poll: ViewerPoll }
| { type: "poll_failed" }
| { type: "reloaded"; hash: string; truncated: boolean; polledHash: string | null }
| { type: "overwrite_on_save" }
| { type: "saved"; hash: string }
| { type: "save_conflict" }
| { type: "save_gone" };
export const initialViewerState: ViewerDocState = {
doc: "clean",
disk: "same",
baseHash: null,
diskHash: null,
containerDown: false,
justReloaded: false,
overwrite: false,
};
export function reduceViewer(state: ViewerDocState, action: ViewerAction): ViewerDocState {
const s = { ...state, justReloaded: false };
switch (action.type) {
case "loaded":
return { ...initialViewerState, baseHash: action.hash, diskHash: action.truncated ? null : action.hash };
case "edited":
return { ...s, doc: "dirty" };
case "polled": {
if (!action.poll.exists) return { ...s, disk: "gone", containerDown: false };
const hash = action.poll.hash;
if (hash === null) return { ...s, containerDown: false };
if (s.diskHash === null) return { ...s, diskHash: hash, disk: s.disk === "gone" ? "same" : s.disk, containerDown: false };
if (hash === s.diskHash) return { ...s, disk: s.disk === "gone" ? "same" : s.disk, containerDown: false };
// Changed on disk. "Overwrite on save" adopted a base; a further change
// on disk invalidates it again.
return { ...s, diskHash: hash, disk: "changed", overwrite: false, containerDown: false };
}
case "poll_failed":
return { ...s, containerDown: true };
case "reloaded":
return {
...s,
doc: "clean",
disk: "same",
baseHash: action.hash,
diskHash: action.truncated ? action.polledHash : action.hash,
justReloaded: true,
overwrite: false,
};
case "overwrite_on_save":
return { ...s, baseHash: s.diskHash, disk: "same", overwrite: true };
case "saved":
return { ...s, doc: "clean", disk: "same", baseHash: action.hash, diskHash: action.hash, overwrite: false };
case "save_conflict":
return { ...s, disk: "changed", overwrite: false };
case "save_gone":
return { ...s, disk: "gone" };
}
}
/** What EditorPane does after a poll: nothing, reload silently, or show the banner. */
export function pollEffect(before: ViewerDocState, after: ViewerDocState): "none" | "reload" | "banner" {
if (after.disk === "gone") return before.disk === "gone" ? "none" : "banner";
if (after.disk !== "changed" || after.diskHash === before.diskHash) return "none";
return after.doc === "clean" ? "reload" : "banner";
}
export function canSave(state: ViewerDocState, editable: boolean): boolean {
return editable && state.doc === "dirty" && state.disk === "same" && !state.containerDown;
}