mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-25 10:33:21 +00:00
Add Issue 1189 Fast Setup diagnostic builds
This commit is contained in:
1 parent
2c2b9c90e4
commit
b533bb2e28
9 files changed
+347
-7
No files matched your search
@@ -12,6 +12,10 @@ import { terserOption } from "./terser.config.mjs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
const prod = process.argv[2] === "production" || process.env?.BUILD_MODE === "production";
|
const prod = process.argv[2] === "production" || process.env?.BUILD_MODE === "production";
|
||||||
|
const issue1189Variant = process.env.ISSUE_1189_VARIANT ?? "normal";
|
||||||
|
if (issue1189Variant !== "normal" && issue1189Variant !== "serial") {
|
||||||
|
throw new Error("ISSUE_1189_VARIANT must be 'normal' or 'serial'.");
|
||||||
|
}
|
||||||
const keepTest = true; //!prod;
|
const keepTest = true; //!prod;
|
||||||
|
|
||||||
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + "");
|
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + "");
|
||||||
@@ -181,6 +185,7 @@ const context = await esbuild.context({
|
|||||||
entryPoints: ["src/main.ts"],
|
entryPoints: ["src/main.ts"],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
define: {
|
define: {
|
||||||
|
ISSUE_1189_VARIANT: JSON.stringify(issue1189Variant),
|
||||||
MANIFEST_VERSION: `"${manifestJson.version}"`,
|
MANIFEST_VERSION: `"${manifestJson.version}"`,
|
||||||
PACKAGE_VERSION: `"${packageJson.version}"`,
|
PACKAGE_VERSION: `"${packageJson.version}"`,
|
||||||
UPDATE_INFO: `${updateInfo}`,
|
UPDATE_INFO: `${updateInfo}`,
|
||||||
|
|||||||
+53
-2
@@ -51,9 +51,13 @@ import { useInteractiveConflictResolutionFeature } from "./serviceFeatures/inter
|
|||||||
import { ConflictResolveModal } from "./modules/features/InteractiveConflictResolving/ConflictResolveModal.ts";
|
import { ConflictResolveModal } from "./modules/features/InteractiveConflictResolving/ConflictResolveModal.ts";
|
||||||
import { useObsidianReplicationRibbonFeature } from "./serviceFeatures/obsidianReplicationRibbon.ts";
|
import { useObsidianReplicationRibbonFeature } from "./serviceFeatures/obsidianReplicationRibbon.ts";
|
||||||
import { useStartupLifecycleFeature } from "./serviceFeatures/startupLifecycle";
|
import { useStartupLifecycleFeature } from "./serviceFeatures/startupLifecycle";
|
||||||
|
import { createIssue1189Diagnostics, type Issue1189Diagnostics, type Issue1189Variant } from "./serviceFeatures/issue1189Diagnostics";
|
||||||
|
declare const ISSUE_1189_VARIANT: Issue1189Variant;
|
||||||
export type LiveSyncCore = LiveSyncBaseCore<ObsidianServiceContext, LiveSyncCommands>;
|
export type LiveSyncCore = LiveSyncBaseCore<ObsidianServiceContext, LiveSyncCommands>;
|
||||||
export default class ObsidianLiveSyncPlugin extends Plugin {
|
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||||
core: LiveSyncCore;
|
core: LiveSyncCore;
|
||||||
|
issue1189Diagnostics?: Issue1189Diagnostics;
|
||||||
|
private stopIssue1189Timer?: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialise service modules.
|
* Initialise service modules.
|
||||||
@@ -174,6 +178,49 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
return addOns;
|
return addOns;
|
||||||
},
|
},
|
||||||
(core) => {
|
(core) => {
|
||||||
|
const diagnostics = createIssue1189Diagnostics(
|
||||||
|
core.services.setting,
|
||||||
|
core.services.API.getSystemVaultName(),
|
||||||
|
ISSUE_1189_VARIANT
|
||||||
|
);
|
||||||
|
this.issue1189Diagnostics = diagnostics;
|
||||||
|
core.services.appLifecycle.onLoaded.addHandler(() => {
|
||||||
|
this.stopIssue1189Timer = diagnostics.startTimer(window.setTimeout.bind(window), window.clearTimeout.bind(window));
|
||||||
|
core.services.API.addCommand({
|
||||||
|
id: "copy-issue-1189-diagnostics",
|
||||||
|
name: "Copy diagnostic record",
|
||||||
|
callback: () => {
|
||||||
|
if (!navigator.clipboard) {
|
||||||
|
new Notice("Could not copy the diagnostic record.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void navigator.clipboard.writeText(diagnostics.report()).then(
|
||||||
|
() => new Notice("Diagnostic record copied."),
|
||||||
|
() => new Notice("Could not copy the diagnostic record.")
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
core.services.API.addCommand({
|
||||||
|
id: "save-issue-1189-diagnostics",
|
||||||
|
name: "Save diagnostic record",
|
||||||
|
callback: () => {
|
||||||
|
const path = `${this.manifest.dir}/issue-1189-diagnostics.json`;
|
||||||
|
void this.app.vault.adapter.write(path, diagnostics.report()).then(
|
||||||
|
() => new Notice("Diagnostic record saved in the plug-in folder."),
|
||||||
|
() => new Notice("Could not save the diagnostic record.")
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
core.services.API.addCommand({
|
||||||
|
id: "resume-issue-1189-fast-setup",
|
||||||
|
name: "Resume diagnostic attempt",
|
||||||
|
callback: () => {
|
||||||
|
diagnostics.allowResume();
|
||||||
|
core.services.appLifecycle.performRestart();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return Promise.resolve(true);
|
||||||
|
});
|
||||||
//TODO Fix: useXXXX
|
//TODO Fix: useXXXX
|
||||||
const featuresInitialiser = enableI18nFeature;
|
const featuresInitialiser = enableI18nFeature;
|
||||||
const curriedFeature = () => featuresInitialiser(core);
|
const curriedFeature = () => featuresInitialiser(core);
|
||||||
@@ -196,8 +243,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
useSetupQRCodeFeature(core);
|
useSetupQRCodeFeature(core);
|
||||||
useSetupURIFeature(core);
|
useSetupURIFeature(core);
|
||||||
useSetupManagerHandlersFeature(core, setupManager);
|
useSetupManagerHandlersFeature(core, setupManager);
|
||||||
useOfflineScanner(core);
|
useOfflineScanner(core, {
|
||||||
useRedFlagFeatures(core);
|
fileConcurrency: ISSUE_1189_VARIANT === "serial" ? 1 : 10,
|
||||||
|
onFileActivity: (activity) => diagnostics.onFileActivity(activity),
|
||||||
|
});
|
||||||
|
useRedFlagFeatures(core, diagnostics);
|
||||||
useCheckRemoteSize(core);
|
useCheckRemoteSize(core);
|
||||||
useInteractiveConflictResolutionFeature(core, (filename, conflictCheckResult) => {
|
useInteractiveConflictResolutionFeature(core, (filename, conflictCheckResult) => {
|
||||||
return new ConflictResolveModal(this.app, filename, conflictCheckResult);
|
return new ConflictResolveModal(this.app, filename, conflictCheckResult);
|
||||||
@@ -224,6 +274,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
void this._startUp();
|
void this._startUp();
|
||||||
}
|
}
|
||||||
override onunload(): void {
|
override onunload(): void {
|
||||||
|
this.stopIssue1189Timer?.();
|
||||||
return void this.core.services.control.onUnload();
|
return void this.core.services.control.onUnload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ declare module "obsidian" {
|
|||||||
// InjectableAppLifecycleService
|
// InjectableAppLifecycleService
|
||||||
export class ObsidianAppLifecycleService<T extends ObsidianServiceContext> extends AppLifecycleServiceBase<T> {
|
export class ObsidianAppLifecycleService<T extends ObsidianServiceContext> extends AppLifecycleServiceBase<T> {
|
||||||
performRestart(): void {
|
performRestart(): void {
|
||||||
|
this.context.liveSyncPlugin.issue1189Diagnostics?.markRestartRequested();
|
||||||
void this.context.plugin.app.commands.executeCommandById("app:reload");
|
void this.context.plugin.app.commands.executeCommandById("app:reload");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
export type Issue1189Variant = "normal" | "serial";
|
||||||
|
export type Issue1189Phase = "boot" | "fetch" | "reflect" | "finalise" | "complete" | "paused";
|
||||||
|
|
||||||
|
type DiagnosticEvent = { at: number; phase: Issue1189Phase; note?: string };
|
||||||
|
type DiagnosticRun = {
|
||||||
|
id: string;
|
||||||
|
variant: Issue1189Variant;
|
||||||
|
startedAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
phase: Issue1189Phase;
|
||||||
|
status: "active" | "complete" | "paused";
|
||||||
|
restartRequested: boolean;
|
||||||
|
activeFiles: number;
|
||||||
|
peakActiveFiles: number;
|
||||||
|
activeBytes: number;
|
||||||
|
peakActiveBytes: number;
|
||||||
|
processedFiles: number;
|
||||||
|
largestFile: number;
|
||||||
|
peakTimerDelayMs: number;
|
||||||
|
events: DiagnosticEvent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type DiagnosticRecord = {
|
||||||
|
schema: 1;
|
||||||
|
current: DiagnosticRun;
|
||||||
|
previous?: DiagnosticRun;
|
||||||
|
resumeAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Issue1189DiagnosticStorage {
|
||||||
|
getDeviceLocalConfig(key: string): string | null;
|
||||||
|
setDeviceLocalConfig(key: string, value: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INTERVAL_MS = 1000;
|
||||||
|
const HISTORY_LIMIT = 24;
|
||||||
|
|
||||||
|
function parseRecord(raw: string | null): DiagnosticRecord | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as DiagnosticRecord;
|
||||||
|
if (parsed?.schema === 1 && parsed.current?.events instanceof Array) return parsed;
|
||||||
|
} catch {
|
||||||
|
// A damaged optional diagnostic record must never prevent plug-in startup.
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createIssue1189Diagnostics(
|
||||||
|
storage: Issue1189DiagnosticStorage,
|
||||||
|
vaultName: string,
|
||||||
|
variant: Issue1189Variant,
|
||||||
|
now: () => number = Date.now
|
||||||
|
) {
|
||||||
|
const key = `issue-1189-diagnostics-${vaultName}`;
|
||||||
|
let raw: string | null = null;
|
||||||
|
try {
|
||||||
|
raw = storage.getDeviceLocalConfig(key);
|
||||||
|
} catch {
|
||||||
|
// Diagnosis is optional when the browser's local store is unavailable.
|
||||||
|
}
|
||||||
|
const saved = parseRecord(raw);
|
||||||
|
const previous =
|
||||||
|
(saved?.current.phase === "boot" || saved?.current.phase === "paused") && saved.previous
|
||||||
|
? saved.previous
|
||||||
|
: saved?.current;
|
||||||
|
const resumeAllowed = saved?.resumeAllowed === true;
|
||||||
|
const startedAt = now();
|
||||||
|
const current: DiagnosticRun = {
|
||||||
|
id: `${startedAt}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
variant,
|
||||||
|
startedAt,
|
||||||
|
updatedAt: startedAt,
|
||||||
|
phase: "boot",
|
||||||
|
status: "active",
|
||||||
|
restartRequested: false,
|
||||||
|
activeFiles: 0,
|
||||||
|
peakActiveFiles: 0,
|
||||||
|
activeBytes: 0,
|
||||||
|
peakActiveBytes: 0,
|
||||||
|
processedFiles: 0,
|
||||||
|
largestFile: 0,
|
||||||
|
peakTimerDelayMs: 0,
|
||||||
|
events: [{ at: startedAt, phase: "boot" }],
|
||||||
|
};
|
||||||
|
const record: DiagnosticRecord = { schema: 1, current, previous, resumeAllowed: false };
|
||||||
|
let lastPersistedAt = 0;
|
||||||
|
let lastStorageError = "";
|
||||||
|
const persist = () => {
|
||||||
|
current.updatedAt = now();
|
||||||
|
try {
|
||||||
|
storage.setDeviceLocalConfig(key, JSON.stringify(record));
|
||||||
|
lastPersistedAt = current.updatedAt;
|
||||||
|
lastStorageError = "";
|
||||||
|
} catch (error) {
|
||||||
|
lastStorageError = error instanceof Error ? error.name : String(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const event = (phase: Issue1189Phase, note?: string) => {
|
||||||
|
current.phase = phase;
|
||||||
|
current.events.push({ at: now(), phase, note });
|
||||||
|
if (current.events.length > HISTORY_LIMIT) current.events.shift();
|
||||||
|
persist();
|
||||||
|
};
|
||||||
|
persist();
|
||||||
|
|
||||||
|
return {
|
||||||
|
variant,
|
||||||
|
shouldPauseFetch: () =>
|
||||||
|
!resumeAllowed &&
|
||||||
|
previous?.status === "active" &&
|
||||||
|
!previous.restartRequested &&
|
||||||
|
(previous.phase === "fetch" || previous.phase === "reflect" || previous.phase === "finalise"),
|
||||||
|
pause() {
|
||||||
|
current.status = "paused";
|
||||||
|
event("paused", "An earlier Fast Setup attempt did not finish.");
|
||||||
|
},
|
||||||
|
allowResume() {
|
||||||
|
record.resumeAllowed = true;
|
||||||
|
persist();
|
||||||
|
},
|
||||||
|
markPhase: event,
|
||||||
|
markRestartRequested() {
|
||||||
|
current.restartRequested = true;
|
||||||
|
persist();
|
||||||
|
},
|
||||||
|
markComplete() {
|
||||||
|
current.status = "complete";
|
||||||
|
event("complete");
|
||||||
|
},
|
||||||
|
onFileActivity({ phase, size }: { phase: "start" | "finish"; size: number }) {
|
||||||
|
const firstFile = current.activeFiles === 0 && current.processedFiles === 0;
|
||||||
|
const previousPeakFiles = current.peakActiveFiles;
|
||||||
|
const previousLargestFile = current.largestFile;
|
||||||
|
if (phase === "start") {
|
||||||
|
current.activeFiles++;
|
||||||
|
current.activeBytes += size;
|
||||||
|
current.peakActiveFiles = Math.max(current.peakActiveFiles, current.activeFiles);
|
||||||
|
current.peakActiveBytes = Math.max(current.peakActiveBytes, current.activeBytes);
|
||||||
|
current.largestFile = Math.max(current.largestFile, size);
|
||||||
|
} else {
|
||||||
|
current.activeFiles = Math.max(0, current.activeFiles - 1);
|
||||||
|
current.activeBytes = Math.max(0, current.activeBytes - size);
|
||||||
|
current.processedFiles++;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(phase === "start" &&
|
||||||
|
(firstFile || current.peakActiveFiles > previousPeakFiles || (size >= 4_000_000 && size > previousLargestFile))) ||
|
||||||
|
now() - lastPersistedAt >= INTERVAL_MS
|
||||||
|
) {
|
||||||
|
persist();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startTimer(setTimer: (callback: () => void, delay: number) => number, clearTimer: (id: number) => void) {
|
||||||
|
let expected = now() + INTERVAL_MS;
|
||||||
|
let stopped = false;
|
||||||
|
let timerId = 0;
|
||||||
|
const tick = () => {
|
||||||
|
if (stopped) return;
|
||||||
|
const actual = now();
|
||||||
|
current.peakTimerDelayMs = Math.max(current.peakTimerDelayMs, Math.max(0, actual - expected));
|
||||||
|
if (actual - lastPersistedAt >= INTERVAL_MS) persist();
|
||||||
|
expected = actual + INTERVAL_MS;
|
||||||
|
timerId = setTimer(tick, INTERVAL_MS);
|
||||||
|
};
|
||||||
|
timerId = setTimer(tick, INTERVAL_MS);
|
||||||
|
return () => {
|
||||||
|
stopped = true;
|
||||||
|
clearTimer(timerId);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
report() {
|
||||||
|
return JSON.stringify({ ...record, storageError: lastStorageError || undefined }, null, 2);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Issue1189Diagnostics = ReturnType<typeof createIssue1189Diagnostics>;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { createIssue1189Diagnostics } from "./issue1189Diagnostics";
|
||||||
|
|
||||||
|
function makeStorage() {
|
||||||
|
const items = new Map<string, string>();
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
getDeviceLocalConfig: (key: string) => items.get(key) ?? null,
|
||||||
|
setDeviceLocalConfig: (key: string, value: string) => {
|
||||||
|
items.set(key, value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Issue 1189 diagnostic record", () => {
|
||||||
|
it("keeps an interrupted reflection readable across startup and pauses its retry", () => {
|
||||||
|
const storage = makeStorage();
|
||||||
|
let clock = 1000;
|
||||||
|
const first = createIssue1189Diagnostics(storage, "Vault", "normal", () => clock);
|
||||||
|
first.markPhase("fetch");
|
||||||
|
first.markPhase("reflect");
|
||||||
|
first.onFileActivity({ phase: "start", size: 40_000_000 });
|
||||||
|
|
||||||
|
clock += 1000;
|
||||||
|
const second = createIssue1189Diagnostics(storage, "Vault", "normal", () => clock);
|
||||||
|
expect(second.shouldPauseFetch()).toBe(true);
|
||||||
|
second.pause();
|
||||||
|
const record = JSON.parse(second.report());
|
||||||
|
expect(record.previous.phase).toBe("reflect");
|
||||||
|
expect(record.previous.peakActiveBytes).toBe(40_000_000);
|
||||||
|
|
||||||
|
clock += 1000;
|
||||||
|
const third = createIssue1189Diagnostics(storage, "Vault", "serial", () => clock);
|
||||||
|
expect(third.shouldPauseFetch()).toBe(true);
|
||||||
|
third.allowResume();
|
||||||
|
clock += 1000;
|
||||||
|
const resumed = createIssue1189Diagnostics(storage, "Vault", "serial", () => clock);
|
||||||
|
expect(resumed.shouldPauseFetch()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not pause after a requested restart or completed attempt", () => {
|
||||||
|
const storage = makeStorage();
|
||||||
|
const first = createIssue1189Diagnostics(storage, "Vault", "normal");
|
||||||
|
first.markPhase("fetch");
|
||||||
|
first.markRestartRequested();
|
||||||
|
expect(createIssue1189Diagnostics(storage, "Vault", "normal").shouldPauseFetch()).toBe(false);
|
||||||
|
|
||||||
|
const completed = createIssue1189Diagnostics(storage, "Other Vault", "serial");
|
||||||
|
completed.markPhase("finalise");
|
||||||
|
completed.markComplete();
|
||||||
|
expect(createIssue1189Diagnostics(storage, "Other Vault", "serial").shouldPauseFetch()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bounds the saved event history and measures timer delay", () => {
|
||||||
|
const storage = makeStorage();
|
||||||
|
let clock = 0;
|
||||||
|
const diagnostics = createIssue1189Diagnostics(storage, "Vault", "normal", () => clock);
|
||||||
|
for (let i = 0; i < 40; i++) diagnostics.markPhase("fetch");
|
||||||
|
let tick: (() => void) | undefined;
|
||||||
|
const stop = diagnostics.startTimer((callback) => {
|
||||||
|
tick = callback;
|
||||||
|
return 1;
|
||||||
|
}, () => {});
|
||||||
|
clock = 1500;
|
||||||
|
tick?.();
|
||||||
|
stop();
|
||||||
|
const record = JSON.parse(diagnostics.report());
|
||||||
|
expect(record.current.events).toHaveLength(24);
|
||||||
|
expect(record.current.peakTimerDelayMs).toBe(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
type FullScanOptions,
|
type FullScanOptions,
|
||||||
} from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner";
|
} from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner";
|
||||||
import { adjustSettingToRemoteIfNeeded, cancelScheduledInitialisation, processVaultInitialisation } from "./redFlag";
|
import { adjustSettingToRemoteIfNeeded, cancelScheduledInitialisation, processVaultInitialisation } from "./redFlag";
|
||||||
|
import type { Issue1189Diagnostics } from "./issue1189Diagnostics";
|
||||||
|
|
||||||
export const SIMPLE_FETCH_STAGE1_REMOTE_WINS = "Overwrite all with remote files";
|
export const SIMPLE_FETCH_STAGE1_REMOTE_WINS = "Overwrite all with remote files";
|
||||||
export const SIMPLE_FETCH_STAGE1_NEWER_WINS = "Compare time and take newer";
|
export const SIMPLE_FETCH_STAGE1_NEWER_WINS = "Compare time and take newer";
|
||||||
@@ -188,7 +189,8 @@ export async function askAndPerformFastSetupOnScheduledFetchAll(
|
|||||||
"storageAccess" | "rebuilder" | "fileHandler"
|
"storageAccess" | "rebuilder" | "fileHandler"
|
||||||
>,
|
>,
|
||||||
log: LogFunction,
|
log: LogFunction,
|
||||||
cleanupFlag: () => Promise<void>
|
cleanupFlag: () => Promise<void>,
|
||||||
|
diagnostics?: Issue1189Diagnostics
|
||||||
): Promise<boolean | undefined> {
|
): Promise<boolean | undefined> {
|
||||||
const result = await askSimpleFetchMode(host);
|
const result = await askSimpleFetchMode(host);
|
||||||
if (result === "cancelled") {
|
if (result === "cancelled") {
|
||||||
@@ -215,9 +217,11 @@ export async function askAndPerformFastSetupOnScheduledFetchAll(
|
|||||||
|
|
||||||
const performFastSetup = async () => {
|
const performFastSetup = async () => {
|
||||||
// 1. Perform fast DB fetch (download remote DB content to local DB)
|
// 1. Perform fast DB fetch (download remote DB content to local DB)
|
||||||
|
diagnostics?.markPhase("fetch", "downloading");
|
||||||
await host.serviceModules.rebuilder.$fetchLocalDBFast(false);
|
await host.serviceModules.rebuilder.$fetchLocalDBFast(false);
|
||||||
|
|
||||||
// 2. Call the extended synchroniseAllFilesBetweenDBandStorage to reflect changes in storage
|
// 2. Call the extended synchroniseAllFilesBetweenDBandStorage to reflect changes in storage
|
||||||
|
diagnostics?.markPhase("reflect");
|
||||||
const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events);
|
const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events);
|
||||||
const syncResult = await synchroniseAllFilesBetweenDBandStorage(
|
const syncResult = await synchroniseAllFilesBetweenDBandStorage(
|
||||||
host,
|
host,
|
||||||
@@ -228,6 +232,8 @@ export async function askAndPerformFastSetupOnScheduledFetchAll(
|
|||||||
showingNotice: true,
|
showingNotice: true,
|
||||||
omitEvents: true,
|
omitEvents: true,
|
||||||
ignoreSuspending: true,
|
ignoreSuspending: true,
|
||||||
|
fileConcurrency: diagnostics?.variant === "serial" ? 1 : 10,
|
||||||
|
onFileActivity: diagnostics ? (activity) => diagnostics.onFileActivity(activity) : undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (!syncResult) {
|
if (!syncResult) {
|
||||||
@@ -244,9 +250,11 @@ export async function askAndPerformFastSetupOnScheduledFetchAll(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
diagnostics?.markPhase("finalise");
|
||||||
await host.serviceModules.rebuilder.finishRebuild();
|
await host.serviceModules.rebuilder.finishRebuild();
|
||||||
await cleanupFlag();
|
await cleanupFlag();
|
||||||
clearRememberedSimpleFetchMode(host);
|
clearRememberedSimpleFetchMode(host);
|
||||||
|
diagnostics?.markComplete();
|
||||||
log("Simple fetch and scan operation completed.", LOG_LEVEL_NOTICE);
|
log("Simple fetch and scan operation completed.", LOG_LEVEL_NOTICE);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common
|
|||||||
import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/remote-configurations";
|
import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/remote-configurations";
|
||||||
import { isP2PMainRemote } from "@/common/remoteConfiguration";
|
import { isP2PMainRemote } from "@/common/remoteConfiguration";
|
||||||
import { $msg } from "@/common/translation";
|
import { $msg } from "@/common/translation";
|
||||||
|
import type { Issue1189Diagnostics } from "./issue1189Diagnostics";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag file handler interface, similar to target filter pattern.
|
* Flag file handler interface, similar to target filter pattern.
|
||||||
@@ -128,7 +129,8 @@ export function createFetchAllFlagHandler(
|
|||||||
| "database",
|
| "database",
|
||||||
"storageAccess" | "rebuilder" | "fileHandler"
|
"storageAccess" | "rebuilder" | "fileHandler"
|
||||||
>,
|
>,
|
||||||
log: LogFunction
|
log: LogFunction,
|
||||||
|
diagnostics?: Issue1189Diagnostics
|
||||||
): FlagFileHandler {
|
): FlagFileHandler {
|
||||||
// Check if fetch all flag is active
|
// Check if fetch all flag is active
|
||||||
const isFlagActive = async () =>
|
const isFlagActive = async () =>
|
||||||
@@ -143,6 +145,11 @@ export function createFetchAllFlagHandler(
|
|||||||
|
|
||||||
// Handle the fetch all scheduled operation
|
// Handle the fetch all scheduled operation
|
||||||
const onScheduled = async () => {
|
const onScheduled = async () => {
|
||||||
|
if (diagnostics?.shouldPauseFetch()) {
|
||||||
|
diagnostics.pause();
|
||||||
|
log("An earlier Fast Setup attempt stopped unexpectedly. Use 'Copy diagnostic record', then 'Resume diagnostic attempt' to continue.", LOG_LEVEL_NOTICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Select the remote database if there are multiple remotes configured.
|
// Select the remote database if there are multiple remotes configured.
|
||||||
const isRemoteActivated = await askAndActivateRemoteDatabase(host, log);
|
const isRemoteActivated = await askAndActivateRemoteDatabase(host, log);
|
||||||
if (!isRemoteActivated) {
|
if (!isRemoteActivated) {
|
||||||
@@ -150,7 +157,7 @@ export function createFetchAllFlagHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ask user for use Fast Setup
|
// Ask user for use Fast Setup
|
||||||
const useFastSetup = await askAndPerformFastSetupOnScheduledFetchAll(host, log, cleanupFlag);
|
const useFastSetup = await askAndPerformFastSetupOnScheduledFetchAll(host, log, cleanupFlag, diagnostics);
|
||||||
if (useFastSetup !== undefined) {
|
if (useFastSetup !== undefined) {
|
||||||
return useFastSetup;
|
return useFastSetup;
|
||||||
}
|
}
|
||||||
@@ -567,10 +574,11 @@ export function useRedFlagFeatures(
|
|||||||
| "keyValueDB"
|
| "keyValueDB"
|
||||||
| "database",
|
| "database",
|
||||||
"storageAccess" | "rebuilder" | "fileHandler"
|
"storageAccess" | "rebuilder" | "fileHandler"
|
||||||
>
|
>,
|
||||||
|
diagnostics?: Issue1189Diagnostics
|
||||||
) {
|
) {
|
||||||
const log = createInstanceLogFunction("SF:RedFlag", host.services.API);
|
const log = createInstanceLogFunction("SF:RedFlag", host.services.API);
|
||||||
const handlerFetch = createFetchAllFlagHandler(host, log);
|
const handlerFetch = createFetchAllFlagHandler(host, log, diagnostics);
|
||||||
const handlerRebuild = createRebuildFlagHandler(host, log);
|
const handlerRebuild = createRebuildFlagHandler(host, log);
|
||||||
const handlerSuspend = createSuspendFlagHandler(host, log);
|
const handlerSuspend = createSuspendFlagHandler(host, log);
|
||||||
host.services.appLifecycle.onLayoutReady.addHandler(flagHandlerToEventHandler(handlerFetch), handlerFetch.priority);
|
host.services.appLifecycle.onLayoutReady.addHandler(flagHandlerToEventHandler(handlerFetch), handlerFetch.priority);
|
||||||
|
|||||||
@@ -401,6 +401,20 @@ describe("Red Flag Feature", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Fetch All Flag Handler", () => {
|
describe("Fetch All Flag Handler", () => {
|
||||||
|
it("pauses an interrupted diagnostic attempt before fetching or deleting its flag", async () => {
|
||||||
|
const host = createHostMock();
|
||||||
|
const log = createLoggerMock();
|
||||||
|
host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
|
||||||
|
const diagnostics = { shouldPauseFetch: () => true, pause: vi.fn() };
|
||||||
|
|
||||||
|
const result = await createFetchAllFlagHandler(host as any, log, diagnostics as any).handle();
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(diagnostics.pause).toHaveBeenCalledOnce();
|
||||||
|
expect(host.mocks.rebuilder.$fetchLocalDBFast).not.toHaveBeenCalled();
|
||||||
|
expect(host.mocks.storageAccess.files.has(FlagFilesOriginal.FETCH_ALL)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("should detect fetch all flag using original filename", async () => {
|
it("should detect fetch all flag using original filename", async () => {
|
||||||
const host = createHostMock();
|
const host = createHostMock();
|
||||||
const log = createLoggerMock();
|
const log = createLoggerMock();
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Diagnostics
|
||||||
|
|
||||||
|
- Issue #1189 diagnostic builds keep a device-local record of Fast Setup progress across restarts. After an interrupted attempt, the automatic retry pauses so you can copy or save the record before trying again.
|
||||||
|
|
||||||
## 1.0.30
|
## 1.0.30
|
||||||
|
|
||||||
18th September, 2026
|
18th September, 2026
|
||||||
|
|||||||
Reference in new issue
Block a user