Merge Hidden File Sync selector coverage

This commit is contained in:
vorotamoroz committed 2026-10-23 15:22:25 +00:00
commit 1a68399dea
11 files changed
+432 -6

No files matched your search

+4 -2
View File
@@ -750,11 +750,13 @@ If enabled, the notification of hidden files change will be suppressed.
#### Synchronising files
(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.
Setting key: syncOnlyRegEx
(RegExp) Limit normal files to matching paths. Leave empty to allow all normal file paths. Hidden File Sync uses its own target patterns.
#### Non-Synchronising files
(RegExp) If this is set, any changes to local and remote files that match this will be skipped.
Setting key: syncIgnoreRegEx
(RegExp) Skip changes to matching normal and hidden files, locally and remotely.
#### Maximum file size
+4
View File
@@ -32,6 +32,10 @@ Enabling Hidden File Sync requires an initialisation direction:
Prefer a narrow target list. Device-specific workspace state and another plug-in's credentials are usually poor candidates for cross-device synchronisation.
`Normal Files` → `Synchronising files` limits ordinary files only. Hidden File Sync uses its own `Target patterns`. The `Non-Synchronising files` patterns under `Normal Files` also exclude matching hidden files, so review them alongside the hidden-file selectors.
If an earlier version skipped hidden files because of the normal-file allow-list, repeat Hidden File Sync initialisation after updating. A routine scan can skip unchanged files which were already checked. Back up the Vault and choose the intended source direction as described above.
Target patterns also control directory traversal. A pattern must therefore match each parent directory as well as the intended files. For example, the following pattern admits the `.obsidian` parent and only its `snippets` subtree:
```text
+1
View File
@@ -92,6 +92,7 @@
"test:e2e:obsidian:two-vault-sync": "tsx test/e2e-obsidian/scripts/two-vault-sync.ts",
"test:e2e:obsidian:security-seed-reconnect": "tsx test/e2e-obsidian/scripts/security-seed-reconnect.ts",
"test:e2e:obsidian:hidden-file-snippet-sync": "tsx test/e2e-obsidian/scripts/hidden-file-snippet-sync.ts",
"test:e2e:obsidian:hidden-file-selectors": "tsx test/e2e-obsidian/scripts/hidden-file-selectors.ts",
"test:e2e:obsidian:customisation-sync": "tsx test/e2e-obsidian/scripts/customisation-sync.ts",
"test:e2e:obsidian:received-change-readiness": "tsx test/e2e-obsidian/scripts/received-change-readiness.ts",
"test:e2e:obsidian:remote-feature-change": "tsx test/e2e-obsidian/scripts/remote-feature-change.ts",
@@ -7,6 +7,10 @@
* remove it from this map in the same change.
*/
export const liveSyncProvisionalEnglishMessages = {
"(RegExp) Limit normal files to matching paths. Leave empty to allow all normal file paths. Hidden File Sync uses its own target patterns.":
"(RegExp) Limit normal files to matching paths. Leave empty to allow all normal file paths. Hidden File Sync uses its own target patterns.",
"(RegExp) Skip changes to matching normal and hidden files, locally and remotely.":
"(RegExp) Skip changes to matching normal and hidden files, locally and remotely.",
"Preparing follow-up report…": "Preparing follow-up report…",
"Cancelling local search. Checked ${COUNT} entries.": "Cancelling local search. Checked ${COUNT} entries.",
"Search current Vault paths and current local Metadata only. Older history and remote-only files are not searched.":
@@ -0,0 +1,127 @@
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("@/deps.ts", () => ({}));
vi.mock("@/features/HiddenFileCommon/JsonResolveModal.ts", () => ({ JsonResolveModal: class JsonResolveModal {} }));
vi.mock("@/modules/features/ModuleLog.ts", () => ({ MARK_DONE: "" }));
vi.mock("./configureHiddenFileSyncMode.ts", () => ({ configureHiddenFileSyncMode: vi.fn() }));
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { readContent } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import { createOptionalFileSyncDatabase, optionalFileEntry } from "../../../test/fixtures/optionalFileSyncDatabase";
import { HiddenFileSync } from "./CmdHiddenFileSync";
const hiddenPath = ".obsidian/plugins/example/main.js" as FilePath;
const prefixedPath = `i:${hiddenPath}`;
const body = "module.exports = class ExamplePlugin {};";
const allowList = "\\.(?:md|png|pdf)$";
const fixtureOwners: Awaited<ReturnType<typeof createOptionalFileSyncDatabase>>[] = [];
async function hiddenFiles() {
const fixture = await createOptionalFileSyncDatabase();
fixtureOwners.push(fixture);
fixture.settings.usePluginSync = false;
const hiddenFileSync = Object.create(HiddenFileSync.prototype) as HiddenFileSync;
Object.assign(hiddenFileSync, {
core: {
settings: fixture.settings,
services: {
...fixture.services,
vault: { isIgnoredByIgnoreFile: vi.fn(async () => false) },
},
},
cacheFileRegExps: new Map(),
cacheCustomisationSyncIgnoredFiles: new Map(),
});
return { ...fixture, hiddenFileSync };
}
afterEach(async () => {
vi.restoreAllMocks();
for (const fixture of fixtureOwners.splice(0)) await fixture.dispose();
});
describe("issue #1272: Hidden File Sync with normal-file selectors", () => {
it("stores a selected hidden file while the normal-file allow-list is set", async () => {
const fixture = await hiddenFiles();
fixture.settings.syncOnlyRegEx = allowList as typeof fixture.settings.syncOnlyRegEx;
await expect(fixture.hiddenFileSync.isTargetFile(hiddenPath)).resolves.toBe(true);
await expect(fixture.localDatabase.putDBEntry(optionalFileEntry(prefixedPath, body))).resolves.toMatchObject({
ok: true,
});
const stored = await fixture.localDatabase.getDBEntry(prefixedPath as FilePath);
expect(stored).not.toBe(false);
if (stored) expect(await readContent(stored)).toBe(body);
});
it("reads existing hidden-file content while the normal-file allow-list is set", async () => {
const fixture = await hiddenFiles();
await expect(fixture.localDatabase.putDBEntry(optionalFileEntry(prefixedPath, body))).resolves.toMatchObject({
ok: true,
});
const meta = await fixture.localDatabase.getDBEntryMeta(prefixedPath as FilePath);
if (!meta) throw new Error("The seeded hidden-file Metadata is unavailable");
fixture.settings.syncOnlyRegEx = allowList as typeof fixture.settings.syncOnlyRegEx;
await expect(fixture.hiddenFileSync.isTargetFile(hiddenPath)).resolves.toBe(true);
const entry = await fixture.localDatabase.getDBEntryFromMeta(meta);
expect(entry).not.toBe(false);
if (entry) expect(await readContent(entry)).toBe(body);
});
it("stores and reads the same hidden file when the normal-file allow-list is empty", async () => {
const fixture = await hiddenFiles();
await expect(fixture.localDatabase.putDBEntry(optionalFileEntry(prefixedPath, body))).resolves.toMatchObject({
ok: true,
});
const entry = await fixture.localDatabase.getDBEntry(prefixedPath as FilePath);
expect(entry).not.toBe(false);
if (entry) expect(await readContent(entry)).toBe(body);
});
it("continues to apply the normal-file allow-list to ordinary files", async () => {
const fixture = await hiddenFiles();
fixture.settings.syncOnlyRegEx = allowList as typeof fixture.settings.syncOnlyRegEx;
expect(fixture.localDatabase.isTargetFile("notes/example.md" as FilePath)).toBe(true);
expect(fixture.localDatabase.isTargetFile("notes/example.js" as FilePath)).toBe(false);
expect(fixture.localDatabase.isTargetFile("i:.obsidian/example.md" as FilePath)).toBe(true);
});
it("keeps Customisation Sync documents usable with the same normal-file allow-list", async () => {
const fixture = await hiddenFiles();
fixture.settings.syncOnlyRegEx = allowList as typeof fixture.settings.syncOnlyRegEx;
const path = "ix:device/PLUGIN_MAIN/example%main.js" as FilePath;
await expect(fixture.localDatabase.putDBEntry(optionalFileEntry(path, body))).resolves.toMatchObject({
ok: true,
});
const entry = await fixture.localDatabase.getDBEntry(path);
expect(entry).not.toBe(false);
if (entry) expect(await readContent(entry)).toBe(body);
});
it("keeps Hidden File Sync target and ignore patterns effective", async () => {
const fixture = await hiddenFiles();
fixture.settings.syncInternalFilesTargetPatterns =
"^\\.obsidian/plugins/" as typeof fixture.settings.syncInternalFilesTargetPatterns;
await expect(fixture.hiddenFileSync.isTargetFile(hiddenPath)).resolves.toBe(true);
await expect(fixture.hiddenFileSync.isTargetFile(".obsidian/snippets/example.css" as FilePath)).resolves.toBe(
false
);
fixture.settings.syncInternalFilesIgnorePatterns =
"main\\.js$" as typeof fixture.settings.syncInternalFilesIgnorePatterns;
await expect(fixture.hiddenFileSync.isTargetFile(hiddenPath)).resolves.toBe(false);
});
it("compatibility: applies the normal-file ignore-list to hidden documents", async () => {
const fixture = await hiddenFiles();
fixture.settings.syncOnlyRegEx = allowList as typeof fixture.settings.syncOnlyRegEx;
fixture.settings.syncIgnoreRegEx = "^\\.obsidian/" as typeof fixture.settings.syncIgnoreRegEx;
expect(fixture.localDatabase.isTargetFile(prefixedPath as FilePath)).toBe(false);
});
it("rejects hidden documents when Hidden File Sync is disabled", async () => {
const fixture = await hiddenFiles();
fixture.settings.syncInternalFiles = false;
expect(fixture.localDatabase.isTargetFile(prefixedPath as FilePath)).toBe(false);
});
});
@@ -13,7 +13,7 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
.setName($msg("Synchronising files"))
.setDesc(
$msg(
"(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files."
"(RegExp) Limit normal files to matching paths. Leave empty to allow all normal file paths. Hidden File Sync uses its own target patterns."
)
);
const syncFilesControl = mount(MultipleRegExpControl, {
@@ -32,9 +32,7 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
const nonSyncFilesSetting = new Setting(paneEl)
.setName($msg("Non-Synchronising files"))
.setDesc(
$msg("(RegExp) If this is set, any changes to local and remote files that match this will be skipped.")
);
.setDesc($msg("(RegExp) Skip changes to matching normal and hidden files, locally and remotely."));
const nonSyncFilesControl = mount(MultipleRegExpControl, {
target: nonSyncFilesSetting.controlEl,
+2
View File
@@ -248,6 +248,8 @@ This proves in real Obsidian the plug-in behaviour shared by supported platforms
`test:e2e:obsidian:hidden-file-snippet-sync` runs a two-vault hidden file round-trip. It verifies creation and deletion of a real `.obsidian/snippets/*.css` file, automatic JSON conflict merging for a hidden file with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target patterns where one vault ignores a hidden file that the other vault synchronises. Initial enablement must open one user-visible progress Notice before the enabled setting is saved, then retain that Notice while its nested rebuild and scan phases continue in the ordinary log. The configured fixture starts with a current CouchDB remote profile, so migration from legacy remote settings remains the responsibility of the upgrade scenarios and cannot add unrelated Notices to this check. It also covers [issue #555](https://github.com/vrtmrz/obsidian-livesync/issues/555) by requiring several plug-in and settings changes to share one separate action Notice whose controls remain usable in mobile layouts; a manually dismissed group must not repeat its acknowledged rows when a later change arrives.
`test:e2e:obsidian:hidden-file-selectors` stores and restores selected JS, CSS, and JSON files with a normal-file allow-list set, and compares both directions with an empty allow-list. It uses one temporary Vault and a local database. Run it through the focused wrapper to build the current bundle first; no remote service is required. Set `E2E_HIDDEN_FILE_SELECTORS_OUTPUT` to choose the JSON report path.
`test:e2e:obsidian:customisation-sync` runs a two-vault Customisation Sync workflow. It scans a real snippet CSS file, config JSON file, and sample plug-in fixture into per-file Customisation Sync data, synchronises the entries through CouchDB, applies them on the second vault, verifies the resulting `.obsidian` files, propagates a snippet update, and verifies deletion of the source-vault snippet sync data without confusing it with the target vault's own applied copy.
The workflow also opens the Customisation Sync dialogue. `--case=visibility` checks that **Hide not applicable items** hides an identical snippet, preserves a different snippet and its source selector, and restores the identical snippet when cleared. `--case=mtime` gives the multi-file plug-in fixture modern millisecond timestamps and requires an older remote copy to remain labelled **Older** and unselected by **Select All Shiny**. With no case argument, both checks run before the existing apply, update, and deletion workflow.
@@ -0,0 +1,202 @@
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { isDeepStrictEqual } from "node:util";
import { fileURLToPath } from "node:url";
import { evalObsidianJson } from "../runner/cli.ts";
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
import {
createE2eCouchDbPluginData,
createE2eObsidianDeviceLocalState,
waitForLiveSyncCoreReady,
} from "../runner/liveSyncWorkflow.ts";
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
import { createTemporaryVault } from "../runner/vault.ts";
const hiddenRoot = ".obsidian/livesync-selector-example";
const hiddenFiles = {
[`${hiddenRoot}/main.js`]: "console.log('hidden fixture');\n",
[`${hiddenRoot}/styles.css`]: ".hidden-fixture { color: blue; }\n",
[`${hiddenRoot}/settings.json`]: '{"fixture":true}\n',
};
const controlRoot = ".obsidian/livesync-selector-control";
const controlFiles = {
[`${controlRoot}/main.js`]: "console.log('control fixture');\n",
[`${controlRoot}/styles.css`]: ".control-fixture { color: red; }\n",
[`${controlRoot}/settings.json`]: '{"control":true}\n',
};
const allowList = "\\.(?:md|png|pdf)$";
const coreCode = "const core=app.plugins.plugins['obsidian-livesync'].core;";
const hiddenCode = `${coreCode}const addOn=core.getAddOn('HiddenFileSync');`;
type Observation = { name: string; expected: unknown; actual: unknown; passed: boolean };
async function writeFixtures(vaultPath: string, files: Record<string, string>) {
for (const [path, content] of Object.entries(files)) {
await mkdir(dirname(join(vaultPath, path)), { recursive: true });
await writeFile(join(vaultPath, path), content);
}
}
async function removeFixtures(vaultPath: string, files: Record<string, string>) {
for (const path of Object.keys(files)) await rm(join(vaultPath, path), { force: true });
}
async function matchingFiles(vaultPath: string, files: Record<string, string>) {
const matches: string[] = [];
for (const [path, content] of Object.entries(files)) {
try {
if ((await readFile(join(vaultPath, path), "utf8")) === content) matches.push(path);
} catch (error) {
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
}
}
return matches.sort();
}
async function main() {
const binary = requireObsidianBinary();
const cli = discoverObsidianCli();
if (!cli.binary) throw new Error("The Obsidian CLI is unavailable");
const cliBinary = cli.binary;
const output =
process.env.E2E_HIDDEN_FILE_SELECTORS_OUTPUT ?? join(tmpdir(), "livesync-hidden-file-selectors.json");
const observations: Observation[] = [];
const report: Record<string, unknown> = {
revision: execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(),
sourceDiffSha256: createHash("sha256")
.update(new Uint8Array(execFileSync("git", ["diff", "--binary", "HEAD"])))
.digest("hex"),
scenarioSha256: createHash("sha256")
.update(new Uint8Array(await readFile(fileURLToPath(import.meta.url))))
.digest("hex"),
commonlibEntrySha256: createHash("sha256")
.update(
new Uint8Array(
await readFile(
"node_modules/@vrtmrz/livesync-commonlib/dist/managers/EntryManager/EntryManagerImpls.js"
)
)
)
.digest("hex"),
bundleSha256: createHash("sha256")
.update(new Uint8Array(await readFile("main.js")))
.digest("hex"),
commonlibVersion: JSON.parse(await readFile("node_modules/@vrtmrz/livesync-commonlib/package.json", "utf8"))
.version,
observations,
};
const vault = await createTemporaryVault("livesync-hidden-file-selectors-");
let session: ObsidianLiveSyncSession | undefined;
const record = (name: string, actual: unknown, expected: unknown) => {
const observation = { name, expected, actual, passed: isDeepStrictEqual(actual, expected) };
observations.push(observation);
console.log(JSON.stringify(observation));
};
const evaluate = async <T>(code: string): Promise<T> => {
if (!session) throw new Error("The isolated Obsidian session has not started");
return await evalObsidianJson<T>(cliBinary, `(async()=>{${code}})()`, session.cliEnv, 60_000);
};
try {
session = await startObsidianLiveSyncSession({
binary,
cliBinary,
vault,
localStorageEntries: createE2eObsidianDeviceLocalState(vault.name),
pluginData: createE2eCouchDbPluginData(
{ uri: "http://127.0.0.1:9", username: "", password: "", dbName: "selector-local-only" },
{ periodicReplication: false, syncOnFileOpen: false, useOnlyLocalChunk: true }
),
});
report.pluginVersion = session.readiness.pluginVersion;
await waitForLiveSyncCoreReady(cliBinary, session.cliEnv);
report.runtime = await evaluate(`${coreCode}
core.services.setting.setDeviceAndVaultName('selector-local');
Object.assign(core.settings,{
isConfigured:true,usePluginSync:false,usePluginSyncV2:true,syncInternalFiles:false,
autoSweepPlugins:false,autoSweepPluginsPeriodic:false,watchInternalFileChanges:false,
syncOnlyRegEx:${JSON.stringify(allowList)},syncIgnoreRegEx:''
});
return JSON.stringify({userAgent:navigator.userAgent,databaseReady:core.localDatabase.isReady});
`);
await writeFixtures(vault.path, hiddenFiles);
const uploadCode = (normalAllowList: string, files: Record<string, string>) => `${hiddenCode}
Object.assign(core.settings,{
syncInternalFiles:true,syncOnlyRegEx:${JSON.stringify(normalAllowList)},
syncInternalFilesTargetPatterns:'^\\\\.obsidian(?:$|/livesync-selector-(?:example|control)(?:/|$))',syncInternalFilesIgnorePatterns:''
});
const candidates=await addOn.scanInternalFileNames();
const selected=candidates.filter(path=>${JSON.stringify(Object.keys(files))}.includes(path));
if(selected.length!==3)throw new Error('Hidden-file fixture selection failed: '+JSON.stringify({selected,target:core.settings.syncInternalFilesTargetPatterns}));
for(const path of selected)await addOn.updateLastProcessedAsActualFile(path);
await addOn.rebuildFromStorage(false,${JSON.stringify(Object.keys(files))},false);
const paths=[];
for await(const doc of core.localDatabase.findEntries('i:','i;')){
if(${JSON.stringify(Object.keys(files).map((path) => `i:${path}`))}.includes(doc.path)&&!doc.deleted&&!doc._deleted)paths.push(doc.path.substring(2));
}
return JSON.stringify(paths.sort());
`;
const hiddenUpload = await evaluate<string[]>(uploadCode(allowList, hiddenFiles));
record(
"#1272: a normal allow-list permits selected hidden uploads",
hiddenUpload,
Object.keys(hiddenFiles).sort()
);
await writeFixtures(vault.path, controlFiles);
const uploadControl = await evaluate<string[]>(uploadCode("", controlFiles));
record(
"#1272 control: clearing the normal allow-list permits hidden uploads",
uploadControl,
Object.keys(controlFiles).sort()
);
const pullCode = (normalAllowList: string, files: Record<string, string>) => `${hiddenCode}
core.settings.syncOnlyRegEx=${JSON.stringify(normalAllowList)};
const results=[];
for await(const doc of core.localDatabase.findEntries('i:','i;')){
if(${JSON.stringify(Object.keys(files).map((path) => `i:${path}`))}.includes(doc.path)&&!doc.deleted&&!doc._deleted){
results.push(await addOn.extractInternalFileFromDatabase(doc.path.substring(2),true,doc,false));
}
}
return JSON.stringify(results);
`;
await removeFixtures(vault.path, hiddenFiles);
report.hiddenPullWithAllowList = await evaluate(pullCode(allowList, hiddenFiles));
record(
"#1272: a normal allow-list permits selected hidden downloads",
await matchingFiles(vault.path, hiddenFiles),
Object.keys(hiddenFiles).sort()
);
await removeFixtures(vault.path, controlFiles);
report.hiddenPullWithoutAllowList = await evaluate(pullCode("", controlFiles));
record(
"#1272 control: clearing the normal allow-list permits hidden downloads",
await matchingFiles(vault.path, controlFiles),
Object.keys(controlFiles).sort()
);
} catch (error) {
report.runtimeError = error instanceof Error ? error.stack : String(error);
if (session) report.processOutput = JSON.stringify(session.app.output()).slice(-4_000);
throw error;
} finally {
try {
await writeFile(output, JSON.stringify(report, null, 2) + "\n");
console.log(`Verification report: ${output}`);
} finally {
try {
if (session) await session.app.stop();
} finally {
await vault.dispose();
}
}
}
if (observations.some((observation) => !observation.passed)) process.exitCode = 1;
}
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.stack : error);
process.exitCode = 1;
});
+1
View File
@@ -39,6 +39,7 @@ const focusedScenarios = new Set([
"two-vault-sync",
"security-seed-reconnect",
"hidden-file-snippet-sync",
"hidden-file-selectors",
"customisation-sync",
"received-change-readiness",
"internal-metadata-doctor",
+78
View File
@@ -0,0 +1,78 @@
import { randomUUID } from "node:crypto";
import PouchDB from "pouchdb-core";
import MemoryAdapter from "pouchdb-adapter-memory";
import { vi } from "vitest";
import {
DEFAULT_SETTINGS,
type EntryDoc,
type SavingEntry,
type FilePathWithPrefix,
type DocumentID,
} from "@vrtmrz/livesync-commonlib/compat/common/types";
import { createTextBlob } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import { createLiveSyncEventHub } from "@vrtmrz/livesync-commonlib/context";
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "@vrtmrz/livesync-commonlib/compat/pouchdb/LiveSyncLocalDB";
PouchDB.plugin(MemoryAdapter);
export function optionalFileEntry(path: string, content: string): SavingEntry {
return {
_id: path as DocumentID,
path: path as FilePathWithPrefix,
ctime: 1_000,
mtime: 2_000,
size: content.length,
type: "plain",
datatype: "plain",
data: createTextBlob(content),
children: [],
eden: {},
};
}
export async function createOptionalFileSyncDatabase() {
const name = `optional-file-sync-${randomUUID()}`;
const database = new PouchDB<EntryDoc>(name, { adapter: "memory" });
const settings = {
...DEFAULT_SETTINGS,
syncInternalFiles: true,
useOnlyLocalChunk: true,
usePluginSync: true,
usePluginSyncV2: true,
pluginSyncExtendedSetting: {},
};
const services = {
API: { addLog: vi.fn(), getSystemConfigDir: () => ".obsidian" },
setting: { currentSettings: () => settings, getDeviceAndVaultName: () => "device" },
path: {
path2id: async (path: string) => path,
id2path: (id: string, entry?: { path?: string }) => entry?.path ?? id,
getPath: (entry: { path: FilePathWithPrefix }) => entry.path,
},
context: { events: createLiveSyncEventHub() },
database: { createPouchDBInstance: () => database },
databaseEvents: {
onDatabaseInitialisation: async () => true,
onDatabaseHasReady: async () => true,
onCloseDatabase: async () => true,
onUnloadDatabase: async () => true,
},
replicator: { onCloseActiveReplication: async () => true },
};
const localDatabase = new LiveSyncLocalDB(name, { services } as unknown as LiveSyncLocalDBEnv);
if (!(await localDatabase.initializeDatabase())) {
await database.destroy();
throw new Error("The optional-file test database could not initialise");
}
return {
database,
localDatabase,
services,
settings,
async dispose() {
localDatabase.offRemoteChunkFetchedHandler?.();
await localDatabase.managers.teardownManagers();
await database.destroy();
},
};
}
+7
View File
@@ -16,6 +16,13 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
- Unreadable or malformed device-local state now stops initialisation instead of being treated as empty. Existing valid state files keep their format.
### Hidden File Sync
#### Fixed
- We can synchronise selected hidden files while the normal-file allow-list is set. (#1272)
- Normal-file exclusion patterns still apply to hidden files, and the Selector descriptions now explain their scope.
## 1.0.39
20th October, 2026