@vrtmrz/livesync-commonlib 0.1.44 → 0.2.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 (92) hide show
  1. package/README.md +80 -4
  2. package/dist/API/DirectFileManipulatorV2.d.ts +2 -0
  3. package/dist/API/DirectFileManipulatorV2.js +3 -0
  4. package/dist/API/processSetting.d.ts +1 -0
  5. package/dist/common/configForDoc.d.ts +1 -0
  6. package/dist/common/configForDoc.js +11 -1
  7. package/dist/common/models/db.definition.d.ts +1 -0
  8. package/dist/common/models/setting.const.defaults.js +1 -0
  9. package/dist/common/models/setting.const.preferred.js +1 -3
  10. package/dist/common/models/setting.const.qr.js +1 -0
  11. package/dist/common/models/setting.policy.d.ts +4 -0
  12. package/dist/common/models/setting.policy.js +1 -0
  13. package/dist/common/models/setting.type.d.ts +2 -0
  14. package/dist/common/models/shared.definition.d.ts +20 -0
  15. package/dist/common/models/tweak.definition.d.ts +1 -0
  16. package/dist/common/settingConstants.d.ts +1 -0
  17. package/dist/common/settingConstants.js +6 -2
  18. package/dist/interfaces/FileHandler.d.ts +5 -1
  19. package/dist/interfaces/StorageEventManager.d.ts +5 -0
  20. package/dist/interfaces/StorageEventManager.js +7 -0
  21. package/dist/managers/EntryManager/EntryManagerImpls.js +3 -2
  22. package/dist/managers/StorageEventManager.d.ts +19 -10
  23. package/dist/managers/StorageEventManager.js +259 -189
  24. package/dist/pouchdb/LiveSyncDBFunctions.js +4 -0
  25. package/dist/pouchdb/StreamingFetch.d.ts +5 -1
  26. package/dist/pouchdb/StreamingFetch.js +529 -65
  27. package/dist/replication/LiveSyncAbstractReplicator.d.ts +2 -0
  28. package/dist/replication/LiveSyncAbstractReplicator.js +5 -2
  29. package/dist/replication/RemoteResource.d.ts +23 -2
  30. package/dist/replication/RemoteResource.js +6 -2
  31. package/dist/replication/couchdb/LiveSyncReplicator.d.ts +14 -0
  32. package/dist/replication/couchdb/LiveSyncReplicator.js +171 -3
  33. package/dist/replication/couchdb/fastForward.d.ts +19 -0
  34. package/dist/replication/couchdb/fastForward.js +119 -0
  35. package/dist/replication/couchdb/replicationProgress.d.ts +6 -0
  36. package/dist/replication/couchdb/replicationProgress.js +105 -0
  37. package/dist/replication/index.d.ts +4 -1
  38. package/dist/replication/joinedDevices.d.ts +25 -0
  39. package/dist/replication/joinedDevices.js +123 -0
  40. package/dist/replication/journal/JournalSyncCore.d.ts +2 -1
  41. package/dist/replication/journal/JournalSyncCore.js +11 -1
  42. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +4 -0
  43. package/dist/replication/journal/LiveSyncJournalReplicator.js +56 -1
  44. package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +3 -0
  45. package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -1
  46. package/dist/replication/journal/objectstore/MinioStorageAdapter.js +27 -1
  47. package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +1 -1
  48. package/dist/replication/trystero/P2PRoomSessionOwner.js +27 -14
  49. package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
  50. package/dist/serviceFeatures/offlineScanner.js +19 -7
  51. package/dist/serviceModules/Rebuilder.js +41 -15
  52. package/dist/serviceModules/ServiceFileAccessBase.js +5 -0
  53. package/dist/serviceModules/ServiceFileHandlerBase.d.ts +8 -5
  54. package/dist/serviceModules/ServiceFileHandlerBase.js +75 -38
  55. package/dist/services/BrowserServices.d.ts +4 -2
  56. package/dist/services/BrowserServices.js +2 -5
  57. package/dist/services/HeadlessServices.d.ts +3 -0
  58. package/dist/services/HeadlessServices.js +2 -5
  59. package/dist/services/ServiceHub.d.ts +4 -4
  60. package/dist/services/base/AppLifecycleService.js +1 -1
  61. package/dist/services/base/ControlService.js +6 -2
  62. package/dist/services/base/FileProcessingService.d.ts +7 -1
  63. package/dist/services/base/FileProcessingService.js +9 -1
  64. package/dist/services/base/IService.d.ts +19 -19
  65. package/dist/services/base/ReplicatorService.d.ts +1 -0
  66. package/dist/services/base/ReplicatorService.remoteResourceResolver.js +1 -1
  67. package/dist/services/base/SettingService.d.ts +8 -18
  68. package/dist/services/base/SettingService.js +28 -46
  69. package/dist/services/implements/base/SvelteDialog.d.ts +2 -2
  70. package/dist/services/implements/base/UIService.d.ts +2 -0
  71. package/dist/services/implements/base/UIService.js +5 -0
  72. package/dist/services/implements/injectable/InjectableServiceHub.d.ts +4 -4
  73. package/dist/services/implements/injectable/InjectableServices.d.ts +2 -2
  74. package/dist/services/implements/injectable/InjectableSettingService.d.ts +0 -3
  75. package/dist/services/implements/injectable/InjectableSettingService.js +0 -11
  76. package/dist/services/lib/HandlerUtils.d.ts +60 -0
  77. package/dist/services/lib/HandlerUtils.js +46 -0
  78. package/dist/string_and_binary/chunks.js +7 -5
  79. package/dist/uiCommands.d.ts +26 -0
  80. package/dist/uiCommands.js +57 -0
  81. package/dist/worker/bgWorker.js +1 -1
  82. package/docs/development.md +15 -3
  83. package/docs/offline-scanner.md +15 -13
  84. package/docs/proven-in-use.md +38 -4
  85. package/docs/releasing.md +20 -13
  86. package/docs/settings-lifecycle.md +39 -5
  87. package/docs/storage-events-and-reflection.md +109 -10
  88. package/package.json +13 -18
  89. package/dist/services/base/ConfigService.d.ts +0 -7
  90. package/dist/services/base/ConfigService.js +0 -6
  91. package/dist/services/implements/browser/ConfigServiceBrowserCompat.d.ts +0 -16
  92. package/dist/services/implements/browser/ConfigServiceBrowserCompat.js +0 -23
@@ -40,7 +40,9 @@ class ControlService extends ServiceBase {
40
40
  await this.services.appLifecycleService.onSuspending();
41
41
  await this.services.settingService.onBeforeRealiseSetting();
42
42
  this.services.databaseService.localDatabase.refreshSettings();
43
- await this.services.fileProcessingService.commitPendingFileEvents();
43
+ if (!await this.services.fileProcessingService.commitPendingFileEvents()) {
44
+ throw new Error("Pending local file operations could not be completed before applying settings");
45
+ }
44
46
  await this.services.settingService.onRealiseSetting();
45
47
  if (this.services.appLifecycleService.isSuspended()) return;
46
48
  await this.services.appLifecycleService.onResuming();
@@ -49,7 +51,8 @@ class ControlService extends ServiceBase {
49
51
  }
50
52
  async _onLiveSyncUnload() {
51
53
  this.context.events.emitEvent(EVENT_PLUGIN_UNLOADED);
52
- await this.services.appLifecycleService.onBeforeUnload();
54
+ const prepared = await this.services.appLifecycleService.onBeforeUnload();
55
+ if (!prepared) this._log("Unload preparation failed; pending storage work may require recovery", LOG_LEVEL_URGENT);
53
56
  await Promise.resolve(this.services.appLifecycleService.onAppUnload());
54
57
  cancelAllPeriodicTask();
55
58
  cancelAllTasks();
@@ -64,6 +67,7 @@ class ControlService extends ServiceBase {
64
67
  this.context.events.emitEvent(EVENT_PLATFORM_UNLOADED);
65
68
  this.context.events.offAll();
66
69
  this._log(this.context.translate("moduleLiveSyncMain.logUnloadingPlugin"));
70
+ if (!prepared) throw new Error("Unload preparation failed");
67
71
  return;
68
72
  }
69
73
  /**
@@ -9,10 +9,16 @@ export declare class FileProcessingService<T extends ServiceContext = ServiceCon
9
9
  */
10
10
  readonly processFileEvent: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(item: import("../../common/types.js").FileEventItem) => Promise<boolean>>;
11
11
  /**
12
- * Process a file event item optionally, if any handler is registered.
12
+ * Dispatch a legacy optional notification, without confirming persistence.
13
13
  * i.e., hidden files synchronisation or customisation sync.
14
14
  */
15
15
  readonly processOptionalFileEvent: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(path: import("../../common/types.js").FilePath) => Promise<boolean>>;
16
+ /**
17
+ * Complete optional file operations through the first applicable handler.
18
+ * Hosts migrate all optional providers together. Once any typed provider is
19
+ * registered, dispatch uses this contract exclusively.
20
+ */
21
+ readonly processOptionalFileEventWithResult?: import("../lib/HandlerUtils.js").FirstHandledFunction<(path: import("../../common/types.js").FilePath, options?: import("./IService.js").OptionalFileEventOptions) => Promise<import("../lib/HandlerUtils.js").HandlerOutcome<void, string>>>;
16
22
  /**
17
23
  * Commit any pending file events that have been queued for processing.
18
24
  */
@@ -7,10 +7,18 @@ class FileProcessingService extends ServiceBase {
7
7
  */
8
8
  processFileEvent = handlers().anySuccess("processFileEvent");
9
9
  /**
10
- * Process a file event item optionally, if any handler is registered.
10
+ * Dispatch a legacy optional notification, without confirming persistence.
11
11
  * i.e., hidden files synchronisation or customisation sync.
12
12
  */
13
13
  processOptionalFileEvent = handlers().anySuccess("processOptionalFileEvent");
14
+ /**
15
+ * Complete optional file operations through the first applicable handler.
16
+ * Hosts migrate all optional providers together. Once any typed provider is
17
+ * registered, dispatch uses this contract exclusively.
18
+ */
19
+ processOptionalFileEventWithResult = handlers().firstHandled(
20
+ "processOptionalFileEventWithResult"
21
+ );
14
22
  /**
15
23
  * Commit any pending file events that have been queued for processing.
16
24
  */
@@ -1,3 +1,4 @@
1
+ import type { ValueStore, ValueDeleter } from "@vrtmrz/value-store";
1
2
  import type { FetchHttpHandler } from "@smithy/fetch-http-handler";
2
3
  import { type LOG_LEVEL } from "octagonal-wheels/common/logger";
3
4
  import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, ConfigPassphraseStore, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, RemotePreferredTweakResult, SettingsMigrationState, TweakAssessment, TweakValues, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
@@ -15,8 +16,9 @@ import type { OwnedCouchDBConnection, RemoteConnectionOpenOptions } from "./Remo
15
16
  import type { ActiveReplicatorContext, ContinuousReplicationRequest, ReplicationAttemptFailure, ReplicationFailureRequest, ReplicatorProviderDefinitionMap, ReplicationOutcome, ReplicationReadinessRequirements, UnattendedOneShotRequest, UserInitiatedOneShotRequest } from "../../replication/ReplicatorProvider.js";
16
17
  import type { RemoteResourceKind, RemoteResourceMap } from "../../replication/RemoteResource.js";
17
18
  import type { CentralRemoteAdministrationRequest, CentralRemoteAdministrationResult } from "../../replication/CentralRemoteAdministration.js";
18
- import type { MultipleHandlerFunction } from "../lib/HandlerUtils.js";
19
+ import type { HandlerOutcome, MultipleHandlerFunction } from "../lib/HandlerUtils.js";
19
20
  import type { VaultScanResult } from "./VaultScanResult.js";
21
+ import type { UICommandRegistry } from "../../uiCommands.js";
20
22
  declare global {
21
23
  interface OPTIONAL_SYNC_FEATURES {
22
24
  DISABLE: "DISABLE";
@@ -95,9 +97,17 @@ export interface IKeyValueDBService {
95
97
  openSimpleStore<T>(kind: string): SimpleStore<T>;
96
98
  simpleStore: SimpleStore<unknown>;
97
99
  }
100
+ /** Options for completing an admitted optional file event. */
101
+ export interface OptionalFileEventOptions {
102
+ /** Resolves when a finite flush needs this event to finish without its debounce delay. */
103
+ whenFlushRequested?: Promise<void>;
104
+ }
98
105
  export interface IFileProcessingService {
99
106
  processFileEvent(item: FileEventItem): Promise<boolean>;
107
+ /** Legacy notification contract; its boolean does not confirm persistence. */
100
108
  processOptionalFileEvent(path: FilePath): Promise<boolean>;
109
+ /** Complete the actual optional file operation, or explain why it is not completed. */
110
+ processOptionalFileEventWithResult?(path: FilePath, options?: OptionalFileEventOptions): Promise<HandlerOutcome<void, string>>;
101
111
  commitPendingFileEvents(): Promise<boolean>;
102
112
  batched: ReactiveSource<number>;
103
113
  processing: ReactiveSource<number>;
@@ -312,16 +322,11 @@ export interface ISettingService {
312
322
  getSettingsMigrationState(): SettingsMigrationState | undefined;
313
323
  getDeviceAndVaultName(): string;
314
324
  setDeviceAndVaultName(name: string): void;
315
- saveDeviceAndVaultName(): void;
316
- /**
317
- * Read host-provided, device-local configuration by its exact key.
318
- * This storage is not part of the synchronised settings document.
319
- */
320
- getDeviceLocalConfig(key: string): string | null;
321
- /** Store host-provided, device-local configuration by its exact key. */
322
- setDeviceLocalConfig(key: string, value: string): void;
323
- /** Delete host-provided, device-local configuration by its exact key. */
324
- deleteDeviceLocalConfig(key: string): void;
325
+ saveDeviceAndVaultName(): Promise<void>;
326
+ /** Exact-key host storage; values are local to this application instance. */
327
+ readonly deviceValues: ValueStore & ValueDeleter;
328
+ /** Storage scoped by the current Vault and database suffix. */
329
+ readonly config: IConfigService;
325
330
  onBeforeSaveSettingData(nextSettings: ObsidianLiveSyncSettings, previousSettings: ObsidianLiveSyncSettings): Promise<(Partial<ObsidianLiveSyncSettings> | void)[]>;
326
331
  saveSettingData(): Promise<void>;
327
332
  /** Save a new configuration wrapper and its device-local key, retaining the protected values. */
@@ -333,9 +338,6 @@ export interface ISettingService {
333
338
  onSettingLoaded(settings: ObsidianLiveSyncSettings): Promise<boolean>;
334
339
  onSettingChanged(settings: ObsidianLiveSyncSettings): Promise<boolean>;
335
340
  onSettingSaved(settings: ObsidianLiveSyncSettings): Promise<boolean>;
336
- getSmallConfig(key: string): string | null;
337
- setSmallConfig(key: string, value: string): void;
338
- deleteSmallConfig(key: string): void;
339
341
  }
340
342
  export interface ITweakValueService {
341
343
  fetchRemotePreferred(trialSetting: RemoteDBSettings): Promise<RemotePreferredTweakResult>;
@@ -373,15 +375,13 @@ export interface ITestService {
373
375
  addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void;
374
376
  }
375
377
  export interface IUIService {
378
+ readonly commands: UICommandRegistry;
376
379
  promptCopyToClipboard(title: string, value: string): Promise<boolean>;
377
380
  showMarkdownDialog<T extends string[]>(title: string, contentMD: string, buttons: T): Promise<(typeof buttons)[number] | false>;
378
381
  get confirm(): Confirm;
379
382
  }
380
- export interface IConfigService {
381
- getSmallConfig(key: string): string | null;
382
- setSmallConfig(key: string, value: string): void;
383
- deleteSmallConfig(key: string): void;
384
- }
383
+ /** Asynchronous device configuration selected by the host composition. */
384
+ export type IConfigService = ValueStore & ValueDeleter;
385
385
  export interface IServiceHub {
386
386
  API: IAPIService;
387
387
  path: IPathService;
@@ -125,5 +125,6 @@ export declare abstract class ReplicatorService<T extends ServiceContext = Servi
125
125
  lastSyncPullSeq: number;
126
126
  lastSyncPushSeq: number;
127
127
  syncStatus: import("../../common/types.js").DatabaseConnectingStatus;
128
+ couchDB?: import("../../common/models/shared.definition.js").CouchDBReplicationProgress;
128
129
  }>;
129
130
  }
@@ -1,7 +1,7 @@
1
1
  import { CAPABILITY_SUPPORT_KINDS } from "../../replication/ProviderCapability.js";
2
2
  async function resolveRemoteResource(capabilities, kind, setting) {
3
3
  const capability = capabilities[kind];
4
- if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
4
+ if (capability?.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
5
5
  return void 0;
6
6
  }
7
7
  return await capability.run(setting);
@@ -1,3 +1,4 @@
1
+ import { type ValueStore, type ValueDeleter } from "@vrtmrz/value-store";
1
2
  import { type ConfigPassphraseStore, type ObsidianLiveSyncSettings, type SettingsMigrationState } from "../../common/types.js";
2
3
  import type { IAPIService, ISettingService } from "./IService.js";
3
4
  import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
@@ -5,6 +6,8 @@ import { createInstanceLogFunction } from "../lib/logUtils.js";
5
6
  import { type PersistedSettings } from "../../common/models/setting.policy.js";
6
7
  export interface SettingServiceDependencies {
7
8
  APIService: IAPIService;
9
+ /** Host-owned exact-key storage, created once for this application instance. */
10
+ deviceValues: ValueStore & ValueDeleter;
8
11
  /** Optional host hook for applying the loaded display language to its catalogue. */
9
12
  onDisplayLanguageChanged?: (language: ObsidianLiveSyncSettings["displayLanguage"]) => void;
10
13
  }
@@ -12,9 +15,10 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
12
15
  deviceAndVaultName: string;
13
16
  protected APIService: IAPIService;
14
17
  private readonly onDisplayLanguageChanged?;
15
- protected abstract setItem(key: string, value: string): void;
16
- protected abstract getItem(key: string): string;
17
- protected abstract deleteItem(key: string): void;
18
+ /** Exact-key device values, including legacy keys outside the Vault namespace. */
19
+ readonly deviceValues: ValueStore & ValueDeleter;
20
+ /** Device values scoped by the current Vault name and database suffix. */
21
+ readonly config: ValueStore & ValueDeleter;
18
22
  _settings: ObsidianLiveSyncSettings;
19
23
  get settings(): ObsidianLiveSyncSettings;
20
24
  set settings(value: ObsidianLiveSyncSettings);
@@ -43,23 +47,9 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
43
47
  /**
44
48
  * Save the current device and vault name to settings, aside from the main settings.
45
49
  */
46
- saveDeviceAndVaultName(): void;
47
- /**
48
- * Read an exact key from the host's device-local configuration store.
49
- *
50
- * Unlike the main settings document, this state is not synchronised. Callers
51
- * which need Vault namespacing should normally use {@link getSmallConfig}.
52
- */
53
- getDeviceLocalConfig(key: string): string | null;
54
- /** Store an exact key in the host's non-synchronised configuration store. */
55
- setDeviceLocalConfig(key: string, value: string): void;
56
- /** Delete an exact key from the host's non-synchronised configuration store. */
57
- deleteDeviceLocalConfig(key: string): void;
50
+ saveDeviceAndVaultName(): Promise<void>;
58
51
  private additionalSuffixOfDatabaseName;
59
52
  private getKey;
60
- setSmallConfig(key: string, value: string): void;
61
- getSmallConfig(key: string): string;
62
- deleteSmallConfig(key: string): void;
63
53
  /**
64
54
  * Save the current settings to storage.
65
55
  */
@@ -1,3 +1,4 @@
1
+ import { createDeletableValueStore } from "@vrtmrz/value-store";
1
2
  import {
2
3
  ChunkAlgorithmNames,
3
4
  LOG_LEVEL_NOTICE,
@@ -42,6 +43,10 @@ class SettingService extends ServiceBase {
42
43
  deviceAndVaultName = "";
43
44
  APIService;
44
45
  onDisplayLanguageChanged;
46
+ /** Exact-key device values, including legacy keys outside the Vault namespace. */
47
+ deviceValues;
48
+ /** Device values scoped by the current Vault name and database suffix. */
49
+ config;
45
50
  _settings;
46
51
  get settings() {
47
52
  return this._settings;
@@ -59,6 +64,12 @@ class SettingService extends ServiceBase {
59
64
  constructor(context, dependencies) {
60
65
  super(context);
61
66
  this.APIService = dependencies.APIService;
67
+ this.deviceValues = dependencies.deviceValues;
68
+ this.config = createDeletableValueStore({
69
+ get: (key) => this.deviceValues.getValue(this.getKey(key)),
70
+ set: (key, value) => this.deviceValues.setValue(this.getKey(key), value),
71
+ delete: (key) => this.deviceValues.delete(this.getKey(key))
72
+ });
62
73
  this.onDisplayLanguageChanged = dependencies.onDisplayLanguageChanged;
63
74
  this._log = createInstanceLogFunction("SettingService", this.APIService);
64
75
  }
@@ -111,26 +122,9 @@ class SettingService extends ServiceBase {
111
122
  /**
112
123
  * Save the current device and vault name to settings, aside from the main settings.
113
124
  */
114
- saveDeviceAndVaultName() {
125
+ async saveDeviceAndVaultName() {
115
126
  const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.APIService.getSystemVaultName() + this.additionalSuffixOfDatabaseName();
116
- this.setDeviceLocalConfig(lsKey, this.deviceAndVaultName);
117
- }
118
- /**
119
- * Read an exact key from the host's device-local configuration store.
120
- *
121
- * Unlike the main settings document, this state is not synchronised. Callers
122
- * which need Vault namespacing should normally use {@link getSmallConfig}.
123
- */
124
- getDeviceLocalConfig(key) {
125
- return this.getItem(key);
126
- }
127
- /** Store an exact key in the host's non-synchronised configuration store. */
128
- setDeviceLocalConfig(key, value) {
129
- this.setItem(key, value);
130
- }
131
- /** Delete an exact key from the host's non-synchronised configuration store. */
132
- deleteDeviceLocalConfig(key) {
133
- this.deleteItem(key);
127
+ await this.deviceValues.setValue(lsKey, this.deviceAndVaultName);
134
128
  }
135
129
  additionalSuffixOfDatabaseName() {
136
130
  const suffix = this.settings?.additionalSuffixOfDatabaseName;
@@ -145,18 +139,6 @@ class SettingService extends ServiceBase {
145
139
  const addSuffix = this.additionalSuffixOfDatabaseName();
146
140
  return `${keyMain}${addSuffix}-${key}`;
147
141
  }
148
- setSmallConfig(key, value) {
149
- const dbKey = this.getKey(key);
150
- this.setDeviceLocalConfig(dbKey, value);
151
- }
152
- getSmallConfig(key) {
153
- const dbKey = this.getKey(key);
154
- return this.getDeviceLocalConfig(dbKey) ?? "";
155
- }
156
- deleteSmallConfig(key) {
157
- const dbKey = this.getKey(key);
158
- this.deleteDeviceLocalConfig(dbKey);
159
- }
160
142
  /**
161
143
  * Save the current settings to storage.
162
144
  */
@@ -178,7 +160,7 @@ class SettingService extends ServiceBase {
178
160
  if (store !== "" && store !== "LOCALSTORAGE" && store !== "ASK_AT_LAUNCH") {
179
161
  throw new Error("The configuration passphrase storage mode is unsupported.");
180
162
  }
181
- const oldLocalKey = this.getDeviceLocalConfig("ls-setting-passphrase");
163
+ const oldLocalKey = await this.deviceValues.getValue("ls-setting-passphrase");
182
164
  const settings = this.cloneSettings(this.settings);
183
165
  let targetKey;
184
166
  if (store === "LOCALSTORAGE") {
@@ -196,10 +178,10 @@ class SettingService extends ServiceBase {
196
178
  const prepared = await this.prepareSettingSave(settings, targetKey);
197
179
  const changesLocalKey = store === "LOCALSTORAGE" && targetKey !== oldLocalKey;
198
180
  try {
199
- if (changesLocalKey) this.setDeviceLocalConfig("ls-setting-passphrase", targetKey);
181
+ if (changesLocalKey) await this.deviceValues.setValue("ls-setting-passphrase", targetKey);
200
182
  await this.writePreparedSettings(prepared);
201
183
  } catch (error) {
202
- if (changesLocalKey) this.restoreConfigurationPassphrase(oldLocalKey, error);
184
+ if (changesLocalKey) await this.restoreConfigurationPassphrase(oldLocalKey, error);
203
185
  throw error;
204
186
  }
205
187
  this.settings.configPassphraseStore = store;
@@ -210,10 +192,10 @@ class SettingService extends ServiceBase {
210
192
  throw error;
211
193
  });
212
194
  }
213
- restoreConfigurationPassphrase(previous, cause) {
195
+ async restoreConfigurationPassphrase(previous, cause) {
214
196
  try {
215
- if (previous) this.setDeviceLocalConfig("ls-setting-passphrase", previous);
216
- else this.deleteDeviceLocalConfig("ls-setting-passphrase");
197
+ if (previous) await this.deviceValues.setValue("ls-setting-passphrase", previous);
198
+ else await this.deviceValues.delete("ls-setting-passphrase");
217
199
  } catch {
218
200
  const message = this.context.translate("SettingService.ConfigurationPassphraseRollbackFailed");
219
201
  this._log(message, LOG_LEVEL_URGENT);
@@ -344,9 +326,9 @@ class SettingService extends ServiceBase {
344
326
  return { persisted, runtime, patch: savedPatch, passphrase: configurationPassphrase };
345
327
  }
346
328
  async writePreparedSettings(prepared) {
347
- this.saveDeviceAndVaultName();
329
+ await this.saveDeviceAndVaultName();
348
330
  if (prepared.runtime.P2P_DevicePeerName?.trim()) {
349
- this.setSmallConfig(SETTING_KEY_P2P_DEVICE_NAME, prepared.runtime.P2P_DevicePeerName.trim());
331
+ await this.config.setValue(SETTING_KEY_P2P_DEVICE_NAME, prepared.runtime.P2P_DevicePeerName.trim());
350
332
  }
351
333
  await this.saveData(prepared.persisted);
352
334
  }
@@ -528,7 +510,7 @@ class SettingService extends ServiceBase {
528
510
  getPassphrase(settings) {
529
511
  const methods = {
530
512
  "": () => Promise.resolve("*"),
531
- LOCALSTORAGE: () => Promise.resolve(this.getDeviceLocalConfig("ls-setting-passphrase") ?? false),
513
+ LOCALSTORAGE: () => this.deviceValues.getValue("ls-setting-passphrase", ""),
532
514
  ASK_AT_LAUNCH: () => this.APIService.confirm.askString(
533
515
  this.context.translate("SettingService.ConfigurationPassphraseTitle"),
534
516
  "passphrase",
@@ -700,7 +682,7 @@ class SettingService extends ServiceBase {
700
682
  } else {
701
683
  configuredIdKey(settings);
702
684
  }
703
- const previous = this.getDeviceLocalConfig("ls-setting-passphrase");
685
+ const previous = await this.deviceValues.getValue("ls-setting-passphrase");
704
686
  let candidate = previous || false;
705
687
  let retry = false;
706
688
  while (true) {
@@ -710,9 +692,9 @@ class SettingService extends ServiceBase {
710
692
  if (result.verified) {
711
693
  if (candidate !== previous) {
712
694
  try {
713
- this.setDeviceLocalConfig("ls-setting-passphrase", candidate);
695
+ await this.deviceValues.setValue("ls-setting-passphrase", candidate);
714
696
  } catch (error) {
715
- this.restoreConfigurationPassphrase(previous, error);
697
+ await this.restoreConfigurationPassphrase(previous, error);
716
698
  throw error;
717
699
  }
718
700
  }
@@ -788,9 +770,9 @@ class SettingService extends ServiceBase {
788
770
  }
789
771
  const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.APIService.getSystemVaultName() + this.additionalSuffixOfDatabaseName();
790
772
  if (this.settings.deviceAndVaultName != "") {
791
- if (!this.getDeviceLocalConfig(lsKey)) {
773
+ if (!await this.deviceValues.getValue(lsKey)) {
792
774
  this.setDeviceAndVaultName(this.settings.deviceAndVaultName);
793
- this.saveDeviceAndVaultName();
775
+ await this.saveDeviceAndVaultName();
794
776
  this.settings.deviceAndVaultName = "";
795
777
  }
796
778
  }
@@ -801,7 +783,7 @@ class SettingService extends ServiceBase {
801
783
  );
802
784
  this.settings.customChunkSize = 0;
803
785
  }
804
- this.setDeviceAndVaultName(this.getDeviceLocalConfig(lsKey) || "");
786
+ this.setDeviceAndVaultName(await this.deviceValues.getValue(lsKey) || "");
805
787
  if (this.getDeviceAndVaultName() == "") {
806
788
  if (this.settings.usePluginSync) {
807
789
  this._log("Device name missing. Disabling plug-in sync.", LOG_LEVEL_NOTICE);
@@ -1,4 +1,4 @@
1
- import type { ConfigService } from "../../base/ConfigService.js";
1
+ import type { IConfigService } from "../../base/IService.js";
2
2
  import type { AppLifecycleService } from "../../base/AppLifecycleService.js";
3
3
  import type { ReplicatorService } from "../../base/ReplicatorService.js";
4
4
  import type { Confirm } from "../../../interfaces/Confirm.js";
@@ -9,7 +9,7 @@ import type { Constructor } from "../../../common/utils.type.js";
9
9
  export type SvelteDialogManagerDependencies<T extends ServiceContext = ServiceContext> = {
10
10
  appLifecycle: AppLifecycleService<T>;
11
11
  replicator: ReplicatorService<T>;
12
- config: ConfigService<T>;
12
+ config: IConfigService;
13
13
  confirm: Confirm;
14
14
  control: IControlService;
15
15
  };
@@ -1,4 +1,5 @@
1
1
  import type { Confirm } from "../../../interfaces/Confirm.js";
2
+ import { UICommandRegistry } from "../../../uiCommands.js";
2
3
  import type { ComponentHasResult, SvelteDialogManager } from "./SvelteDialog.js";
3
4
  import type { IAPIService, IUIService } from "../../base/IService.js";
4
5
  import { ServiceBase, type ServiceContext } from "../../base/ServiceBase.js";
@@ -12,6 +13,7 @@ type DialogParams = {
12
13
  dataToCopy: string;
13
14
  };
14
15
  export declare abstract class UIService<T extends ServiceContext = ServiceContext> extends ServiceBase<T> implements IUIService {
16
+ readonly commands: UICommandRegistry;
15
17
  private _dialogManager;
16
18
  protected _APIService: IAPIService;
17
19
  abstract get dialogToCopy(): ComponentHasResult<DialogResult, DialogParams>;
@@ -1,9 +1,14 @@
1
+ import { EVENT_PLUGIN_UNLOADED } from "../../../events/coreEvents.js";
2
+ import { UICommandRegistry } from "../../../uiCommands.js";
1
3
  import { ServiceBase } from "../../base/ServiceBase.js";
2
4
  class UIService extends ServiceBase {
5
+ commands;
3
6
  _dialogManager;
4
7
  _APIService;
5
8
  constructor(context, dependents) {
6
9
  super(context);
10
+ this.commands = new UICommandRegistry();
11
+ this.context.events.once(EVENT_PLUGIN_UNLOADED, () => this.commands.dispose());
7
12
  this._dialogManager = dependents.dialogManager;
8
13
  this._APIService = dependents.APIService;
9
14
  }
@@ -1,4 +1,4 @@
1
- import type { ConfigService } from "../../base/ConfigService.js";
1
+ import type { IConfigService } from "../../base/IService.js";
2
2
  import { ControlService } from "../../base/ControlService.js";
3
3
  import type { KeyValueDBService } from "../../base/KeyValueDBService.js";
4
4
  import { PathService } from "../../base/PathService.js";
@@ -35,7 +35,7 @@ export declare class InjectableServiceHub<T extends ServiceContext = ServiceCont
35
35
  protected readonly _vault: InjectableVaultService<T>;
36
36
  protected readonly _test: InjectableTestService<T>;
37
37
  protected readonly _ui: UIService<T>;
38
- protected readonly _config: ConfigService<T>;
38
+ protected readonly _config: IConfigService;
39
39
  protected readonly _keyValueDB: KeyValueDBService<T>;
40
40
  protected readonly _control: ControlService<T>;
41
41
  get API(): InjectableAPIService<T>;
@@ -55,14 +55,14 @@ export declare class InjectableServiceHub<T extends ServiceContext = ServiceCont
55
55
  get control(): ControlService<T>;
56
56
  get keyValueDB(): KeyValueDBService<T>;
57
57
  get UI(): UIService<T>;
58
- get config(): ConfigService<T>;
58
+ get config(): IConfigService;
59
59
  constructor(context: T, services: InjectableServiceInstances<T> & {
60
60
  setting: SettingService<T>;
61
61
  appLifecycle: AppLifecycleServiceBase<T>;
62
62
  path: PathService<T>;
63
63
  API: InjectableAPIService<T>;
64
64
  ui: UIService<T>;
65
- config: ConfigService<T>;
65
+ config: IConfigService;
66
66
  database: DatabaseService<T>;
67
67
  remote: InjectableRemoteService<T>;
68
68
  vault: InjectableVaultService<T>;
@@ -1,6 +1,6 @@
1
+ import type { IConfigService } from "../../base/IService.js";
1
2
  import { type ServiceInstances } from "../../ServiceHub.js";
2
3
  import type { UIService } from "../base/UIService.js";
3
- import type { ConfigService } from "../../base/ConfigService.js";
4
4
  import type { ServiceContext } from "../../base/ServiceBase.js";
5
5
  import type { InjectableAPIService } from "./InjectableAPIService.js";
6
6
  import type { InjectableDatabaseEventService } from "./InjectableDatabaseEventService.js";
@@ -32,5 +32,5 @@ export type InjectableServiceInstances<T extends ServiceContext> = ServiceInstan
32
32
  vault?: InjectableVaultService<T>;
33
33
  test?: InjectableTestService<T>;
34
34
  ui?: UIService<T>;
35
- config?: ConfigService<T>;
35
+ config?: IConfigService;
36
36
  };
@@ -4,9 +4,6 @@ import type { ObsidianLiveSyncSettings } from "../../../common/types.js";
4
4
  import type { PersistedSettings } from "../../../common/models/setting.policy.js";
5
5
  export declare class InjectableSettingService<T extends ServiceContext> extends SettingService<T> {
6
6
  constructor(context: T, dependencies: SettingServiceDependencies);
7
- protected setItem(key: string, value: string): void;
8
- protected getItem(key: string): string;
9
- protected deleteItem(key: string): void;
10
7
  saveData: import("../../lib/HandlerUtils.js").HandlerFunction<(data: PersistedSettings) => Promise<void>, unknown>;
11
8
  loadData: import("../../lib/HandlerUtils.js").HandlerFunction<() => Promise<ObsidianLiveSyncSettings | undefined>, unknown>;
12
9
  }
@@ -2,7 +2,6 @@ import { EVENT_SETTING_SAVED } from "../../../events/coreEvents.js";
2
2
  import { SettingService } from "../../base/SettingService.js";
3
3
  import { EVENT_REQUEST_RELOAD_SETTING_TAB } from "../../../events/coreEvents.js";
4
4
  import { handlers } from "../../lib/HandlerUtils.js";
5
- import { compatGlobal } from "../../../common/coreEnvFunctions.js";
6
5
  class InjectableSettingService extends SettingService {
7
6
  constructor(context, dependencies) {
8
7
  super(context, dependencies);
@@ -15,16 +14,6 @@ class InjectableSettingService extends SettingService {
15
14
  return Promise.resolve(true);
16
15
  });
17
16
  }
18
- setItem(key, value) {
19
- return compatGlobal.localStorage.setItem(key, value);
20
- }
21
- getItem(key) {
22
- return compatGlobal.localStorage.getItem(key) ?? "";
23
- }
24
- deleteItem(key) {
25
- compatGlobal.localStorage.removeItem(key);
26
- }
27
- // override currentSettings = handlers<SettingService<T>>().binder("currentSettings");
28
17
  saveData = handlers().binder("saveData");
29
18
  loadData = handlers().binder("loadData");
30
19
  }
@@ -2,6 +2,27 @@
2
2
  * A function type that can be used as a handler.
3
3
  */
4
4
  type HandlerFunc<TArg extends unknown[], TResult> = (...args: TArg) => TResult | Promise<TResult>;
5
+ /**
6
+ * The outcome of a handler.
7
+ *
8
+ * A completed outcome carries a value when `TValue` is not void, and a veto
9
+ * carries a reason when `TVetoReason` is not void. Not-applicable outcomes
10
+ * always continue to the next registered handler.
11
+ */
12
+ export type HandlerOutcome<TValue = void, TVetoReason = void> = {
13
+ kind: "not-applicable";
14
+ } | ([TValue] extends [void] ? {
15
+ kind: "completed";
16
+ } : {
17
+ kind: "completed";
18
+ value: TValue;
19
+ }) | ([TVetoReason] extends [void] ? {
20
+ kind: "veto";
21
+ } : {
22
+ kind: "veto";
23
+ value: TVetoReason;
24
+ });
25
+ type AnyHandlerOutcome = HandlerOutcome | HandlerOutcome<unknown, unknown>;
5
26
  /**
6
27
  * A function type that returns a boolean or a Promise of boolean.
7
28
  */
@@ -244,6 +265,29 @@ export declare class FirstResultHandler<T extends unknown[], U> extends MultiBin
244
265
  */
245
266
  invoke(...args: T): Promise<U | false>;
246
267
  }
268
+ /**
269
+ * Invokes registered handlers in priority and registration order until one
270
+ * completes or vetoes. Each invocation uses a snapshot of registrations.
271
+ * Callback errors and rejections propagate unchanged, and registrations are
272
+ * owned by this handler.
273
+ */
274
+ export declare class FirstHandledHandler<T extends unknown[], U extends AnyHandlerOutcome = HandlerOutcome> extends MultiBinder<HandlerFunc<T, U>> implements InvokableHandler<T, U | {
275
+ kind: "not-applicable";
276
+ }> {
277
+ /**
278
+ * Whether at least one callback is currently registered.
279
+ */
280
+ get hasHandlers(): boolean;
281
+ /**
282
+ * Invokes handlers until one returns a completed or veto outcome.
283
+ * @param args The arguments to pass to the handlers.
284
+ * @returns The first handled outcome, or a not-applicable outcome when no handler applies.
285
+ * @throws Any synchronous callback error or rejected callback value, unchanged.
286
+ */
287
+ invoke(...args: T): Promise<U | {
288
+ kind: "not-applicable";
289
+ }>;
290
+ }
247
291
  /**
248
292
  * A function type that can be used as a handler with assignable functionality.
249
293
  */
@@ -297,6 +341,11 @@ export interface MultipleHandlerFunction<TFunc extends (...args: any[]) => U | P
297
341
  */
298
342
  removeHandler: (callback: TFunc) => void;
299
343
  }
344
+ export type FirstHandledFunction<TFunc extends (...args: any[]) => AnyHandlerOutcome | Promise<AnyHandlerOutcome>> = ((...args: Parameters<TFunc>) => Promise<Awaited<ReturnType<TFunc>> | {
345
+ kind: "not-applicable";
346
+ }>) & Pick<MultipleHandlerFunction<TFunc>, "addHandler" | "removeHandler"> & {
347
+ readonly hasHandlers: boolean;
348
+ };
300
349
  /**
301
350
  * A function type that can be used as a value-collecting handler with add/remove functionality.
302
351
  */
@@ -351,12 +400,16 @@ export declare function bailFirstFailureWithResultFunction<TFunc extends (...arg
351
400
  export declare function allParallelFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
352
401
  export declare function anySuccessFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
353
402
  export declare function firstResultFunction<TFunc extends (...args: any[]) => Promise<unknown>>(name?: string): MultipleHandlerFunction<TFunc>;
403
+ export declare function firstHandledFunction<TFunc extends (...args: any[]) => AnyHandlerOutcome | Promise<AnyHandlerOutcome>>(name?: string): FirstHandledFunction<TFunc>;
354
404
  export declare function dispatchParallelFunction<TFunc extends (...args: any[]) => Promise<unknown[]>>(name?: string): CollectiveHandlerFunction<TFunc>;
355
405
  export declare function bindableFunction<TFunc extends (...args: any[]) => any>(name?: string): HandlerFunction<TFunc>;
356
406
  export declare function lazyBindableFunction<TFunc extends (...args: any[]) => any>(name?: string): LazyHandlerFunction<TFunc>;
357
407
  type FunctionKeys<T> = Extract<{
358
408
  [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
359
409
  }[keyof T], string>;
410
+ type FirstHandledFunctionKeys<T> = Extract<{
411
+ [K in keyof T]: [NonNullable<T[K]>] extends [never] ? never : NonNullable<T[K]> extends (...args: any[]) => AnyHandlerOutcome | Promise<AnyHandlerOutcome> ? K : never;
412
+ }[keyof T], string>;
360
413
  export declare function handlers<T extends object>(): {
361
414
  /**
362
415
  * Create a handler that invokes all added handler functions sequentially until one returns false.
@@ -403,6 +456,13 @@ export declare function handlers<T extends object>(): {
403
456
  * @returns
404
457
  */
405
458
  firstResult<K extends FunctionKeys<T>>(name: K): MultipleHandlerFunction<Extract<T[K], (...args: any[]) => Promise<unknown>>>;
459
+ /**
460
+ * Create a handler that invokes registered handlers until one completes or vetoes.
461
+ * Callback errors and rejections propagate unchanged.
462
+ * @param name The name of the callback on `T`.
463
+ * @returns A bound handler with a live `hasHandlers` property.
464
+ */
465
+ firstHandled<K extends FirstHandledFunctionKeys<T>>(name: K): FirstHandledFunction<Extract<NonNullable<T[K]>, (...args: any[]) => AnyHandlerOutcome | Promise<AnyHandlerOutcome>>>;
406
466
  /**
407
467
  * Create a handler that invokes all added handler functions in parallel.
408
468
  * @param name