feat: distinguish remote operation and request activity

This commit is contained in:
vorotamoroz
2026-07-16 04:09:53 +00:00
parent 9ffde2dd3e
commit 7439b75cd0
15 changed files with 579 additions and 91 deletions
+71 -14
View File
@@ -2,8 +2,24 @@ import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import type { Page } from "playwright";
import { withObsidianPage } from "./ui.ts";
import {
REMOTE_OPERATION_ACTIVITY_ICON,
REMOTE_REQUEST_ACTIVITY_ICON,
} from "../../../src/modules/features/RemoteActivityStatus.ts";
export const REMOTE_ACTIVITY_E2E_STATE_KEY = "__livesyncE2ERemoteActivity";
export const REMOTE_ACTIVITY_GATE_KIND = {
chunkFetch: "chunk-fetch",
oneShot: "one-shot",
trackedRequest: "tracked-request",
} as const;
export const REMOTE_ACTIVITY_EXPECTED_STATE = {
chunkFetchActive: "chunk-fetch-active",
finiteReplicationActive: "finite-replication-active",
idle: "idle",
trackedRequestActive: "tracked-request-active",
} as const;
export type RemoteActivityGateKind = (typeof REMOTE_ACTIVITY_GATE_KIND)[keyof typeof REMOTE_ACTIVITY_GATE_KIND];
export type RemoteActivitySnapshot = {
boundedRemoteActivityCount: number;
@@ -11,14 +27,17 @@ export type RemoteActivitySnapshot = {
gateDone?: boolean;
gateEntered?: boolean;
gateError?: string;
gateKind?: string;
gateKind?: RemoteActivityGateKind;
requestCount: number;
remoteOperationIndicatorVisible: boolean;
remoteRequestIndicatorVisible: boolean;
responseCount: number;
statusBarFound: boolean;
statusBarText: string;
};
export type ExpectedRemoteActivityState = "chunk-fetch-active" | "finite-replication-active" | "idle";
export type ExpectedRemoteActivityState =
(typeof REMOTE_ACTIVITY_EXPECTED_STATE)[keyof typeof REMOTE_ACTIVITY_EXPECTED_STATE];
type RuntimeCounter = { value?: number };
@@ -39,7 +58,7 @@ type RuntimeGate = {
done?: boolean;
entered?: boolean;
error?: string;
kind?: string;
kind?: RemoteActivityGateKind;
};
type RendererGlobals = typeof globalThis & {
@@ -52,7 +71,7 @@ type RendererGlobals = typeof globalThis & {
async function readRemoteActivitySnapshotFromPage(page: Page): Promise<RemoteActivitySnapshot> {
return await page.evaluate(
({ pluginId, stateKey }) => {
({ operationIcon, pluginId, requestIcon, stateKey }) => {
const globals = globalThis as RendererGlobals;
const core = globals.app?.plugins?.plugins?.[pluginId]?.core;
if (!core) throw new Error(`Obsidian plug-in is not loaded: ${pluginId}`);
@@ -68,12 +87,23 @@ async function readRemoteActivitySnapshotFromPage(page: Page): Promise<RemoteAct
gateError: gate?.error,
gateKind: gate?.kind,
requestCount: Number(core.services?.API?.requestCount?.value ?? -1),
remoteOperationIndicatorVisible: statusBars.some((element) =>
(element.textContent ?? "").includes(operationIcon)
),
remoteRequestIndicatorVisible: statusBars.some((element) =>
(element.textContent ?? "").includes(requestIcon)
),
responseCount: Number(core.services?.API?.responseCount?.value ?? -1),
statusBarFound: statusBars.length > 0,
statusBarText: statusBars.map((element) => element.textContent ?? "").join("\n"),
} satisfies RemoteActivitySnapshot;
},
{ pluginId: "obsidian-livesync", stateKey: REMOTE_ACTIVITY_E2E_STATE_KEY }
{
operationIcon: REMOTE_OPERATION_ACTIVITY_ICON,
pluginId: "obsidian-livesync",
requestIcon: REMOTE_REQUEST_ACTIVITY_ICON,
stateKey: REMOTE_ACTIVITY_E2E_STATE_KEY,
}
);
}
@@ -105,7 +135,7 @@ export async function waitForRemoteActivityState(
try {
return await withObsidianPage(port, async (page) => {
await page.waitForFunction(
({ expectedState, pluginId, stateKey }) => {
({ expectedState, expectedStates, gateKinds, operationIcon, pluginId, requestIcon, stateKey }) => {
const globals = globalThis as RendererGlobals;
const core = globals.app?.plugins?.plugins?.[pluginId]?.core;
if (!core) return false;
@@ -117,31 +147,58 @@ export async function waitForRemoteActivityState(
const finite = Number(core.services?.replicator?.finiteReplicationActivityCount?.value ?? -1);
const requests = Number(core.services?.API?.requestCount?.value ?? -1);
const responses = Number(core.services?.API?.responseCount?.value ?? -1);
const iconVisible = statusBarText.includes("📲");
const operationIconVisible = statusBarText.includes(operationIcon);
const requestIconVisible = statusBarText.includes(requestIcon);
const trackedRequests = Math.max(0, requests - responses);
if (expectedState === "finite-replication-active") {
if (expectedState === expectedStates.finiteReplicationActive) {
return (
gate?.kind === "one-shot" &&
gate?.kind === gateKinds.oneShot &&
gate.entered === true &&
bounded > 0 &&
finite > 0 &&
iconVisible
operationIconVisible &&
!requestIconVisible &&
trackedRequests === 0
);
}
if (expectedState === "chunk-fetch-active") {
if (expectedState === expectedStates.chunkFetchActive) {
return (
gate?.kind === "chunk-fetch" &&
gate?.kind === gateKinds.chunkFetch &&
gate.entered === true &&
bounded > 0 &&
finite === 0 &&
iconVisible
operationIconVisible &&
!requestIconVisible &&
trackedRequests === 0
);
}
return bounded === 0 && finite === 0 && requests === responses && !iconVisible;
if (expectedState === expectedStates.trackedRequestActive) {
return (
gate?.kind === gateKinds.trackedRequest &&
gate.entered === true &&
bounded === 0 &&
finite === 0 &&
trackedRequests > 0 &&
!operationIconVisible &&
statusBarText.includes(`${requestIcon}${trackedRequests}`)
);
}
return (
bounded === 0 &&
finite === 0 &&
requests === responses &&
!operationIconVisible &&
!requestIconVisible
);
},
{
expectedState: expected,
expectedStates: REMOTE_ACTIVITY_EXPECTED_STATE,
gateKinds: REMOTE_ACTIVITY_GATE_KIND,
operationIcon: REMOTE_OPERATION_ACTIVITY_ICON,
pluginId: "obsidian-livesync",
requestIcon: REMOTE_REQUEST_ACTIVITY_ICON,
stateKey: REMOTE_ACTIVITY_E2E_STATE_KEY,
},
{ timeout: timeoutMs }