@vrtmrz/livesync-commonlib 0.2.3 → 0.3.1

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.
Files changed (91) hide show
  1. package/README.md +46 -8
  2. package/dist/API/DirectFileManipulatorV2.d.ts +8 -1
  3. package/dist/API/DirectFileManipulatorV2.js +52 -79
  4. package/dist/API/DirectFileWatchOwner.d.ts +24 -0
  5. package/dist/API/DirectFileWatchOwner.js +256 -0
  6. package/dist/API/processSetting.d.ts +1 -0
  7. package/dist/API/processSetting.js +3 -3
  8. package/dist/common/models/setting.const.defaults.js +1 -0
  9. package/dist/common/models/setting.const.qr.js +1 -0
  10. package/dist/common/models/setting.policy.d.ts +4 -0
  11. package/dist/common/models/setting.policy.js +1 -0
  12. package/dist/common/models/setting.type.d.ts +3 -1
  13. package/dist/common/models/shared.definition.configNames.js +1 -2
  14. package/dist/common/models/tweak.definition.d.ts +2 -1
  15. package/dist/common/pathNamespace.d.ts +4 -0
  16. package/dist/common/pathNamespace.js +10 -0
  17. package/dist/common/pathNormalization.d.ts +2 -0
  18. package/dist/common/pathNormalization.js +7 -2
  19. package/dist/common/settingConstants.d.ts +1 -0
  20. package/dist/common/settingConstants.js +4 -0
  21. package/dist/managers/ChunkFetcher.d.ts +3 -0
  22. package/dist/managers/ChunkFetcher.js +20 -13
  23. package/dist/managers/LiveSyncManagers.d.ts +2 -0
  24. package/dist/managers/LiveSyncManagers.js +1 -0
  25. package/dist/managers/chunkFetchSource.d.ts +14 -0
  26. package/dist/managers/chunkFetchSource.js +12 -0
  27. package/dist/platform/AmbiguousNormalisedPathError.d.ts +16 -0
  28. package/dist/platform/AmbiguousNormalisedPathError.js +21 -0
  29. package/dist/platform/StoragePathResolver.d.ts +15 -0
  30. package/dist/platform/StoragePathResolver.js +113 -0
  31. package/dist/platform/browser/UnicodeResolvingFileSystemAccessStorageAdapter.d.ts +74 -0
  32. package/dist/platform/browser/UnicodeResolvingFileSystemAccessStorageAdapter.js +169 -0
  33. package/dist/platform/browser/index.d.ts +1 -0
  34. package/dist/platform/browser/index.js +2 -0
  35. package/dist/platform/browser/storage.d.ts +23 -0
  36. package/dist/platform/browser/storage.js +138 -0
  37. package/dist/platform/node/UnicodeResolvingNodeStorageAdapter.d.ts +79 -0
  38. package/dist/platform/node/UnicodeResolvingNodeStorageAdapter.js +218 -0
  39. package/dist/platform/node/index.d.ts +1 -0
  40. package/dist/platform/node/index.js +2 -0
  41. package/dist/platform/node/storage.d.ts +30 -1
  42. package/dist/platform/node/storage.js +88 -0
  43. package/dist/platform/pathComparison.d.ts +53 -0
  44. package/dist/platform/pathComparison.js +39 -0
  45. package/dist/platform/pathTypes.d.ts +154 -0
  46. package/dist/platform/pathTypes.js +0 -0
  47. package/dist/platform/storagePathResolution.d.ts +13 -0
  48. package/dist/platform/storagePathResolution.js +9 -0
  49. package/dist/pouchdb/LiveSyncDBFunctions.js +5 -0
  50. package/dist/pouchdb/LiveSyncLocalDB.d.ts +7 -1
  51. package/dist/pouchdb/LiveSyncLocalDB.js +13 -10
  52. package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
  53. package/dist/pouchdb/pathIdentityCompatibility.js +19 -1
  54. package/dist/replication/RemoteResource.d.ts +3 -0
  55. package/dist/replication/couchdb/LiveSyncReplicator.js +5 -3
  56. package/dist/replication/index.d.ts +2 -0
  57. package/dist/replication/index.js +2 -0
  58. package/dist/replication/journal/JournalSyncCore.d.ts +1 -0
  59. package/dist/replication/journal/JournalSyncCore.js +35 -19
  60. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +11 -3
  61. package/dist/replication/journal/LiveSyncJournalReplicator.js +123 -45
  62. package/dist/replication/trystero/P2PRoomSessionOwner.js +14 -13
  63. package/dist/replication/trystero/TrysteroReplicator.d.ts +9 -5
  64. package/dist/replication/trystero/TrysteroReplicator.js +98 -70
  65. package/dist/replication/trystero/TrysteroReplicator.pathIdentity.d.ts +11 -0
  66. package/dist/replication/trystero/TrysteroReplicator.pathIdentity.js +72 -0
  67. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +2 -0
  68. package/dist/replication/trystero/useP2PReplicatorFeature.js +6 -2
  69. package/dist/serviceModules/Rebuilder.d.ts +1 -0
  70. package/dist/serviceModules/Rebuilder.js +35 -6
  71. package/dist/services/base/FileProcessingService.d.ts +2 -2
  72. package/dist/services/base/RemoteConnection.d.ts +2 -0
  73. package/dist/services/base/RemoteService.d.ts +1 -1
  74. package/dist/services/base/RemoteService.js +13 -5
  75. package/dist/services/implements/injectable/InjectableVaultService.d.ts +1 -1
  76. package/dist/services/lib/couchdbHttpCache.d.ts +5 -0
  77. package/dist/services/lib/couchdbHttpCache.js +36 -0
  78. package/dist/storage.d.ts +4 -0
  79. package/dist/storage.js +19 -0
  80. package/dist/string_and_binary/path.js +4 -7
  81. package/dist/worker/bgWorker.js +1 -1
  82. package/docs/adr/2026_11_shared_storage_path_resolution.md +73 -0
  83. package/docs/conflict-resolution.md +1 -1
  84. package/docs/design_docs/storage_path_resolution.md +305 -0
  85. package/docs/development.md +15 -1
  86. package/docs/platform-storage.md +61 -0
  87. package/docs/proven-in-use.md +30 -13
  88. package/docs/releasing.md +27 -0
  89. package/docs/service-feature-composition.md +7 -1
  90. package/docs/unicode-path-identity.md +104 -12
  91. package/package.json +10 -2
@@ -0,0 +1,73 @@
1
+ ---
2
+ date: 2026-11-03
3
+ commonlib-version: "0.3.0"
4
+ self-hosted-livesync-version: "1.0.44"
5
+ status: accepted-unreleased
6
+ ---
7
+
8
+ # Architectural Decision Record: Shared Storage Path Resolution
9
+
10
+ ## Conclusion
11
+
12
+ Commonlib owns strict Unicode path comparison and resolution behind a focused
13
+ `/storage` entry. Node and File System Access provide thin storage integrations;
14
+ hosts retain filesystem handles, original paths, caches, watchers, and operation
15
+ lifecycle. The [design](../design_docs/storage_path_resolution.md) defines the
16
+ accepted contract and remaining adoption work.
17
+
18
+ Accepted on 2026-11-03 against the versions recorded above. The implementation
19
+ is unreleased. Publication does not by itself enable NFC in WebApp or Bridge.
20
+
21
+ ## Context
22
+
23
+ The Self-hosted LiveSync CLI already resolves NFC-equivalent names component by
24
+ component while preserving physical spelling. Repeating that logic in browser
25
+ and Deno hosts would duplicate ambiguity, missing-path, and rename rules.
26
+ Meanwhile, the legacy path union includes comparison keys, so removing a
27
+ namespace can appear to turn a lossy comparison value back into a file path.
28
+
29
+ ## Decisions
30
+
31
+ - Separate original document paths, unprefixed file paths, NFC refinements,
32
+ resolved storage paths, and comparison keys. Preserve the old public types;
33
+ new namespace helpers exclude their legacy comparison-key member.
34
+ - Inject only strict directory enumeration into the resolver. Return `null`
35
+ for absence, and propagate permission, non-directory, and other I/O failures.
36
+ Resolve ambiguity before mutation, including when an exact spelling exists.
37
+ - Copy comparison settings for one operation or scan. Retain no cross-operation
38
+ directory cache or persistent path mapping. Scan already resolved entries
39
+ once per directory, and re-resolve later lookups after external changes.
40
+ - Preserve ordinary writes' physical spelling. An explicit rename may request
41
+ another spelling of the same leaf. Mutation and replacement semantics remain
42
+ with the storage owner; resolution is not an atomic reservation.
43
+ - Keep string-taking storage APIs and Legacy behaviour. New physical-I/O ports
44
+ take `StoragePath`. Namespace routing and cryptographic document-ID derivation
45
+ remain separate from filesystem resolution.
46
+ - Adopt the shared implementation in the CLI and the shared comparison and
47
+ namespace helpers at applicable Obsidian boundaries. WebApp and Bridge NFC
48
+ enablement remains separate work, including admission, events, and caches.
49
+
50
+ ## Alternatives considered
51
+
52
+ - Duplicating the CLI resolver per host would duplicate the behavioural contract
53
+ and its failure handling.
54
+ - Renaming all physical files to NFC would alter user-owned storage and require
55
+ collision recovery beyond the accepted scope.
56
+ - A persistent alias map or shared directory cache would introduce invalidation,
57
+ restart, and lifecycle obligations which current directory reads avoid.
58
+ - Combining namespace, Unicode form, and case in a family of filename types
59
+ would conflate independent properties. Comparison keys instead name their
60
+ purpose and their storage or document domain.
61
+
62
+ ## Consequences and verification
63
+
64
+ Hosts supply trustworthy complete listings and preserve distinct names before
65
+ constructing keyed collections. Filesystems or APIs which hide aliases limit
66
+ what the resolver can observe. Host concurrency and permission checks remain
67
+ necessary after resolution.
68
+
69
+ Contract tests cover spelling, ambiguity, strict failures, snapshots, scans,
70
+ and renames. Packed-consumer tests check assignability and platform bundling.
71
+ The downstream CLI retains its behavioural suite against the exact packed
72
+ candidate. Memory-backed File System Access tests establish adapter behaviour;
73
+ real-browser permissions and Deno integration require their own host evidence.
@@ -74,7 +74,7 @@ A host must not apply a dialogue result after either compared revision has cease
74
74
 
75
75
  ## Scheduling conflict checks
76
76
 
77
- The unreleased `@vrtmrz/livesync-commonlib/conflicts` entry exports
77
+ The `@vrtmrz/livesync-commonlib/conflicts` entry, published in Commonlib 0.2.3, exports
78
78
  `createConflictCheckQueue`. The host supplies a `process(path, scope)` callback
79
79
  and may observe pending plus active checks through `onProgress`. Concurrency
80
80
  defaults to ten and includes each item's deferred cleanup. The host continues
@@ -0,0 +1,305 @@
1
+ ---
2
+ date: 2026-11-03
3
+ commonlib-version: "0.3.0"
4
+ self-hosted-livesync-version: "1.0.44"
5
+ status: accepted-unreleased
6
+ ---
7
+
8
+ # Shared storage path resolution
9
+
10
+ This design moves the CLI's existing Unicode-aware path resolution into
11
+ Commonlib so Node, File System Access, and Bridge can apply the same naming
12
+ rules. The shared resolver, path types, and Node and File System Access
13
+ integrations are implemented as unreleased additions. Installing the package
14
+ does not enable NFC in an existing host or migrate its database.
15
+
16
+ Accepted on 2026-11-03. The [decision record](../adr/2026_11_shared_storage_path_resolution.md)
17
+ records the ownership and compatibility choices. The [platform guide](../platform-storage.md#unicode-path-resolution)
18
+ provides consumer examples.
19
+
20
+ The baseline is Commonlib 0.3.0 and the CLI implementation reviewed in
21
+ [Self-hosted LiveSync #1309](https://github.com/vrtmrz/obsidian-livesync/pull/1309)
22
+ at `2716d68b3d21d493cdfdefa0857fad4c6a3c522a`. The existing
23
+ [Unicode identity contract](../unicode-path-identity.md) continues to govern
24
+ Metadata IDs, remote admission, defaults, and database migration.
25
+
26
+ ## Ownership
27
+
28
+ Commonlib owns path comparison, component-by-component resolution, ambiguity
29
+ checks, and thin platform-storage integrations. Hosts own root selection,
30
+ permissions, filesystem handles, file objects, watchers, caches, and operation
31
+ lifecycle. A resolver performs directory reads but no filesystem mutation.
32
+
33
+ Keep the existing Node and File System Access storage APIs and their Legacy
34
+ behaviour. Compose the resolver only for NFC mode. Its comparison policy is
35
+ an immutable snapshot for one operation or scan, copied at construction;
36
+ a settings change requires a new composition. Explicit case-sensitive and
37
+ case-insensitive lookups use
38
+ the corresponding snapshot, without mutating a shared resolver.
39
+
40
+ ## Path types
41
+
42
+ Separate namespace, Unicode form, comparison use, and storage resolution.
43
+ An actual filename can already be NFC, so these are not mutually exclusive
44
+ string categories.
45
+
46
+ | Type | Meaning | Producer |
47
+ | --- | --- | --- |
48
+ | `FilePath` | Original spelling without a Metadata namespace; no NFC or resolution guarantee. | Existing input and host boundaries. |
49
+ | `DocumentPath` | Original spelling with an optional recognised Metadata namespace; excludes comparison keys. | Document input and namespace helpers. |
50
+ | `NfcFilePath` | A `FilePath` normalised to NFC, without lower-casing. | The shared NFC normalisation function. |
51
+ | `StoragePath` | A root-relative storage target whose existing components use their resolved spelling. | Resolver or supported physical enumeration. |
52
+ | `PathComparisonKey<"storage">` | A comparison key for a path within one storage root. | The shared comparison helper. |
53
+ | `PathComparisonKey<"document">` | A comparison key which retains the document namespace. | The shared comparison helper. |
54
+
55
+ `StoragePath` is not proof that an entry exists, still exists, is a file,
56
+ or cannot change concurrently. A write target may contain missing components.
57
+ The root is the empty path; each operation retains its existing root rules.
58
+
59
+ Reuse the existing `FilePath` and OW `TaggedType` definitions. Add independent
60
+ symbols for refinements: nesting `TaggedType<FilePath, "StoragePath">` would
61
+ intersect incompatible values of OW's existing tag property.
62
+
63
+ ```ts
64
+ declare const unicodeForm: unique symbol;
65
+ declare const storagePath: unique symbol;
66
+
67
+ type DocumentPath = Exclude<FilePathWithPrefix, FilePathWithPrefixLC>;
68
+
69
+ type NfcFilePath = FilePath & {
70
+ readonly [unicodeForm]: "NFC";
71
+ };
72
+
73
+ type StoragePath = FilePath & {
74
+ readonly [storagePath]: true;
75
+ };
76
+
77
+ type PathComparisonKey<Domain extends "storage" | "document"> =
78
+ TaggedType<string, `PathComparisonKey:${Domain}`>;
79
+ ```
80
+
81
+ Use `NfcFilePath` within the normalisation boundary initially. Publish the
82
+ path and key types needed by consumers of the new focused API. Preserve the
83
+ existing definitions of `FilePathWithPrefix` and `FilePathWithPrefixLC` at
84
+ compatibility entry points. Do not alias either old type to a new key type:
85
+ that would change existing assignability.
86
+
87
+ The old `FilePathWithPrefix` union includes both `FilePath` and
88
+ `FilePathWithPrefixLC`. The new `DocumentPath` intentionally removes the latter.
89
+ The old scanner's `convertCase()` returns its `LC` type even when case-sensitive
90
+ handling retains letter case. New code therefore names its output by its
91
+ comparison purpose, not an assumed lower-case form.
92
+
93
+ Brand values at the boundary which establishes their meaning. `parseFilePath`
94
+ validates raw storage input using the existing path vocabulary without changing
95
+ its spelling. `parseDocumentPath` accepts original document-path strings;
96
+ namespace routing precedes storage validation. Neither input helper recovers
97
+ original spelling from a comparison key. NFC conversion and resolution establish their own refinements.
98
+ Keep casts inside those owning implementations; consumer code receives the
99
+ typed result. The existing string-taking APIs remain compatibility escape
100
+ routes, so this migration does not claim to make all old call sites type-safe.
101
+ Within the new integrations, physical-I/O ports accept `StoragePath`; NFC
102
+ normalisation or key generation alone cannot supply that argument.
103
+
104
+ ## Namespaces and comparison
105
+
106
+ Namespace helpers accept `DocumentPath`, return a separate namespace and
107
+ `FilePath`, and recognise the existing `i:`, `ix:`, and `ps:` rules. Split one
108
+ namespace only; further colons belong to the body. The caller handles the
109
+ namespace's meaning before submitting a filesystem path to the resolver.
110
+ Chunk prefixes and obfuscated IDs remain `DocumentID` concerns.
111
+
112
+ Document comparison preserves the namespace; storage comparison does not
113
+ receive one. A normal document and an internal document with the same body
114
+ remain distinct document identities. Neither comparison-key domain is accepted
115
+ by the resolver or by the new namespace helpers.
116
+
117
+ The comparison policy uses the existing `normalisePathForIdentity` operation,
118
+ then the existing `toLowerCase()` rule only when configured. NFC conversion
119
+ alone produces `NfcFilePath`; the complete comparison pipeline produces a key.
120
+ It does not infer the host filesystem's case rules or introduce a new Unicode
121
+ case-folding rule. Preserve original paths alongside keys; a key is never
122
+ converted back into an original path.
123
+
124
+ ```ts
125
+ type PathComparisonSettings = Readonly<{
126
+ pathNormalizationVersion: 0 | 1;
127
+ handleFilenameCaseSensitive: boolean;
128
+ }>;
129
+
130
+ type DocumentPathParts = Readonly<{
131
+ prefix: "" | "i:" | "ix:" | "ps:";
132
+ path: FilePath;
133
+ }>;
134
+
135
+ declare function splitDocumentPath(path: DocumentPath): DocumentPathParts;
136
+ declare function joinDocumentPath(parts: DocumentPathParts): DocumentPath;
137
+
138
+ declare function getStoragePathComparisonKey(
139
+ path: FilePath, settings: PathComparisonSettings
140
+ ): PathComparisonKey<"storage">;
141
+
142
+ declare function getDocumentPathComparisonKey(
143
+ path: DocumentPath, settings: PathComparisonSettings
144
+ ): PathComparisonKey<"document">;
145
+ ```
146
+
147
+ Namespace routing and cryptographic document-ID derivation remain outside the
148
+ resolver. The new comparison helpers must retain the existing key bytes for
149
+ each supported policy; extracting them does not rewrite IDs or stored paths.
150
+
151
+ ## Directory and resolver contract
152
+
153
+ The only injected I/O operation enumerates immediate children using their
154
+ actual names and kinds. Paths are relative to the selected root and use `/`.
155
+ `null` means the directory is absent. Permission failures, a non-directory
156
+ parent, and other I/O failures reject; they are not empty listings.
157
+
158
+ The existing storage `list()` methods can hide some failures as empty results.
159
+ Provide a strict enumeration path for the resolver while preserving the old
160
+ methods' published behaviour. Reuse each platform's containment and symbolic-link
161
+ checks. Keep native handles and platform stat types behind this boundary.
162
+
163
+ ```ts
164
+ type DirectoryEntry = Readonly<{
165
+ name: string;
166
+ kind: "file" | "directory";
167
+ }>;
168
+
169
+ type ReadDirectory = (
170
+ physicalDirectory: StoragePath
171
+ ) => Promise<readonly DirectoryEntry[] | null>;
172
+
173
+ type StorageEntry = Readonly<{
174
+ path: StoragePath;
175
+ kind: "file" | "directory";
176
+ }>;
177
+
178
+ type ResolvedRename = Readonly<{
179
+ source: StoragePath;
180
+ target: StoragePath;
181
+ }>;
182
+
183
+ interface StoragePathResolver {
184
+ resolveExisting(path: FilePath): Promise<StoragePath | null>;
185
+ resolveForWrite(path: FilePath): Promise<StoragePath>;
186
+ resolveRename(source: FilePath, target: FilePath): Promise<ResolvedRename | null>;
187
+ listResolvedDirectory(path: StoragePath): Promise<readonly StorageEntry[] | null>;
188
+ }
189
+
190
+ declare function createStoragePathResolver(options: Readonly<{
191
+ readDirectory: ReadDirectory;
192
+ policy: Readonly<{
193
+ pathNormalizationVersion: 1;
194
+ handleFilenameCaseSensitive: boolean;
195
+ }>;
196
+ }>): StoragePathResolver;
197
+ ```
198
+
199
+ `DirectoryEntry.name` is an immediate child name, never a path. Reject malformed
200
+ entries at the enumeration boundary. Add a separate filename brand only when a
201
+ consumer boundary needs it; do not publish a family of filename variants now.
202
+
203
+ The resolver uses complete sibling enumeration before deciding uniqueness;
204
+ a literal match does not override a second equivalent name. File/directory
205
+ collisions count as ambiguity. A unique file cannot be traversed as a parent.
206
+ An individual lookup checks candidates for each requested component; a full
207
+ scan checks every sibling group before returning that directory's entries.
208
+
209
+ | Operation | Result |
210
+ | --- | --- |
211
+ | `resolveExisting` | Resolve each existing component to its listed spelling; return `null` on absence. |
212
+ | `resolveForWrite` | Preserve listed names for existing components and the requested spelling for the missing suffix. |
213
+ | `resolveRename` | Resolve both ends. If the target resolves to the source, retain the resolved parent and the explicitly requested leaf spelling. Return `null` when the source is absent. |
214
+ | `listResolvedDirectory` | Read an already resolved directory, check all sibling identities, and return actual child paths for scanning and direct access. |
215
+
216
+ All operations reject ambiguity using the shared `AmbiguousNormalisedPathError`.
217
+ Include the requested path and conflicting actual names for diagnosis. Preserve
218
+ I/O failures separately from absence. Host catch blocks must propagate these
219
+ failures instead of converting them to a missing file or a successful partial
220
+ scan.
221
+
222
+ Normal writes preserve existing Unicode spelling. Explicit renames may change
223
+ it. Existing-destination replacement policy and the actual rename, write,
224
+ rollback, or deletion remain the storage owner's responsibility. Resolution is
225
+ not an atomic reservation; existing host operation ordering and platform
226
+ protections remain necessary. The guarantee is limited to names exposed by the
227
+ host's supported listing.
228
+
229
+ ## Composition and adoption
230
+
231
+ The `/storage` entry contains the host-neutral types, comparison and
232
+ namespace helpers, and resolver. It imports no Node, DOM, database, or Service
233
+ Hub runtime. `/node` and `/browser` expose their platform integrations through
234
+ the existing bundling boundaries.
235
+
236
+ Commonlib owns `UnicodeResolvingNodeStorageAdapter` and delegates
237
+ its resolution to the common implementation. The current CLI candidate adopts
238
+ the focused API while retaining Legacy delegation and thin compatibility
239
+ methods. File System Access uses the same resolver, including host paths which
240
+ operate directly on handles. Bridge supplies a Deno directory reader and uses
241
+ the same contract in an unreleased integration candidate.
242
+
243
+ Keep the resolver free of persistent mappings, cross-operation caches, watchers,
244
+ retry owners, and disposal requirements. A full scan descends through the
245
+ resolved entries and reads each directory once. A later lookup re-reads the
246
+ relevant directories, including after a file-object cache hit. OW Scope or
247
+ context can own host operation resources where needed; the resolver itself
248
+ requires no resource scope.
249
+
250
+ Hosts retain original physical paths alongside any comparison keys. Detect
251
+ aliases before collapsing them into a keyed collection. Cache invalidation,
252
+ queue ownership, and watch-event reconciliation must follow the same comparison
253
+ policy without losing distinct names. A deletion event may name an entry which
254
+ is already absent; preserve that event path rather than trying to recover its
255
+ spelling from a live directory lookup. Check rename events as an ordered change
256
+ of state, including hosts which report them as deletion and creation.
257
+
258
+ ## Current adoption and publication status
259
+
260
+ The shared `/storage` contract, Node and File System Access adapters, and
261
+ explicit `DirectFileManipulator` path-version option are unreleased additions
262
+ after the published Commonlib `0.3.0` artefact. They require a new immutable
263
+ package version; a source checkout with version string `0.3.0` is not the
264
+ registry artefact.
265
+
266
+ The CLI candidate uses the shared Node resolver, and the current Obsidian
267
+ candidate uses shared namespace and comparison helpers while retaining Vault
268
+ APIs and its alias guard. The matching Journal and P2P host adoption remains
269
+ unreleased in Self-hosted LiveSync [#1309](https://github.com/vrtmrz/obsidian-livesync/pull/1309).
270
+ The browser resolver is a general primitive, not WebApp NFC support: WebApp
271
+ remains Legacy-only and rejects an explicit version `1` at startup. WebPeer
272
+ has no Vault filesystem and is not an NFC filesystem host.
273
+
274
+ Bridge's Deno integration is also an unreleased candidate. NFC mode is an
275
+ explicit per-peer opt-in, and every peer in a group must use the same path
276
+ version. Its ordinary case-mismatch check remains enabled; the Bridge-wide
277
+ `disableCheckingConfigMismatch: true` override disables that case check only
278
+ and does not bypass path-version admission. Preserve the existing coordinated
279
+ Rebuild/Fetch migration and remote mismatch protection. This design adds no
280
+ automatic migration or translation between identity modes.
281
+
282
+ ## Verification
283
+
284
+ Check the public declarations against the actual published path types and OW
285
+ tag implementation. Positive cases cover source paths, namespace round trips,
286
+ and independent NFC/storage refinements. Negative cases cover raw strings,
287
+ prefix-bearing paths at the resolver, comparison keys at resolution and namespace
288
+ boundaries, and mixing the two key domains. Retain an assertion that the legacy
289
+ `LC` type is still assignable to the legacy union, but not to `DocumentPath`.
290
+
291
+ Implementation must preserve the existing CLI behavioural cases and apply one
292
+ resolver contract to Node and File System Access, with Bridge-specific checks:
293
+
294
+ - NFD files and mixed-spelling parents remain addressable without implicit renames.
295
+ - New components retain the requested spelling under the actual parent.
296
+ - File, directory, and case collisions reject before mutation, including exact-match requests.
297
+ - Permission and listing failures do not become absence, creation, deletion, or partial-scan success.
298
+ - Explicit spelling renames preserve contents and the host's replacement and recovery semantics.
299
+ - External additions, deletions, renames, and collisions are observed after earlier cache hits.
300
+ - Full scans inspect each directory once; Legacy behaviour and namespace separation remain unchanged.
301
+ - Watcher deletion/creation sequences, restart state, and cache keys preserve the intended identity.
302
+
303
+ Packed-package checks must verify the focused entry and platform bundling
304
+ boundaries. Real browser and Deno checks establish their host behaviour;
305
+ Commonlib's memory and Node tests alone do not establish those guarantees.
@@ -6,7 +6,7 @@ This document is for Commonlib developers. Package consumers should begin with t
6
6
 
7
7
  The source tree is larger than the supported package surface. Consumers may import only paths in the generated package export map:
8
8
 
9
- - `src/index.ts`, `src/context.ts`, `src/settings.ts`, `src/remoteConfigurations.ts`, `src/p2p/index.ts`, `src/platform/browser/index.ts`, and `src/platform/node/index.ts` define the focused entries;
9
+ - `src/index.ts`, `src/context.ts`, `src/storage.ts`, `src/settings.ts`, `src/remoteConfigurations.ts`, `src/p2p/index.ts`, `src/platform/browser/index.ts`, and `src/platform/node/index.ts` define the focused entries;
10
10
  - `src/conflicts.ts` defines the host-owned conflict-check queue contract; `src/concurrency/keyedPendingWork.ts` remains package-private;
11
11
  - `src/replication/index.ts` defines the focused provider-capability and interaction contract for hosts which compose replication;
12
12
  - `src/diagnostics/index.ts` defines read-only Usage and integrity inspection, report projection, and formatting; hosts own the run dialogue and output destination;
@@ -19,6 +19,16 @@ The source tree is larger than the supported package surface. Consumers may impo
19
19
 
20
20
  Files elsewhere under `src` are implementation details unless a focused entry re-exports them or the compatibility inventory names them. Do not use the source layout, TypeScript path aliases, or the presence of generated declarations as evidence of a supported consumer import.
21
21
 
22
+ ## Storage path boundary
23
+
24
+ The unreleased `/storage` entry owns namespace helpers, comparison keys, and
25
+ strict component resolution. The [design](design_docs/storage_path_resolution.md)
26
+ and [decision record](adr/2026_11_shared_storage_path_resolution.md) separate
27
+ that contract from host handles, caches, watchers, and migration. See the
28
+ [platform guide](platform-storage.md#unicode-path-resolution) for composition.
29
+ The [Unicode identity contract](unicode-path-identity.md) retains its existing
30
+ Metadata, remote-admission, and database-migration rules.
31
+
22
32
  ## Module filenames
23
33
 
24
34
  For new or renamed TypeScript and JavaScript source modules, start the filename
@@ -111,6 +121,10 @@ The revision-tree safety rules are documented in [Conflict resolution and file p
111
121
 
112
122
  `LiveSyncReplicator.unit.spec.ts` owns the CouchDB remote-chunk batch contract: a mixed `_all_docs` result must return each available document even when another keyed row reports an error. `ChunkFetcher.unit.spec.ts` owns the consumer behaviour: it writes valid returned chunks and emits `missingChunkRemote` only for requested IDs which are absent from the returned set. Extend both sides when changing remote chunk acquisition or missing-chunk delivery.
113
123
 
124
+ Direct CouchDB hosts supply a database-bound `ChunkFetchSource` through the optional `LiveSyncLocalDB` runtime factory. `LiveSyncManagers` passes it to the shared `ChunkFetcher`; application hosts omit it and retain active-replicator retrieval and local persistence. The direct source reads the prepared database and accepts results without writing them back. Recreate it with each database instance so reopening cannot retain a closed connection. `ChunkFetcher.source.unit.spec.ts` covers direct retrieval, delayed availability without change notifications, bounded failure, and teardown. `DirectFileManipulatorV2.integration.spec.ts` covers encrypted arrival before and during an outstanding read, with assertions that the reader does not write Chunks back.
125
+
126
+ `DirectFileWatchOwner` owns the direct watch lifecycle. Its per-client state retains unfinished reads, same-document delivery ownership, and pending checkpoint persistence across stop and restart. Each watch session separately owns its pending documents, connection generation, and timers. Named operations receive explicit contexts through Octagonal Wheels `bindContext`; per-file and per-delivery scopes release their bookkeeping after the operation settles. Feed cancellation and timer cancellation remain synchronous in `endWatch()`, independently of unfinished reads or host callbacks. The owner tests cover retries, safe checkpoint positions, replacement sessions, and failure handling; `DirectFileManipulatorV2.unit.spec.ts` retains the public state, database binding, and close-order checks.
127
+
114
128
  `RemoteService.connect()` returns an owned CouchDB connection as the existing flat `{ db, info }` result with an idempotent `close()` method. `close()` first aborts outstanding abort-capable requests in that connection, then closes the PouchDB handle. The owner must transfer or close the complete connection; passing only `db` loses the cancellation boundary. A skipped information request retains the established placeholder in `info` for compatibility. `OwnedCouchDBConnection`, its opening options, and the checked `CouchDBReplicationConnection` are Commonlib-owned types exported by the existing CouchDB replicator module. The legacy `_ensureConnection()` helper continues to return a raw PouchDB handle for source compatibility; new Commonlib code must retain and close the complete owned connection instead.
115
129
 
116
130
  The one-shot CouchDB connectivity preflight is the first bounded consumer. It applies an internal 60-second safety fuse to the complete preflight on the web-compatible fetch path, disables fallback into the non-abortable native request adapter, and clears the fuse before transferring the connection to replication. The fuse is a last-resort wall-clock bound on an owner which would otherwise remain pending indefinitely; it is not the expected completion time, a per-request inactivity timeout, a user setting, or the `useTimeouts` changes-feed setting. The explicitly selected native Request API retains its previous unbounded behaviour because its current host adapter cannot honour `AbortSignal`. Extend `RemoteService.unit.spec.ts` and the one-shot ownership table in `LiveSyncReplicator.unit.spec.ts` when changing this boundary. Do not use `Promise.race()` to report a mutating native request as cancelled while it can still complete remotely.
@@ -61,6 +61,67 @@ Writes create missing parent directories. Listing returns direct child paths rel
61
61
 
62
62
  The File System Access API does not expose portable creation times or permit setting file times. Consumers must not rely on identical timestamp fidelity across the two adapters.
63
63
 
64
+ ## Unicode path resolution
65
+
66
+ The following APIs are unreleased additions after Commonlib 0.3.0. Node and
67
+ File System Access hosts can use the same NFC comparison and component
68
+ resolution while retaining the names listed by their filesystem. Hosts still
69
+ own the selected identity mode and its database migration.
70
+
71
+ ```ts
72
+ import { UnicodeResolvingNodeStorageAdapter } from '@vrtmrz/livesync-commonlib/node';
73
+ import { parseFilePath } from '@vrtmrz/livesync-commonlib/storage';
74
+
75
+ const storage = new UnicodeResolvingNodeStorageAdapter(
76
+ '/srv/livesync',
77
+ () => settings.pathNormalizationVersion,
78
+ () => !settings.handleFilenameCaseSensitive,
79
+ );
80
+ const existing = await storage.resolveLogicalPath(parseFilePath('Café.md'), undefined, false);
81
+ if (existing !== null) {
82
+ const content = await storage.readResolved(existing);
83
+ }
84
+ ```
85
+
86
+ The browser equivalent is `UnicodeResolvingFileSystemAccessStorageAdapter`
87
+ from `/browser`, constructed with an authorised root handle and the same
88
+ settings callbacks. Version `0` delegates to the existing storage behaviour;
89
+ version `1` resolves every component against complete sibling listings. Normal
90
+ writes preserve existing spelling. Node's explicit `rename` can change a leaf's
91
+ Unicode or letter-case spelling; the browser adapter retains its existing
92
+ operation set and does not add a rename primitive.
93
+
94
+ `AmbiguousNormalisedPathError` from `/storage` reports multiple equivalent
95
+ names, even when one is a literal match. Preserve that error and directory or
96
+ permission failures. Only absence produces `null`; a failure must not be used
97
+ as permission to create a file or as a successful partial scan.
98
+
99
+ For another host, compose `createStoragePathResolver({ readDirectory, policy })`
100
+ from `/storage`. `readDirectory` accepts a `StoragePath` and returns immediate
101
+ child names and kinds, or `null` for absence. Propagate other failures. The
102
+ resolver copies one NFC policy and retains no directory cache. For a full scan,
103
+ start at `resolveForWrite(parseFilePath(''))`, then recurse through
104
+ `listResolvedDirectory` entries using the same resolver. Each directory is read
105
+ once. Re-resolve later operations so external changes are visible.
106
+
107
+ `FilePath` retains original, unprefixed spelling. `StoragePath` records that
108
+ existing components have been resolved; it does not guarantee existence or
109
+ reserve a target. `parseDocumentPath` marks an original document-path input,
110
+ and `splitDocumentPath` separates one recognised namespace before host routing.
111
+ Use `joinDocumentPath` to restore it. `getStoragePathComparisonKey` and
112
+ `getDocumentPathComparisonKey` return distinct comparison-only types; the latter
113
+ preserves the namespace. Keep the original path beside each key. The old
114
+ `FilePathWithPrefix` and `FilePathWithPrefixLC` remain available for compatibility.
115
+
116
+ The [design](design_docs/storage_path_resolution.md) defines the detailed
117
+ contract. These primitives do not enable NFC automatically in a host
118
+ application. WebApp remains Legacy-only and rejects explicit version `1` at
119
+ startup. The Bridge Deno integration is an unreleased candidate; version `1`
120
+ is an explicit peer opt-in, and every peer in a group must use the same path
121
+ version. Its ordinary case-mismatch check remains separate from path-version
122
+ admission. See [Unicode path identity](unicode-path-identity.md#supported-hosts)
123
+ for the current publication and host-adoption boundaries.
124
+
64
125
  ## Bundling boundary
65
126
 
66
127
  Browser consumers import only `@vrtmrz/livesync-commonlib/browser`. Packed-package tests build that entry with browser conditions and verify that the Node implementation is absent. Node consumers import `@vrtmrz/livesync-commonlib/node`; the root and context entries do not load Node built-ins.
@@ -6,9 +6,23 @@ The useful evidence is host- and contract-specific. Focused Commonlib tests esta
6
6
 
7
7
  The code paths below identify maintained Self-hosted LiveSync composition points as examples. The reviewed downstream revision and compatibility imports are recorded in [`migration/downstream-imports.json`](migration/downstream-imports.json), which is regenerated by the package-boundary tooling rather than copied into prose links. These snapshots may become historical as the maintained downstream changes; this page is optional context, not a support entitlement or release or adoption gate.
8
8
 
9
- ## Blob construction and comparison: candidate integration
9
+ ## Published shared-runtime integration
10
10
 
11
- The unreleased Blob extraction delegates construction to
11
+ Commonlib [0.2.3](https://github.com/vrtmrz/livesync-commonlib/releases/tag/0.2.3)
12
+ is published on npm through `latest` and `next`, with an exact dependency on
13
+ Octagonal Wheels 0.1.56. Self-hosted LiveSync
14
+ [1.0.44](https://github.com/vrtmrz/obsidian-livesync/releases/tag/1.0.44)
15
+ adopts the published Commonlib package. Its
16
+ [integration record](https://github.com/vrtmrz/obsidian-livesync/pull/1307)
17
+ separates the final source checks from the focused browser, CLI, and real-Obsidian
18
+ results for unchanged consumer boundaries. The earlier candidate measurements
19
+ below remain historical evidence at their recorded versions.
20
+
21
+ <a id="blob-construction-and-comparison-candidate-integration"></a>
22
+
23
+ ## Blob construction and comparison: published integration
24
+
25
+ Commonlib 0.2.3 delegates Blob construction to
12
26
  `octagonal-wheels/binary/blob`. Commonlib retains the old creation imports and
13
27
  wraps `toBlob` as `createBlob`, while document classification, binary decoding,
14
28
  and content-comparison policy remain in Commonlib. The legacy
@@ -31,13 +45,12 @@ absence on the native path. The
31
45
  the existing Commonlib imports after packaging.
32
46
 
33
47
  Earlier candidate checks explicitly installed the selected unpublished Octagonal
34
- Wheels tarball in the disposable consumer. The current unreleased Commonlib
35
- integration pins the published Octagonal Wheels 0.1.56 dependency and uses the
36
- ordinary package gate without a candidate override. Self-hosted LiveSync tests its hidden-file
37
- loading and revision handling through the legacy entry with matching
38
- candidate tarballs. Those consumer checks are distinct from Commonlib-owned
39
- compatibility contracts; real-Obsidian and mobile validation of this extraction
40
- remain pending.
48
+ Wheels tarball in the disposable consumer. Commonlib 0.2.3 pins the published
49
+ Octagonal Wheels 0.1.56 dependency and uses the ordinary package gate without a
50
+ candidate override. The subsequent Self-hosted LiveSync integration uses the
51
+ exact published Commonlib package. Its recorded application builds and focused
52
+ real-Obsidian conflict and catalogue checks complement the Commonlib-owned
53
+ compatibility contracts; they do not establish physical mobile-device behaviour.
41
54
 
42
55
  ## Obsidian plug-in
43
56
 
@@ -254,7 +267,9 @@ Use the named consumer paths as integration examples, while retaining the owners
254
267
  This optional page preserves examples, current integration evidence, and history. It does not define supported behaviour or create a release or adoption gate; package guarantees are described by the published contracts and release policy. When this page is updated, distinguish current use, historical use, Commonlib-owned tests, downstream tests, and real-runtime observations, and keep each claim within its evidence.
255
268
 
256
269
 
257
- ## Shared value helpers: candidate integration
270
+ <a id="shared-value-helpers-candidate-integration"></a>
271
+
272
+ ## Shared value helpers: published integration
258
273
 
259
274
  The [runtime helper tests](../src/common/utils.runtime-tools.unit.spec.ts)
260
275
  characterise duration thresholds, delimiter empty parts, and JSON conversion
@@ -264,16 +279,18 @@ to detach settings before an operation. The shared package owns the value
264
279
  operations. The packed [type fixture](../_tools/fixtures/runtime-tools-types.ts) and
265
280
  [runtime fixture](../_tools/fixtures/runtime-tools-runtime.mjs) exercise those
266
281
  public duration and delimiter compatibility entries. The original checks used an
267
- unpublished dependency candidate; the current integration pins the published
282
+ unpublished dependency candidate; Commonlib 0.2.3 pins the published
268
283
  Octagonal Wheels 0.1.56 release. Source tests cover the internal copy adapter.
269
284
  LiveSync continues to use the existing compatibility entry points, including
270
285
  duration labels in ConfigSync and tagged lists in settings. Source and packed
271
286
  checks do not establish real-Obsidian rendering or device behaviour.
272
287
 
273
288
 
274
- ## Shared multi-key locking: candidate integration
289
+ <a id="shared-multi-key-locking-candidate-integration"></a>
290
+
291
+ ## Shared multi-key locking: published integration
275
292
 
276
- The unreleased candidate imports `withLocks` directly from
293
+ Commonlib 0.2.3 imports `withLocks` directly from
277
294
  `octagonal-wheels/concurrency/lock` for offline Metadata repair and file-event
278
295
  processing. Commonlib retains the `processFileEvent-` namespace, unique sorted
279
296
  document IDs, and conflict hooks deferred until every lock has been released.
package/docs/releasing.md CHANGED
@@ -34,12 +34,39 @@ Inspect the lockfile diff before committing it. A dependency-only maintenance ch
34
34
 
35
35
  When a named host-specific risk is not established by the Commonlib owner checks, run the relevant downstream workflow against an exact Self-hosted LiveSync ref which consumes the package. It installs the tarball produced from the selected Commonlib commit and runs LiveSync type checks, unit tests, plug-in and application builds, and CLI E2E. Real Obsidian E2E remains local-only and is appropriate when the named risk concerns actual plug-in composition, storage, UI, or platform behaviour. These checks are conditional on the risk; LiveSync adoption is not a general release prerequisite.
36
36
 
37
+ ## Coordinating package and application releases
38
+
39
+ A downstream release which imports a new package entry point or depends on new
40
+ behaviour must use a published immutable Commonlib version. Prepare and test
41
+ consumer branches against an exact local pack while the package is unreleased,
42
+ but retain those results as local-candidate evidence.
43
+
44
+ 1. Complete the Commonlib owner checks and review the release source. For a
45
+ stable package, the exact reviewed release commit must be on `main` before
46
+ staging, as required by the trusted workflow below. An optional prerelease
47
+ can select a reviewed draft branch instead.
48
+ 2. Stage the exact package to `next`, validate the published registry artefact,
49
+ and complete any specifically required downstream checks before promoting
50
+ that same version to `latest`. These remain separate maintainer operations.
51
+ 3. Update each releasing consumer to the exact validated version and registry
52
+ integrity. Self-hosted LiveSync records one root dependency for its plug-in,
53
+ CLI, WebApp, and WebPeer; Bridge records its own Deno import map and lockfile.
54
+ Verify clean installation and the affected consumer boundaries before their
55
+ release workflows run.
56
+
57
+ Commonlib publication does not require every consumer branch to be merged.
58
+ Conversely, publishing Commonlib does not release an application or migrate an
59
+ existing database. A source checkout with the same version label as a registry
60
+ package is not a substitute for that immutable registry artefact.
61
+
37
62
  ## Version compatibility policy
38
63
 
39
64
  The Commonlib 0.2 policy begins with 0.2.0 and does not retroactively change compatibility expectations for published 0.1.x versions. Within 0.2.x, preserve published APIs, types, behaviour, supported persisted data, and exported `compat/*` paths. Intentional breaking changes belong in 0.3.0. Consumers may declare a range such as `^0.2.0`; lockfiles and `npm ci` remain the reproducible install path.
40
65
 
41
66
  For 0.2.3, the documented exceptions are the `HandlerFunction.setHandler` and `LazyHandlerFunction.setHandler` return types changing from `void` to `UnregisterFunction`, and `timeDeltaToHumanReadable(Infinity)` returning `∞y` instead of `Infinityy`. The setter types now describe the existing runtime return. Custom implementations and test doubles whose setters return `void` must provide an unregister function; ordinary callers may continue to ignore the result. These are the specific 0.2.3 exceptions to the policy.
42
67
 
68
+ Version 0.3.0 introduces a source compatibility change: the custom client passed to `LiveSyncJournalReplicator.ensureBucketIsCompatible` must provide `hasRemoteJournals(): Promise<boolean>`. The operation distinguishes an empty bucket from Journal files left without an identifying milestone. Keeping it required makes missing client capability a type error. The standard `JournalSyncCore` client already supplies it. This change uses the next minor version under the policy above.
69
+
43
70
  ## Preparing a release
44
71
 
45
72
  Choose the version explicitly. For the 0.2 line, start at 0.2.0. A stable candidate can use its final version on the `next` dist-tag, then be checked by exact source revision and tarball SHA-256 before the same version is promoted to `latest`. No `-next.N` or `-rc.N` version is required; use a separate prerelease version only when it is useful for a distinct candidate. Package-proof versions are local artefacts and cannot be staged.
@@ -2,11 +2,17 @@
2
2
  date: 2026-10-25
3
3
  commonlib-version: "0.2.0"
4
4
  self-hosted-livesync-version: "1.0.40"
5
- status: unreleased
5
+ status: published
6
6
  ---
7
7
 
8
8
  # Service feature composition
9
9
 
10
+ Publication update, 2nd November, 2026: the Service Hub source selection, narrowed
11
+ host dependencies, and signature-preserving composition helpers are published
12
+ in Commonlib [0.2.3](https://github.com/vrtmrz/livesync-commonlib/releases/tag/0.2.3)
13
+ and used by Self-hosted LiveSync 1.0.44. The front matter retains the original
14
+ review date and version baseline.
15
+
10
16
  This guide is for Commonlib and maintained-host developers who are adding behaviour to the Service Hub composition. It explains when to extend a Service, create a serviceFeature, share a ServiceModule, or introduce a class which owns a resource.
11
17
 
12
18
  The choice is primarily about dependency direction and ownership. Mutable state alone does not require a class or a ServiceModule.