@vrtmrz/livesync-commonlib 0.1.0-rc.1 → 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.
- package/README.md +87 -19
- package/dist/common/i18n.js +7 -7099
- package/dist/common/messages/combinedMessages.prod.d.ts +3 -0
- package/dist/common/messages/combinedMessages.prod.js +3 -0
- package/dist/common/messages/def.d.ts +1 -0
- package/dist/common/messagesJson/en.json +1 -0
- package/dist/common/models/setting.const.defaults.d.ts +19 -0
- package/dist/common/models/setting.const.defaults.js +20 -2
- package/dist/common/models/setting.lifecycle.d.ts +31 -0
- package/dist/common/models/setting.lifecycle.js +109 -0
- package/dist/common/types.d.ts +4 -2
- package/dist/common/types.js +16 -1
- package/dist/interfaces/Confirm.d.ts +8 -1
- package/dist/interfaces/DatabaseRebuilder.d.ts +12 -2
- package/dist/remoteConfigurations.d.ts +9 -0
- package/dist/remoteConfigurations.js +14 -0
- package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +4 -1
- package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +41 -26
- package/dist/replication/trystero/P2PReplicatorCore.d.ts +2 -6
- package/dist/replication/trystero/P2PReplicatorCore.js +16 -86
- package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -1
- package/dist/replication/trystero/TrysteroReplicator.js +10 -4
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +4 -5
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +22 -30
- package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -2
- package/dist/replication/trystero/addP2PEventHandlers.d.ts +4 -2
- package/dist/replication/trystero/addP2PEventHandlers.js +8 -6
- package/dist/replication/trystero/types.d.ts +2 -0
- package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +1 -1
- package/dist/replication/trystero/useP2PReplicatorCommands.js +3 -1
- package/dist/replication/trystero/useP2PReplicatorFeature.js +1 -1
- package/dist/rpc/RpcRoom.d.ts +1 -1
- package/dist/rpc/RpcRoom.js +22 -16
- package/dist/rpc/transports/TrysteroTransport.d.ts +2 -0
- package/dist/rpc/transports/TrysteroTransport.js +24 -17
- package/dist/rpc/transports/trysteroRoomEvents.d.ts +14 -0
- package/dist/rpc/transports/trysteroRoomEvents.js +34 -0
- package/dist/serviceFeatures/remoteConfig.d.ts +37 -1
- package/dist/serviceFeatures/remoteConfig.js +90 -0
- package/dist/serviceModules/Rebuilder.d.ts +3 -2
- package/dist/serviceModules/Rebuilder.js +21 -9
- package/dist/services/base/IService.d.ts +12 -1
- package/dist/services/base/ReplicationService.d.ts +3 -1
- package/dist/services/base/ReplicationService.js +6 -1
- package/dist/services/base/SettingService.d.ts +14 -1
- package/dist/services/base/SettingService.js +35 -12
- package/dist/services/implements/headless/HeadlessAPIService.d.ts +1 -1
- package/dist/services/implements/headless/HeadlessAPIService.js +1 -1
- package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +3 -3
- package/dist/settings.d.ts +10 -0
- package/dist/settings.js +32 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/development.md +75 -1
- package/docs/p2p-transport-lifecycle.md +52 -0
- package/docs/proven-in-use.md +55 -0
- package/docs/releasing.md +9 -5
- package/docs/remote-configurations.md +63 -0
- package/docs/settings-lifecycle.md +60 -0
- 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
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
137
|
+
async scheduleInitialisation(flag, prepareBeforeRestart) {
|
|
138
138
|
try {
|
|
139
|
-
await this.storageAccess.writeFileAuto(
|
|
139
|
+
await this.storageAccess.writeFileAuto(flag, "");
|
|
140
140
|
} catch (ex) {
|
|
141
|
-
this._log(`Could not create ${
|
|
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.
|
|
145
|
-
}
|
|
146
|
-
async scheduleFetch() {
|
|
145
|
+
this.appLifecycle.setSuspended(true);
|
|
147
146
|
try {
|
|
148
|
-
await
|
|
147
|
+
await prepareBeforeRestart?.();
|
|
149
148
|
} catch (ex) {
|
|
150
|
-
|
|
151
|
-
|
|
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.
|
|
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.
|
|
129
|
+
this.setDeviceLocalConfig(dbKey, value);
|
|
109
130
|
}
|
|
110
131
|
getSmallConfig(key) {
|
|
111
132
|
const dbKey = this.getKey(key);
|
|
112
|
-
return this.
|
|
133
|
+
return this.getDeviceLocalConfig(dbKey) ?? "";
|
|
113
134
|
}
|
|
114
135
|
deleteSmallConfig(key) {
|
|
115
136
|
const dbKey = this.getKey(key);
|
|
116
|
-
this.
|
|
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.
|
|
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
|
|
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 (
|
|
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.
|
|
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.
|
|
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("../../../
|
|
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("../../../
|
|
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("../../../
|
|
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";
|
package/dist/settings.js
ADDED
|
@@ -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
|
+
};
|