@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,218 @@
1
+ import { getPathNormalizationVersion as getSupportedPathNormalizationVersion } from "../../common/pathNormalization.js";
2
+ import { createStoragePathResolver } from "../storagePathResolution.js";
3
+ import { parseFilePath } from "../pathComparison.js";
4
+ import { NodeStorageAdapter } from "./storage.js";
5
+ class UnicodeResolvingNodeStorageAdapter extends NodeStorageAdapter {
6
+ /**
7
+ * Bind a host-owned root and callbacks for each operation's comparison settings.
8
+ *
9
+ * @param rootPath - Host-selected storage directory.
10
+ * @param getPathNormalizationVersion - Supplies `0` or `1`; defaults to Legacy `0`.
11
+ * @param isCaseInsensitive - Whether comparisons ignore case; defaults to false.
12
+ */
13
+ constructor(rootPath, getPathNormalizationVersion = () => 0, isCaseInsensitive = () => false) {
14
+ super(rootPath);
15
+ this.getPathNormalizationVersion = getPathNormalizationVersion;
16
+ this.isCaseInsensitive = isCaseInsensitive;
17
+ }
18
+ getPathNormalizationVersion;
19
+ isCaseInsensitive;
20
+ /** Whether the current settings select NFC path resolution. */
21
+ get isUnicodePathNormalizationEnabled() {
22
+ return this.getPathNormalizationVersionSnapshot() === 1;
23
+ }
24
+ /** Read metadata for an entry already resolved or obtained from a storage listing. */
25
+ async statExact(path) {
26
+ const settings = this.getSettings();
27
+ if (settings.pathNormalizationVersion === 0) return await super.trystat(path);
28
+ return await super.statResolved(this.asStoragePath(path));
29
+ }
30
+ /** Inspect a physical directory once before descending into its enumerated entries. */
31
+ async listExact(path) {
32
+ const settings = this.getSettings();
33
+ if (settings.pathNormalizationVersion === 0) {
34
+ const entries = await super.list(path);
35
+ return {
36
+ files: entries.files.map((entry) => this.asStoragePath(entry)),
37
+ folders: entries.folders.map((entry) => this.asStoragePath(entry))
38
+ };
39
+ }
40
+ return await this.listPhysicalDirectory(this.asStoragePath(path), settings);
41
+ }
42
+ /**
43
+ * Resolve a logical path under one snapshot of the current settings.
44
+ *
45
+ * @param path - Original root-relative file path.
46
+ * @param caseInsensitive - Override this lookup's case rule without changing settings.
47
+ * @param allowMissing - Keep a missing write suffix when true; defaults to true.
48
+ * @returns The physical target, or `null` for an absent path when `allowMissing`
49
+ * is false in NFC mode. Legacy mode returns the validated original path.
50
+ * @throws Propagates NFC ambiguity and reader failures, and rejects invalid input.
51
+ */
52
+ async resolveLogicalPath(path, caseInsensitive, allowMissing = true) {
53
+ const settings = this.getSettings(caseInsensitive);
54
+ const filePath = parseFilePath(path);
55
+ if (settings.pathNormalizationVersion === 0) return filePath;
56
+ await this.preflightRequestedPath(filePath);
57
+ const resolver = this.createResolver(settings);
58
+ return allowMissing ? await resolver.resolveForWrite(filePath) : await resolver.resolveExisting(filePath);
59
+ }
60
+ /** Check whether the logical path exists under the current Unicode policy. */
61
+ async exists(path, caseInsensitive) {
62
+ const settings = this.getSettings(caseInsensitive);
63
+ if (settings.pathNormalizationVersion === 0) return await super.exists(path);
64
+ await this.preflightRequestedPath(path);
65
+ const resolver = this.createResolver(settings);
66
+ const resolvedPath = await resolver.resolveExisting(parseFilePath(path));
67
+ if (resolvedPath === null) return false;
68
+ return await super.statResolved(resolvedPath) !== null;
69
+ }
70
+ /** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
71
+ async trystat(path, caseInsensitive) {
72
+ const settings = this.getSettings(caseInsensitive);
73
+ return await this.statWithSettings(path, settings);
74
+ }
75
+ /** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
76
+ async stat(path, caseInsensitive) {
77
+ const settings = this.getSettings(caseInsensitive);
78
+ return await this.statWithSettings(path, settings);
79
+ }
80
+ /** Create a directory at the resolved write target. */
81
+ async mkdir(path, caseInsensitive) {
82
+ const settings = this.getSettings(caseInsensitive);
83
+ if (settings.pathNormalizationVersion === 0) return await super.mkdir(path);
84
+ await this.preflightRequestedPath(path);
85
+ const resolvedPath = await this.createResolver(settings).resolveForWrite(parseFilePath(path));
86
+ await super.mkdirResolved(resolvedPath);
87
+ }
88
+ /** Remove the entry at its resolved physical path. */
89
+ async remove(path, caseInsensitive) {
90
+ const settings = this.getSettings(caseInsensitive);
91
+ if (settings.pathNormalizationVersion === 0) return await super.remove(path);
92
+ const resolvedPath = await this.requireExistingPath(path, this.createResolver(settings));
93
+ await super.removeResolved(resolvedPath);
94
+ }
95
+ /** Read text from the resolved physical file. */
96
+ async read(path, caseInsensitive) {
97
+ const settings = this.getSettings(caseInsensitive);
98
+ if (settings.pathNormalizationVersion === 0) return await super.read(path);
99
+ const resolvedPath = await this.requireExistingPath(path, this.createResolver(settings));
100
+ return await super.readResolved(resolvedPath);
101
+ }
102
+ /** Read bytes from the resolved physical file. */
103
+ async readBinary(path, caseInsensitive) {
104
+ const settings = this.getSettings(caseInsensitive);
105
+ if (settings.pathNormalizationVersion === 0) return await super.readBinary(path);
106
+ const resolvedPath = await this.requireExistingPath(path, this.createResolver(settings));
107
+ return await super.readBinaryResolved(resolvedPath);
108
+ }
109
+ /** Write text at the resolved target while retaining existing physical spelling. */
110
+ async write(path, data, options, caseInsensitive) {
111
+ const settings = this.getSettings(caseInsensitive);
112
+ if (settings.pathNormalizationVersion === 0) return await super.write(path, data, options);
113
+ await this.preflightRequestedPath(path);
114
+ const resolvedPath = await this.createResolver(settings).resolveForWrite(parseFilePath(path));
115
+ await super.writeResolved(resolvedPath, data, options);
116
+ }
117
+ /** Write bytes at the resolved target while retaining existing physical spelling. */
118
+ async writeBinary(path, data, options, caseInsensitive) {
119
+ const settings = this.getSettings(caseInsensitive);
120
+ if (settings.pathNormalizationVersion === 0) return await super.writeBinary(path, data, options);
121
+ await this.preflightRequestedPath(path);
122
+ const resolvedPath = await this.createResolver(settings).resolveForWrite(parseFilePath(path));
123
+ await super.writeBinaryResolved(resolvedPath, data, options);
124
+ }
125
+ /** Append text at the resolved target using the host adapter semantics. */
126
+ async append(path, data, options, caseInsensitive) {
127
+ const settings = this.getSettings(caseInsensitive);
128
+ if (settings.pathNormalizationVersion === 0) return await super.append(path, data, options);
129
+ await this.preflightRequestedPath(path);
130
+ const resolvedPath = await this.createResolver(settings).resolveForWrite(parseFilePath(path));
131
+ await super.appendResolved(resolvedPath, data, options);
132
+ }
133
+ /** Resolve and perform an explicit rename, including a requested leaf spelling change. */
134
+ async rename(sourcePath, targetPath, caseInsensitive) {
135
+ const settings = this.getSettings(caseInsensitive);
136
+ if (settings.pathNormalizationVersion === 0) return await super.rename(sourcePath, targetPath);
137
+ await this.preflightRequestedPath(sourcePath, false);
138
+ await this.preflightRequestedPath(targetPath, false);
139
+ const resolver = this.createResolver(settings);
140
+ const resolved = await resolver.resolveRename(parseFilePath(sourcePath), parseFilePath(targetPath));
141
+ if (resolved === null) throw this.missingPathError(sourcePath);
142
+ await super.renameResolved(resolved.source, resolved.target);
143
+ }
144
+ /** List logical directory contents, checking equivalent child names in NFC mode. */
145
+ async list(basePath, caseInsensitive) {
146
+ const settings = this.getSettings(caseInsensitive);
147
+ if (settings.pathNormalizationVersion === 0) return await super.list(basePath);
148
+ await this.preflightRequestedPath(basePath);
149
+ const resolver = this.createResolver(settings);
150
+ const resolvedPath = await resolver.resolveExisting(parseFilePath(basePath));
151
+ if (resolvedPath === null) return { files: [], folders: [] };
152
+ return await this.listPhysicalDirectory(resolvedPath, settings, resolver);
153
+ }
154
+ async statWithSettings(path, settings) {
155
+ if (settings.pathNormalizationVersion === 0) return await super.trystat(path);
156
+ await this.preflightRequestedPath(path);
157
+ const resolvedPath = await this.createResolver(settings).resolveExisting(parseFilePath(path));
158
+ if (resolvedPath === null) return null;
159
+ return await super.statResolved(resolvedPath);
160
+ }
161
+ async requireExistingPath(path, resolver) {
162
+ await this.preflightRequestedPath(path);
163
+ const resolvedPath = await resolver.resolveExisting(parseFilePath(path));
164
+ if (resolvedPath === null) throw this.missingPathError(path);
165
+ return resolvedPath;
166
+ }
167
+ async preflightRequestedPath(path, allowRoot = true) {
168
+ try {
169
+ await this.resolvePath(path, allowRoot);
170
+ } catch (error) {
171
+ if (error.code === "ENOTDIR") return;
172
+ throw error;
173
+ }
174
+ }
175
+ async listPhysicalDirectory(path, settings, resolver = this.createResolver(settings)) {
176
+ const entries = await resolver.listResolvedDirectory(path);
177
+ const files = [];
178
+ const folders = [];
179
+ for (const entry of entries ?? []) {
180
+ if (entry.kind === "directory") folders.push(entry.path);
181
+ else files.push(entry.path);
182
+ }
183
+ return { files, folders };
184
+ }
185
+ createResolver(settings) {
186
+ return createStoragePathResolver({
187
+ readDirectory: async (path) => await super.readDirectory(path),
188
+ policy: {
189
+ pathNormalizationVersion: 1,
190
+ handleFilenameCaseSensitive: !settings.caseInsensitive
191
+ }
192
+ });
193
+ }
194
+ getSettings(caseInsensitive) {
195
+ const pathNormalizationVersion = this.getPathNormalizationVersionSnapshot();
196
+ return {
197
+ pathNormalizationVersion,
198
+ caseInsensitive: caseInsensitive ?? this.isCaseInsensitive()
199
+ };
200
+ }
201
+ getPathNormalizationVersionSnapshot() {
202
+ return getSupportedPathNormalizationVersion({
203
+ pathNormalizationVersion: this.getPathNormalizationVersion()
204
+ });
205
+ }
206
+ asStoragePath(path) {
207
+ return parseFilePath(path);
208
+ }
209
+ missingPathError(path) {
210
+ const error = new Error("ENOENT: no such file or directory, " + path);
211
+ error.code = "ENOENT";
212
+ error.path = path;
213
+ return error;
214
+ }
215
+ }
216
+ export {
217
+ UnicodeResolvingNodeStorageAdapter
218
+ };
@@ -20,5 +20,6 @@ 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 { UnicodeResolvingNodeStorageAdapter } from "./UnicodeResolvingNodeStorageAdapter.js";
23
24
  export { NodeStorageAdapter, createNodeStorage, getNodeFileCreationTime, validateStoragePath, type CreateNodeStorageOptions, type NodeStorageStat, } from "./storage.js";
24
25
  export { createNodeStandardIo, type CreateNodeStandardIoOptions, type NodeStandardInput, type NodeStandardOutput, } from "./standardIo.js";
@@ -12,6 +12,7 @@ const os = nodeOs;
12
12
  const readline = nodeReadlinePromises;
13
13
  const builtinModules = nodeBuiltinModules;
14
14
  const isBuiltin = nodeIsBuiltin;
15
+ import { UnicodeResolvingNodeStorageAdapter } from "./UnicodeResolvingNodeStorageAdapter.js";
15
16
  import {
16
17
  NodeStorageAdapter,
17
18
  createNodeStorage,
@@ -23,6 +24,7 @@ import {
23
24
  } from "./standardIo.js";
24
25
  export {
25
26
  NodeStorageAdapter,
27
+ UnicodeResolvingNodeStorageAdapter,
26
28
  builtinModules,
27
29
  createNodeStandardIo,
28
30
  createNodeStorage,
@@ -1,6 +1,7 @@
1
1
  import type { Stats } from "node:fs";
2
2
  import type { UXDataWriteOptions, UXStat } from "../../common/types.js";
3
3
  import type { IStorageAdapter } from "../../serviceModules/adapters/IStorageAdapter.js";
4
+ import type { DirectoryEntry, StoragePath } from "../pathTypes.js";
4
5
  export type NodeStorageStat = UXStat;
5
6
  /** Read creation time in integer milliseconds, using change time when birth time is unavailable. */
6
7
  export declare function getNodeFileCreationTime(stat: Pick<Stats, "birthtimeMs" | "ctimeMs">): number;
@@ -17,20 +18,48 @@ export interface CreateNodeStorageOptions {
17
18
  export declare class NodeStorageAdapter implements IStorageAdapter<NodeStorageStat> {
18
19
  private readonly rootPath;
19
20
  constructor(rootPath: string);
20
- private resolvePath;
21
+ protected resolvePath(path: string, allowRoot?: boolean): Promise<string>;
21
22
  private assertNoSymbolicLinks;
22
23
  exists(path: string): Promise<boolean>;
23
24
  trystat(path: string): Promise<NodeStorageStat | null>;
25
+ /** Read metadata for an exact physical path, returning null only when it is absent. */
26
+ statResolved(path: StoragePath): Promise<NodeStorageStat | null>;
27
+ /** Enumerate immediate physical children, returning null only when the directory is absent. */
28
+ readDirectory(path: StoragePath): Promise<readonly DirectoryEntry[] | null>;
24
29
  stat(path: string): Promise<NodeStorageStat | null>;
25
30
  mkdir(path: string): Promise<void>;
31
+ /** Create an exact resolved directory path. */
32
+ mkdirResolved(path: StoragePath): Promise<void>;
33
+ private mkdirPath;
26
34
  remove(path: string): Promise<void>;
35
+ /** Remove an exact resolved file or directory path. */
36
+ removeResolved(path: StoragePath): Promise<void>;
37
+ private removePath;
27
38
  read(path: string): Promise<string>;
39
+ /** Read a file by its exact resolved physical path. */
40
+ readResolved(path: StoragePath): Promise<string>;
41
+ private readPath;
28
42
  readBinary(path: string): Promise<ArrayBuffer>;
43
+ /** Read bytes by their exact resolved physical path. */
44
+ readBinaryResolved(path: StoragePath): Promise<ArrayBuffer>;
45
+ private readBinaryPath;
29
46
  write(path: string, data: string, options?: UXDataWriteOptions): Promise<void>;
47
+ /** Write text to an exact resolved physical path. */
48
+ writeResolved(path: StoragePath, data: string, options?: UXDataWriteOptions): Promise<void>;
49
+ private writePath;
30
50
  writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void>;
51
+ /** Write bytes to an exact resolved physical path. */
52
+ writeBinaryResolved(path: StoragePath, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void>;
53
+ private writeBinaryPath;
31
54
  append(path: string, data: string, options?: UXDataWriteOptions): Promise<void>;
55
+ /** Append text to an exact resolved physical path. */
56
+ appendResolved(path: StoragePath, data: string, options?: UXDataWriteOptions): Promise<void>;
57
+ private appendPath;
32
58
  /** Atomically rename or move an entry without leaving the injected root. */
33
59
  rename(sourcePath: string, targetPath: string): Promise<void>;
60
+ /** Rename or move exact resolved physical paths without leaving the injected root. */
61
+ renameResolved(sourcePath: StoragePath, targetPath: StoragePath): Promise<void>;
62
+ private renamePath;
34
63
  list(basePath: string): Promise<{
35
64
  files: string[];
36
65
  folders: string[];
@@ -59,15 +59,61 @@ class NodeStorageAdapter {
59
59
  return null;
60
60
  }
61
61
  }
62
+ /** Read metadata for an exact physical path, returning null only when it is absent. */
63
+ async statResolved(path) {
64
+ let stat;
65
+ try {
66
+ stat = await nodeFsPromises.stat(await this.resolvePath(path));
67
+ } catch (error) {
68
+ if (error.code === "ENOENT") return null;
69
+ throw error;
70
+ }
71
+ return {
72
+ size: stat.size,
73
+ mtime: Math.floor(stat.mtimeMs),
74
+ ctime: getNodeFileCreationTime(stat),
75
+ type: stat.isDirectory() ? "folder" : "file"
76
+ };
77
+ }
78
+ /** Enumerate immediate physical children, returning null only when the directory is absent. */
79
+ async readDirectory(path) {
80
+ let entries;
81
+ try {
82
+ entries = await nodeFsPromises.readdir(await this.resolvePath(path), { withFileTypes: true });
83
+ } catch (error) {
84
+ if (error.code === "ENOENT") return null;
85
+ throw error;
86
+ }
87
+ const directoryEntries = [];
88
+ for (const entry of entries) {
89
+ if (entry.isDirectory()) directoryEntries.push({ name: entry.name, kind: "directory" });
90
+ else if (entry.isFile()) directoryEntries.push({ name: entry.name, kind: "file" });
91
+ }
92
+ return directoryEntries;
93
+ }
62
94
  async stat(path) {
63
95
  return await this.trystat(path);
64
96
  }
65
97
  async mkdir(path) {
98
+ await this.mkdirPath(path);
99
+ }
100
+ /** Create an exact resolved directory path. */
101
+ async mkdirResolved(path) {
102
+ await this.mkdirPath(path);
103
+ }
104
+ async mkdirPath(path) {
66
105
  const fullPath = await this.resolvePath(path);
67
106
  await nodeFsPromises.mkdir(fullPath, { recursive: true });
68
107
  await this.assertNoSymbolicLinks(fullPath);
69
108
  }
70
109
  async remove(path) {
110
+ await this.removePath(path);
111
+ }
112
+ /** Remove an exact resolved file or directory path. */
113
+ async removeResolved(path) {
114
+ await this.removePath(path);
115
+ }
116
+ async removePath(path) {
71
117
  const fullPath = await this.resolvePath(path, false);
72
118
  const stat = await nodeFsPromises.stat(fullPath);
73
119
  if (stat.isDirectory()) {
@@ -77,6 +123,13 @@ class NodeStorageAdapter {
77
123
  }
78
124
  }
79
125
  async read(path) {
126
+ return await this.readPath(path);
127
+ }
128
+ /** Read a file by its exact resolved physical path. */
129
+ async readResolved(path) {
130
+ return await this.readPath(path);
131
+ }
132
+ async readPath(path) {
80
133
  const handle = await nodeFsPromises.open(
81
134
  await this.resolvePath(path, false),
82
135
  nodeFsConstants.O_RDONLY | NO_FOLLOW
@@ -88,6 +141,13 @@ class NodeStorageAdapter {
88
141
  }
89
142
  }
90
143
  async readBinary(path) {
144
+ return await this.readBinaryPath(path);
145
+ }
146
+ /** Read bytes by their exact resolved physical path. */
147
+ async readBinaryResolved(path) {
148
+ return await this.readBinaryPath(path);
149
+ }
150
+ async readBinaryPath(path) {
91
151
  const handle = await nodeFsPromises.open(
92
152
  await this.resolvePath(path, false),
93
153
  nodeFsConstants.O_RDONLY | NO_FOLLOW
@@ -100,22 +160,50 @@ class NodeStorageAdapter {
100
160
  }
101
161
  }
102
162
  async write(path, data, options) {
163
+ await this.writePath(path, data, options);
164
+ }
165
+ /** Write text to an exact resolved physical path. */
166
+ async writeResolved(path, data, options) {
167
+ await this.writePath(path, data, options);
168
+ }
169
+ async writePath(path, data, options) {
103
170
  const fullPath = await this.resolvePath(path, false);
104
171
  await nodeFsPromises.mkdir(nodePath.dirname(fullPath), { recursive: true });
105
172
  await this.writeFile(fullPath, data, options, false);
106
173
  }
107
174
  async writeBinary(path, data, options) {
175
+ await this.writeBinaryPath(path, data, options);
176
+ }
177
+ /** Write bytes to an exact resolved physical path. */
178
+ async writeBinaryResolved(path, data, options) {
179
+ await this.writeBinaryPath(path, data, options);
180
+ }
181
+ async writeBinaryPath(path, data, options) {
108
182
  const fullPath = await this.resolvePath(path, false);
109
183
  await nodeFsPromises.mkdir(nodePath.dirname(fullPath), { recursive: true });
110
184
  await this.writeFile(fullPath, new Uint8Array(data), options, false);
111
185
  }
112
186
  async append(path, data, options) {
187
+ await this.appendPath(path, data, options);
188
+ }
189
+ /** Append text to an exact resolved physical path. */
190
+ async appendResolved(path, data, options) {
191
+ await this.appendPath(path, data, options);
192
+ }
193
+ async appendPath(path, data, options) {
113
194
  const fullPath = await this.resolvePath(path, false);
114
195
  await nodeFsPromises.mkdir(nodePath.dirname(fullPath), { recursive: true });
115
196
  await this.writeFile(fullPath, data, options, true);
116
197
  }
117
198
  /** Atomically rename or move an entry without leaving the injected root. */
118
199
  async rename(sourcePath, targetPath) {
200
+ await this.renamePath(sourcePath, targetPath);
201
+ }
202
+ /** Rename or move exact resolved physical paths without leaving the injected root. */
203
+ async renameResolved(sourcePath, targetPath) {
204
+ await this.renamePath(sourcePath, targetPath);
205
+ }
206
+ async renamePath(sourcePath, targetPath) {
119
207
  const fullSourcePath = await this.resolvePath(sourcePath, false);
120
208
  const fullTargetPath = await this.resolvePath(targetPath, false);
121
209
  await nodeFsPromises.mkdir(nodePath.dirname(fullTargetPath), { recursive: true });
@@ -0,0 +1,53 @@
1
+ import type { DocumentPath, DocumentPathParts, FilePath, PathComparisonKey, PathComparisonSettings } from "./pathTypes.js";
2
+ /**
3
+ * Validate raw storage input without changing its spelling.
4
+ *
5
+ * @param path - A slash-separated path relative to the host-selected root.
6
+ * @returns The original path branded as `FilePath`; the empty path denotes the root.
7
+ * @throws Rejects absolute, drive-qualified, backslash, and traversal-segment input.
8
+ * @remarks Does not normalise Unicode, resolve physical names, or check existence.
9
+ */
10
+ export declare function parseFilePath(path: string): FilePath;
11
+ /**
12
+ * Mark an original document path before routing its Metadata namespace.
13
+ *
14
+ * @param path - Original document-path text, optionally including one namespace.
15
+ * @returns The same text branded as `DocumentPath`.
16
+ * @throws Rejects a non-string value.
17
+ * @remarks Storage validation follows namespace routing. This function does not
18
+ * recover original spelling from a comparison key or validate a physical path.
19
+ */
20
+ export declare function parseDocumentPath(path: string): DocumentPath;
21
+ /**
22
+ * Split one recognised Metadata namespace from an original document path.
23
+ *
24
+ * @param path - Original document path; a further colon remains in the file path.
25
+ * @returns The namespace, or an empty prefix, and the original remaining spelling.
26
+ * @remarks Does not validate the resulting file path for storage access.
27
+ */
28
+ export declare function splitDocumentPath(path: DocumentPath): DocumentPathParts;
29
+ /**
30
+ * Join a routed namespace and its original file path without normalising either.
31
+ *
32
+ * @param parts - One recognised namespace and an original file path.
33
+ * @returns Their concatenation as an original document path.
34
+ */
35
+ export declare function joinDocumentPath({ prefix, path }: DocumentPathParts): DocumentPath;
36
+ /**
37
+ * Derive an identity key for a file path within one storage root.
38
+ *
39
+ * @param path - Original file path without a Metadata namespace.
40
+ * @param settings - Explicit Unicode and case comparison settings.
41
+ * @returns A storage-domain comparison key; retain the original path for I/O.
42
+ * @throws Rejects an unsupported path normalisation version.
43
+ */
44
+ export declare function getStoragePathComparisonKey(path: FilePath, settings: PathComparisonSettings): PathComparisonKey<"storage">;
45
+ /**
46
+ * Derive a document identity key while preserving its Metadata namespace.
47
+ *
48
+ * @param path - Original document path, optionally including one namespace.
49
+ * @param settings - Explicit Unicode and case comparison settings for the body.
50
+ * @returns A document-domain key which distinguishes namespaces with equal bodies.
51
+ * @throws Rejects an unsupported path normalisation version.
52
+ */
53
+ export declare function getDocumentPathComparisonKey(path: DocumentPath, settings: PathComparisonSettings): PathComparisonKey<"document">;
@@ -0,0 +1,39 @@
1
+ import { normalisePathForIdentity } from "../common/pathNormalization.js";
2
+ import { splitPathNamespace } from "../common/pathNamespace.js";
3
+ import { validateStoragePath } from "./storagePath.js";
4
+ function parseFilePath(path) {
5
+ return validateStoragePath(path);
6
+ }
7
+ function parseDocumentPath(path) {
8
+ if (typeof path !== "string") throw new TypeError("A document path must be a string");
9
+ return path;
10
+ }
11
+ function splitDocumentPath(path) {
12
+ const [prefix, body] = splitPathNamespace(path);
13
+ return { prefix, path: body };
14
+ }
15
+ function joinDocumentPath({ prefix, path }) {
16
+ return prefix + path;
17
+ }
18
+ function normaliseFilePath(path) {
19
+ return normalisePathForIdentity(path, 1);
20
+ }
21
+ function comparisonPath(path, settings) {
22
+ const normalised = settings.pathNormalizationVersion === 1 ? normaliseFilePath(path) : normalisePathForIdentity(path, settings.pathNormalizationVersion);
23
+ return settings.handleFilenameCaseSensitive ? normalised : normalised.toLowerCase();
24
+ }
25
+ function getStoragePathComparisonKey(path, settings) {
26
+ return comparisonPath(path, settings);
27
+ }
28
+ function getDocumentPathComparisonKey(path, settings) {
29
+ const { prefix, path: body } = splitDocumentPath(path);
30
+ return prefix + comparisonPath(body, settings);
31
+ }
32
+ export {
33
+ getDocumentPathComparisonKey,
34
+ getStoragePathComparisonKey,
35
+ joinDocumentPath,
36
+ parseDocumentPath,
37
+ parseFilePath,
38
+ splitDocumentPath
39
+ };