mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-31 13:28:18 +00:00
fix(syncguard): retire bypass on catch-up completion and close CSS rule
This commit is contained in:
1 parent
2d3a5e5ff8
commit
47828e934b
6 files changed
+100
-4
No files matched your search
@@ -59,7 +59,16 @@ export class SyncGuardState {
|
|||||||
|
|
||||||
/** The CouchDB catch-up phase ends before continuous replication and Vault reflection finish. */
|
/** The CouchDB catch-up phase ends before continuous replication and Vault reflection finish. */
|
||||||
catchUpActivityChanged(active: boolean, pendingCount: number, now: number): void {
|
catchUpActivityChanged(active: boolean, pendingCount: number, now: number): void {
|
||||||
if (this.phase === "idle" || this.bypassed || this.catchUpActive === active) return;
|
if (this.catchUpActive === active) return;
|
||||||
|
// A manual bypass lasts only for this catch-up generation. Even while the
|
||||||
|
// overlay is closed, observe its terminal transition so that a later
|
||||||
|
// reconnect can protect the workspace again.
|
||||||
|
if (this.bypassed) {
|
||||||
|
this.catchUpActive = active;
|
||||||
|
if (!active) this.bypassed = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.phase === "idle") return;
|
||||||
if (active) {
|
if (active) {
|
||||||
// A retry or a second pull can begin while the Vault is still applying an earlier batch.
|
// A retry or a second pull can begin while the Vault is still applying an earlier batch.
|
||||||
// Do not let the previous successful completion unlock an active newer transfer.
|
// Do not let the previous successful completion unlock an active newer transfer.
|
||||||
|
|||||||
@@ -54,6 +54,28 @@ describe("SyncGuardState", () => {
|
|||||||
expect(guard.presentation.phase).toBe("idle");
|
expect(guard.presentation.phase).toBe("idle");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("expires the manual bypass when its catch-up completes and protects the next catch-up", () => {
|
||||||
|
const { guard } = setup();
|
||||||
|
guard.resume(true, 2);
|
||||||
|
guard.catchUpActivityChanged(true, 0, 0);
|
||||||
|
guard.receivedDocuments(["first", "second"]);
|
||||||
|
expect(guard.presentation.phase).toBe("transferring");
|
||||||
|
guard.bypass();
|
||||||
|
expect(guard.wasBypassed).toBe(true);
|
||||||
|
guard.catchUpActivityChanged(true, 0, 5);
|
||||||
|
expect(guard.isCatchUpActive).toBe(true);
|
||||||
|
guard.catchUpSucceeded();
|
||||||
|
guard.catchUpActivityChanged(false, 0, 10);
|
||||||
|
expect(guard.isCatchUpActive).toBe(false);
|
||||||
|
expect(guard.wasBypassed).toBe(false);
|
||||||
|
expect(guard.presentation.phase).toBe("idle");
|
||||||
|
|
||||||
|
guard.resume(true, 2);
|
||||||
|
guard.catchUpActivityChanged(true, 0, 20);
|
||||||
|
guard.receivedDocuments(["another-first", "another-second"]);
|
||||||
|
expect(guard.presentation).toEqual({ phase: "transferring", receivedFiles: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
it("does not unlock when a new pull starts while applying an earlier completed transfer", () => {
|
it("does not unlock when a new pull starts while applying an earlier completed transfer", () => {
|
||||||
const { guard } = setup();
|
const { guard } = setup();
|
||||||
guard.resume(true, 1);
|
guard.resume(true, 1);
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export function useSyncGuardFeature(host: LiveSyncCore, createOverlay: SyncGuard
|
|||||||
// Commonlib enters continuous only after a successful pull-only catch-up.
|
// Commonlib enters continuous only after a successful pull-only catch-up.
|
||||||
guard.catchUpSucceeded();
|
guard.catchUpSucceeded();
|
||||||
guard.catchUpActivityChanged(false, readPendingCount(), Date.now());
|
guard.catchUpActivityChanged(false, readPendingCount(), Date.now());
|
||||||
} else if (guard.presentation.phase !== "idle" && terminalPhaseTimer === undefined) {
|
} else if ((guard.presentation.phase !== "idle" || guard.isCatchUpActive) && terminalPhaseTimer === undefined) {
|
||||||
// The one-shot PouchDB progress observer can report 'idle' before the
|
// The one-shot PouchDB progress observer can report 'idle' before the
|
||||||
// enclosing finite CouchDB pull has finished releasing its resources.
|
// enclosing finite CouchDB pull has finished releasing its resources.
|
||||||
// This counter is not used to identify the provider; CouchDB phase owns that.
|
// This counter is not used to identify the provider; CouchDB phase owns that.
|
||||||
|
|||||||
@@ -211,6 +211,51 @@ describe("useSyncGuardFeature", () => {
|
|||||||
await h.onUnload[0]();
|
await h.onUnload[0]();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("re-arms protection for a new substantial catch-up after bypass, completion, suspension and resume", async () => {
|
||||||
|
const h = setup({ minimumFiles: 2 });
|
||||||
|
await h.onResumed[0]();
|
||||||
|
h.setCouchDbPhase("catch-up");
|
||||||
|
await h.parseHandlers[0]([receivedNote("first"), receivedNote("second")]);
|
||||||
|
expect(h.updates[h.updates.length - 1]?.phase).toBe("transferring");
|
||||||
|
h.bypass();
|
||||||
|
|
||||||
|
// A duplicate resume before the first transfer finishes must honour the bypass.
|
||||||
|
await h.onResumed[0]();
|
||||||
|
await h.parseHandlers[0]([receivedNote("third"), receivedNote("fourth")]);
|
||||||
|
expect(h.updates[h.updates.length - 1]?.phase).toBe("idle");
|
||||||
|
|
||||||
|
// Successful hand-off to Continuous retires the bypassed generation.
|
||||||
|
h.setCouchDbPhase("continuous");
|
||||||
|
await h.onSuspending[0]();
|
||||||
|
await h.onResumed[0]();
|
||||||
|
h.setCouchDbPhase("catch-up");
|
||||||
|
await h.parseHandlers[0]([receivedNote("new-first"), receivedNote("new-second")]);
|
||||||
|
expect(h.updates[h.updates.length - 1]).toMatchObject({
|
||||||
|
phase: "transferring",
|
||||||
|
receivedFiles: 2,
|
||||||
|
});
|
||||||
|
await h.onUnload[0]();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-arms protection after bypassed catch-up fails and later restarts", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const h = setup({ minimumFiles: 1 });
|
||||||
|
await h.onResumed[0]();
|
||||||
|
h.setCouchDbPhase("catch-up");
|
||||||
|
h.finiteReplicationActivityCount.value = 1;
|
||||||
|
await h.parseHandlers[0]([receivedNote("failed")]);
|
||||||
|
h.bypass();
|
||||||
|
h.setCouchDbPhase("idle");
|
||||||
|
h.finiteReplicationActivityCount.value = 0;
|
||||||
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
|
await h.onSuspending[0]();
|
||||||
|
await h.onResumed[0]();
|
||||||
|
h.setCouchDbPhase("catch-up");
|
||||||
|
await h.parseHandlers[0]([receivedNote("after-restart")]);
|
||||||
|
expect(h.updates[h.updates.length - 1]?.phase).toBe("transferring");
|
||||||
|
await h.onUnload[0]();
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores large P2P finite transfers when CouchDB catch-up is not running", async () => {
|
it("ignores large P2P finite transfers when CouchDB catch-up is not running", async () => {
|
||||||
const h = setup({ minimumFiles: 2 });
|
const h = setup({ minimumFiles: 2 });
|
||||||
await h.onResumed[0]();
|
await h.onResumed[0]();
|
||||||
@@ -335,7 +380,7 @@ describe("useSyncGuardFeature", () => {
|
|||||||
await h.onUnload[0]();
|
await h.onUnload[0]();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not re-lock after manual bypass when an internal reconnect starts", async () => {
|
it("re-locks on a new internal catch-up after a bypassed transfer has completed", async () => {
|
||||||
const h = setup({ minimumFiles: 1 });
|
const h = setup({ minimumFiles: 1 });
|
||||||
await h.onResumed[0]();
|
await h.onResumed[0]();
|
||||||
h.setCouchDbPhase("catch-up");
|
h.setCouchDbPhase("catch-up");
|
||||||
@@ -345,7 +390,7 @@ describe("useSyncGuardFeature", () => {
|
|||||||
h.setCouchDbPhase("continuous");
|
h.setCouchDbPhase("continuous");
|
||||||
h.setCouchDbPhase("catch-up");
|
h.setCouchDbPhase("catch-up");
|
||||||
await h.parseHandlers[0]([receivedNote("second")]);
|
await h.parseHandlers[0]([receivedNote("second")]);
|
||||||
expect(h.updates[h.updates.length - 1]?.phase).toBe("idle");
|
expect(h.updates[h.updates.length - 1]?.phase).toBe("transferring");
|
||||||
await h.onUnload[0]();
|
await h.onUnload[0]();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -873,6 +873,8 @@ body.is-mobile .livesync-compatibility-review-notice {
|
|||||||
.sls-sync-guard-bypass {
|
.sls-sync-guard-bypass {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
min-width: 44px;
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
.livesync-stat-item {
|
.livesync-stat-item {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const stylesheet = readFileSync(fileURLToPath(new URL("../styles.css", import.meta.url)), "utf8");
|
||||||
|
|
||||||
|
describe("SyncGuard CSS", () => {
|
||||||
|
it("closes its bypass rule before the existing status display rules", () => {
|
||||||
|
const bypassStart = stylesheet.indexOf(".sls-sync-guard-bypass {");
|
||||||
|
const statusStart = stylesheet.indexOf(".livesync-stat-item {", bypassStart);
|
||||||
|
expect(bypassStart).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(statusStart).toBeGreaterThan(bypassStart);
|
||||||
|
const bypassRule = stylesheet.slice(bypassStart, statusStart);
|
||||||
|
expect(bypassRule).toMatch(/min-height:\s*44px;/);
|
||||||
|
expect(bypassRule).toMatch(/min-width:\s*44px;/);
|
||||||
|
expect(bypassRule).toMatch(/}\s*$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user