@vrtmrz/livesync-commonlib 0.1.39 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -247,12 +247,29 @@ async function getDBEntryMetaByPath(host, { localDatabase }, path, opt, includeD
247
247
  const id = await host.services.path.path2id(path);
248
248
  try {
249
249
  let obj = null;
250
- if (opt) {
251
- obj = await localDatabase.get(id, opt);
252
- } else {
253
- obj = await localDatabase.get(id);
250
+ try {
251
+ if (opt) {
252
+ obj = await localDatabase.get(id, opt);
253
+ } else {
254
+ obj = await localDatabase.get(id);
255
+ }
256
+ } catch (ex) {
257
+ if (!includeDeleted || opt?.rev !== void 0 || !isErrorOfMissingDoc(ex)) throw ex;
258
+ for (let attempt = 0; attempt < 3; attempt++) {
259
+ const result = await localDatabase.allDocs({ keys: [id] });
260
+ const row = result.rows[0];
261
+ if (!row || !("value" in row) || !row.value.rev) return false;
262
+ const candidate = await localDatabase.get(id, { ...opt, rev: row.value.rev });
263
+ const current = (await localDatabase.allDocs({ keys: [id] })).rows[0];
264
+ if (!current || !("value" in current) || !current.value.rev) return false;
265
+ if (current.value.rev === candidate._rev) {
266
+ obj = candidate;
267
+ break;
268
+ }
269
+ }
270
+ if (!obj) return false;
254
271
  }
255
- const deleted = obj?.deleted ?? obj._deleted ?? void 0;
272
+ const deleted = obj._deleted || obj.deleted;
256
273
  if (!includeDeleted && deleted) return false;
257
274
  if (obj.type && obj.type == "leaf") {
258
275
  return false;
@@ -20,5 +20,5 @@ export declare const readline: typeof nodeReadlinePromises;
20
20
  export declare const builtinModules: readonly string[];
21
21
  /** Returns whether a module specifier refers to a Node built-in. */
22
22
  export declare const isBuiltin: typeof nodeIsBuiltin;
23
- export { NodeStorageAdapter, createNodeStorage, validateStoragePath, type CreateNodeStorageOptions, type NodeStorageStat, } from "./storage.js";
23
+ export { NodeStorageAdapter, createNodeStorage, getNodeFileCreationTime, validateStoragePath, type CreateNodeStorageOptions, type NodeStorageStat, } from "./storage.js";
24
24
  export { createNodeStandardIo, type CreateNodeStandardIoOptions, type NodeStandardInput, type NodeStandardOutput, } from "./standardIo.js";
@@ -15,6 +15,7 @@ const isBuiltin = nodeIsBuiltin;
15
15
  import {
16
16
  NodeStorageAdapter,
17
17
  createNodeStorage,
18
+ getNodeFileCreationTime,
18
19
  validateStoragePath
19
20
  } from "./storage.js";
20
21
  import {
@@ -28,6 +29,7 @@ export {
28
29
  fileURLToPath,
29
30
  fs,
30
31
  fsPromises,
32
+ getNodeFileCreationTime,
31
33
  isBuiltin,
32
34
  os,
33
35
  path,
@@ -1,6 +1,9 @@
1
+ import type { Stats } from "node:fs";
1
2
  import type { UXDataWriteOptions, UXStat } from "../../common/types.js";
2
3
  import type { IStorageAdapter } from "../../serviceModules/adapters/IStorageAdapter.js";
3
4
  export type NodeStorageStat = UXStat;
5
+ /** Read creation time in integer milliseconds, using change time when birth time is unavailable. */
6
+ export declare function getNodeFileCreationTime(stat: Pick<Stats, "birthtimeMs" | "ctimeMs">): number;
4
7
  export interface CreateNodeStorageOptions {
5
8
  /**
6
9
  * Host-selected root directory. Adapter paths are resolved relative to this root.
@@ -2,6 +2,9 @@ import { constants as nodeFsConstants } from "node:fs";
2
2
  import * as nodeFsPromises from "node:fs/promises";
3
3
  import * as nodePath from "node:path";
4
4
  import { validateStoragePath } from "../storagePath.js";
5
+ function getNodeFileCreationTime(stat) {
6
+ return Math.floor(stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.ctimeMs);
7
+ }
5
8
  const NO_FOLLOW = nodeFsConstants.O_NOFOLLOW ?? 0;
6
9
  class SymbolicLinkStoragePathError extends Error {
7
10
  constructor(path) {
@@ -48,7 +51,7 @@ class NodeStorageAdapter {
48
51
  return {
49
52
  size: stat.size,
50
53
  mtime: Math.floor(stat.mtimeMs),
51
- ctime: Math.floor(stat.ctimeMs),
54
+ ctime: getNodeFileCreationTime(stat),
52
55
  type: stat.isDirectory() ? "folder" : "file"
53
56
  };
54
57
  } catch (error) {
@@ -161,5 +164,6 @@ import { validateStoragePath as validateStoragePath2 } from "../storagePath.js";
161
164
  export {
162
165
  NodeStorageAdapter,
163
166
  createNodeStorage,
167
+ getNodeFileCreationTime,
164
168
  validateStoragePath2 as validateStoragePath
165
169
  };
@@ -18,6 +18,8 @@ A1
18
18
 
19
19
  Resolving a conflict writes the selected or merged result on one branch and deletes every losing live leaf which the resolver has observed. Deleted leaves remain part of the tree until compaction removes their bodies. A stale client can therefore receive the resolved branch and the tombstone for a branch whose old content is still present in its storage.
20
20
 
21
+ Metadata reads which explicitly include deleted entries also return a current PouchDB `_deleted` winner. An ordinary read still excludes deleted entries, and a request for an unavailable specific revision does not fall back to the current winner. A deleted-winner lookup rechecks the winning revision after fetching its body. If it changes, the lookup follows the new winner for at most three attempts and returns no entry if it cannot establish a stable revision. Incoming file reflection uses the current winner and rechecks it before deleting storage, so an older deletion notification does not override a subsequent restoration. Existing conflict and unsynchronised-edit protection still applies.
22
+
21
23
  ## Chunk reachability during conflicts
22
24
 
23
25
  A host which collects unreferenced chunks must include the current winner, every other live conflict leaf, their available divergent revisions, and their nearest available shared ancestor in its reachability scan. This preserves the content and merge base required to review an unresolved conflict. Chunk identifiers are shared across documents, so one reachability set must cover the whole database.
@@ -39,6 +39,8 @@ await storage.write('notes/example.md', '# Example');
39
39
 
40
40
  `rootPath` may be absolute or process-relative. The host should normally resolve and validate configuration before constructing the adapter.
41
41
 
42
+ Node storage reports creation time from a positive `birthtimeMs`, falling back to `ctimeMs` when birth time is unavailable. Timestamps are rounded down to integer milliseconds. Hosts converting native Node stats can use `getNodeFileCreationTime` from the `/node` entry for the same conversion. Some platforms already return change time as birth time, so this fallback cannot guarantee a stable creation time there. Replacing a file can also change its birth time. Write options preserve modification time; the Node adapter does not restore creation time from `ctime`.
43
+
42
44
  The Node adapter rejects symbolic-link components which it observes in an adapter path before an operation. File opens also use `O_NOFOLLOW` for the final component where the platform exposes that flag. These checks protect against invalid paths and symbolic links which already exist when an operation begins. Its concrete `rename(sourcePath, targetPath)` operation validates both paths before providing the atomic rename expected by Node hosts.
43
45
 
44
46
  The host-selected `rootPath` is a trust boundary, not an operating-system filesystem sandbox. The adapter assumes that an untrusted local process cannot replace that root or its descendants concurrently with an operation. A host which grants another local actor write access to the tree must not rely on the adapter as a privilege boundary.
@@ -8,6 +8,8 @@ The code paths below identify the maintained Self-hosted LiveSync composition po
8
8
 
9
9
  ## Obsidian plug-in
10
10
 
11
+ Unreleased deletion tests exercise `deleteMetadataOfDeletedFiles` enabled and disabled with two in-memory PouchDB replicas and injected file storage. They verify deletion and rename reflection, preservation of genuine local edits, and restoration before or during a deleted-winner lookup, including the final revision check before deletion. They also verify that missing provenance or a compacted base preserves local storage, and that continuously advancing deleted revisions do not return stale Metadata. These tests cover the Commonlib file handler and revision lookup, including the latest-revision check before deletion; they do not exercise Obsidian file watchers or a remote CouchDB server.
12
+
11
13
  Unreleased configuration-encryption tests cover changing the wrapper key without changing connection, E2EE, profile, or independent ID values. They also cover custom-key input before loading, failed writes, concurrent saves, cancellation, and unreadable-payload retention. Commonlib owns these service results. LiveSync owns the Apply controls, setting-tab registration, and database startup order; actual Apply and restart behaviour requires its focused real-Obsidian scenario.
12
14
 
13
15
  The CouchDB connection fix bypasses browser HTTP caching so encrypted replication can continue after a remote Rebuild preserves document IDs and revisions. Commonlib's regression tests cover Metadata and Chunks encrypted with a replacement Security Seed while an earlier response remains cached. Self-hosted LiveSync's `couchdb-http-cache` scenario additionally populates the real browser HTTP cache before rebuilding, confirms that the Seed and ciphertext change while the revision remains unchanged, and requires subsequent replication to succeed without clearing that cache. The released LiveSync 1.0.34 fails this scenario; a local checked Commonlib artefact passes in the maintained LiveSync 1.0.34 composition on Linux Obsidian 1.12.7. This establishes the tested build's behaviour, with the reporter's Windows and Obsidian 1.13.7 environment still unverified.
@@ -86,6 +88,8 @@ The CLI extends the same neutral context with its database path and injected `St
86
88
 
87
89
  The CLI filesystem composition receives a configured base directory and constructs the rooted Node storage adapter in `src/apps/cli/adapters/NodeFileSystemAdapter.ts`. Commonlib's shared storage result table checks the focused root contract; the consumer adds its file cache, Vault-shaped operations, case handling, and command policy.
88
90
 
91
+ Unreleased Node timestamp tests verify that `stat` and `trystat` retain an available birth time when change time advances, fall back when birth time is zero, and return integer milliseconds. Packed-package checks exercise the shared `getNodeFileCreationTime` export. With the exact locally packed candidate installed, downstream CLI tests verify that an in-place edit retains creation time in local Metadata. The CLI-to-real-Obsidian scenario additionally verifies the edited content and creation time in received Metadata. These checks do not establish the reporter's ZFS behaviour or physical-file creation times in Obsidian.
92
+
89
93
  The maintained CLI checks include unit tests, Deno-driven CLI E2E, Compose-based compatibility runs, and the CLI-to-real-Obsidian synchronisation scenario in `test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts`. The latter is the strongest current evidence that the CLI and plug-in compose compatible Commonlib behaviour across two hosts.
90
94
 
91
95
  The CLI mirror command returns Commonlib's full-scan aggregate, and daemon start-up stops when that aggregate reports an actual pair failure. Deliberately skipped pairs remain a successful scan result. Commonlib owns the pair classification and aggregate; the CLI owns its command and process policy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrtmrz/livesync-commonlib",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "Shared data, storage, and replication primitives for Self-hosted LiveSync clients.",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",