@vrtmrz/livesync-commonlib 0.1.0-rc.0 → 0.1.0-rc.2

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 (67) hide show
  1. package/README.md +98 -16
  2. package/dist/common/i18n.js +7 -7099
  3. package/dist/common/messages/combinedMessages.prod.d.ts +3 -0
  4. package/dist/common/messages/combinedMessages.prod.js +3 -0
  5. package/dist/common/messages/def.d.ts +1 -0
  6. package/dist/common/messagesJson/en.json +1 -0
  7. package/dist/common/models/setting.const.defaults.d.ts +19 -0
  8. package/dist/common/models/setting.const.defaults.js +20 -2
  9. package/dist/common/models/setting.lifecycle.d.ts +31 -0
  10. package/dist/common/models/setting.lifecycle.js +109 -0
  11. package/dist/common/types.d.ts +4 -2
  12. package/dist/common/types.js +16 -1
  13. package/dist/context.d.ts +1 -0
  14. package/dist/interfaces/Confirm.d.ts +8 -1
  15. package/dist/interfaces/DatabaseRebuilder.d.ts +12 -2
  16. package/dist/platform/node/index.d.ts +1 -0
  17. package/dist/platform/node/index.js +4 -0
  18. package/dist/platform/node/standardIo.d.ts +24 -0
  19. package/dist/platform/node/standardIo.js +39 -0
  20. package/dist/platform/standardIo.d.ts +19 -0
  21. package/dist/platform/standardIo.js +0 -0
  22. package/dist/remoteConfigurations.d.ts +9 -0
  23. package/dist/remoteConfigurations.js +14 -0
  24. package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +4 -1
  25. package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +41 -26
  26. package/dist/replication/trystero/P2PReplicatorCore.d.ts +2 -6
  27. package/dist/replication/trystero/P2PReplicatorCore.js +16 -86
  28. package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -1
  29. package/dist/replication/trystero/TrysteroReplicator.js +10 -4
  30. package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +4 -5
  31. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +22 -30
  32. package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -2
  33. package/dist/replication/trystero/addP2PEventHandlers.d.ts +4 -2
  34. package/dist/replication/trystero/addP2PEventHandlers.js +8 -6
  35. package/dist/replication/trystero/types.d.ts +2 -0
  36. package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +1 -1
  37. package/dist/replication/trystero/useP2PReplicatorCommands.js +3 -1
  38. package/dist/replication/trystero/useP2PReplicatorFeature.js +1 -1
  39. package/dist/rpc/RpcRoom.d.ts +1 -1
  40. package/dist/rpc/RpcRoom.js +22 -16
  41. package/dist/rpc/transports/TrysteroTransport.d.ts +2 -0
  42. package/dist/rpc/transports/TrysteroTransport.js +24 -17
  43. package/dist/rpc/transports/trysteroRoomEvents.d.ts +14 -0
  44. package/dist/rpc/transports/trysteroRoomEvents.js +34 -0
  45. package/dist/serviceFeatures/remoteConfig.d.ts +37 -1
  46. package/dist/serviceFeatures/remoteConfig.js +90 -0
  47. package/dist/serviceModules/Rebuilder.d.ts +3 -2
  48. package/dist/serviceModules/Rebuilder.js +21 -9
  49. package/dist/services/base/IService.d.ts +12 -1
  50. package/dist/services/base/ReplicationService.d.ts +3 -1
  51. package/dist/services/base/ReplicationService.js +6 -1
  52. package/dist/services/base/SettingService.d.ts +14 -1
  53. package/dist/services/base/SettingService.js +35 -12
  54. package/dist/services/implements/headless/HeadlessAPIService.d.ts +1 -1
  55. package/dist/services/implements/headless/HeadlessAPIService.js +1 -1
  56. package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +3 -3
  57. package/dist/settings.d.ts +10 -0
  58. package/dist/settings.js +32 -0
  59. package/dist/worker/bgWorker.js +1 -1
  60. package/docs/development.md +77 -2
  61. package/docs/p2p-transport-lifecycle.md +52 -0
  62. package/docs/platform-standard-io.md +44 -0
  63. package/docs/proven-in-use.md +55 -0
  64. package/docs/releasing.md +9 -5
  65. package/docs/remote-configurations.md +63 -0
  66. package/docs/settings-lifecycle.md +60 -0
  67. package/package.json +12 -2
@@ -0,0 +1,34 @@
1
+ const subscribersByRoom = /* @__PURE__ */ new WeakMap();
2
+ function getSubscribers(room) {
3
+ const existing = subscribersByRoom.get(room);
4
+ if (existing) return existing;
5
+ const subscribers = {
6
+ join: /* @__PURE__ */ new Set(),
7
+ leave: /* @__PURE__ */ new Set()
8
+ };
9
+ subscribersByRoom.set(room, subscribers);
10
+ room.onPeerJoin = (peerId) => {
11
+ for (const handler of [...subscribers.join]) handler(peerId);
12
+ };
13
+ room.onPeerLeave = (peerId) => {
14
+ for (const handler of [...subscribers.leave]) handler(peerId);
15
+ };
16
+ return subscribers;
17
+ }
18
+ function subscribeTrysteroPeerEvents(room, handlers) {
19
+ const subscribers = getSubscribers(room);
20
+ if (handlers.onJoin) subscribers.join.add(handlers.onJoin);
21
+ if (handlers.onLeave) subscribers.leave.add(handlers.onLeave);
22
+ return () => {
23
+ if (handlers.onJoin) subscribers.join.delete(handlers.onJoin);
24
+ if (handlers.onLeave) subscribers.leave.delete(handlers.onLeave);
25
+ if (subscribers.join.size === 0 && subscribers.leave.size === 0) {
26
+ room.onPeerJoin = null;
27
+ room.onPeerLeave = null;
28
+ subscribersByRoom.delete(room);
29
+ }
30
+ };
31
+ }
32
+ export {
33
+ subscribeTrysteroPeerEvents
34
+ };
@@ -1,5 +1,6 @@
1
1
  import { type LOG_LEVEL } from "../common/logger.js";
2
- import type { ObsidianLiveSyncSettings } from "../common/models/setting.type.js";
2
+ import { type RemoteConfigurationResult } from "../common/ConnectionString.js";
3
+ import type { ObsidianLiveSyncSettings, RemoteConfiguration } from "../common/models/setting.type.js";
3
4
  import type { NecessaryServices } from "../interfaces/ServiceModule.js";
4
5
  export type RemoteConfigHost = NecessaryServices<"setting" | "UI" | "replication" | "control" | "appLifecycle" | "API", never>;
5
6
  export declare function migrateLegacyRemoteConfigurationsInPlace(settings: ObsidianLiveSyncSettings, log?: (message: string, level?: LOG_LEVEL) => void): boolean;
@@ -8,6 +9,41 @@ export declare function migrateLegacyRemoteConfigurationsInPlace(settings: Obsid
8
9
  * @returns A unique string identifier.
9
10
  */
10
11
  export declare function createRemoteConfigurationId(): string;
12
+ export type SerializableRemoteConfigurationType = Exclude<RemoteConfigurationResult["type"], "webdav">;
13
+ export interface UpsertRemoteConfigurationOptions {
14
+ /**
15
+ * Reuse this identifier to update a known profile. Omit it to allocate a new opaque identifier.
16
+ */
17
+ id?: string;
18
+ /**
19
+ * User-visible name. Omit it to derive a descriptive name from the connection settings.
20
+ */
21
+ name?: string;
22
+ /**
23
+ * Select this profile as the main remote and project it onto the compatibility fields.
24
+ */
25
+ activate?: boolean;
26
+ /**
27
+ * Select this profile for P2P features without changing the main remote selection.
28
+ */
29
+ activateForP2P?: boolean;
30
+ }
31
+ /**
32
+ * Suggest a concise display name from a serialisable remote configuration.
33
+ *
34
+ * The name is presentation only. Callers must continue to use the profile ID for identity and
35
+ * `activeConfigurationId` for the selected main remote.
36
+ */
37
+ export declare function suggestRemoteConfigurationName(configuration: RemoteConfigurationResult): string;
38
+ /**
39
+ * Create or update a multiple-remote profile from the corresponding compatibility fields.
40
+ *
41
+ * This mutates `settings`. Passing an existing `id` intentionally replaces that profile; omitting
42
+ * it always allocates a new opaque ID and preserves all existing profiles. Generated display names
43
+ * are made unique for readability, but names never act as identifiers. The stored connection URI is
44
+ * plaintext at this boundary; `SettingService` applies configured at-rest encryption when saving it.
45
+ */
46
+ export declare function upsertRemoteConfigurationInPlace(settings: ObsidianLiveSyncSettings, type: SerializableRemoteConfigurationType, options?: UpsertRemoteConfigurationOptions): RemoteConfiguration;
11
47
  /**
12
48
  * Keep compatibility for users who were already using P2P as their main active remote.
13
49
  */
@@ -51,6 +51,94 @@ function migrateLegacyRemoteConfigurationsInPlace(settings, log) {
51
51
  function createRemoteConfigurationId() {
52
52
  return `remote-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
53
53
  }
54
+ function toRemoteConfigurationResult(type, settings) {
55
+ if (type === "couchdb") {
56
+ return { type, settings };
57
+ }
58
+ if (type === "s3") {
59
+ return { type, settings };
60
+ }
61
+ return { type, settings };
62
+ }
63
+ function suggestRemoteConfigurationName(configuration) {
64
+ if (configuration.type === "couchdb") {
65
+ try {
66
+ const host = new URL(configuration.settings.couchDB_URI).host;
67
+ return host ? `CouchDB ${host}` : "CouchDB remote";
68
+ } catch {
69
+ return "CouchDB remote";
70
+ }
71
+ }
72
+ if (configuration.type === "s3") {
73
+ const bucket = configuration.settings.bucket.trim();
74
+ if (bucket) {
75
+ return `S3 ${bucket}`;
76
+ }
77
+ try {
78
+ const host = new URL(configuration.settings.endpoint).host;
79
+ return host ? `S3 ${host}` : "Object Storage remote";
80
+ } catch {
81
+ return "Object Storage remote";
82
+ }
83
+ }
84
+ if (configuration.type === "p2p") {
85
+ const room = configuration.settings.P2P_roomID.trim();
86
+ return room ? `P2P ${room}` : "P2P remote";
87
+ }
88
+ return "Remote configuration";
89
+ }
90
+ function allocateRemoteConfigurationId(configurations) {
91
+ let id = createRemoteConfigurationId();
92
+ while (configurations[id]) {
93
+ id = createRemoteConfigurationId();
94
+ }
95
+ return id;
96
+ }
97
+ function allocateRemoteConfigurationName(configurations, baseName, updatingId) {
98
+ const usedNames = new Set(
99
+ Object.values(configurations).filter((configuration) => configuration.id !== updatingId).map((configuration) => configuration.name)
100
+ );
101
+ if (!usedNames.has(baseName)) {
102
+ return baseName;
103
+ }
104
+ let suffix = 2;
105
+ while (usedNames.has(`${baseName} (${suffix})`)) {
106
+ suffix += 1;
107
+ }
108
+ return `${baseName} (${suffix})`;
109
+ }
110
+ function upsertRemoteConfigurationInPlace(settings, type, options = {}) {
111
+ if (options.activateForP2P && type !== "p2p") {
112
+ throw new Error("Only a P2P remote configuration can be selected for P2P features.");
113
+ }
114
+ const configurations = settings.remoteConfigurations ?? {};
115
+ const requestedId = options.id?.trim();
116
+ const id = requestedId || allocateRemoteConfigurationId(configurations);
117
+ const existing = configurations[id];
118
+ const serialisable = toRemoteConfigurationResult(type, settings);
119
+ const suggestedName = suggestRemoteConfigurationName(serialisable);
120
+ const requestedName = options.name?.trim();
121
+ const name = requestedName || existing?.name || allocateRemoteConfigurationName(configurations, suggestedName, existing?.id);
122
+ const configuration = {
123
+ id,
124
+ name,
125
+ uri: ConnectionStringParser.serialize(serialisable),
126
+ isEncrypted: false
127
+ };
128
+ settings.remoteConfigurations ??= configurations;
129
+ configurations[id] = configuration;
130
+ if (options.activate) {
131
+ if (!activateRemoteConfiguration(settings, id)) {
132
+ throw new Error(`Failed to activate remote configuration '${id}'.`);
133
+ }
134
+ }
135
+ if (options.activateForP2P) {
136
+ if (!activateP2PRemoteConfiguration(settings, id)) {
137
+ throw new Error(`Failed to activate P2P remote configuration '${id}'.`);
138
+ }
139
+ }
140
+ return configuration;
141
+ }
54
142
  function migrateP2PActiveRemoteConfigurationIdInPlace(settings) {
55
143
  if ((settings.P2P_ActiveRemoteConfigurationId ?? "").trim() !== "") {
56
144
  return false;
@@ -242,6 +330,8 @@ export {
242
330
  migrateLegacyRemoteConfigurationsInPlace,
243
331
  migrateP2PActiveRemoteConfigurationIdInPlace,
244
332
  migrateToMultipleRemoteConfigurations,
333
+ suggestRemoteConfigurationName,
334
+ upsertRemoteConfigurationInPlace,
245
335
  useRemoteConfiguration,
246
336
  useRemoteConfigurationMigration
247
337
  };
@@ -60,8 +60,9 @@ export declare class ServiceRebuilder extends ServiceModuleBase<ServiceRebuilder
60
60
  $rebuildEverything(): Promise<void>;
61
61
  $fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean): Promise<void>;
62
62
  $fetchLocalDBFast(autoResume: boolean): Promise<void>;
63
- scheduleRebuild(): Promise<void>;
64
- scheduleFetch(): Promise<void>;
63
+ private scheduleInitialisation;
64
+ scheduleRebuild(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
65
+ scheduleFetch(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
65
66
  private _tryResetRemoteDatabase;
66
67
  private _onResetLocalDatabase;
67
68
  suspendAllSync(): Promise<void>;
@@ -134,23 +134,35 @@ Please enable them from the settings screen after setup is complete.`,
134
134
  $fetchLocalDBFast(autoResume) {
135
135
  return this.fetchLocalDBFast(autoResume);
136
136
  }
137
- async scheduleRebuild() {
137
+ async scheduleInitialisation(flag, prepareBeforeRestart) {
138
138
  try {
139
- await this.storageAccess.writeFileAuto(FlagFilesHumanReadable.REBUILD_ALL, "");
139
+ await this.storageAccess.writeFileAuto(flag, "");
140
140
  } catch (ex) {
141
- this._log(`Could not create ${FlagFilesHumanReadable.REBUILD_ALL}`, LOG_LEVEL_NOTICE);
141
+ this._log(`Could not create ${flag}`, LOG_LEVEL_NOTICE);
142
142
  this._log(ex, LOG_LEVEL_VERBOSE);
143
+ return false;
143
144
  }
144
- this.appLifecycle.performRestart();
145
- }
146
- async scheduleFetch() {
145
+ this.appLifecycle.setSuspended(true);
147
146
  try {
148
- await this.storageAccess.writeFileAuto(FlagFilesHumanReadable.FETCH_ALL, "");
147
+ await prepareBeforeRestart?.();
149
148
  } catch (ex) {
150
- this._log(`Could not create ${FlagFilesHumanReadable.FETCH_ALL}`, LOG_LEVEL_NOTICE);
151
- this._log(ex, LOG_LEVEL_VERBOSE);
149
+ try {
150
+ await this.storageAccess.delete(flag, true);
151
+ } catch (cleanupError) {
152
+ this._log(`Could not remove ${flag} after restart preparation failed`, LOG_LEVEL_NOTICE);
153
+ this._log(cleanupError, LOG_LEVEL_VERBOSE);
154
+ }
155
+ this.appLifecycle.setSuspended(false);
156
+ throw ex;
152
157
  }
153
158
  this.appLifecycle.performRestart();
159
+ return true;
160
+ }
161
+ scheduleRebuild(prepareBeforeRestart) {
162
+ return this.scheduleInitialisation(FlagFilesHumanReadable.REBUILD_ALL, prepareBeforeRestart);
163
+ }
164
+ scheduleFetch(prepareBeforeRestart) {
165
+ return this.scheduleInitialisation(FlagFilesHumanReadable.FETCH_ALL, prepareBeforeRestart);
154
166
  }
155
167
  async _tryResetRemoteDatabase() {
156
168
  const currentReplicator = this.replicator.getActiveReplicator();
@@ -1,6 +1,6 @@
1
1
  import type { FetchHttpHandler } from "@smithy/fetch-http-handler";
2
2
  import { type LOG_LEVEL } from "octagonal-wheels/common/logger";
3
- import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, TweakValues, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
3
+ import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, SettingsMigrationState, TweakValues, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
4
4
  import type { LiveSyncLocalDB } from "../../pouchdb/LiveSyncLocalDB.js";
5
5
  import type { LiveSyncAbstractReplicator } from "../../replication/LiveSyncAbstractReplicator.js";
6
6
  import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
@@ -117,6 +117,7 @@ export interface IReplicationService {
117
117
  processVirtualDocument(docs: PouchDB.Core.ExistingDocument<EntryDoc>): Promise<boolean>;
118
118
  onBeforeReplicate(showMessage: boolean): Promise<boolean>;
119
119
  checkConnectionFailure(): Promise<boolean | "CHECKAGAIN" | undefined>;
120
+ /** Lightweight, idempotent policy checks which every replication entry point may invoke. */
120
121
  onCheckReplicationReady(showMessage: boolean): Promise<boolean>;
121
122
  isReplicationReady(showMessage: boolean): Promise<boolean>;
122
123
  performReplication(showMessage?: boolean): Promise<boolean | void>;
@@ -197,9 +198,19 @@ export interface ISettingService {
197
198
  decryptSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings>;
198
199
  adjustSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings>;
199
200
  loadSettings(): Promise<void>;
201
+ getSettingsMigrationState(): SettingsMigrationState | undefined;
200
202
  getDeviceAndVaultName(): string;
201
203
  setDeviceAndVaultName(name: string): void;
202
204
  saveDeviceAndVaultName(): void;
205
+ /**
206
+ * Read host-provided, device-local configuration by its exact key.
207
+ * This storage is not part of the synchronised settings document.
208
+ */
209
+ getDeviceLocalConfig(key: string): string | null;
210
+ /** Store host-provided, device-local configuration by its exact key. */
211
+ setDeviceLocalConfig(key: string, value: string): void;
212
+ /** Delete host-provided, device-local configuration by its exact key. */
213
+ deleteDeviceLocalConfig(key: string): void;
203
214
  onBeforeSaveSettingData(nextSettings: ObsidianLiveSyncSettings, previousSettings: ObsidianLiveSyncSettings): Promise<(Partial<ObsidianLiveSyncSettings> | void)[]>;
204
215
  saveSettingData(): Promise<void>;
205
216
  currentSettings(): ObsidianLiveSyncSettings;
@@ -49,7 +49,9 @@ export declare abstract class ReplicationService<T extends ServiceContext = Serv
49
49
  */
50
50
  readonly onBeforeReplicate: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(showMessage: boolean) => Promise<boolean>>;
51
51
  /**
52
- *
52
+ * Lightweight, repeatable policy checks shared by every replication entry point.
53
+ * Handlers must remain idempotent because a high-level replication may cross
54
+ * more than one entry point before work begins.
53
55
  */
54
56
  readonly onCheckReplicationReady: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(showMessage: boolean) => Promise<boolean>>;
55
57
  /**
@@ -60,7 +60,9 @@ class ReplicationService extends ServiceBase {
60
60
  */
61
61
  onBeforeReplicate = handlers().bailFirstFailure("onBeforeReplicate");
62
62
  /**
63
- *
63
+ * Lightweight, repeatable policy checks shared by every replication entry point.
64
+ * Handlers must remain idempotent because a high-level replication may cross
65
+ * more than one entry point before work begins.
64
66
  */
65
67
  onCheckReplicationReady = handlers().bailFirstFailure("onCheckReplicationReady");
66
68
  /**
@@ -72,6 +74,9 @@ class ReplicationService extends ServiceBase {
72
74
  this._log(`Not ready`);
73
75
  return false;
74
76
  }
77
+ if (!await this.onCheckReplicationReady(showMessage)) {
78
+ return false;
79
+ }
75
80
  const currentSettings = this.settingService.currentSettings();
76
81
  if (isLockAcquired("cleanup")) {
77
82
  this._log(this.context.translate("Replicator.Message.Cleaned"), LOG_LEVEL_NOTICE);
@@ -1,4 +1,4 @@
1
- import { type ObsidianLiveSyncSettings } from "../../common/types.js";
1
+ import { type ObsidianLiveSyncSettings, type SettingsMigrationState } from "../../common/types.js";
2
2
  import type { IAPIService, ISettingService } from "./IService.js";
3
3
  import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
4
4
  import { createInstanceLogFunction } from "../lib/logUtils.js";
@@ -17,6 +17,8 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
17
17
  protected abstract saveData(setting: ObsidianLiveSyncSettings): Promise<void>;
18
18
  protected abstract loadData(): Promise<ObsidianLiveSyncSettings | undefined>;
19
19
  private _lastPersistedSettings?;
20
+ private _settingsMigrationState?;
21
+ getSettingsMigrationState(): SettingsMigrationState | undefined;
20
22
  _log: ReturnType<typeof createInstanceLogFunction>;
21
23
  constructor(context: T, dependencies: SettingServiceDependencies);
22
24
  /**
@@ -37,6 +39,17 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
37
39
  * Save the current device and vault name to settings, aside from the main settings.
38
40
  */
39
41
  saveDeviceAndVaultName(): void;
42
+ /**
43
+ * Read an exact key from the host's device-local configuration store.
44
+ *
45
+ * Unlike the main settings document, this state is not synchronised. Callers
46
+ * which need Vault namespacing should normally use {@link getSmallConfig}.
47
+ */
48
+ getDeviceLocalConfig(key: string): string | null;
49
+ /** Store an exact key in the host's non-synchronised configuration store. */
50
+ setDeviceLocalConfig(key: string, value: string): void;
51
+ /** Delete an exact key from the host's non-synchronised configuration store. */
52
+ deleteDeviceLocalConfig(key: string): void;
40
53
  private additionalSuffixOfDatabaseName;
41
54
  private getKey;
42
55
  setSmallConfig(key: string, value: string): void;
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  ChunkAlgorithmNames,
3
- DEFAULT_SETTINGS,
4
3
  LOG_LEVEL_NOTICE,
5
4
  LOG_LEVEL_URGENT,
6
5
  LOG_LEVEL_VERBOSE,
7
6
  SALT_OF_PASSPHRASE,
8
- SETTING_KEY_P2P_DEVICE_NAME
7
+ SETTING_KEY_P2P_DEVICE_NAME,
8
+ prepareSettingsForLoad
9
9
  } from "../../common/types.js";
10
10
  import { handlers } from "../lib/HandlerUtils.js";
11
11
  import { ServiceBase } from "./ServiceBase.js";
@@ -31,6 +31,10 @@ class SettingService extends ServiceBase {
31
31
  this._settings = value;
32
32
  }
33
33
  _lastPersistedSettings;
34
+ _settingsMigrationState;
35
+ getSettingsMigrationState() {
36
+ return this._settingsMigrationState;
37
+ }
34
38
  _log;
35
39
  constructor(context, dependencies) {
36
40
  super(context);
@@ -88,7 +92,24 @@ class SettingService extends ServiceBase {
88
92
  */
89
93
  saveDeviceAndVaultName() {
90
94
  const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.APIService.getSystemVaultName() + this.additionalSuffixOfDatabaseName();
91
- this.setItem(lsKey, this.deviceAndVaultName);
95
+ this.setDeviceLocalConfig(lsKey, this.deviceAndVaultName);
96
+ }
97
+ /**
98
+ * Read an exact key from the host's device-local configuration store.
99
+ *
100
+ * Unlike the main settings document, this state is not synchronised. Callers
101
+ * which need Vault namespacing should normally use {@link getSmallConfig}.
102
+ */
103
+ getDeviceLocalConfig(key) {
104
+ return this.getItem(key);
105
+ }
106
+ /** Store an exact key in the host's non-synchronised configuration store. */
107
+ setDeviceLocalConfig(key, value) {
108
+ this.setItem(key, value);
109
+ }
110
+ /** Delete an exact key from the host's non-synchronised configuration store. */
111
+ deleteDeviceLocalConfig(key) {
112
+ this.deleteItem(key);
92
113
  }
93
114
  additionalSuffixOfDatabaseName() {
94
115
  const suffix = this.settings?.additionalSuffixOfDatabaseName;
@@ -105,15 +126,15 @@ class SettingService extends ServiceBase {
105
126
  }
106
127
  setSmallConfig(key, value) {
107
128
  const dbKey = this.getKey(key);
108
- this.setItem(dbKey, value);
129
+ this.setDeviceLocalConfig(dbKey, value);
109
130
  }
110
131
  getSmallConfig(key) {
111
132
  const dbKey = this.getKey(key);
112
- return this.getItem(dbKey);
133
+ return this.getDeviceLocalConfig(dbKey) ?? "";
113
134
  }
114
135
  deleteSmallConfig(key) {
115
136
  const dbKey = this.getKey(key);
116
- this.deleteItem(dbKey);
137
+ this.deleteDeviceLocalConfig(dbKey);
117
138
  }
118
139
  /**
119
140
  * Save the current settings to storage.
@@ -332,7 +353,7 @@ class SettingService extends ServiceBase {
332
353
  getPassphrase(settings) {
333
354
  const methods = {
334
355
  "": () => Promise.resolve("*"),
335
- LOCALSTORAGE: () => Promise.resolve(this.getItem("ls-setting-passphrase") ?? false),
356
+ LOCALSTORAGE: () => Promise.resolve(this.getDeviceLocalConfig("ls-setting-passphrase") ?? false),
336
357
  ASK_AT_LAUNCH: () => this.APIService.confirm.askString("Passphrase", "passphrase", "")
337
358
  };
338
359
  const method = settings.configPassphraseStore;
@@ -445,10 +466,12 @@ class SettingService extends ServiceBase {
445
466
  return settings;
446
467
  }
447
468
  async loadSettings() {
448
- const settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
469
+ const prepared = prepareSettingsForLoad(await this.loadData());
470
+ const { settings, ...migrationState } = prepared;
471
+ this._settingsMigrationState = migrationState;
449
472
  const hadRemoteConfigurations = Object.keys(settings.remoteConfigurations ?? {}).length > 0;
450
473
  if (typeof settings.isConfigured == "undefined") {
451
- if (JSON.stringify(settings) !== JSON.stringify(DEFAULT_SETTINGS)) {
474
+ if (!prepared.isNewVault) {
452
475
  settings.isConfigured = true;
453
476
  } else {
454
477
  const appId = this.APIService.getAppID();
@@ -493,7 +516,7 @@ class SettingService extends ServiceBase {
493
516
  }
494
517
  const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.APIService.getSystemVaultName() + this.additionalSuffixOfDatabaseName();
495
518
  if (this.settings.deviceAndVaultName != "") {
496
- if (!this.getItem(lsKey)) {
519
+ if (!this.getDeviceLocalConfig(lsKey)) {
497
520
  this.setDeviceAndVaultName(this.settings.deviceAndVaultName);
498
521
  this.saveDeviceAndVaultName();
499
522
  this.settings.deviceAndVaultName = "";
@@ -506,14 +529,14 @@ class SettingService extends ServiceBase {
506
529
  );
507
530
  this.settings.customChunkSize = 0;
508
531
  }
509
- this.setDeviceAndVaultName(this.getItem(lsKey) || "");
532
+ this.setDeviceAndVaultName(this.getDeviceLocalConfig(lsKey) || "");
510
533
  if (this.getDeviceAndVaultName() == "") {
511
534
  if (this.settings.usePluginSync) {
512
535
  this._log("Device name missing. Disabling plug-in sync.", LOG_LEVEL_NOTICE);
513
536
  this.settings.usePluginSync = false;
514
537
  }
515
538
  }
516
- if (migratedLegacyRemoteConfigurations || migratedP2PActiveRemoteConfiguration) {
539
+ if (!prepared.isFromFutureSchema && (prepared.changed || migratedLegacyRemoteConfigurations || migratedP2PActiveRemoteConfiguration)) {
517
540
  await this.saveSettingData();
518
541
  }
519
542
  this._lastPersistedSettings = this.cloneSettings(this.settings);
@@ -22,7 +22,7 @@ export declare class HeadlessConfirm implements Confirm {
22
22
  defaultAction: T[number];
23
23
  timeout?: number;
24
24
  }): Promise<T[number] | false>;
25
- askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void;
25
+ askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void, durationMs?: number): void;
26
26
  confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>;
27
27
  }
28
28
  export declare class HeadlessAPIService<T extends ServiceContext> extends InjectableAPIService<T> {
@@ -23,7 +23,7 @@ class HeadlessConfirm {
23
23
  console.error(`[Headless] ${opt.title ?? "Confirm"}: ${message} \u2192 ${String(opt.defaultAction)}`);
24
24
  return Promise.resolve(opt.defaultAction);
25
25
  }
26
- askInPopup(key, dialogText, anchorCallback) {
26
+ askInPopup(key, dialogText, anchorCallback, durationMs) {
27
27
  console.error(`[Headless] Popup (${key}): ${dialogText}`);
28
28
  }
29
29
  confirmWithMessage(title, contentMd, buttons, defaultAction, timeout) {
@@ -1,14 +1,14 @@
1
1
  import type { ServiceContext } from "../../base/ServiceBase.js";
2
2
  import { TweakValueService } from "../../base/TweakValueService.js";
3
3
  export declare class InjectableTweakValueService<T extends ServiceContext> extends TweakValueService<T> {
4
- fetchRemotePreferred: import("../../lib/HandlerUtils.js").HandlerFunction<(trialSetting: import("../../../common/types.js").RemoteDBSettings) => Promise<import("../../../common/types.js").TweakValues | false>, unknown>;
4
+ fetchRemotePreferred: import("../../lib/HandlerUtils.js").HandlerFunction<(trialSetting: import("../../../settings.js").RemoteDBSettings) => Promise<import("../../../common/types.js").TweakValues | false>, unknown>;
5
5
  checkAndAskResolvingMismatched: import("../../lib/HandlerUtils.js").HandlerFunction<(preferred: Partial<import("../../../common/types.js").TweakValues>) => Promise<[import("../../../common/types.js").TweakValues | boolean, boolean]>, unknown>;
6
6
  askResolvingMismatched: import("../../lib/HandlerUtils.js").HandlerFunction<(preferredSource: import("../../../common/types.js").TweakValues) => Promise<"OK" | "CHECKAGAIN" | "IGNORE">, unknown>;
7
- checkAndAskUseRemoteConfiguration: import("../../lib/HandlerUtils.js").HandlerFunction<(settings: import("../../../common/types.js").RemoteDBSettings) => Promise<{
7
+ checkAndAskUseRemoteConfiguration: import("../../lib/HandlerUtils.js").HandlerFunction<(settings: import("../../../settings.js").RemoteDBSettings) => Promise<{
8
8
  result: false | import("../../../common/types.js").TweakValues;
9
9
  requireFetch: boolean;
10
10
  }>, unknown>;
11
- askUseRemoteConfiguration: import("../../lib/HandlerUtils.js").HandlerFunction<(trialSetting: import("../../../common/types.js").RemoteDBSettings, preferred: import("../../../common/types.js").TweakValues) => Promise<{
11
+ askUseRemoteConfiguration: import("../../lib/HandlerUtils.js").HandlerFunction<(trialSetting: import("../../../settings.js").RemoteDBSettings, preferred: import("../../../common/types.js").TweakValues) => Promise<{
12
12
  result: false | import("../../../common/types.js").TweakValues;
13
13
  requireFetch: boolean;
14
14
  }>, unknown>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Settings defaults and migration contracts shared by maintained hosts.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ export { DEFAULT_SETTINGS, NEW_VAULT_SETTINGS, P2P_DEFAULT_SETTINGS, SETTINGS_SCHEMA_DEFAULTS, createNewVaultSettings, } from "./common/models/setting.const.defaults.js";
7
+ export { PREFERRED_BASE, PREFERRED_JOURNAL_SYNC, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, } from "./common/models/setting.const.preferred.js";
8
+ export { CURRENT_SETTING_VERSION } from "./common/models/setting.const.js";
9
+ export { prepareSettingsForLoad, SettingsMigrationReviewCodes, type PreparedSettings, type SettingsMigrationReviewCode, type SettingsMigrationReviewReason, type SettingsMigrationState, } from "./common/models/setting.lifecycle.js";
10
+ export type { ObsidianLiveSyncSettings, RemoteDBSettings, RemoteTypeSettings, } from "./common/models/setting.type.js";
@@ -0,0 +1,32 @@
1
+ import {
2
+ DEFAULT_SETTINGS,
3
+ NEW_VAULT_SETTINGS,
4
+ P2P_DEFAULT_SETTINGS,
5
+ SETTINGS_SCHEMA_DEFAULTS,
6
+ createNewVaultSettings
7
+ } from "./common/models/setting.const.defaults.js";
8
+ import {
9
+ PREFERRED_BASE,
10
+ PREFERRED_JOURNAL_SYNC,
11
+ PREFERRED_SETTING_CLOUDANT,
12
+ PREFERRED_SETTING_SELF_HOSTED
13
+ } from "./common/models/setting.const.preferred.js";
14
+ import { CURRENT_SETTING_VERSION } from "./common/models/setting.const.js";
15
+ import {
16
+ prepareSettingsForLoad,
17
+ SettingsMigrationReviewCodes
18
+ } from "./common/models/setting.lifecycle.js";
19
+ export {
20
+ CURRENT_SETTING_VERSION,
21
+ DEFAULT_SETTINGS,
22
+ NEW_VAULT_SETTINGS,
23
+ P2P_DEFAULT_SETTINGS,
24
+ PREFERRED_BASE,
25
+ PREFERRED_JOURNAL_SYNC,
26
+ PREFERRED_SETTING_CLOUDANT,
27
+ PREFERRED_SETTING_SELF_HOSTED,
28
+ SETTINGS_SCHEMA_DEFAULTS,
29
+ SettingsMigrationReviewCodes,
30
+ createNewVaultSettings,
31
+ prepareSettingsForLoad
32
+ };