fix(storage): floor write-option timestamps in Obsidian adapters

Obsidian's mobile storage layer forwards mtime/ctime to Capacitor's
Filesystem.setTimes, whose native binding casts the value to a Java Long.
A non-integer (float) timestamp makes that cast throw (ClassCastException:
Double cannot be cast to Long), which crashes the app on launch as soon as
such a document is replicated in.

Float timestamps can reach the database from any client that stores
fs.Stats.mtimeMs without flooring. Coerce mtime/ctime to integer ms at the
Obsidian vault and storage adapter write boundary, so a float already
present in the mesh can't crash the app regardless of where it came from.

The truly central choke point is dbToStorage in livesync-commonlib; a
matching guard there would cover the CLI and webapp too. This change
protects the platform that actually crashes.

Claude-Session: https://claude.ai/code/session_0123E9jVQrsgu3zb82Csuwhi
This commit is contained in:
Andrew Leech
2026-07-22 14:08:21 +10:00
parent 4393a49cba
commit a3a09df3c8
3 changed files with 34 additions and 7 deletions
@@ -2,6 +2,7 @@ import type { UXDataWriteOptions } from "@lib/common/types";
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase";
import type { TFile, App, TFolder } from "obsidian";
import { toIntegerTimestamps } from "./sanitizeWriteOptions";
/**
* Vault adapter implementation for Obsidian
@@ -22,19 +23,19 @@ export class ObsidianVaultAdapter implements IVaultAdapter<TFile, TFolder> {
}
async modify(file: TFile, data: string, options?: UXDataWriteOptions): Promise<void> {
return await this.app.vault.modify(file, data, options);
return await this.app.vault.modify(file, data, toIntegerTimestamps(options));
}
async modifyBinary(file: TFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
return await this.app.vault.modifyBinary(file, toArrayBuffer(data), options);
return await this.app.vault.modifyBinary(file, toArrayBuffer(data), toIntegerTimestamps(options));
}
async create(path: string, data: string, options?: UXDataWriteOptions): Promise<TFile> {
return await this.app.vault.create(path, data, options);
return await this.app.vault.create(path, data, toIntegerTimestamps(options));
}
async createBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<TFile> {
return await this.app.vault.createBinary(path, toArrayBuffer(data), options);
return await this.app.vault.createBinary(path, toArrayBuffer(data), toIntegerTimestamps(options));
}
async delete(file: TFile | TFolder, force = false): Promise<void> {