mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-01 15:02:31 +00:00
Simplify remote feature checks and clarify manual rebuild guidance
This commit is contained in:
@@ -368,13 +368,20 @@ async function uploadHiddenFile(
|
||||
return ids.has(entry.id) && entry.children.every((childId) => ids.has(childId));
|
||||
});
|
||||
const remoteEntry = await fetchCouchDbDocument(context.couchDb, context.dbName, entry.id);
|
||||
if (!remoteEntry.path?.startsWith("/\\:") || remoteEntry.children?.length !== 0 ||
|
||||
remoteEntry.ctime !== 0 || remoteEntry.mtime !== 0 || remoteEntry.size !== 0) {
|
||||
if (
|
||||
!remoteEntry.path?.startsWith("/\\:") ||
|
||||
remoteEntry.children?.length !== 0 ||
|
||||
remoteEntry.ctime !== 0 ||
|
||||
remoteEntry.mtime !== 0 ||
|
||||
remoteEntry.size !== 0
|
||||
) {
|
||||
throw new Error(`Hidden File Sync Metadata was not encrypted for ${entry.id}.`);
|
||||
}
|
||||
const versionInfo = await fetchCouchDbDocument(context.couchDb, context.dbName, VERSIONING_DOCID);
|
||||
if (versionInfo.version !== REMOTE_FEATURE_GENERATION ||
|
||||
!(versionInfo.used_features as unknown[] | undefined)?.includes(ENCRYPTED_INTERNAL_METADATA_FEATURE)) {
|
||||
if (
|
||||
versionInfo.version !== REMOTE_FEATURE_GENERATION ||
|
||||
!(versionInfo.used_features as unknown[] | undefined)?.includes(ENCRYPTED_INTERNAL_METADATA_FEATURE)
|
||||
) {
|
||||
throw new Error("The remote feature list does not declare encrypted internal Metadata.");
|
||||
}
|
||||
return entry;
|
||||
@@ -400,6 +407,29 @@ async function runCreateRoundTrip(
|
||||
await writeVaultFile(vaultA.path, snippetPath, snippetContent);
|
||||
let session = await startConfiguredSession(context, vaultA);
|
||||
const entry = await uploadHiddenFile(context, session, snippetPath);
|
||||
await evalObsidianJson(
|
||||
context.cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const rebuilder=app.plugins.plugins['obsidian-livesync'].core.rebuilder;",
|
||||
"const inform=rebuilder.informOptionalFeatures;",
|
||||
"rebuilder.informOptionalFeatures=async()=>{};",
|
||||
"try{await rebuilder.$rebuildRemote();}finally{rebuilder.informOptionalFeatures=inform;}",
|
||||
"return JSON.stringify(true);",
|
||||
"})()",
|
||||
].join(""),
|
||||
session.cliEnv
|
||||
);
|
||||
const rebuiltVersion = await fetchCouchDbDocument(context.couchDb, context.dbName, VERSIONING_DOCID);
|
||||
const rebuiltEntry = await fetchCouchDbDocument(context.couchDb, context.dbName, entry.id);
|
||||
if (
|
||||
rebuiltVersion.version !== REMOTE_FEATURE_GENERATION ||
|
||||
!(rebuiltVersion.used_features as unknown[] | undefined)?.includes(ENCRYPTED_INTERNAL_METADATA_FEATURE) ||
|
||||
!rebuiltEntry.path?.startsWith("/\\:")
|
||||
) {
|
||||
throw new Error("Remote Rebuild did not declare and encrypt internal Metadata for its accepted writer.");
|
||||
}
|
||||
console.log("Remote Rebuild declared the feature and preserved encrypted Hidden File Sync Metadata.");
|
||||
await session.app.stop();
|
||||
|
||||
session = await startConfiguredSession(context, vaultB);
|
||||
@@ -588,11 +618,14 @@ async function runInitialisationNoticeGrouping(context: RunnerContext, vault: Te
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while ((await page.locator(".notice:visible").count()) > 0 && Date.now() < deadline) {
|
||||
await page.locator(".notice:visible").first().click({
|
||||
force: true,
|
||||
position: { x: 2, y: 2 },
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
await page
|
||||
.locator(".notice:visible")
|
||||
.first()
|
||||
.click({
|
||||
force: true,
|
||||
position: { x: 2, y: 2 },
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
}
|
||||
assertEqual(
|
||||
await page.locator(".notice:visible").count(),
|
||||
@@ -724,17 +757,15 @@ async function runInitialisationNoticeGrouping(context: RunnerContext, vault: Te
|
||||
|
||||
const result = await withObsidianPage(port, async (page) => {
|
||||
await page.evaluate((stateKey) => {
|
||||
const state = (globalThis as unknown as Record<
|
||||
string,
|
||||
{ releasePreparation?: () => void } | undefined
|
||||
>)[stateKey];
|
||||
const state = (
|
||||
globalThis as unknown as Record<string, { releasePreparation?: () => void } | undefined>
|
||||
)[stateKey];
|
||||
state?.releasePreparation?.();
|
||||
}, hiddenFileInitialisationStateKey);
|
||||
await page.waitForFunction(
|
||||
(stateKey) =>
|
||||
(globalThis as unknown as Record<string, { reachedInitialisation?: boolean } | undefined>)[
|
||||
stateKey
|
||||
]?.reachedInitialisation === true,
|
||||
(globalThis as unknown as Record<string, { reachedInitialisation?: boolean } | undefined>)[stateKey]
|
||||
?.reachedInitialisation === true,
|
||||
hiddenFileInitialisationStateKey,
|
||||
{ timeout: timeoutMs }
|
||||
);
|
||||
|
||||
@@ -32,9 +32,6 @@ const unknownFeature = "future-format-v7";
|
||||
type FeatureState = {
|
||||
version: number | null;
|
||||
features: string[];
|
||||
snapshotFeatures: string[];
|
||||
snapshotDatabaseId: string | null;
|
||||
databaseId: string | null;
|
||||
hasActiveReplicator: boolean;
|
||||
};
|
||||
|
||||
@@ -46,14 +43,9 @@ async function readFeatureState(cliBinary: string, env: NodeJS.ProcessEnv): Prom
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
`const id=${JSON.stringify(VERSIONING_DOCID)};`,
|
||||
"const info=await core.localDatabase.getRaw(id).catch(()=>null);",
|
||||
"const snapshot=await core.kvDB.get('replicationResultProcessorSnapshot');",
|
||||
"const databaseId=await core.localDatabase.localDatabase.id();",
|
||||
"return JSON.stringify({",
|
||||
"version:typeof info?.version==='number'?info.version:null,",
|
||||
"features:Array.isArray(info?.used_features)?info.used_features:[],",
|
||||
"snapshotFeatures:Array.isArray(snapshot?.observedFeatures)?snapshot.observedFeatures:[],",
|
||||
"snapshotDatabaseId:snapshot?.databaseId??null,",
|
||||
"databaseId,",
|
||||
"hasActiveReplicator:!!core.services.replicator.getActiveReplicator(),",
|
||||
"});",
|
||||
"})()",
|
||||
@@ -153,12 +145,8 @@ async function main(): Promise<void> {
|
||||
const observed = await waitForState(
|
||||
cli.binary,
|
||||
session.cliEnv,
|
||||
(state) =>
|
||||
state.version === 13 &&
|
||||
state.features.includes(unknownFeature) &&
|
||||
state.snapshotFeatures.includes(unknownFeature) &&
|
||||
!state.hasActiveReplicator,
|
||||
"the live feature change, durable observation, and Replicator retirement"
|
||||
(state) => state.version === 13 && state.features.includes(unknownFeature) && !state.hasActiveReplicator,
|
||||
"the live feature change and Replicator retirement"
|
||||
);
|
||||
|
||||
const replicated = await evalObsidianJson<boolean>(
|
||||
@@ -171,25 +159,6 @@ async function main(): Promise<void> {
|
||||
if (acceptedAfterStop !== acceptedContent)
|
||||
throw new Error("Previously accepted Vault content changed on stop.");
|
||||
|
||||
// Shorten both visible lists so only the snapshot can retain the observed requirement.
|
||||
const shortenedRemoteVersion = await fetchCouchDbDocument(couchDb, dbName, VERSIONING_DOCID);
|
||||
await putCouchDbDocument(couchDb, dbName, { ...shortenedRemoteVersion, used_features: [] });
|
||||
const shortenedLocalFeatures = await evalObsidianJson<string[]>(
|
||||
cli.binary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
`const id=${JSON.stringify(VERSIONING_DOCID)};`,
|
||||
"const info=await core.localDatabase.getRaw(id);",
|
||||
"await core.localDatabase.putRaw({...info,used_features:[]});",
|
||||
"return JSON.stringify((await core.localDatabase.getRaw(id)).used_features);",
|
||||
"})()",
|
||||
].join(""),
|
||||
session.cliEnv
|
||||
);
|
||||
if (shortenedLocalFeatures.length !== 0)
|
||||
throw new Error("The local control document did not shorten for the restart scenario.");
|
||||
|
||||
await session.app.stop();
|
||||
session = undefined;
|
||||
session = await startObsidianLiveSyncSession({ binary, cliBinary: cli.binary, vault });
|
||||
@@ -197,36 +166,40 @@ async function main(): Promise<void> {
|
||||
const afterRestart = await waitForState(
|
||||
cli.binary,
|
||||
session.cliEnv,
|
||||
(state) =>
|
||||
state.version === 13 && state.features.length === 0 && state.snapshotFeatures.includes(unknownFeature),
|
||||
"the retained unknown feature after restart"
|
||||
(state) => state.version === 13 && state.features.includes(unknownFeature),
|
||||
"the remote feature requirement after restart"
|
||||
);
|
||||
if (afterRestart.snapshotDatabaseId !== afterRestart.databaseId)
|
||||
throw new Error("The retained feature observation belongs to a different physical database.");
|
||||
const replicatedAfterRestart = await evalObsidianJson<boolean>(
|
||||
cli.binary,
|
||||
"(async()=>JSON.stringify(!!(await app.plugins.plugins['obsidian-livesync'].core.services.replication.replicate(true))))()",
|
||||
session.cliEnv
|
||||
);
|
||||
const continuousAfterRestart = await evalObsidianJson<{ status: string }>(
|
||||
const continuousAfterRestart = await evalObsidianJson<{ requestStatus: string; connected: boolean }>(
|
||||
cli.binary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const replicator=core.services.replicator.getActiveReplicator();",
|
||||
"const original=replicator.openContinuousReplication;",
|
||||
"let completion;",
|
||||
"replicator.openContinuousReplication=function(...args){completion=original.apply(this,args);return completion;};",
|
||||
"try{",
|
||||
"const result=await core.services.replication.startContinuous({trigger:'daemon',interaction:{kind:'forbidden'}});",
|
||||
"return JSON.stringify(result);",
|
||||
"if(!completion) throw new Error('The continuous provider did not attempt its remote check');",
|
||||
"return JSON.stringify({requestStatus:result.status,connected:await completion});",
|
||||
"}finally{replicator.openContinuousReplication=original;}",
|
||||
"})()",
|
||||
].join(""),
|
||||
session.cliEnv
|
||||
);
|
||||
if (replicatedAfterRestart || continuousAfterRestart.status !== "blocked")
|
||||
if (replicatedAfterRestart || continuousAfterRestart.connected !== false)
|
||||
throw new Error(
|
||||
`Replication resumed after restart despite a previously observed unknown feature: ${JSON.stringify({ afterRestart, replicatedAfterRestart, continuousAfterRestart })}`
|
||||
`Replication resumed after restart despite an unknown remote feature: ${JSON.stringify({ afterRestart, replicatedAfterRestart, continuousAfterRestart })}`
|
||||
);
|
||||
if ((await readFile(fullPath, "utf-8")) !== acceptedContent)
|
||||
throw new Error("Previously accepted Vault content changed after restart.");
|
||||
console.log(
|
||||
`Active feature change retired the Replicator; a shortened control document did not clear the block after restart: ${JSON.stringify({ observed, afterRestart })}`
|
||||
`Active feature change retired the Replicator; the remote declaration refused synchronisation after restart: ${JSON.stringify({ observed, afterRestart })}`
|
||||
);
|
||||
} finally {
|
||||
await session?.app.stop();
|
||||
|
||||
Reference in New Issue
Block a user