A reload's reply or error is dropped when an open was running as it started, an open started after it, or an open is still running when it answers. An open's reply or error is dropped only when a newer open started after it.
This commit is contained in:
+18
-16
@@ -11,7 +11,7 @@ import { layout } from "./lib/layout";
|
|||||||
import { textWidth } from "./lib/measure";
|
import { textWidth } from "./lib/measure";
|
||||||
import { colorOf } from "./lib/palette";
|
import { colorOf } from "./lib/palette";
|
||||||
import { addRecent, mergeRecents } from "./lib/recents";
|
import { addRecent, mergeRecents } from "./lib/recents";
|
||||||
import { tickets } from "./lib/latest";
|
import { repoRequests, type Request } from "./lib/latest";
|
||||||
import type { FileDiff, Layout } from "./lib/diff";
|
import type { FileDiff, Layout } from "./lib/diff";
|
||||||
import type { ChangedFile, CommitDetails, FilterState, Ref, RefKind, RepoData } from "./lib/types";
|
import type { ChangedFile, CommitDetails, FilterState, Ref, RefKind, RepoData } from "./lib/types";
|
||||||
import DiffView from "./components/DiffView.vue";
|
import DiffView from "./components/DiffView.vue";
|
||||||
@@ -107,10 +107,10 @@ let filtersLoadedFor: string | null = null;
|
|||||||
let loading = false;
|
let loading = false;
|
||||||
/**
|
/**
|
||||||
* Git runs away from the window, so replies can arrive in another order than the requests
|
* Git runs away from the window, so replies can arrive in another order than the requests
|
||||||
* went out. Every request that can put a new repository on screen (a reload or opening one)
|
* went out. Every reload and every open is noted here, and its reply is used only if
|
||||||
* takes a ticket; a reply is used only if no newer such request was sent after it.
|
* `requests.isCurrent` allows it: opening a repository wins over a reload.
|
||||||
*/
|
*/
|
||||||
const repoRequests = tickets();
|
const requests = repoRequests();
|
||||||
/** False until the first load answers, so the welcome screen does not flash up at start. */
|
/** False until the first load answers, so the welcome screen does not flash up at start. */
|
||||||
const started = ref(false);
|
const started = ref(false);
|
||||||
|
|
||||||
@@ -118,14 +118,14 @@ async function load() {
|
|||||||
// The window gets focus often; one reload at a time is enough.
|
// The window gets focus often; one reload at a time is enough.
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
const ticket = repoRequests.take();
|
const request = requests.startReload();
|
||||||
try {
|
try {
|
||||||
// null: gites was started outside a repository and none is open yet.
|
// null: gites was started outside a repository and none is open yet.
|
||||||
const data = await invoke<RepoData | null>("load_repo");
|
const data = await invoke<RepoData | null>("load_repo");
|
||||||
if (data) await applyRepo(data, ticket);
|
if (data) await applyRepo(data, request);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// A newer request replaced this one; its error no longer matters.
|
// An open replaced this reload; its error no longer matters.
|
||||||
if (!repoRequests.isLatest(ticket)) return;
|
if (!requests.isCurrent(request)) return;
|
||||||
// A repository already on screen stays, for example when a connection drops.
|
// A repository already on screen stays, for example when a connection drops.
|
||||||
if (repo.value) showToast(`could not reload: ${e}`, { error: true });
|
if (repo.value) showToast(`could not reload: ${e}`, { error: true });
|
||||||
else error.value = String(e);
|
else error.value = String(e);
|
||||||
@@ -137,14 +137,16 @@ async function load() {
|
|||||||
|
|
||||||
async function openRepo(path: string) {
|
async function openRepo(path: string) {
|
||||||
menuOpen.value = false;
|
menuOpen.value = false;
|
||||||
const ticket = repoRequests.take();
|
const request = requests.startOpen();
|
||||||
try {
|
try {
|
||||||
await applyRepo(await invoke<RepoData>("open_repo", { path }), ticket);
|
await applyRepo(await invoke<RepoData>("open_repo", { path }), request);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// A newer request replaced this one; its error no longer matters.
|
// A newer open replaced this one; its error no longer matters.
|
||||||
if (!repoRequests.isLatest(ticket)) return;
|
if (!requests.isCurrent(request)) return;
|
||||||
// The repository that was open stays open.
|
// The repository that was open stays open.
|
||||||
showToast(`could not open ${path}: ${e}`, { error: true });
|
showToast(`could not open ${path}: ${e}`, { error: true });
|
||||||
|
} finally {
|
||||||
|
requests.finish(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,16 +159,16 @@ async function browse() {
|
|||||||
|
|
||||||
const menuOpen = ref(false);
|
const menuOpen = ref(false);
|
||||||
|
|
||||||
/** Puts `data` on screen, unless a request newer than the one holding `ticket` was sent. */
|
/** Puts `data` on screen, unless `request` was replaced (see `requests`). */
|
||||||
async function applyRepo(data: RepoData, ticket: number) {
|
async function applyRepo(data: RepoData, request: Request) {
|
||||||
if (!repoRequests.isLatest(ticket)) return;
|
if (!requests.isCurrent(request)) return;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
if (filtersLoadedFor !== data.root) {
|
if (filtersLoadedFor !== data.root) {
|
||||||
const saved = await invoke<Partial<FilterState & { collapseAll: boolean }> | null>("load_filters", {
|
const saved = await invoke<Partial<FilterState & { collapseAll: boolean }> | null>("load_filters", {
|
||||||
repo: data.root,
|
repo: data.root,
|
||||||
});
|
});
|
||||||
// Another repository may have been asked for while the filters were read.
|
// Another repository may have been asked for while the filters were read.
|
||||||
if (!repoRequests.isLatest(ticket)) return;
|
if (!requests.isCurrent(request)) return;
|
||||||
filters.hidden = saved?.hidden ?? [];
|
filters.hidden = saved?.hidden ?? [];
|
||||||
filters.rules = saved?.rules ?? [];
|
filters.rules = saved?.rules ?? [];
|
||||||
filters.hideTags = saved?.hideTags ?? false;
|
filters.hideTags = saved?.hideTags ?? false;
|
||||||
|
|||||||
+50
-26
@@ -1,33 +1,57 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { tickets } from "./latest";
|
import { repoRequests } from "./latest";
|
||||||
|
|
||||||
describe("tickets", () => {
|
describe("repoRequests", () => {
|
||||||
it("treats the only ticket taken as the newest", () => {
|
it("applies a reload with no open around", () => {
|
||||||
const t = tickets();
|
const r = repoRequests();
|
||||||
const a = t.take();
|
const reload = r.startReload();
|
||||||
expect(t.isLatest(a)).toBe(true);
|
expect(r.isCurrent(reload)).toBe(true);
|
||||||
});
|
});
|
||||||
it("treats an older ticket as out of date once a newer one is taken", () => {
|
it("applies a reload that starts after an open has finished", () => {
|
||||||
const t = tickets();
|
const r = repoRequests();
|
||||||
const older = t.take();
|
const open = r.startOpen();
|
||||||
const newer = t.take();
|
r.finish(open);
|
||||||
expect(t.isLatest(older)).toBe(false);
|
const reload = r.startReload();
|
||||||
expect(t.isLatest(newer)).toBe(true);
|
expect(r.isCurrent(reload)).toBe(true);
|
||||||
});
|
});
|
||||||
it("keeps an older ticket out of date after more tickets are taken", () => {
|
it("drops a reload started during a running open, and applies the open", () => {
|
||||||
const t = tickets();
|
const r = repoRequests();
|
||||||
const first = t.take();
|
const open = r.startOpen();
|
||||||
t.take();
|
const reload = r.startReload();
|
||||||
const last = t.take();
|
// The reload answers first, while the open is still running.
|
||||||
expect(t.isLatest(first)).toBe(false);
|
expect(r.isCurrent(reload)).toBe(false);
|
||||||
expect(t.isLatest(last)).toBe(true);
|
expect(r.isCurrent(open)).toBe(true);
|
||||||
|
r.finish(open);
|
||||||
|
// Its reply may hold the repository from before the open, so it stays dropped.
|
||||||
|
expect(r.isCurrent(reload)).toBe(false);
|
||||||
});
|
});
|
||||||
it("keeps separate sets of tickets apart", () => {
|
it("drops a reload when an open starts after it", () => {
|
||||||
const a = tickets();
|
const r = repoRequests();
|
||||||
const b = tickets();
|
const reload = r.startReload();
|
||||||
const fromA = a.take();
|
const open = r.startOpen();
|
||||||
b.take();
|
expect(r.isCurrent(reload)).toBe(false);
|
||||||
b.take();
|
r.finish(open);
|
||||||
expect(a.isLatest(fromA)).toBe(true);
|
expect(r.isCurrent(reload)).toBe(false);
|
||||||
|
expect(r.isCurrent(open)).toBe(true);
|
||||||
|
});
|
||||||
|
it("applies only the newer of two opens", () => {
|
||||||
|
const r = repoRequests();
|
||||||
|
const older = r.startOpen();
|
||||||
|
const newer = r.startOpen();
|
||||||
|
expect(r.isCurrent(older)).toBe(false);
|
||||||
|
expect(r.isCurrent(newer)).toBe(true);
|
||||||
|
});
|
||||||
|
it("applies an open even when a reload started after it", () => {
|
||||||
|
const r = repoRequests();
|
||||||
|
const open = r.startOpen();
|
||||||
|
r.startReload();
|
||||||
|
expect(r.isCurrent(open)).toBe(true);
|
||||||
|
});
|
||||||
|
it("keeps separate sets of requests apart", () => {
|
||||||
|
const a = repoRequests();
|
||||||
|
const b = repoRequests();
|
||||||
|
const reload = a.startReload();
|
||||||
|
b.startOpen();
|
||||||
|
expect(a.isCurrent(reload)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+34
-10
@@ -1,17 +1,41 @@
|
|||||||
/**
|
/**
|
||||||
* Numbers requests so that only the reply to the newest one is used. Take a ticket before
|
* Decides which replies may put a repository on screen. Git runs away from the window, so
|
||||||
* sending a request; when its reply arrives, use it only if `isLatest` says no newer ticket
|
* replies can arrive in another order than the requests went out. Opening a repository wins
|
||||||
* was taken in the meantime.
|
* over a reload:
|
||||||
|
* - a reload's reply is dropped if an open was running when the reload started, if an open
|
||||||
|
* started after it, or if an open is still running when the reply arrives;
|
||||||
|
* - an open's reply is dropped only if a newer open started after it.
|
||||||
*/
|
*/
|
||||||
export interface Tickets {
|
export type Request = { kind: "open" | "reload"; number: number; duringOpen: boolean };
|
||||||
take(): number;
|
|
||||||
isLatest(ticket: number): boolean;
|
export interface RepoRequests {
|
||||||
|
startOpen(): Request;
|
||||||
|
startReload(): Request;
|
||||||
|
/** Call when an open has finished, whether its reply was used or not. */
|
||||||
|
finish(request: Request): void;
|
||||||
|
isCurrent(request: Request): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tickets(): Tickets {
|
export function repoRequests(): RepoRequests {
|
||||||
let newest = 0;
|
let counter = 0;
|
||||||
|
let newestOpen = 0;
|
||||||
|
const runningOpens = new Set<number>();
|
||||||
return {
|
return {
|
||||||
take: () => ++newest,
|
startOpen() {
|
||||||
isLatest: (ticket) => ticket === newest,
|
const number = ++counter;
|
||||||
|
newestOpen = number;
|
||||||
|
runningOpens.add(number);
|
||||||
|
return { kind: "open", number, duringOpen: false };
|
||||||
|
},
|
||||||
|
startReload() {
|
||||||
|
return { kind: "reload", number: ++counter, duringOpen: runningOpens.size > 0 };
|
||||||
|
},
|
||||||
|
finish(request) {
|
||||||
|
runningOpens.delete(request.number);
|
||||||
|
},
|
||||||
|
isCurrent(request) {
|
||||||
|
if (request.kind === "open") return request.number === newestOpen;
|
||||||
|
return !request.duringOpen && newestOpen < request.number && runningOpens.size === 0;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user