@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
@@ -184,6 +184,7 @@ const KeyIndexOfSettings = {
184
184
  idDerivationKey: 167,
185
185
  pathNormalizationVersion: 168,
186
186
  batchSaveInLiveSync: 169,
187
+ useCouchDBCacheControlHeadersOnly: 170,
187
188
  encryptedIdDerivationKey: -1
188
189
  };
189
190
  export {
@@ -441,6 +441,10 @@ export declare const SETTINGS_PROPERTY_POLICY: {
441
441
  readonly markdown: "include";
442
442
  readonly persistence: "include";
443
443
  };
444
+ readonly useCouchDBCacheControlHeadersOnly: {
445
+ readonly markdown: "include";
446
+ readonly persistence: "include";
447
+ };
444
448
  readonly accessKey: {
445
449
  readonly markdown: "credentials";
446
450
  readonly persistence: "connection";
@@ -111,6 +111,7 @@ const SETTINGS_PROPERTY_POLICY = {
111
111
  jwtSub: SettingPolicies.ConnectionCredentials,
112
112
  jwtExpDuration: SettingPolicies.Default,
113
113
  useRequestAPI: SettingPolicies.Default,
114
+ useCouchDBCacheControlHeadersOnly: SettingPolicies.Default,
114
115
  accessKey: SettingPolicies.ConnectionCredentials,
115
116
  secretKey: SettingPolicies.ConnectionCredentials,
116
117
  bucket: SettingPolicies.LocalConnection,
@@ -38,6 +38,8 @@ export interface CouchDBConnection {
38
38
  * Seems stable, so promoted to the normal setting.
39
39
  */
40
40
  useRequestAPI: boolean;
41
+ /** Control CouchDB HTTP caching through headers alone. Defaults to false. */
42
+ useCouchDBCacheControlHeadersOnly?: boolean;
41
43
  }
42
44
  /**
43
45
  * Interface representing the settings for periodic replication.
@@ -557,7 +559,7 @@ export interface P2PConnectionInfo {
557
559
  */
558
560
  P2P_useDiagRTC?: boolean;
559
561
  }
560
- export interface P2PSyncSetting extends P2PConnectionInfo {
562
+ export interface P2PSyncSetting extends P2PConnectionInfo, Partial<PathIdentitySettings> {
561
563
  P2P_AutoAccepting: AutoAccepting;
562
564
  P2P_AutoSyncPeers: string;
563
565
  P2P_AutoWatchPeers: string;
@@ -66,8 +66,7 @@ const configurationNames = {
66
66
  },
67
67
  pathNormalizationVersion: {
68
68
  name: "Path identity mode",
69
- desc: "CouchDB only. Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild both databases from a complete authoritative Vault and Fetch on the other devices. Resolve colliding file names first. P2P and Object Storage do not support version 1.",
70
- isAdvanced: true
69
+ desc: "Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild from a complete authoritative Vault and Fetch on the other devices. With P2P, Rebuild prepares only the source device's local database. Resolve colliding file names first."
71
70
  },
72
71
  doNotUseFixedRevisionForChunks: {
73
72
  name: "Content-derived chunk revisions (obsolete setting)",
@@ -120,8 +120,8 @@ export declare const TweakValuesRecommendedTemplate: Partial<ObsidianLiveSyncSet
120
120
  export declare const TweakValuesDefault: Partial<ObsidianLiveSyncSettings>;
121
121
  export declare const TweakValuesTemplate: {
122
122
  tweakModified: number;
123
- usePluginSyncV2: boolean;
124
123
  pathNormalizationVersion: import("./setting.type.js").PathNormalizationVersion;
124
+ usePluginSyncV2: boolean;
125
125
  encrypt: boolean;
126
126
  idDerivationVersion: 0 | 1;
127
127
  usePathObfuscation: boolean;
@@ -238,6 +238,7 @@ export declare const TweakValuesTemplate: {
238
238
  jwtSub?: string;
239
239
  jwtExpDuration?: number;
240
240
  useRequestAPI?: boolean;
241
+ useCouchDBCacheControlHeadersOnly?: boolean;
241
242
  accessKey?: string;
242
243
  secretKey?: string;
243
244
  bucket?: string;
@@ -0,0 +1,4 @@
1
+ import { ICHeader, ICXHeader, PSCHeader } from "./models/fileaccess.const.js";
2
+ export type PathNamespace = "" | typeof ICHeader | typeof ICXHeader | typeof PSCHeader;
3
+ /** Separate one recognised Metadata namespace while retaining the body's spelling. */
4
+ export declare function splitPathNamespace(path: string): readonly [PathNamespace, string];
@@ -0,0 +1,10 @@
1
+ import { ICHeader, ICXHeader, PSCHeader } from "./models/fileaccess.const.js";
2
+ function splitPathNamespace(path) {
3
+ for (const prefix of [ICHeader, ICXHeader, PSCHeader]) {
4
+ if (path.startsWith(prefix)) return [prefix, path.slice(prefix.length)];
5
+ }
6
+ return ["", path];
7
+ }
8
+ export {
9
+ splitPathNamespace
10
+ };
@@ -1,4 +1,6 @@
1
1
  import type { PathNormalizationVersion } from "./models/setting.type.js";
2
+ /** Reject unsupported path identity versions while allowing an absent legacy value. */
3
+ export declare function assertSupportedPathNormalizationVersion(version: number | undefined): asserts version is PathNormalizationVersion | undefined;
2
4
  /** Resolve the stored path identity version, treating an absent legacy value as version zero. */
3
5
  export declare function getPathNormalizationVersion(settings: {
4
6
  pathNormalizationVersion?: number;
@@ -1,13 +1,18 @@
1
+ function assertSupportedPathNormalizationVersion(version) {
2
+ if (version === void 0 || version === 0 || version === 1) return;
3
+ throw new Error(`Unsupported path normalisation version: ${version}`);
4
+ }
1
5
  function getPathNormalizationVersion(settings) {
2
6
  const version = settings.pathNormalizationVersion === void 0 ? 0 : settings.pathNormalizationVersion;
3
- if (version === 0 || version === 1) return version;
4
- throw new Error(`Unsupported path normalisation version: ${version}`);
7
+ assertSupportedPathNormalizationVersion(version);
8
+ return version;
5
9
  }
6
10
  function normalisePathForIdentity(path, version) {
7
11
  const resolvedVersion = getPathNormalizationVersion({ pathNormalizationVersion: version });
8
12
  return resolvedVersion === 1 ? path.normalize("NFC") : path;
9
13
  }
10
14
  export {
15
+ assertSupportedPathNormalizationVersion,
11
16
  getPathNormalizationVersion,
12
17
  normalisePathForIdentity
13
18
  };
@@ -126,6 +126,7 @@ export declare const AllSettingDefault: {
126
126
  jwtSub: string;
127
127
  jwtExpDuration: number;
128
128
  useRequestAPI: boolean;
129
+ useCouchDBCacheControlHeadersOnly?: boolean;
129
130
  accessKey: string;
130
131
  secretKey: string;
131
132
  bucket: string;
@@ -375,6 +375,10 @@ const SettingInformation = {
375
375
  name: "Use Request API to avoid `inevitable` CORS problem",
376
376
  desc: "If enabled, the request API will be used to avoid `inevitable` CORS problems. This is a workaround and may not work in all cases. PLEASE READ THE DOCUMENTATION BEFORE USING THIS OPTION. This is a less-secure option."
377
377
  },
378
+ useCouchDBCacheControlHeadersOnly: {
379
+ name: "Use headers only for HTTP cache control (compatibility)",
380
+ desc: "Remove the query parameter used to invalidate cached responses (the same URL behaviour as in 1.0.44 and earlier). Enable only if your CouchDB server or proxy rejects the additional cache query parameter. Older cached encrypted responses may then be reused after a remote Rebuild."
381
+ },
378
382
  hideFileWarningNotice: {
379
383
  name: "Show status icon instead of file warnings banner",
380
384
  desc: "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown."
@@ -1,5 +1,6 @@
1
1
  import { type DocumentID } from "../common/types.js";
2
2
  import { type ChunkManager } from "./ChunkManager.js";
3
+ import type { ChunkFetchSource } from "./chunkFetchSource.js";
3
4
  import type { IReplicatorService, ISettingService } from "../services/base/IService.js";
4
5
  export declare const EVENT_MISSING_CHUNKS = "missingChunks";
5
6
  export declare const EVENT_MISSING_CHUNK_REMOTE = "missingChunkRemote";
@@ -9,6 +10,8 @@ export type ChunkFetcherOptions = {
9
10
  chunkManager: ChunkManager;
10
11
  replicatorService: IReplicatorService;
11
12
  deliveryStallTimeoutMs?: number;
13
+ /** Direct hosts replace both retrieval and acceptance; the default uses the active replicator. */
14
+ source?: ChunkFetchSource;
12
15
  };
13
16
  export declare class ChunkFetcher {
14
17
  options: ChunkFetcherOptions;
@@ -327,20 +327,25 @@ class ChunkFetcher {
327
327
  await this.resolveLocallyAvailableRetries(requestClaims);
328
328
  let pendingIDs = await this.waitBeforeRemoteRequest(requestIDs, requestClaims, localCheckCompletionVersion);
329
329
  if (pendingIDs.length === 0) return;
330
- const replicator = this.options.replicatorService.getActiveReplicator();
331
- if (!replicator) {
332
- Logger("No active replicator was found to request missing chunks.");
333
- return;
334
- }
335
- if (!canFetchRemoteChunks(replicator)) {
336
- Logger("The active replicator does not support fetching individual remote chunks.");
337
- for (const chunkID of pendingIDs) {
338
- this.chunkManager.emitEvent(EVENT_MISSING_CHUNK_REMOTE, chunkID);
330
+ const requestCompletionVersion = this.finiteCompletionVersion;
331
+ let fetched;
332
+ if (this.options.source) {
333
+ fetched = await this.options.source.fetchChunks(pendingIDs);
334
+ } else {
335
+ const replicator = this.options.replicatorService.getActiveReplicator();
336
+ if (!replicator) {
337
+ Logger("No active replicator was found to request missing chunks.");
338
+ return;
339
339
  }
340
- return;
340
+ if (!canFetchRemoteChunks(replicator)) {
341
+ Logger("The active replicator does not support fetching individual remote chunks.");
342
+ for (const chunkID of pendingIDs) {
343
+ this.chunkManager.emitEvent(EVENT_MISSING_CHUNK_REMOTE, chunkID);
344
+ }
345
+ return;
346
+ }
347
+ fetched = await replicator.fetchRemoteChunks(pendingIDs, false);
341
348
  }
342
- const requestCompletionVersion = this.finiteCompletionVersion;
343
- const fetched = await replicator.fetchRemoteChunks(pendingIDs, false);
344
349
  pendingIDs = this.getActiveClaimIds(pendingIDs, requestClaims);
345
350
  this.touchClaims(pendingIDs, requestClaims);
346
351
  if (!fetched) {
@@ -354,7 +359,9 @@ class ChunkFetcher {
354
359
  this.logFetchedChunks(pendingIDs, classification);
355
360
  const { chunks, missingIds, invalidIds } = classification;
356
361
  try {
357
- if (chunks.length > 0) {
362
+ if (chunks.length > 0 && this.options.source) {
363
+ await this.options.source.storeChunks(chunks);
364
+ } else if (chunks.length > 0) {
358
365
  Logger(`Writing fetched chunks (${chunks.length}) to the database...`);
359
366
  const result = await this.chunkManager.write(
360
367
  chunks,
@@ -2,6 +2,7 @@ import { type EntryDoc } from "../common/types.js";
2
2
  import { ContentSplitter } from "../ContentSplitter/ContentSplitters.js";
3
3
  import { ChangeManager } from "./ChangeManager.js";
4
4
  import { ChunkFetcher } from "./ChunkFetcher.js";
5
+ import type { ChunkFetchSource } from "./chunkFetchSource.js";
5
6
  import { ChunkManager } from "./ChunkManager.js";
6
7
  import { ConflictManager } from "./ConflictManager.js";
7
8
  import { EntryManager } from "./EntryManager/EntryManager.js";
@@ -15,6 +16,7 @@ export interface LiveSyncManagersOptions<TSettingService extends ISettingService
15
16
  pathService: IPathService;
16
17
  replicatorService: IReplicatorService;
17
18
  APIService: IAPIService;
19
+ chunkFetchSource?: ChunkFetchSource;
18
20
  }
19
21
  export declare class LiveSyncManagers {
20
22
  protected _pathService: IPathService;
@@ -76,6 +76,7 @@ class LiveSyncManagers {
76
76
  });
77
77
  const chunkFetcher = new ChunkFetcher({
78
78
  chunkManager,
79
+ source: this.options.chunkFetchSource,
79
80
  replicatorService: this.options.replicatorService,
80
81
  settingService: this.options.settingService
81
82
  });
@@ -0,0 +1,14 @@
1
+ import type { DocumentID, EntryDoc, EntryLeaf } from "../common/types.js";
2
+ /** IO supplied by a host whose chunk source does not use the active replicator. */
3
+ export interface ChunkFetchSource {
4
+ /** Missing identifiers are omitted; false means that this request could not retrieve chunks. */
5
+ fetchChunks(ids: DocumentID[]): Promise<EntryLeaf[] | false>;
6
+ /** Accept fetched chunks before notifying waiting readers. */
7
+ storeChunks(chunks: EntryLeaf[]): Promise<void>;
8
+ }
9
+ /**
10
+ * Read through the prepared direct database, including its decryption transforms.
11
+ * The source already owns these documents, so accepting them performs no write.
12
+ * Construct a new source for each database lifetime.
13
+ */
14
+ export declare function createDirectChunkFetchSource(database: PouchDB.Database<EntryDoc>): ChunkFetchSource;
@@ -0,0 +1,12 @@
1
+ function createDirectChunkFetchSource(database) {
2
+ return {
3
+ async fetchChunks(ids) {
4
+ const result = await database.allDocs({ keys: ids, include_docs: true });
5
+ return result.rows.flatMap((row) => "doc" in row && row.doc ? [row.doc] : []);
6
+ },
7
+ storeChunks: () => Promise.resolve()
8
+ };
9
+ }
10
+ export {
11
+ createDirectChunkFetchSource
12
+ };
@@ -0,0 +1,16 @@
1
+ /** More than one listed entry has the requested logical identity. */
2
+ export declare class AmbiguousNormalisedPathError extends Error {
3
+ /** Path whose logical identity was ambiguous. */
4
+ readonly path: string;
5
+ /** Physical candidate paths copied from the listing which exposed the ambiguity. */
6
+ readonly candidates: readonly string[];
7
+ /**
8
+ * Describe an ambiguous request without choosing a physical candidate.
9
+ *
10
+ * @param path - Path whose logical identity matched multiple entries.
11
+ * @param candidates - Matching physical paths, copied into a frozen array.
12
+ */
13
+ constructor(
14
+ /** Path whose logical identity was ambiguous. */
15
+ path: string, candidates?: readonly string[]);
16
+ }
@@ -0,0 +1,21 @@
1
+ class AmbiguousNormalisedPathError extends Error {
2
+ /**
3
+ * Describe an ambiguous request without choosing a physical candidate.
4
+ *
5
+ * @param path - Path whose logical identity matched multiple entries.
6
+ * @param candidates - Matching physical paths, copied into a frozen array.
7
+ */
8
+ constructor(path, candidates = []) {
9
+ const detail = candidates.length ? ` (${candidates.map((name) => JSON.stringify(name)).join(", ")})` : "";
10
+ super(`Ambiguous path after Unicode normalisation: ${path}${detail}`);
11
+ this.path = path;
12
+ this.name = "AmbiguousNormalisedPathError";
13
+ this.candidates = Object.freeze([...candidates]);
14
+ }
15
+ path;
16
+ /** Physical candidate paths copied from the listing which exposed the ambiguity. */
17
+ candidates;
18
+ }
19
+ export {
20
+ AmbiguousNormalisedPathError
21
+ };
@@ -0,0 +1,15 @@
1
+ import type { FilePath, ResolvedRename, StorageEntry, StoragePath, StoragePathResolver as ResolverContract, StoragePathResolverOptions } from "./pathTypes.js";
2
+ /** Resolves listed names using one immutable NFC policy and no retained directory cache. */
3
+ export declare class StoragePathResolver implements ResolverContract {
4
+ private readonly readDirectory;
5
+ private readonly policy;
6
+ constructor({ readDirectory, policy }: StoragePathResolverOptions);
7
+ resolveExisting(path: FilePath): Promise<StoragePath | null>;
8
+ resolveForWrite(path: FilePath): Promise<StoragePath>;
9
+ resolveRename(source: FilePath, target: FilePath): Promise<ResolvedRename | null>;
10
+ listResolvedDirectory(path: StoragePath): Promise<readonly StorageEntry[] | null>;
11
+ private resolve;
12
+ private findEntry;
13
+ private readEntries;
14
+ private key;
15
+ }
@@ -0,0 +1,113 @@
1
+ import { getPathNormalizationVersion } from "../common/pathNormalization.js";
2
+ import { AmbiguousNormalisedPathError } from "./AmbiguousNormalisedPathError.js";
3
+ import { getStoragePathComparisonKey } from "./pathComparison.js";
4
+ import { validateStoragePath } from "./storagePath.js";
5
+ function storagePath(path) {
6
+ return validateStoragePath(path);
7
+ }
8
+ function childPath(parent, name) {
9
+ return storagePath(parent === "" ? name : `${parent}/${name}`);
10
+ }
11
+ function segments(path) {
12
+ validateStoragePath(path);
13
+ return path.split("/").filter(Boolean);
14
+ }
15
+ class StoragePathResolver {
16
+ readDirectory;
17
+ policy;
18
+ constructor({ readDirectory, policy }) {
19
+ if (getPathNormalizationVersion(policy) !== 1) {
20
+ throw new Error("Storage path resolution requires NFC path identity");
21
+ }
22
+ if (typeof policy.handleFilenameCaseSensitive !== "boolean") {
23
+ throw new TypeError("Filename case handling must be explicit");
24
+ }
25
+ this.readDirectory = readDirectory;
26
+ this.policy = Object.freeze({ ...policy });
27
+ }
28
+ async resolveExisting(path) {
29
+ return await this.resolve(path, false);
30
+ }
31
+ async resolveForWrite(path) {
32
+ const resolved = await this.resolve(path, true);
33
+ if (resolved === null) throw new Error(`Could not resolve storage target: ${path}`);
34
+ return resolved;
35
+ }
36
+ async resolveRename(source, target) {
37
+ validateStoragePath(source, false);
38
+ validateStoragePath(target, false);
39
+ const resolvedSource = await this.resolveExisting(source);
40
+ if (resolvedSource === null) return null;
41
+ const resolvedTarget = await this.resolveForWrite(target);
42
+ if (resolvedTarget !== resolvedSource || target === resolvedSource) {
43
+ return { source: resolvedSource, target: resolvedTarget };
44
+ }
45
+ const requestedParts = segments(target);
46
+ const leaf = requestedParts[requestedParts.length - 1];
47
+ const parent = storagePath(resolvedTarget.slice(0, Math.max(0, resolvedTarget.lastIndexOf("/"))));
48
+ return { source: resolvedSource, target: childPath(parent, leaf) };
49
+ }
50
+ async listResolvedDirectory(path) {
51
+ validateStoragePath(path);
52
+ const entries = await this.readEntries(path);
53
+ if (entries === null) return null;
54
+ const names = /* @__PURE__ */ new Map();
55
+ return entries.map((entry) => {
56
+ const actualPath = childPath(path, entry.name);
57
+ const key = this.key(entry.name);
58
+ const previous = names.get(key);
59
+ if (previous !== void 0) {
60
+ throw new AmbiguousNormalisedPathError(path, [previous, actualPath]);
61
+ }
62
+ names.set(key, actualPath);
63
+ return { path: actualPath, kind: entry.kind };
64
+ });
65
+ }
66
+ async resolve(path, allowMissing) {
67
+ const parts = segments(path);
68
+ let parent = storagePath("");
69
+ if (parts.length === 0) {
70
+ return allowMissing || await this.readEntries(parent) !== null ? parent : null;
71
+ }
72
+ for (let index = 0; index < parts.length; index++) {
73
+ const entries = await this.readEntries(parent);
74
+ const match = entries === null ? void 0 : this.findEntry(entries, parts[index], parent, path);
75
+ if (!match) {
76
+ return allowMissing ? childPath(parent, parts.slice(index).join("/")) : null;
77
+ }
78
+ parent = childPath(parent, match.name);
79
+ if (index < parts.length - 1 && match.kind !== "directory") {
80
+ throw new Error(`A storage path parent is not a directory: ${parent}`);
81
+ }
82
+ }
83
+ return parent;
84
+ }
85
+ findEntry(entries, name, parent, requestedPath) {
86
+ const key = this.key(name);
87
+ const matches = entries.filter((entry) => this.key(entry.name) === key);
88
+ if (matches.length > 1) {
89
+ throw new AmbiguousNormalisedPathError(
90
+ requestedPath,
91
+ matches.map((entry) => childPath(parent, entry.name))
92
+ );
93
+ }
94
+ return matches[0];
95
+ }
96
+ async readEntries(path) {
97
+ const entries = await this.readDirectory(path);
98
+ if (entries === null) return null;
99
+ for (const entry of entries) {
100
+ if (typeof entry.name !== "string" || entry.name === "" || entry.name === "." || entry.name === ".." || /[\/\\\0]/u.test(entry.name) || entry.kind !== "file" && entry.kind !== "directory") {
101
+ throw new TypeError(`Invalid directory entry below ${JSON.stringify(path)}`);
102
+ }
103
+ childPath(path, entry.name);
104
+ }
105
+ return entries;
106
+ }
107
+ key(name) {
108
+ return getStoragePathComparisonKey(name, this.policy);
109
+ }
110
+ }
111
+ export {
112
+ StoragePathResolver
113
+ };
@@ -0,0 +1,74 @@
1
+ import type { UXDataWriteOptions } from "../../common/types.js";
2
+ import type { IStorageAdapter } from "../../serviceModules/adapters/IStorageAdapter.js";
3
+ import type { FilePath, StoragePath } from "../pathTypes.js";
4
+ import { type FileSystemAccessStorageStat } from "./storage.js";
5
+ /**
6
+ * Resolve NFC-equivalent logical paths against current File System Access directory names.
7
+ *
8
+ * @remarks
9
+ * Each operation snapshots Unicode and case settings. Version `0` delegates to
10
+ * the existing adapter; version `1` resolves names and rejects observed aliases
11
+ * before I/O. Physical spelling and the host adapter's mutation semantics remain
12
+ * unchanged. Resolution does not reserve paths against concurrent changes.
13
+ */
14
+ export declare class UnicodeResolvingFileSystemAccessStorageAdapter implements IStorageAdapter<FileSystemAccessStorageStat> {
15
+ private readonly getPathNormalizationVersion;
16
+ private readonly isCaseInsensitive;
17
+ private readonly storage;
18
+ /**
19
+ * Bind a host-owned root and callbacks for each operation's comparison settings.
20
+ *
21
+ * @param rootHandle - Host-selected directory handle with the required permissions.
22
+ * @param getPathNormalizationVersion - Supplies `0` or `1`; defaults to Legacy `0`.
23
+ * @param isCaseInsensitive - Whether comparisons ignore case; defaults to false.
24
+ */
25
+ constructor(rootHandle: FileSystemDirectoryHandle, getPathNormalizationVersion?: () => number, isCaseInsensitive?: () => boolean);
26
+ /** Whether the current settings select NFC path resolution. */
27
+ get isUnicodePathNormalizationEnabled(): boolean;
28
+ /**
29
+ * Resolve a logical path under one snapshot of the current settings.
30
+ *
31
+ * @param path - Original root-relative file path.
32
+ * @param caseInsensitive - Override this lookup's case rule without changing settings.
33
+ * @param allowMissing - Keep a missing write suffix when true; defaults to true.
34
+ * @returns The physical target, or `null` for an absent path when `allowMissing`
35
+ * is false in NFC mode. Legacy mode returns the validated original path.
36
+ * @throws Propagates NFC ambiguity and reader failures, and rejects invalid input.
37
+ */
38
+ resolveLogicalPath(path: FilePath, caseInsensitive?: boolean, allowMissing?: boolean): Promise<StoragePath | null>;
39
+ /** Read metadata for a physical path without performing another logical lookup. */
40
+ statExact(path: StoragePath): Promise<FileSystemAccessStorageStat | null>;
41
+ /** List a physical directory, rejecting equivalent child names in NFC mode. */
42
+ listExact(path: StoragePath): Promise<{
43
+ files: string[];
44
+ folders: string[];
45
+ }>;
46
+ /** Check whether the logical path exists under the current Unicode policy. */
47
+ exists(path: string): Promise<boolean>;
48
+ /** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
49
+ trystat(path: string): Promise<FileSystemAccessStorageStat | null>;
50
+ /** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
51
+ stat(path: string): Promise<FileSystemAccessStorageStat | null>;
52
+ /** Create a directory at the resolved write target. */
53
+ mkdir(path: string): Promise<void>;
54
+ /** Remove the entry at its resolved physical path. */
55
+ remove(path: string): Promise<void>;
56
+ /** Read text from the resolved physical file. */
57
+ read(path: string): Promise<string>;
58
+ /** Read bytes from the resolved physical file. */
59
+ readBinary(path: string): Promise<ArrayBuffer>;
60
+ /** Write text at the resolved target while retaining existing physical spelling. */
61
+ write(path: string, data: string, options?: UXDataWriteOptions): Promise<void>;
62
+ /** Write bytes at the resolved target while retaining existing physical spelling. */
63
+ writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void>;
64
+ /** Append text at the resolved target using the host adapter semantics. */
65
+ append(path: string, data: string, options?: UXDataWriteOptions): Promise<void>;
66
+ /** List logical directory contents, checking equivalent child names in NFC mode. */
67
+ list(basePath: string): Promise<{
68
+ files: string[];
69
+ folders: string[];
70
+ }>;
71
+ private snapshotPolicy;
72
+ private createResolver;
73
+ private groupStorageEntries;
74
+ }