@vrtmrz/livesync-commonlib 0.1.0-rc.1 → 0.1.0-rc.11

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 (141) hide show
  1. package/README.md +90 -20
  2. package/dist/API/DirectFileManipulatorV2.d.ts +3 -0
  3. package/dist/common/configForDoc.d.ts +1 -0
  4. package/dist/common/configForDoc.js +24 -7
  5. package/dist/common/models/setting.const.defaults.d.ts +19 -0
  6. package/dist/common/models/setting.const.defaults.js +21 -2
  7. package/dist/common/models/setting.const.preferred.js +1 -2
  8. package/dist/common/models/setting.lifecycle.d.ts +33 -0
  9. package/dist/common/models/setting.lifecycle.js +121 -0
  10. package/dist/common/models/setting.type.d.ts +10 -4
  11. package/dist/common/models/shared.definition.configNames.js +3 -3
  12. package/dist/common/models/tweak.definition.d.ts +1 -1
  13. package/dist/common/models/tweak.definition.js +1 -6
  14. package/dist/common/settingConstants.d.ts +3 -2
  15. package/dist/common/settingConstants.js +6 -6
  16. package/dist/common/types.d.ts +4 -2
  17. package/dist/common/types.js +16 -1
  18. package/dist/context.d.ts +2 -1
  19. package/dist/context.js +4 -0
  20. package/dist/interfaces/Confirm.d.ts +10 -2
  21. package/dist/interfaces/DatabaseFileAccess.d.ts +8 -0
  22. package/dist/interfaces/DatabaseRebuilder.d.ts +12 -2
  23. package/dist/interfaces/FileReflectionProvenance.d.ts +32 -0
  24. package/dist/interfaces/FileReflectionProvenance.js +25 -0
  25. package/dist/managers/ConflictManager.js +42 -9
  26. package/dist/managers/EntryManager/EntryManager.d.ts +1 -0
  27. package/dist/managers/EntryManager/EntryManager.js +5 -1
  28. package/dist/managers/EntryManager/EntryManagerImpls.d.ts +1 -0
  29. package/dist/managers/EntryManager/EntryManagerImpls.js +29 -1
  30. package/dist/platform/node/storage.d.ts +10 -1
  31. package/dist/platform/node/storage.js +86 -25
  32. package/dist/pouchdb/LiveSyncLocalDB.d.ts +1 -0
  33. package/dist/pouchdb/LiveSyncLocalDB.js +3 -0
  34. package/dist/remoteConfigurations.d.ts +9 -0
  35. package/dist/remoteConfigurations.js +14 -0
  36. package/dist/replication/LiveSyncAbstractReplicator.d.ts +3 -3
  37. package/dist/replication/couchdb/LiveSyncReplicator.d.ts +0 -1
  38. package/dist/replication/couchdb/LiveSyncReplicator.js +0 -3
  39. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +0 -1
  40. package/dist/replication/journal/LiveSyncJournalReplicator.js +0 -3
  41. package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +5 -3
  42. package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +42 -30
  43. package/dist/replication/trystero/P2PReplicatorCore.d.ts +2 -6
  44. package/dist/replication/trystero/P2PReplicatorCore.js +16 -86
  45. package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -1
  46. package/dist/replication/trystero/TrysteroReplicator.js +10 -4
  47. package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +4 -5
  48. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +22 -30
  49. package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -2
  50. package/dist/replication/trystero/addP2PEventHandlers.d.ts +4 -2
  51. package/dist/replication/trystero/addP2PEventHandlers.js +9 -10
  52. package/dist/replication/trystero/types.d.ts +2 -0
  53. package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +1 -1
  54. package/dist/replication/trystero/useP2PReplicatorCommands.js +3 -1
  55. package/dist/replication/trystero/useP2PReplicatorFeature.js +25 -15
  56. package/dist/rpc/RpcRoom.d.ts +1 -1
  57. package/dist/rpc/RpcRoom.js +22 -16
  58. package/dist/rpc/transports/TrysteroTransport.d.ts +2 -0
  59. package/dist/rpc/transports/TrysteroTransport.js +24 -17
  60. package/dist/rpc/transports/trysteroRoomEvents.d.ts +14 -0
  61. package/dist/rpc/transports/trysteroRoomEvents.js +34 -0
  62. package/dist/serviceFeatures/checkRemoteSize.d.ts +5 -3
  63. package/dist/serviceFeatures/checkRemoteSize.js +51 -11
  64. package/dist/serviceFeatures/remoteConfig.d.ts +37 -1
  65. package/dist/serviceFeatures/remoteConfig.js +90 -0
  66. package/dist/serviceModules/Rebuilder.d.ts +3 -6
  67. package/dist/serviceModules/Rebuilder.js +31 -22
  68. package/dist/serviceModules/ServiceDatabaseFileAccessBase.d.ts +5 -0
  69. package/dist/serviceModules/ServiceDatabaseFileAccessBase.js +69 -13
  70. package/dist/serviceModules/ServiceFileHandlerBase.d.ts +20 -0
  71. package/dist/serviceModules/ServiceFileHandlerBase.js +224 -40
  72. package/dist/services/BrowserServices.d.ts +2 -0
  73. package/dist/services/BrowserServices.js +4 -1
  74. package/dist/services/HeadlessServices.d.ts +2 -0
  75. package/dist/services/HeadlessServices.js +2 -1
  76. package/dist/services/base/CommonlibMessages.d.ts +318 -0
  77. package/dist/services/base/CommonlibMessages.js +313 -0
  78. package/dist/services/base/IService.d.ts +13 -6
  79. package/dist/services/base/KeyValueDBService.d.ts +1 -1
  80. package/dist/services/base/KeyValueDBService.js +3 -1
  81. package/dist/services/base/MessageTranslator.d.ts +5 -2
  82. package/dist/services/base/MessageTranslator.js +21 -0
  83. package/dist/services/base/ReplicationService.d.ts +4 -2
  84. package/dist/services/base/ReplicationService.js +11 -16
  85. package/dist/services/base/ServiceBase.d.ts +1 -1
  86. package/dist/services/base/ServiceBase.js +2 -4
  87. package/dist/services/base/SettingService.d.ts +17 -9
  88. package/dist/services/base/SettingService.js +38 -19
  89. package/dist/services/implements/headless/HeadlessAPIService.d.ts +3 -3
  90. package/dist/services/implements/headless/HeadlessAPIService.js +2 -2
  91. package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +3 -3
  92. package/dist/settings.d.ts +10 -0
  93. package/dist/settings.js +32 -0
  94. package/dist/worker/bgWorker.js +1 -1
  95. package/docs/conflict-resolution.md +161 -0
  96. package/docs/development.md +106 -7
  97. package/docs/p2p-transport-lifecycle.md +62 -0
  98. package/docs/platform-storage.md +4 -0
  99. package/docs/proven-in-use.md +65 -0
  100. package/docs/releasing.md +26 -6
  101. package/docs/remote-configurations.md +63 -0
  102. package/docs/settings-lifecycle.md +85 -0
  103. package/package.json +17 -27
  104. package/dist/common/i18n.d.ts +0 -18
  105. package/dist/common/i18n.js +0 -7193
  106. package/dist/common/messages/combinedMessages.dev.d.ts +0 -7
  107. package/dist/common/messages/combinedMessages.dev.js +0 -44
  108. package/dist/common/messages/combinedMessages.prod.d.ts +0 -9264
  109. package/dist/common/messages/combinedMessages.prod.js +0 -9271
  110. package/dist/common/messages/de.d.ts +0 -297
  111. package/dist/common/messages/de.js +0 -7
  112. package/dist/common/messages/def.d.ts +0 -1119
  113. package/dist/common/messages/def.js +0 -7
  114. package/dist/common/messages/es.d.ts +0 -689
  115. package/dist/common/messages/es.js +0 -7
  116. package/dist/common/messages/fr.d.ts +0 -582
  117. package/dist/common/messages/fr.js +0 -7
  118. package/dist/common/messages/he.d.ts +0 -583
  119. package/dist/common/messages/he.js +0 -7
  120. package/dist/common/messages/ja.d.ts +0 -838
  121. package/dist/common/messages/ja.js +0 -7
  122. package/dist/common/messages/ko.d.ts +0 -800
  123. package/dist/common/messages/ko.js +0 -7
  124. package/dist/common/messages/ru.d.ts +0 -858
  125. package/dist/common/messages/ru.js +0 -7
  126. package/dist/common/messages/zh-tw.d.ts +0 -384
  127. package/dist/common/messages/zh-tw.js +0 -7
  128. package/dist/common/messages/zh.d.ts +0 -1093
  129. package/dist/common/messages/zh.js +0 -7
  130. package/dist/common/messagesJson/de.json +0 -295
  131. package/dist/common/messagesJson/en.json +0 -1117
  132. package/dist/common/messagesJson/es.json +0 -687
  133. package/dist/common/messagesJson/fr.json +0 -580
  134. package/dist/common/messagesJson/he.json +0 -581
  135. package/dist/common/messagesJson/ja.json +0 -836
  136. package/dist/common/messagesJson/ko.json +0 -798
  137. package/dist/common/messagesJson/ru.json +0 -856
  138. package/dist/common/messagesJson/zh-tw.json +0 -382
  139. package/dist/common/messagesJson/zh.json +0 -1091
  140. package/dist/common/rosetta.d.ts +0 -42
  141. package/dist/common/rosetta.js +0 -57
@@ -1,4 +1,4 @@
1
- import { $t } from "./i18n.js";
1
+ import { englishMessageTranslator } from "../services/base/MessageTranslator.js";
2
2
  import {
3
3
  DEFAULT_SETTINGS,
4
4
  configurationNames
@@ -324,12 +324,12 @@ const SettingInformation = {
324
324
  desc: "If this enabled, All files are handled as case-Sensitive (Previous behaviour)."
325
325
  },
326
326
  doNotUseFixedRevisionForChunks: {
327
- name: "Compute revisions for chunks",
328
- desc: "If this enabled, all chunks will be stored with the revision made from its content."
327
+ name: "Content-derived chunk revisions (obsolete setting)",
328
+ desc: "Chunk revisions are always derived from their content. This stored key is retained only for compatibility."
329
329
  },
330
330
  sendChunksBulkMaxSize: {
331
- name: "Maximum size of chunks to send in one request",
332
- desc: "MB"
331
+ name: "Maximum request size for manually resending chunks",
332
+ desc: "MB per request"
333
333
  },
334
334
  useAdvancedMode: {
335
335
  name: "Enable advanced features"
@@ -399,7 +399,7 @@ const SettingInformation = {
399
399
  desc: "Files with modification times greater than this value (in seconds since the Unix epoch) will not have their events reflected. Set to 0 to disable this limit."
400
400
  }
401
401
  };
402
- function translateInfo(infoSrc, translate = (message) => $t(message)) {
402
+ function translateInfo(infoSrc, translate = englishMessageTranslator) {
403
403
  if (!infoSrc) return false;
404
404
  const info = { ...infoSrc };
405
405
  info.name = translate(info.name);
@@ -10,7 +10,8 @@ import { IDPrefixes, LEAF_WAIT_ONLY_REMOTE, LEAF_WAIT_TIMEOUT, LEAF_WAIT_TIMEOUT
10
10
  import { AutoAccepting, type BucketSyncSetting, type ChunkSplitterVersion, type ConfigPassphraseStore, type CouchDBConnection, type E2EEAlgorithm, type EncryptionSettings, type HashAlgorithm, type HasSettings, type LocalDBSettings, type ObsidianLiveSyncSettings, type P2PConnectionInfo, type P2PSyncSetting, type PluginSyncSettingEntry, type RemoteDBSettings, type RemoteType, type RemoteTypeSettings, type SYNC_MODE } from "./models/setting.type.js";
11
11
  import { ChunkAlgorithmNames, ChunkAlgorithms, CURRENT_SETTING_VERSION, E2EEAlgorithmNames, E2EEAlgorithms, HashAlgorithms, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, RemoteTypes, SETTING_VERSION_INITIAL, SETTING_VERSION_SUPPORT_CASE_INSENSITIVE, MODE_AUTOMATIC, MODE_PAUSED, MODE_SELECTIVE, MODE_SHINY } from "./models/setting.const.js";
12
12
  import { PREFERRED_BASE, PREFERRED_JOURNAL_SYNC, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED } from "./models/setting.const.preferred.js";
13
- import { P2P_DEFAULT_SETTINGS, DEFAULT_SETTINGS } from "./models/setting.const.defaults.js";
13
+ import { P2P_DEFAULT_SETTINGS, DEFAULT_SETTINGS, NEW_VAULT_SETTINGS, SETTINGS_SCHEMA_DEFAULTS, createNewVaultSettings } from "./models/setting.const.defaults.js";
14
+ import { prepareSettingsForLoad, SettingsMigrationReviewCodes, type PreparedSettings, type SettingsMigrationReviewCode, type SettingsMigrationReviewReason, type SettingsMigrationState } from "./models/setting.lifecycle.js";
14
15
  import { KeyIndexOfSettings } from "./models/setting.const.qr.js";
15
16
  import type { DeviceInfo, EntryBody, EntryDoc, EntryDocResponse, EntryMilestoneInfo, EntryNodeInfo, NodeData, NodeKey } from "./models/db.definition.js";
16
17
  import { isMetaEntry } from "./models/db.definition.js";
@@ -42,7 +43,8 @@ export type { EncryptionSettings };
42
43
  export { HashAlgorithms, type HashAlgorithm, ChunkAlgorithmNames, ChunkAlgorithms, type ChunkSplitterVersion };
43
44
  export type { RemoteDBSettings };
44
45
  export type { ObsidianLiveSyncSettings };
45
- export { DEFAULT_SETTINGS };
46
+ export { DEFAULT_SETTINGS, NEW_VAULT_SETTINGS, SETTINGS_SCHEMA_DEFAULTS, createNewVaultSettings };
47
+ export { prepareSettingsForLoad, SettingsMigrationReviewCodes, type PreparedSettings, type SettingsMigrationReviewCode, type SettingsMigrationReviewReason, type SettingsMigrationState, };
46
48
  export { KeyIndexOfSettings };
47
49
  export { type HasSettings };
48
50
  export { PREFERRED_BASE, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, PREFERRED_JOURNAL_SYNC };
@@ -67,7 +67,17 @@ import {
67
67
  PREFERRED_SETTING_CLOUDANT,
68
68
  PREFERRED_SETTING_SELF_HOSTED
69
69
  } from "./models/setting.const.preferred.js";
70
- import { P2P_DEFAULT_SETTINGS, DEFAULT_SETTINGS } from "./models/setting.const.defaults.js";
70
+ import {
71
+ P2P_DEFAULT_SETTINGS,
72
+ DEFAULT_SETTINGS,
73
+ NEW_VAULT_SETTINGS,
74
+ SETTINGS_SCHEMA_DEFAULTS,
75
+ createNewVaultSettings
76
+ } from "./models/setting.const.defaults.js";
77
+ import {
78
+ prepareSettingsForLoad,
79
+ SettingsMigrationReviewCodes
80
+ } from "./models/setting.lifecycle.js";
71
81
  import { KeyIndexOfSettings } from "./models/setting.const.qr.js";
72
82
  import { isMetaEntry } from "./models/db.definition.js";
73
83
  import {
@@ -165,6 +175,7 @@ export {
165
175
  MODE_PAUSED,
166
176
  MODE_SELECTIVE,
167
177
  MODE_SHINY,
178
+ NEW_VAULT_SETTINGS,
168
179
  NODEINFO_DOCID,
169
180
  NOT_CONFLICTED,
170
181
  NoteTypes,
@@ -190,10 +201,12 @@ export {
190
201
  SALT_OF_ID,
191
202
  SALT_OF_PASSPHRASE,
192
203
  SEED_MURMURHASH,
204
+ SETTINGS_SCHEMA_DEFAULTS,
193
205
  SETTING_KEY_P2P_DEVICE_NAME,
194
206
  SETTING_VERSION_INITIAL,
195
207
  SETTING_VERSION_SUPPORT_CASE_INSENSITIVE,
196
208
  SYNCINFO_ID,
209
+ SettingsMigrationReviewCodes,
197
210
  SuffixDatabaseName,
198
211
  TIME_ARGUMENT_INFINITY,
199
212
  TweakValuesDefault,
@@ -207,6 +220,8 @@ export {
207
220
  configURIBase,
208
221
  configURIBaseQR,
209
222
  configurationNames,
223
+ createNewVaultSettings,
210
224
  isMetaEntry,
225
+ prepareSettingsForLoad,
211
226
  statusDisplay
212
227
  };
package/dist/context.d.ts CHANGED
@@ -5,5 +5,6 @@
5
5
  */
6
6
  export { ServiceContext, createServiceContext, type ServiceContextContract, type ServiceContextOptions, } from "./services/base/ServiceBase.js";
7
7
  export { createLiveSyncEventHub, type LiveSyncEventHub } from "./hub/hub.js";
8
- export { passthroughMessageTranslator, type MessageTranslator, type TranslationParameters, } from "./services/base/MessageTranslator.js";
8
+ export { englishMessageTranslator, passthroughMessageTranslator, type MessageTranslator, type TranslationParameters, } from "./services/base/MessageTranslator.js";
9
+ export { commonlibEnglishMessages, type CommonlibMessageKey } from "./services/base/CommonlibMessages.js";
9
10
  export type { StandardIo, StandardIoChunk } from "./platform/standardIo.js";
package/dist/context.js CHANGED
@@ -4,11 +4,15 @@ import {
4
4
  } from "./services/base/ServiceBase.js";
5
5
  import { createLiveSyncEventHub } from "./hub/hub.js";
6
6
  import {
7
+ englishMessageTranslator,
7
8
  passthroughMessageTranslator
8
9
  } from "./services/base/MessageTranslator.js";
10
+ import { commonlibEnglishMessages } from "./services/base/CommonlibMessages.js";
9
11
  export {
10
12
  ServiceContext,
13
+ commonlibEnglishMessages,
11
14
  createLiveSyncEventHub,
12
15
  createServiceContext,
16
+ englishMessageTranslator,
13
17
  passthroughMessageTranslator
14
18
  };
@@ -1,3 +1,4 @@
1
+ export type ConfirmActionLayout = "auto" | "vertical";
1
2
  export interface Confirm {
2
3
  askYesNo(message: string): Promise<"yes" | "no">;
3
4
  askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise<string | false>;
@@ -12,6 +13,13 @@ export interface Confirm {
12
13
  defaultAction: T[number];
13
14
  timeout?: number;
14
15
  }): Promise<T[number] | false>;
15
- askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void;
16
- confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>;
16
+ /**
17
+ * Shows a non-blocking message containing one host-rendered action link.
18
+ *
19
+ * Hosts which can display transient UI should keep the message visible for
20
+ * `durationMs`. Headless hosts may report the message without presenting an
21
+ * interactive action.
22
+ */
23
+ askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void, durationMs?: number): void;
24
+ confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number, actionLayout?: ConfirmActionLayout): Promise<(typeof buttons)[number] | false>;
17
25
  }
@@ -2,10 +2,18 @@ import type { FilePathWithPrefix, LoadedEntry, MetaEntry, UXFileInfo, UXFileInfo
2
2
  export interface DatabaseFileAccess {
3
3
  delete: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string) => Promise<boolean>;
4
4
  store: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise<boolean>;
5
+ /** Store a file as a child of an exact revision and return the created revision. */
6
+ storeWithBaseRevision: (file: UXFileInfo, baseRevision: string | undefined, skipCheck?: boolean) => Promise<string | false>;
5
7
  storeAsConflictedRevision: (file: UXFileInfo, currentRev: string, skipCheck?: boolean) => Promise<boolean>;
8
+ /** Preserve unknown storage content as a conflict and return its exact revision. */
9
+ storeAsConflictedRevisionWithResult: (file: UXFileInfo, currentRev: string, skipCheck?: boolean) => Promise<string | false>;
10
+ /** Store a user deletion as a visible logical-deletion child of an exact revision. */
11
+ storeDeletionWithBaseRevision: (file: UXFileInfoStub | FilePathWithPrefix, baseRevision: string) => Promise<string | false>;
6
12
  storeContent(path: FilePathWithPrefix, content: string): Promise<boolean>;
7
13
  createChunks: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise<boolean>;
8
14
  hasContentInRevisionHistory: (file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer, currentRev?: string) => Promise<boolean>;
15
+ /** Return every available revision whose content exactly matches the supplied bytes. */
16
+ findContentRevisions: (file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer, currentRev?: string) => Promise<string[]>;
9
17
  fetch: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean) => Promise<UXFileInfo | false>;
10
18
  fetchEntryFromMeta: (meta: MetaEntry, waitForReady?: boolean, skipCheck?: boolean) => Promise<LoadedEntry | false>;
11
19
  fetchEntryMeta: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, skipCheck?: boolean) => Promise<MetaEntry | false>;
@@ -4,8 +4,18 @@ export interface Rebuilder {
4
4
  $rebuildEverything(): Promise<void>;
5
5
  $fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean): Promise<void>;
6
6
  $fetchLocalDBFast(autoResume: boolean): Promise<void>;
7
- scheduleRebuild(): Promise<void>;
8
- scheduleFetch(): Promise<void>;
7
+ /**
8
+ * Writes the Rebuild flag, suspends the current runtime, optionally prepares
9
+ * persisted state, and requests a restart, in that order.
10
+ *
11
+ * The preparation callback runs only after the flag exists. If it fails,
12
+ * the flag is removed, the current runtime is resumed, and the error is
13
+ * rethrown. A false result means that the flag could not be written and no
14
+ * restart was requested.
15
+ */
16
+ scheduleRebuild(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
17
+ /** See {@link scheduleRebuild}; this variant writes the Fetch flag. */
18
+ scheduleFetch(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
9
19
  /**
10
20
  * Declares the finish of the rebuild process and unlock remote, resume reflecting the changes.
11
21
  */
@@ -0,0 +1,32 @@
1
+ import type { FilePathWithPrefix } from "../common/types.js";
2
+ import type { SimpleStore } from "../common/utils.js";
3
+ export type FileReflectionProvenanceRecord = {
4
+ /** Exact database revision which most recently produced the storage state. */
5
+ revision: string;
6
+ /** Raw modification time observed from this device's storage after reflection. */
7
+ observedStorageMtime?: number;
8
+ };
9
+ export interface FileReflectionProvenance {
10
+ get(path: FilePathWithPrefix): Promise<FileReflectionProvenanceRecord | undefined>;
11
+ set(path: FilePathWithPrefix, record: FileReflectionProvenanceRecord): Promise<void>;
12
+ delete(path: FilePathWithPrefix): Promise<void>;
13
+ move(from: FilePathWithPrefix, to: FilePathWithPrefix): Promise<void>;
14
+ }
15
+ /**
16
+ * Device-local provenance backed by an existing host-owned key-value store.
17
+ *
18
+ * The revision is authoritative. The raw storage mtime is diagnostic and may
19
+ * be used as a fast change hint, but it never proves content or branch identity.
20
+ * The host may construct this object before opening its store, but it must not
21
+ * invoke provenance operations until its normal storage lifecycle is ready.
22
+ * Store failures are reported to the caller rather than hidden by readiness
23
+ * waits, so lifecycle violations and reset races cannot hang file processing.
24
+ */
25
+ export declare class StoredFileReflectionProvenance implements FileReflectionProvenance {
26
+ private readonly store;
27
+ constructor(store: SimpleStore<FileReflectionProvenanceRecord>);
28
+ get(path: FilePathWithPrefix): Promise<FileReflectionProvenanceRecord | undefined>;
29
+ set(path: FilePathWithPrefix, record: FileReflectionProvenanceRecord): Promise<void>;
30
+ delete(path: FilePathWithPrefix): Promise<void>;
31
+ move(from: FilePathWithPrefix, to: FilePathWithPrefix): Promise<void>;
32
+ }
@@ -0,0 +1,25 @@
1
+ class StoredFileReflectionProvenance {
2
+ constructor(store) {
3
+ this.store = store;
4
+ }
5
+ store;
6
+ async get(path) {
7
+ return await this.store.get(path) ?? void 0;
8
+ }
9
+ async set(path, record) {
10
+ await this.store.set(path, record);
11
+ }
12
+ async delete(path) {
13
+ await this.store.delete(path);
14
+ }
15
+ async move(from, to) {
16
+ const record = await this.get(from);
17
+ if (record) {
18
+ await this.set(to, record);
19
+ }
20
+ await this.delete(from);
21
+ }
22
+ }
23
+ export {
24
+ StoredFileReflectionProvenance
25
+ };
@@ -15,6 +15,21 @@ import {
15
15
  isObjectMargeApplicable
16
16
  } from "../common/utils.js";
17
17
  import { isErrorOfMissingDoc } from "../pouchdb/utils_couchdb.js";
18
+ function revisionGeneration(revision) {
19
+ const generation = Number(revision.split("-", 1)[0]);
20
+ return Number.isFinite(generation) ? generation : Number.MAX_SAFE_INTEGER;
21
+ }
22
+ function compareConflictCandidates(left, right) {
23
+ const generationDifference = revisionGeneration(left.revision) - revisionGeneration(right.revision);
24
+ if (generationDifference !== 0) return generationDifference;
25
+ const leftMtime = left.leaf === false || !Number.isFinite(left.leaf.mtime) ? Number.NEGATIVE_INFINITY : left.leaf.mtime;
26
+ const rightMtime = right.leaf === false || !Number.isFinite(right.leaf.mtime) ? Number.NEGATIVE_INFINITY : right.leaf.mtime;
27
+ const mtimeDifference = leftMtime - rightMtime;
28
+ if (mtimeDifference !== 0) return mtimeDifference;
29
+ if (left.revision < right.revision) return -1;
30
+ if (left.revision > right.revision) return 1;
31
+ return 0;
32
+ }
18
33
  class ConflictManager {
19
34
  options;
20
35
  constructor(options) {
@@ -258,13 +273,22 @@ class ConflictManager {
258
273
  }
259
274
  async tryAutoMergeSensibly(path, test, conflicts) {
260
275
  const conflictedRev = conflicts[0];
261
- const conflictedRevNo = Number(conflictedRev.split("-")[0]);
262
- const revFrom = await this.database.get(await this.options.pathService.path2id(path), {
263
- revs_info: true
264
- });
265
- const commonBase = (revFrom._revs_info || []).filter(
266
- (e) => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo
267
- )?.[0]?.rev ?? "";
276
+ let commonBase = "";
277
+ try {
278
+ const documentId = await this.options.pathService.path2id(path);
279
+ const [currentBranch, conflictedBranch] = await Promise.all([
280
+ this.database.get(documentId, { rev: test._rev, revs_info: true }),
281
+ this.database.get(documentId, { rev: conflictedRev, revs_info: true })
282
+ ]);
283
+ const currentAvailable = new Set(
284
+ (currentBranch._revs_info || []).filter((revision) => revision.status === "available").map((revision) => revision.rev)
285
+ );
286
+ commonBase = (conflictedBranch._revs_info || []).filter((revision) => revision.status === "available" && currentAvailable.has(revision.rev)).sort((left, right) => Number(right.rev.split("-")[0]) - Number(left.rev.split("-")[0]))[0]?.rev ?? "";
287
+ } catch (ex) {
288
+ Logger(`Could not determine the common revision for ${path}`, LOG_LEVEL_VERBOSE);
289
+ Logger(ex, LOG_LEVEL_VERBOSE);
290
+ return false;
291
+ }
268
292
  let p = void 0;
269
293
  if (commonBase) {
270
294
  if (isSensibleMargeApplicable(path)) {
@@ -302,9 +326,18 @@ class ConflictManager {
302
326
  if (test == null) return { ok: MISSING_OR_ERROR };
303
327
  if (!test._conflicts) return { ok: NOT_CONFLICTED };
304
328
  if (test._conflicts.length == 0) return { ok: NOT_CONFLICTED };
305
- const conflicts = test._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
329
+ const conflictCandidates = await Promise.all(
330
+ test._conflicts.map(
331
+ async (revision) => ({
332
+ revision,
333
+ leaf: await this.getConflictedDoc(path, revision)
334
+ })
335
+ )
336
+ );
337
+ conflictCandidates.sort(compareConflictCandidates);
338
+ const conflicts = conflictCandidates.map(({ revision }) => revision);
306
339
  const leftLeaf = await this.getConflictedDoc(path, test._rev);
307
- const rightLeaf = await this.getConflictedDoc(path, conflicts[0]);
340
+ const rightLeaf = conflictCandidates[0].leaf;
308
341
  if (leftLeaf !== false && rightLeaf !== false && leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted) {
309
342
  return { leftRev: test._rev, rightRev: conflicts[0], leftLeaf, rightLeaf };
310
343
  }
@@ -33,5 +33,6 @@ export declare class EntryManager {
33
33
  getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise<false | LoadedEntry>;
34
34
  getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise<false | LoadedEntry>;
35
35
  deleteDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise<boolean>;
36
+ storeDeletionAtRevision(path: FilePathWithPrefix | FilePath, baseRevision: string): Promise<false | PouchDB.Core.Response>;
36
37
  putDBEntry(note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise<false | PouchDB.Core.Response>;
37
38
  }
@@ -8,7 +8,8 @@ import {
8
8
  getDBEntryMetaByPath,
9
9
  isTargetFile,
10
10
  prepareChunk,
11
- putDBEntry
11
+ putDBEntry,
12
+ storeDeletionByPathAtRevision
12
13
  } from "./EntryManagerImpls.js";
13
14
  class EntryManager {
14
15
  options;
@@ -64,6 +65,9 @@ class EntryManager {
64
65
  async deleteDBEntry(path, opt) {
65
66
  return await deleteDBEntryByPath(this.serviceHost, this, path, opt);
66
67
  }
68
+ async storeDeletionAtRevision(path, baseRevision) {
69
+ return await storeDeletionByPathAtRevision(this.serviceHost, this, path, baseRevision);
70
+ }
67
71
  async putDBEntry(note, onlyChunks, conflictBaseRev) {
68
72
  return await putDBEntry(this.serviceHost, this, note, onlyChunks, conflictBaseRev);
69
73
  }
@@ -71,4 +71,5 @@ export declare function getDBEntryByPath(host: NecessaryServicesInterfaces<"path
71
71
  datatype: import("../../common/types.js").EntryTypeNotes;
72
72
  } & PouchDB.Core.IdMeta)>;
73
73
  export declare function deleteDBEntryByPath(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase }: NecessaryManagers<"localDatabase">, path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise<boolean>;
74
+ export declare function storeDeletionByPathAtRevision(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase }: NecessaryManagers<"localDatabase">, path: FilePathWithPrefix | FilePath, baseRevision: string): Promise<PouchDB.Core.Response | false>;
74
75
  export {};
@@ -484,6 +484,33 @@ async function deleteDBEntryByPath(host, { localDatabase }, path, opt) {
484
484
  throw ex;
485
485
  }
486
486
  }
487
+ async function storeDeletionByPathAtRevision(host, { localDatabase }, path, baseRevision) {
488
+ if (!isTargetFile(host, path)) {
489
+ return false;
490
+ }
491
+ const id = await host.services.path.path2id(path);
492
+ try {
493
+ return await serialized("file:" + path, async () => {
494
+ const obj = await localDatabase.get(id, {
495
+ rev: baseRevision
496
+ });
497
+ if (obj.type === "leaf") {
498
+ return false;
499
+ }
500
+ delete obj._deleted;
501
+ obj.deleted = true;
502
+ obj.mtime = Date.now();
503
+ const result = await localDatabase.put(obj, { force: true });
504
+ Logger(`Entry soft-deleted from revision: ${path} (${obj._id.substring(0, 8)}-${result.rev})`);
505
+ return result;
506
+ }) ?? false;
507
+ } catch (ex) {
508
+ if (isErrorOfMissingDoc(ex)) {
509
+ return false;
510
+ }
511
+ throw ex;
512
+ }
513
+ }
487
514
  export {
488
515
  canUseOnDemandChunking,
489
516
  computeChunkRetrievalMethod,
@@ -495,5 +522,6 @@ export {
495
522
  isLegacyNote,
496
523
  isTargetFile,
497
524
  prepareChunk,
498
- putDBEntry
525
+ putDBEntry,
526
+ storeDeletionByPathAtRevision
499
527
  };
@@ -2,7 +2,12 @@ import type { UXDataWriteOptions, UXStat } from "../../common/types.js";
2
2
  import type { IStorageAdapter } from "../../serviceModules/adapters/IStorageAdapter.js";
3
3
  export type NodeStorageStat = UXStat;
4
4
  export interface CreateNodeStorageOptions {
5
- /** Absolute or process-relative directory which bounds every adapter operation. */
5
+ /**
6
+ * Host-selected root directory. Adapter paths are resolved relative to this root.
7
+ *
8
+ * The host owns this trust boundary. The adapter is not a filesystem sandbox against
9
+ * an untrusted local process which can replace path components concurrently.
10
+ */
6
11
  rootPath: string;
7
12
  }
8
13
  /** Rooted storage adapter backed by Node's file-system APIs. */
@@ -10,6 +15,7 @@ export declare class NodeStorageAdapter implements IStorageAdapter<NodeStorageSt
10
15
  private readonly rootPath;
11
16
  constructor(rootPath: string);
12
17
  private resolvePath;
18
+ private assertNoSymbolicLinks;
13
19
  exists(path: string): Promise<boolean>;
14
20
  trystat(path: string): Promise<NodeStorageStat | null>;
15
21
  stat(path: string): Promise<NodeStorageStat | null>;
@@ -20,10 +26,13 @@ export declare class NodeStorageAdapter implements IStorageAdapter<NodeStorageSt
20
26
  write(path: string, data: string, options?: UXDataWriteOptions): Promise<void>;
21
27
  writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void>;
22
28
  append(path: string, data: string, options?: UXDataWriteOptions): Promise<void>;
29
+ /** Atomically rename or move an entry without leaving the injected root. */
30
+ rename(sourcePath: string, targetPath: string): Promise<void>;
23
31
  list(basePath: string): Promise<{
24
32
  files: string[];
25
33
  folders: string[];
26
34
  }>;
35
+ private writeFile;
27
36
  private applyTimes;
28
37
  }
29
38
  /** Bind a Node storage adapter to an explicitly selected root directory. */
@@ -1,16 +1,40 @@
1
+ import { constants as nodeFsConstants } from "node:fs";
1
2
  import * as nodeFsPromises from "node:fs/promises";
2
3
  import * as nodePath from "node:path";
3
4
  import { validateStoragePath } from "../storagePath.js";
5
+ const NO_FOLLOW = nodeFsConstants.O_NOFOLLOW ?? 0;
6
+ class SymbolicLinkStoragePathError extends Error {
7
+ constructor(path) {
8
+ super(`Storage paths must not contain symbolic links: ${path}`);
9
+ this.name = "SymbolicLinkStoragePathError";
10
+ }
11
+ }
4
12
  class NodeStorageAdapter {
13
+ rootPath;
5
14
  constructor(rootPath) {
6
- this.rootPath = rootPath;
15
+ this.rootPath = nodePath.resolve(rootPath);
7
16
  }
8
- rootPath;
9
- resolvePath(path, allowRoot = true) {
10
- return nodePath.join(this.rootPath, validateStoragePath(path, allowRoot));
17
+ async resolvePath(path, allowRoot = true) {
18
+ const fullPath = nodePath.join(this.rootPath, validateStoragePath(path, allowRoot));
19
+ await this.assertNoSymbolicLinks(fullPath);
20
+ return fullPath;
21
+ }
22
+ async assertNoSymbolicLinks(fullPath) {
23
+ const relativePath = nodePath.relative(this.rootPath, fullPath);
24
+ let currentPath = this.rootPath;
25
+ for (const segment of relativePath.split(nodePath.sep).filter(Boolean)) {
26
+ currentPath = nodePath.join(currentPath, segment);
27
+ try {
28
+ const stat = await nodeFsPromises.lstat(currentPath);
29
+ if (stat.isSymbolicLink()) throw new SymbolicLinkStoragePathError(currentPath);
30
+ } catch (error) {
31
+ if (error.code === "ENOENT") return;
32
+ throw error;
33
+ }
34
+ }
11
35
  }
12
36
  async exists(path) {
13
- const fullPath = this.resolvePath(path);
37
+ const fullPath = await this.resolvePath(path);
14
38
  try {
15
39
  await nodeFsPromises.access(fullPath);
16
40
  return true;
@@ -20,14 +44,15 @@ class NodeStorageAdapter {
20
44
  }
21
45
  async trystat(path) {
22
46
  try {
23
- const stat = await nodeFsPromises.stat(this.resolvePath(path));
47
+ const stat = await nodeFsPromises.stat(await this.resolvePath(path));
24
48
  return {
25
49
  size: stat.size,
26
50
  mtime: Math.floor(stat.mtimeMs),
27
51
  ctime: Math.floor(stat.ctimeMs),
28
52
  type: stat.isDirectory() ? "folder" : "file"
29
53
  };
30
- } catch {
54
+ } catch (error) {
55
+ if (error instanceof SymbolicLinkStoragePathError) throw error;
31
56
  return null;
32
57
  }
33
58
  }
@@ -35,10 +60,12 @@ class NodeStorageAdapter {
35
60
  return await this.trystat(path);
36
61
  }
37
62
  async mkdir(path) {
38
- await nodeFsPromises.mkdir(this.resolvePath(path), { recursive: true });
63
+ const fullPath = await this.resolvePath(path);
64
+ await nodeFsPromises.mkdir(fullPath, { recursive: true });
65
+ await this.assertNoSymbolicLinks(fullPath);
39
66
  }
40
67
  async remove(path) {
41
- const fullPath = this.resolvePath(path, false);
68
+ const fullPath = await this.resolvePath(path, false);
42
69
  const stat = await nodeFsPromises.stat(fullPath);
43
70
  if (stat.isDirectory()) {
44
71
  await nodeFsPromises.rm(fullPath, { recursive: true, force: true });
@@ -47,33 +74,55 @@ class NodeStorageAdapter {
47
74
  }
48
75
  }
49
76
  async read(path) {
50
- return await nodeFsPromises.readFile(this.resolvePath(path, false), "utf-8");
77
+ const handle = await nodeFsPromises.open(
78
+ await this.resolvePath(path, false),
79
+ nodeFsConstants.O_RDONLY | NO_FOLLOW
80
+ );
81
+ try {
82
+ return await handle.readFile("utf-8");
83
+ } finally {
84
+ await handle.close();
85
+ }
51
86
  }
52
87
  async readBinary(path) {
53
- const buffer = await nodeFsPromises.readFile(this.resolvePath(path, false));
54
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
88
+ const handle = await nodeFsPromises.open(
89
+ await this.resolvePath(path, false),
90
+ nodeFsConstants.O_RDONLY | NO_FOLLOW
91
+ );
92
+ try {
93
+ const buffer = await handle.readFile();
94
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
95
+ } finally {
96
+ await handle.close();
97
+ }
55
98
  }
56
99
  async write(path, data, options) {
57
- const fullPath = this.resolvePath(path, false);
100
+ const fullPath = await this.resolvePath(path, false);
58
101
  await nodeFsPromises.mkdir(nodePath.dirname(fullPath), { recursive: true });
59
- await nodeFsPromises.writeFile(fullPath, data, "utf-8");
60
- await this.applyTimes(fullPath, options);
102
+ await this.writeFile(fullPath, data, options, false);
61
103
  }
62
104
  async writeBinary(path, data, options) {
63
- const fullPath = this.resolvePath(path, false);
105
+ const fullPath = await this.resolvePath(path, false);
64
106
  await nodeFsPromises.mkdir(nodePath.dirname(fullPath), { recursive: true });
65
- await nodeFsPromises.writeFile(fullPath, new Uint8Array(data));
66
- await this.applyTimes(fullPath, options);
107
+ await this.writeFile(fullPath, new Uint8Array(data), options, false);
67
108
  }
68
109
  async append(path, data, options) {
69
- const fullPath = this.resolvePath(path, false);
110
+ const fullPath = await this.resolvePath(path, false);
70
111
  await nodeFsPromises.mkdir(nodePath.dirname(fullPath), { recursive: true });
71
- await nodeFsPromises.appendFile(fullPath, data, "utf-8");
72
- await this.applyTimes(fullPath, options);
112
+ await this.writeFile(fullPath, data, options, true);
113
+ }
114
+ /** Atomically rename or move an entry without leaving the injected root. */
115
+ async rename(sourcePath, targetPath) {
116
+ const fullSourcePath = await this.resolvePath(sourcePath, false);
117
+ const fullTargetPath = await this.resolvePath(targetPath, false);
118
+ await nodeFsPromises.mkdir(nodePath.dirname(fullTargetPath), { recursive: true });
119
+ await this.assertNoSymbolicLinks(fullSourcePath);
120
+ await this.assertNoSymbolicLinks(fullTargetPath);
121
+ await nodeFsPromises.rename(fullSourcePath, fullTargetPath);
73
122
  }
74
123
  async list(basePath) {
75
124
  try {
76
- const entries = await nodeFsPromises.readdir(this.resolvePath(basePath), { withFileTypes: true });
125
+ const entries = await nodeFsPromises.readdir(await this.resolvePath(basePath), { withFileTypes: true });
77
126
  const files = [];
78
127
  const folders = [];
79
128
  for (const entry of entries) {
@@ -82,15 +131,27 @@ class NodeStorageAdapter {
82
131
  else if (entry.isFile()) files.push(entryPath);
83
132
  }
84
133
  return { files, folders };
85
- } catch {
134
+ } catch (error) {
135
+ if (error instanceof SymbolicLinkStoragePathError) throw error;
86
136
  return { files: [], folders: [] };
87
137
  }
88
138
  }
89
- async applyTimes(fullPath, options) {
139
+ async writeFile(fullPath, data, options, append) {
140
+ await this.assertNoSymbolicLinks(fullPath);
141
+ const flags = nodeFsConstants.O_WRONLY | nodeFsConstants.O_CREAT | NO_FOLLOW | (append ? nodeFsConstants.O_APPEND : nodeFsConstants.O_TRUNC);
142
+ const handle = await nodeFsPromises.open(fullPath, flags, 438);
143
+ try {
144
+ await handle.writeFile(data, typeof data === "string" ? "utf-8" : void 0);
145
+ await this.applyTimes(handle, options);
146
+ } finally {
147
+ await handle.close();
148
+ }
149
+ }
150
+ async applyTimes(handle, options) {
90
151
  if (!options?.mtime && !options?.ctime) return;
91
152
  const accessTime = options.mtime ? new Date(options.mtime) : /* @__PURE__ */ new Date();
92
153
  const modificationTime = options.mtime ? new Date(options.mtime) : /* @__PURE__ */ new Date();
93
- await nodeFsPromises.utimes(fullPath, accessTime, modificationTime);
154
+ await handle.utimes(accessTime, modificationTime);
94
155
  }
95
156
  }
96
157
  function createNodeStorage({ rootPath }) {
@@ -190,6 +190,7 @@ export declare class LiveSyncLocalDB {
190
190
  getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise<false | LoadedEntry>;
191
191
  getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise<false | LoadedEntry>;
192
192
  deleteDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise<boolean>;
193
+ storeDeletionAtRevision(path: FilePathWithPrefix | FilePath, baseRevision: string): Promise<false | PouchDB.Core.Response>;
193
194
  putDBEntry(note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise<false | PouchDB.Core.Response>;
194
195
  getConflictedDoc(path: FilePathWithPrefix, rev: string): Promise<false | diff_result_leaf>;
195
196
  tryAutoMerge(path: FilePathWithPrefix, enableMarkdownAutoMerge: boolean): AutoMergeResult;
@@ -364,6 +364,9 @@ class LiveSyncLocalDB {
364
364
  async deleteDBEntry(path, opt) {
365
365
  return await this.managers.entryManager.deleteDBEntry(path, opt);
366
366
  }
367
+ async storeDeletionAtRevision(path, baseRevision) {
368
+ return await this.managers.entryManager.storeDeletionAtRevision(path, baseRevision);
369
+ }
367
370
  async putDBEntry(note, onlyChunks, conflictBaseRev) {
368
371
  return await this.managers.entryManager.putDBEntry(note, onlyChunks, conflictBaseRev);
369
372
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Focused public primitives for storing and selecting multiple remote connection profiles.
3
+ *
4
+ * The compatibility fields remain available to existing hosts, but new profile-management code
5
+ * should use this entry point instead of importing the wider service feature module.
6
+ */
7
+ export { activateP2PRemoteConfiguration, activateRemoteConfiguration, createRemoteConfigurationId, suggestRemoteConfigurationName, upsertRemoteConfigurationInPlace, } from "./serviceFeatures/remoteConfig.js";
8
+ export type { SerializableRemoteConfigurationType, UpsertRemoteConfigurationOptions, } from "./serviceFeatures/remoteConfig.js";
9
+ export type { RemoteConfiguration } from "./common/models/setting.type.js";