mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-02 08:47:07 +00:00
fix(cli): floor watch-mode stat timestamps to integer ms
CLIWatchAdapter._toNodeFile passed chokidar's raw stats.ctimeMs/mtimeMs
straight through. On Linux those carry sub-millisecond precision (e.g.
1778511180024.462), so watch-mode file changes wrote non-integer
timestamps into the database.
The earlier floor fix (3f7bb047) covered the scan/stat adapters but missed
this watch path, which is the one the daemon actually uses at runtime.
Mobile clients then crash on such a document because Capacitor's
Filesystem.setTimes casts the value to a Java Long (ClassCastException:
Double cannot be cast to Long).
Floor ctimeMs/mtimeMs here, with a null guard so a partial stat still
falls back to Date.now(). Adds a regression test that a fractional stat is
floored.
Claude-Session: https://claude.ai/code/session_0123E9jVQrsgu3zb82Csuwhi
This commit is contained in:
@@ -116,8 +116,10 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
return {
|
return {
|
||||||
path: path.relative(this.basePath, filePath).replace(/\\/g, "/") as FilePath,
|
path: path.relative(this.basePath, filePath).replace(/\\/g, "/") as FilePath,
|
||||||
stat: {
|
stat: {
|
||||||
ctime: stats?.ctimeMs ?? Date.now(),
|
// Floor to integer milliseconds; Linux fs.Stats.*Ms carry sub-millisecond
|
||||||
mtime: stats?.mtimeMs ?? Date.now(),
|
// precision, and timestamps are stored as integer ms everywhere else.
|
||||||
|
ctime: Math.floor(stats?.ctimeMs ?? Date.now()),
|
||||||
|
mtime: Math.floor(stats?.mtimeMs ?? Date.now()),
|
||||||
size: stats?.size ?? 0,
|
size: stats?.size ?? 0,
|
||||||
type: "file",
|
type: "file",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -84,6 +84,27 @@ describe("CLIStorageEventManagerAdapter", () => {
|
|||||||
expect(created.stat?.size).toBe(42);
|
expect(created.stat?.size).toBe(42);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("floors sub-millisecond stat timestamps so mobile clients do not receive floats", async () => {
|
||||||
|
const basePath = "/vault/base";
|
||||||
|
const adapter = new CLIStorageEventManagerAdapter(basePath, undefined, true);
|
||||||
|
const handlers = makeHandlers();
|
||||||
|
|
||||||
|
await adapter.watch.beginWatch(handlers);
|
||||||
|
|
||||||
|
const addCallback = mockWatcher.on.mock.calls.find(([event]) => event === "add")![1] as (
|
||||||
|
filePath: string,
|
||||||
|
stats: any
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
// Linux fs.Stats carry nanosecond-derived sub-millisecond precision.
|
||||||
|
const floatStats = { ctimeMs: 1778511180024.462, mtimeMs: 1778511180999.913, size: 7 };
|
||||||
|
addCallback(`${basePath}/note.md`, floatStats);
|
||||||
|
|
||||||
|
const created = (handlers.onCreate as ReturnType<typeof vi.fn>).mock.calls[0][0] as NodeFile;
|
||||||
|
expect(created.stat?.ctime).toBe(1778511180024);
|
||||||
|
expect(created.stat?.mtime).toBe(1778511180999);
|
||||||
|
});
|
||||||
|
|
||||||
it("close() calls watcher.close()", async () => {
|
it("close() calls watcher.close()", async () => {
|
||||||
const adapter = new CLIStorageEventManagerAdapter("/base", undefined, true);
|
const adapter = new CLIStorageEventManagerAdapter("/base", undefined, true);
|
||||||
const handlers = makeHandlers();
|
const handlers = makeHandlers();
|
||||||
|
|||||||
Reference in New Issue
Block a user