diff --git a/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts index a9133018..508b64a9 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts @@ -2,6 +2,7 @@ import type { UXDataWriteOptions } from "@lib/common/types"; import type { IStorageAdapter } from "@lib/serviceModules/adapters"; import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase"; import type { Stat, App } from "obsidian"; +import { toIntegerTimestamps } from "./sanitizeWriteOptions"; /** * Storage adapter implementation for Obsidian @@ -40,15 +41,15 @@ export class ObsidianStorageAdapter implements IStorageAdapter { } async write(path: string, data: string, options?: UXDataWriteOptions): Promise { - return await this.app.vault.adapter.write(path, data, options); + return await this.app.vault.adapter.write(path, data, toIntegerTimestamps(options)); } async writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise { - return await this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options); + return await this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), toIntegerTimestamps(options)); } async append(path: string, data: string, options?: UXDataWriteOptions): Promise { - return await this.app.vault.adapter.append(path, data, options); + return await this.app.vault.adapter.append(path, data, toIntegerTimestamps(options)); } list(basePath: string): Promise<{ files: string[]; folders: string[] }> { diff --git a/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts index 42ab566c..6a5c021b 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts @@ -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 { } async modify(file: TFile, data: string, options?: UXDataWriteOptions): Promise { - 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 { - 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 { - 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 { - 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 { diff --git a/src/serviceModules/FileSystemAdapters/sanitizeWriteOptions.ts b/src/serviceModules/FileSystemAdapters/sanitizeWriteOptions.ts new file mode 100644 index 00000000..4cb92355 --- /dev/null +++ b/src/serviceModules/FileSystemAdapters/sanitizeWriteOptions.ts @@ -0,0 +1,25 @@ +import type { UXDataWriteOptions } from "@lib/common/types"; + +/** + * Coerce the timestamp fields of a write-options object to integer milliseconds. + * + * On mobile, Obsidian 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 + * enter the database from any client that stores `fs.Stats.mtimeMs` without + * flooring. Flooring at the storage boundary guarantees every Obsidian write + * carries an integer, so a float timestamp already present in the mesh cannot + * brick the app. + * + * Returns a shallow copy so the caller's options object is not mutated; passes + * `undefined` through unchanged. + */ +export function toIntegerTimestamps(options?: UXDataWriteOptions): UXDataWriteOptions | undefined { + if (!options) return options; + const sanitized: UXDataWriteOptions = { ...options }; + if (typeof sanitized.mtime === "number") sanitized.mtime = Math.floor(sanitized.mtime); + if (typeof sanitized.ctime === "number") sanitized.ctime = Math.floor(sanitized.ctime); + return sanitized; +}