@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,169 @@
1
+ import { getPathNormalizationVersion } from "../../common/pathNormalization.js";
2
+ import { parseFilePath } from "../pathComparison.js";
3
+ import { createStoragePathResolver } from "../storagePathResolution.js";
4
+ import { FileSystemAccessStorageAdapter } from "./storage.js";
5
+ class UnicodeResolvingFileSystemAccessStorageAdapter {
6
+ /**
7
+ * Bind a host-owned root and callbacks for each operation's comparison settings.
8
+ *
9
+ * @param rootHandle - Host-selected directory handle with the required permissions.
10
+ * @param getPathNormalizationVersion - Supplies `0` or `1`; defaults to Legacy `0`.
11
+ * @param isCaseInsensitive - Whether comparisons ignore case; defaults to false.
12
+ */
13
+ constructor(rootHandle, getPathNormalizationVersion2 = () => 0, isCaseInsensitive = () => false) {
14
+ this.getPathNormalizationVersion = getPathNormalizationVersion2;
15
+ this.isCaseInsensitive = isCaseInsensitive;
16
+ this.storage = new FileSystemAccessStorageAdapter(rootHandle);
17
+ }
18
+ getPathNormalizationVersion;
19
+ isCaseInsensitive;
20
+ storage;
21
+ /** Whether the current settings select NFC path resolution. */
22
+ get isUnicodePathNormalizationEnabled() {
23
+ return this.snapshotPolicy().pathNormalizationVersion === 1;
24
+ }
25
+ /**
26
+ * Resolve a logical path under one snapshot of the current settings.
27
+ *
28
+ * @param path - Original root-relative file path.
29
+ * @param caseInsensitive - Override this lookup's case rule without changing settings.
30
+ * @param allowMissing - Keep a missing write suffix when true; defaults to true.
31
+ * @returns The physical target, or `null` for an absent path when `allowMissing`
32
+ * is false in NFC mode. Legacy mode returns the validated original path.
33
+ * @throws Propagates NFC ambiguity and reader failures, and rejects invalid input.
34
+ */
35
+ async resolveLogicalPath(path, caseInsensitive, allowMissing = true) {
36
+ const policy = this.snapshotPolicy(caseInsensitive);
37
+ const logicalPath = parseFilePath(path);
38
+ if (policy.pathNormalizationVersion === 0) return logicalPath;
39
+ const resolver = this.createResolver(policy);
40
+ return allowMissing ? await resolver.resolveForWrite(logicalPath) : await resolver.resolveExisting(logicalPath);
41
+ }
42
+ /** Read metadata for a physical path without performing another logical lookup. */
43
+ async statExact(path) {
44
+ const policy = this.snapshotPolicy();
45
+ if (policy.pathNormalizationVersion === 0) return await this.storage.trystat(path);
46
+ return await this.storage.statResolved(path);
47
+ }
48
+ /** List a physical directory, rejecting equivalent child names in NFC mode. */
49
+ async listExact(path) {
50
+ const policy = this.snapshotPolicy();
51
+ if (policy.pathNormalizationVersion === 0) return await this.storage.list(path);
52
+ const entries = await this.createResolver(policy).listResolvedDirectory(path);
53
+ return this.groupStorageEntries(entries ?? []);
54
+ }
55
+ /** Check whether the logical path exists under the current Unicode policy. */
56
+ async exists(path) {
57
+ const policy = this.snapshotPolicy();
58
+ if (policy.pathNormalizationVersion === 0) return await this.storage.exists(path);
59
+ return await this.createResolver(policy).resolveExisting(parseFilePath(path)) !== null;
60
+ }
61
+ /** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
62
+ async trystat(path) {
63
+ const policy = this.snapshotPolicy();
64
+ if (policy.pathNormalizationVersion === 0) return await this.storage.trystat(path);
65
+ const resolved = await this.createResolver(policy).resolveExisting(parseFilePath(path));
66
+ return resolved === null ? null : await this.storage.statResolved(resolved);
67
+ }
68
+ /** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
69
+ async stat(path) {
70
+ return await this.trystat(path);
71
+ }
72
+ /** Create a directory at the resolved write target. */
73
+ async mkdir(path) {
74
+ const policy = this.snapshotPolicy();
75
+ if (policy.pathNormalizationVersion === 0) return await this.storage.mkdir(path);
76
+ const resolved = await this.createResolver(policy).resolveForWrite(parseFilePath(path));
77
+ await this.storage.mkdirResolved(resolved);
78
+ }
79
+ /** Remove the entry at its resolved physical path. */
80
+ async remove(path) {
81
+ const policy = this.snapshotPolicy();
82
+ if (policy.pathNormalizationVersion === 0) return await this.storage.remove(path);
83
+ const resolved = await this.createResolver(policy).resolveExisting(parseFilePath(path));
84
+ if (resolved !== null) await this.storage.removeResolved(resolved);
85
+ }
86
+ /** Read text from the resolved physical file. */
87
+ async read(path) {
88
+ const policy = this.snapshotPolicy();
89
+ if (policy.pathNormalizationVersion === 0) return await this.storage.read(path);
90
+ const resolved = await this.createResolver(policy).resolveExisting(parseFilePath(path));
91
+ if (resolved === null) throw new Error(`File not found: ${path}`);
92
+ return await this.storage.readResolved(resolved);
93
+ }
94
+ /** Read bytes from the resolved physical file. */
95
+ async readBinary(path) {
96
+ const policy = this.snapshotPolicy();
97
+ if (policy.pathNormalizationVersion === 0) return await this.storage.readBinary(path);
98
+ const resolved = await this.createResolver(policy).resolveExisting(parseFilePath(path));
99
+ if (resolved === null) throw new Error(`File not found: ${path}`);
100
+ return await this.storage.readBinaryResolved(resolved);
101
+ }
102
+ /** Write text at the resolved target while retaining existing physical spelling. */
103
+ async write(path, data, options) {
104
+ const policy = this.snapshotPolicy();
105
+ if (policy.pathNormalizationVersion === 0) return await this.storage.write(path, data, options);
106
+ const resolved = await this.createResolver(policy).resolveForWrite(parseFilePath(path));
107
+ await this.storage.writeResolved(resolved, data, options);
108
+ }
109
+ /** Write bytes at the resolved target while retaining existing physical spelling. */
110
+ async writeBinary(path, data, options) {
111
+ const policy = this.snapshotPolicy();
112
+ if (policy.pathNormalizationVersion === 0) {
113
+ return await this.storage.writeBinary(path, data, options);
114
+ }
115
+ const resolved = await this.createResolver(policy).resolveForWrite(parseFilePath(path));
116
+ await this.storage.writeBinaryResolved(resolved, data, options);
117
+ }
118
+ /** Append text at the resolved target using the host adapter semantics. */
119
+ async append(path, data, options) {
120
+ const policy = this.snapshotPolicy();
121
+ if (policy.pathNormalizationVersion === 0) return await this.storage.append(path, data, options);
122
+ const resolved = await this.createResolver(policy).resolveForWrite(parseFilePath(path));
123
+ await this.storage.appendResolved(resolved, data, options);
124
+ }
125
+ /** List logical directory contents, checking equivalent child names in NFC mode. */
126
+ async list(basePath) {
127
+ const policy = this.snapshotPolicy();
128
+ if (policy.pathNormalizationVersion === 0) return await this.storage.list(basePath);
129
+ const resolver = this.createResolver(policy);
130
+ const resolved = await resolver.resolveExisting(parseFilePath(basePath));
131
+ if (resolved === null) return { files: [], folders: [] };
132
+ const entries = await resolver.listResolvedDirectory(resolved);
133
+ return this.groupStorageEntries(entries ?? []);
134
+ }
135
+ snapshotPolicy(caseInsensitive) {
136
+ const pathNormalizationVersion = getPathNormalizationVersion({
137
+ pathNormalizationVersion: this.getPathNormalizationVersion()
138
+ });
139
+ const isCaseInsensitive = caseInsensitive ?? this.isCaseInsensitive();
140
+ return Object.freeze({
141
+ pathNormalizationVersion,
142
+ handleFilenameCaseSensitive: !isCaseInsensitive
143
+ });
144
+ }
145
+ createResolver(policy) {
146
+ if (policy.pathNormalizationVersion !== 1) {
147
+ throw new Error("The storage path resolver requires NFC path normalisation");
148
+ }
149
+ return createStoragePathResolver({
150
+ readDirectory: (path) => this.storage.readDirectory(path),
151
+ policy: {
152
+ pathNormalizationVersion: 1,
153
+ handleFilenameCaseSensitive: policy.handleFilenameCaseSensitive
154
+ }
155
+ });
156
+ }
157
+ groupStorageEntries(entries) {
158
+ const files = [];
159
+ const folders = [];
160
+ for (const entry of entries) {
161
+ if (entry.kind === "file") files.push(entry.path);
162
+ else folders.push(entry.path);
163
+ }
164
+ return { files, folders };
165
+ }
166
+ }
167
+ export {
168
+ UnicodeResolvingFileSystemAccessStorageAdapter
169
+ };
@@ -1 +1,2 @@
1
1
  export { FileSystemAccessStorageAdapter, createFileSystemAccessStorage, validateStoragePath, type CreateFileSystemAccessStorageOptions, type FileSystemAccessStorageStat, } from "./storage.js";
2
+ export { UnicodeResolvingFileSystemAccessStorageAdapter } from "./UnicodeResolvingFileSystemAccessStorageAdapter.js";
@@ -3,8 +3,10 @@ import {
3
3
  createFileSystemAccessStorage,
4
4
  validateStoragePath
5
5
  } from "./storage.js";
6
+ import { UnicodeResolvingFileSystemAccessStorageAdapter } from "./UnicodeResolvingFileSystemAccessStorageAdapter.js";
6
7
  export {
7
8
  FileSystemAccessStorageAdapter,
9
+ UnicodeResolvingFileSystemAccessStorageAdapter,
8
10
  createFileSystemAccessStorage,
9
11
  validateStoragePath
10
12
  };
@@ -1,5 +1,6 @@
1
1
  import type { UXDataWriteOptions, UXStat } from "../../common/types.js";
2
2
  import type { IStorageAdapter } from "../../serviceModules/adapters/IStorageAdapter.js";
3
+ import type { DirectoryEntry, StoragePath } from "../pathTypes.js";
3
4
  export type FileSystemAccessStorageStat = UXStat;
4
5
  export interface CreateFileSystemAccessStorageOptions {
5
6
  /** A directory handle already selected and authorised by the host application. */
@@ -12,10 +13,32 @@ export declare class FileSystemAccessStorageAdapter implements IStorageAdapter<F
12
13
  private resolvePath;
13
14
  private getFileHandle;
14
15
  private getDirectoryHandle;
16
+ private getStrictDirectoryHandle;
17
+ private getStrictFileHandle;
18
+ private requireStrictFileHandle;
19
+ private getStrictWritableFileHandle;
15
20
  private resolveWritablePath;
16
21
  exists(path: string): Promise<boolean>;
17
22
  trystat(path: string): Promise<FileSystemAccessStorageStat | null>;
18
23
  stat(path: string): Promise<FileSystemAccessStorageStat | null>;
24
+ /** Enumerate one directory without converting permission or type errors into absence. */
25
+ readDirectory(path: StoragePath): Promise<readonly DirectoryEntry[] | null>;
26
+ /** Stat a path using its exact physical spelling and preserve non-missing errors. */
27
+ statResolved(path: StoragePath): Promise<FileSystemAccessStorageStat | null>;
28
+ /** Read an exact resolved file path while preserving missing, type, and permission errors. */
29
+ readResolved(path: StoragePath): Promise<string>;
30
+ /** Read an exact resolved file path as bytes while preserving host errors. */
31
+ readBinaryResolved(path: StoragePath): Promise<ArrayBuffer>;
32
+ /** Write an exact resolved file path, creating missing parent directories. */
33
+ writeResolved(path: StoragePath, data: string, _options?: UXDataWriteOptions): Promise<void>;
34
+ /** Write bytes to an exact resolved file path, creating missing parent directories. */
35
+ writeBinaryResolved(path: StoragePath, data: ArrayBuffer, _options?: UXDataWriteOptions): Promise<void>;
36
+ /** Append through strict reads and writes so denied reads cannot become truncating writes. */
37
+ appendResolved(path: StoragePath, data: string, options?: UXDataWriteOptions): Promise<void>;
38
+ /** Remove an exact resolved path, retaining the adapter's no-op behaviour when it is absent. */
39
+ removeResolved(path: StoragePath): Promise<void>;
40
+ /** Create an exact resolved directory path without hiding browser errors. */
41
+ mkdirResolved(path: StoragePath): Promise<void>;
19
42
  mkdir(path: string): Promise<void>;
20
43
  remove(path: string): Promise<void>;
21
44
  read(path: string): Promise<string>;
@@ -40,6 +40,47 @@ class FileSystemAccessStorageAdapter {
40
40
  return null;
41
41
  }
42
42
  }
43
+ async getStrictDirectoryHandle(path) {
44
+ validateStoragePath(path);
45
+ let currentHandle = this.rootHandle;
46
+ for (const part of path.split("/").filter((segment) => segment !== "")) {
47
+ try {
48
+ currentHandle = await currentHandle.getDirectoryHandle(part);
49
+ } catch (error) {
50
+ if (isNamedError(error, "NotFoundError")) return null;
51
+ throw error;
52
+ }
53
+ }
54
+ return currentHandle;
55
+ }
56
+ async getStrictFileHandle(path) {
57
+ validateStoragePath(path, false);
58
+ const parts = path.split("/").filter((part) => part !== "");
59
+ const name = parts.pop();
60
+ const dirHandle = await this.getStrictDirectoryHandle(parts.join("/"));
61
+ if (!dirHandle) return null;
62
+ try {
63
+ return await dirHandle.getFileHandle(name);
64
+ } catch (error) {
65
+ if (isNamedError(error, "NotFoundError")) return null;
66
+ throw error;
67
+ }
68
+ }
69
+ async requireStrictFileHandle(path) {
70
+ const handle = await this.getStrictFileHandle(path);
71
+ if (!handle) throw new Error(`File not found: ${path}`);
72
+ return handle;
73
+ }
74
+ async getStrictWritableFileHandle(path) {
75
+ validateStoragePath(path, false);
76
+ const parts = path.split("/").filter((part) => part !== "");
77
+ const name = parts.pop();
78
+ const parentPath = parts.join("/");
79
+ await this.mkdir(parentPath);
80
+ const dirHandle = await this.getStrictDirectoryHandle(parentPath);
81
+ if (!dirHandle) throw new Error(`Invalid path: ${path}`);
82
+ return await dirHandle.getFileHandle(name, { create: true });
83
+ }
43
84
  async resolveWritablePath(path) {
44
85
  validateStoragePath(path, false);
45
86
  const parts = path.split("/").filter((part) => part !== "");
@@ -71,6 +112,100 @@ class FileSystemAccessStorageAdapter {
71
112
  async stat(path) {
72
113
  return await this.trystat(path);
73
114
  }
115
+ /** Enumerate one directory without converting permission or type errors into absence. */
116
+ async readDirectory(path) {
117
+ const dirHandle = await this.getStrictDirectoryHandle(path);
118
+ if (!dirHandle) return null;
119
+ const entries = [];
120
+ for await (const [name, entry] of dirHandle.entries()) {
121
+ if (entry.kind === "directory" || entry.kind === "file") {
122
+ entries.push({ name, kind: entry.kind });
123
+ }
124
+ }
125
+ return entries;
126
+ }
127
+ /** Stat a path using its exact physical spelling and preserve non-missing errors. */
128
+ async statResolved(path) {
129
+ validateStoragePath(path);
130
+ if (path === "") {
131
+ const now = Date.now();
132
+ return { size: 0, mtime: now, ctime: now, type: "folder" };
133
+ }
134
+ const parts = path.split("/");
135
+ const name = parts.pop();
136
+ const parentHandle = await this.getStrictDirectoryHandle(parts.join("/"));
137
+ if (!parentHandle) return null;
138
+ let fileHandle;
139
+ try {
140
+ fileHandle = await parentHandle.getFileHandle(name);
141
+ } catch (error) {
142
+ if (isNamedError(error, "NotFoundError")) return null;
143
+ if (!isNamedError(error, "TypeMismatchError")) throw error;
144
+ try {
145
+ await parentHandle.getDirectoryHandle(name);
146
+ } catch (directoryError) {
147
+ if (isNamedError(directoryError, "NotFoundError")) return null;
148
+ throw directoryError;
149
+ }
150
+ const now = Date.now();
151
+ return { size: 0, mtime: now, ctime: now, type: "folder" };
152
+ }
153
+ const file = await fileHandle.getFile();
154
+ return {
155
+ size: file.size,
156
+ mtime: file.lastModified,
157
+ ctime: file.lastModified,
158
+ type: "file"
159
+ };
160
+ }
161
+ /** Read an exact resolved file path while preserving missing, type, and permission errors. */
162
+ async readResolved(path) {
163
+ const handle = await this.requireStrictFileHandle(path);
164
+ return await (await handle.getFile()).text();
165
+ }
166
+ /** Read an exact resolved file path as bytes while preserving host errors. */
167
+ async readBinaryResolved(path) {
168
+ const handle = await this.requireStrictFileHandle(path);
169
+ return await (await handle.getFile()).arrayBuffer();
170
+ }
171
+ /** Write an exact resolved file path, creating missing parent directories. */
172
+ async writeResolved(path, data, _options) {
173
+ const handle = await this.getStrictWritableFileHandle(path);
174
+ const writable = await handle.createWritable();
175
+ await writable.write(data);
176
+ await writable.close();
177
+ }
178
+ /** Write bytes to an exact resolved file path, creating missing parent directories. */
179
+ async writeBinaryResolved(path, data, _options) {
180
+ const handle = await this.getStrictWritableFileHandle(path);
181
+ const writable = await handle.createWritable();
182
+ await writable.write(data);
183
+ await writable.close();
184
+ }
185
+ /** Append through strict reads and writes so denied reads cannot become truncating writes. */
186
+ async appendResolved(path, data, options) {
187
+ const handle = await this.getStrictFileHandle(path);
188
+ const existing = handle ? await (await handle.getFile()).text() : "";
189
+ await this.writeResolved(path, existing + data, options);
190
+ }
191
+ /** Remove an exact resolved path, retaining the adapter's no-op behaviour when it is absent. */
192
+ async removeResolved(path) {
193
+ validateStoragePath(path, false);
194
+ const parts = path.split("/").filter((part) => part !== "");
195
+ const name = parts.pop();
196
+ const dirHandle = await this.getStrictDirectoryHandle(parts.join("/"));
197
+ if (!dirHandle) return;
198
+ try {
199
+ await dirHandle.removeEntry(name, { recursive: true });
200
+ } catch (error) {
201
+ if (isNamedError(error, "NotFoundError")) return;
202
+ throw error;
203
+ }
204
+ }
205
+ /** Create an exact resolved directory path without hiding browser errors. */
206
+ async mkdirResolved(path) {
207
+ await this.mkdir(path);
208
+ }
74
209
  async mkdir(path) {
75
210
  validateStoragePath(path);
76
211
  let currentHandle = this.rootHandle;
@@ -131,6 +266,9 @@ function createFileSystemAccessStorage({
131
266
  }) {
132
267
  return new FileSystemAccessStorageAdapter(rootHandle);
133
268
  }
269
+ function isNamedError(error, name) {
270
+ return typeof error === "object" && error !== null && "name" in error && error.name === name;
271
+ }
134
272
  import { validateStoragePath as validateStoragePath2 } from "../storagePath.js";
135
273
  export {
136
274
  FileSystemAccessStorageAdapter,
@@ -0,0 +1,79 @@
1
+ import type { FilePath, StoragePath } from "../pathTypes.js";
2
+ import { NodeStorageAdapter } from "./storage.js";
3
+ /**
4
+ * Resolve NFC-equivalent logical paths against current Node directory names.
5
+ *
6
+ * @remarks
7
+ * Each operation snapshots Unicode and case settings. Version `0` delegates to
8
+ * the existing adapter; version `1` resolves names and rejects observed aliases
9
+ * before I/O. Physical spelling and the host adapter's mutation semantics remain
10
+ * unchanged. Resolution does not reserve paths against concurrent changes.
11
+ */
12
+ export declare class UnicodeResolvingNodeStorageAdapter extends NodeStorageAdapter {
13
+ private readonly getPathNormalizationVersion;
14
+ private readonly isCaseInsensitive;
15
+ /**
16
+ * Bind a host-owned root and callbacks for each operation's comparison settings.
17
+ *
18
+ * @param rootPath - Host-selected storage directory.
19
+ * @param getPathNormalizationVersion - Supplies `0` or `1`; defaults to Legacy `0`.
20
+ * @param isCaseInsensitive - Whether comparisons ignore case; defaults to false.
21
+ */
22
+ constructor(rootPath: string, getPathNormalizationVersion?: () => number, isCaseInsensitive?: () => boolean);
23
+ /** Whether the current settings select NFC path resolution. */
24
+ get isUnicodePathNormalizationEnabled(): boolean;
25
+ /** Read metadata for an entry already resolved or obtained from a storage listing. */
26
+ statExact(path: StoragePath): Promise<import("../../common/types.js").UXStat>;
27
+ /** Inspect a physical directory once before descending into its enumerated entries. */
28
+ listExact(path: StoragePath): Promise<{
29
+ files: StoragePath[];
30
+ folders: StoragePath[];
31
+ }>;
32
+ /**
33
+ * Resolve a logical path under one snapshot of the current settings.
34
+ *
35
+ * @param path - Original root-relative file path.
36
+ * @param caseInsensitive - Override this lookup's case rule without changing settings.
37
+ * @param allowMissing - Keep a missing write suffix when true; defaults to true.
38
+ * @returns The physical target, or `null` for an absent path when `allowMissing`
39
+ * is false in NFC mode. Legacy mode returns the validated original path.
40
+ * @throws Propagates NFC ambiguity and reader failures, and rejects invalid input.
41
+ */
42
+ resolveLogicalPath(path: FilePath, caseInsensitive?: boolean, allowMissing?: boolean): Promise<StoragePath | null>;
43
+ /** Check whether the logical path exists under the current Unicode policy. */
44
+ exists(path: string, caseInsensitive?: boolean): Promise<boolean>;
45
+ /** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
46
+ trystat(path: string, caseInsensitive?: boolean): Promise<import("../../common/types.js").UXStat>;
47
+ /** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
48
+ stat(path: string, caseInsensitive?: boolean): Promise<import("../../common/types.js").UXStat>;
49
+ /** Create a directory at the resolved write target. */
50
+ mkdir(path: string, caseInsensitive?: boolean): Promise<void>;
51
+ /** Remove the entry at its resolved physical path. */
52
+ remove(path: string, caseInsensitive?: boolean): Promise<void>;
53
+ /** Read text from the resolved physical file. */
54
+ read(path: string, caseInsensitive?: boolean): Promise<string>;
55
+ /** Read bytes from the resolved physical file. */
56
+ readBinary(path: string, caseInsensitive?: boolean): Promise<ArrayBuffer>;
57
+ /** Write text at the resolved target while retaining existing physical spelling. */
58
+ write(path: string, data: string, options?: Parameters<NodeStorageAdapter["write"]>[2], caseInsensitive?: boolean): Promise<void>;
59
+ /** Write bytes at the resolved target while retaining existing physical spelling. */
60
+ writeBinary(path: string, data: ArrayBuffer, options?: Parameters<NodeStorageAdapter["writeBinary"]>[2], caseInsensitive?: boolean): Promise<void>;
61
+ /** Append text at the resolved target using the host adapter semantics. */
62
+ append(path: string, data: string, options?: Parameters<NodeStorageAdapter["append"]>[2], caseInsensitive?: boolean): Promise<void>;
63
+ /** Resolve and perform an explicit rename, including a requested leaf spelling change. */
64
+ rename(sourcePath: string, targetPath: string, caseInsensitive?: boolean): Promise<void>;
65
+ /** List logical directory contents, checking equivalent child names in NFC mode. */
66
+ list(basePath: string, caseInsensitive?: boolean): Promise<{
67
+ files: string[];
68
+ folders: string[];
69
+ }>;
70
+ private statWithSettings;
71
+ private requireExistingPath;
72
+ private preflightRequestedPath;
73
+ private listPhysicalDirectory;
74
+ private createResolver;
75
+ private getSettings;
76
+ private getPathNormalizationVersionSnapshot;
77
+ private asStoragePath;
78
+ private missingPathError;
79
+ }