@vrtmrz/livesync-commonlib 0.1.0-rc.1 → 0.1.0-rc.3
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/configForDoc.js +6 -3
- package/dist/common/i18n.js +7 -7099
- package/dist/common/messages/combinedMessages.prod.d.ts +12 -0
- package/dist/common/messages/combinedMessages.prod.js +12 -0
- package/dist/common/messages/def.d.ts +4 -0
- package/dist/common/messagesJson/en.json +4 -0
- package/dist/common/models/setting.const.defaults.d.ts +19 -0
- package/dist/common/models/setting.const.defaults.js +21 -2
- package/dist/common/models/setting.lifecycle.d.ts +31 -0
- package/dist/common/models/setting.lifecycle.js +109 -0
- package/dist/common/models/setting.type.d.ts +4 -2
- package/dist/common/settingConstants.js +2 -2
- package/dist/common/types.d.ts +4 -2
- package/dist/common/types.js +16 -1
- package/dist/interfaces/Confirm.d.ts +10 -2
- package/dist/interfaces/DatabaseRebuilder.d.ts +12 -2
- package/dist/remoteConfigurations.d.ts +9 -0
- package/dist/remoteConfigurations.js +14 -0
- package/dist/replication/LiveSyncAbstractReplicator.d.ts +1 -2
- package/dist/replication/couchdb/LiveSyncReplicator.d.ts +0 -1
- package/dist/replication/couchdb/LiveSyncReplicator.js +0 -3
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +0 -1
- package/dist/replication/journal/LiveSyncJournalReplicator.js +0 -3
- package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +5 -3
- package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +42 -30
- 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/checkRemoteSize.d.ts +5 -3
- package/dist/serviceFeatures/checkRemoteSize.js +51 -11
- package/dist/serviceFeatures/remoteConfig.d.ts +37 -1
- package/dist/serviceFeatures/remoteConfig.js +90 -0
- package/dist/serviceModules/Rebuilder.d.ts +3 -6
- package/dist/serviceModules/Rebuilder.js +23 -19
- package/dist/services/base/IService.d.ts +13 -6
- package/dist/services/base/ReplicationService.d.ts +4 -2
- package/dist/services/base/ReplicationService.js +11 -16
- package/dist/services/base/SettingService.d.ts +14 -9
- package/dist/services/base/SettingService.js +35 -17
- package/dist/services/implements/headless/HeadlessAPIService.d.ts +3 -3
- package/dist/services/implements/headless/HeadlessAPIService.js +2 -2
- 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 +83 -1
- package/docs/p2p-transport-lifecycle.md +52 -0
- package/docs/proven-in-use.md +57 -0
- package/docs/releasing.md +12 -6
- package/docs/remote-configurations.md +63 -0
- package/docs/settings-lifecycle.md +60 -0
- package/package.json +12 -2
package/dist/rpc/RpcRoom.js
CHANGED
|
@@ -23,34 +23,40 @@ class RpcRoom {
|
|
|
23
23
|
incomingChunkMap = /* @__PURE__ */ new Map();
|
|
24
24
|
incomingChunkTimers = /* @__PURE__ */ new Map();
|
|
25
25
|
peerVersion = /* @__PURE__ */ new Map();
|
|
26
|
-
|
|
26
|
+
disposers = [];
|
|
27
27
|
constructor(options) {
|
|
28
28
|
this.options = {
|
|
29
29
|
maxWirePayloadBytes: options.maxWirePayloadBytes ?? 32 * 1024,
|
|
30
30
|
chunkMissingRetryMs: options.chunkMissingRetryMs ?? 350,
|
|
31
31
|
...options
|
|
32
32
|
};
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
this.disposers.push(
|
|
34
|
+
this.options.transport.onMessage((msg, peerId) => {
|
|
35
|
+
void this.onWireMessage(msg, peerId);
|
|
36
|
+
})
|
|
37
|
+
);
|
|
36
38
|
if (this.options.transport.onPeerJoin) {
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
this.disposers.push(
|
|
40
|
+
this.options.transport.onPeerJoin((peerId) => {
|
|
41
|
+
void this.sendEnvelope(peerId, {
|
|
42
|
+
kind: "handshake",
|
|
43
|
+
versionMajor: RPC_VERSION_MAJOR,
|
|
44
|
+
versionMinor: RPC_VERSION_MINOR
|
|
45
|
+
});
|
|
46
|
+
})
|
|
47
|
+
);
|
|
44
48
|
}
|
|
45
49
|
if (this.options.transport.onPeerLeave) {
|
|
46
|
-
this.
|
|
47
|
-
this.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
this.disposers.push(
|
|
51
|
+
this.options.transport.onPeerLeave((peerId) => {
|
|
52
|
+
this.sessions.delete(peerId);
|
|
53
|
+
this.peerVersion.delete(peerId);
|
|
54
|
+
})
|
|
55
|
+
);
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
close() {
|
|
53
|
-
this.
|
|
59
|
+
this.disposers.splice(0).forEach((dispose) => dispose());
|
|
54
60
|
this.pending.forEach((pending) => pending.reject(new RpcError("NOT_CONNECTED", "Room closed")));
|
|
55
61
|
this.pending.clear();
|
|
56
62
|
this.inboundCalls.clear();
|
|
@@ -96,6 +96,8 @@ export type TrysteroRoomHandle = {
|
|
|
96
96
|
/**
|
|
97
97
|
* Returns the current WebRTC peer connections keyed by peer ID.
|
|
98
98
|
* Equivalent to the Trystero `room.getPeers()` call.
|
|
99
|
+
* Treat these as diagnostic handles. Their lifecycle belongs to Trystero;
|
|
100
|
+
* call {@link TrysteroRoomHandle.leave} instead of closing them directly.
|
|
99
101
|
*/
|
|
100
102
|
getPeers: () => Record<string, RTCPeerConnection>;
|
|
101
103
|
/**
|
|
@@ -5,30 +5,37 @@ import { RpcPouchDBProxy } from "../pouchdb/RpcPouchDBProxy.js";
|
|
|
5
5
|
import { ConnectionStringParser } from "../../common/ConnectionString.js";
|
|
6
6
|
import { compatGlobal } from "../../common/coreEnvFunctions.js";
|
|
7
7
|
import { generateJoinRoomOptions } from "./trysteroUtils.js";
|
|
8
|
+
import { subscribeTrysteroPeerEvents } from "./trysteroRoomEvents.js";
|
|
8
9
|
const RPC_ACTION_NAME = "rpc2";
|
|
9
10
|
const AD_ACTION_NAME = "ad";
|
|
10
11
|
function attachAdvertisement(room, localPeerId, name, platform = "unknown") {
|
|
11
12
|
const peerMap = /* @__PURE__ */ new Map();
|
|
12
13
|
const localAd = { peerId: localPeerId, name, platform };
|
|
13
|
-
const
|
|
14
|
-
const sendAdvertisement = (toPeerId) =>
|
|
15
|
-
room
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
const advertisementAction = room.makeAction(AD_ACTION_NAME);
|
|
15
|
+
const sendAdvertisement = (toPeerId) => advertisementAction.send(localAd, toPeerId ? { target: toPeerId } : void 0);
|
|
16
|
+
const disposePeerEvents = subscribeTrysteroPeerEvents(room, {
|
|
17
|
+
// When a new peer joins, introduce ourselves to them specifically.
|
|
18
|
+
onJoin: (peerId) => {
|
|
19
|
+
void sendAdvertisement(peerId);
|
|
20
|
+
},
|
|
21
|
+
// Remove stale entries when a peer disconnects.
|
|
22
|
+
onLeave: (peerId) => {
|
|
23
|
+
peerMap.delete(peerId);
|
|
24
|
+
}
|
|
20
25
|
});
|
|
21
|
-
|
|
26
|
+
advertisementAction.onMessage = (data, { peerId }) => {
|
|
22
27
|
if (data.peerId === localPeerId) return;
|
|
23
28
|
if (data.peerId !== peerId) return;
|
|
24
29
|
peerMap.set(peerId, data);
|
|
25
|
-
}
|
|
30
|
+
};
|
|
26
31
|
return {
|
|
27
32
|
get peers() {
|
|
28
33
|
return peerMap;
|
|
29
34
|
},
|
|
30
35
|
sendAdvertisement,
|
|
31
36
|
stop() {
|
|
37
|
+
disposePeerEvents();
|
|
38
|
+
advertisementAction.onMessage = null;
|
|
32
39
|
peerMap.clear();
|
|
33
40
|
}
|
|
34
41
|
};
|
|
@@ -40,22 +47,22 @@ const TRYSTERO_RPC_DEFAULTS = {
|
|
|
40
47
|
chunkMissingRetryMs: 150
|
|
41
48
|
};
|
|
42
49
|
function wrapTrysteroRoom(room) {
|
|
43
|
-
const
|
|
50
|
+
const rpcAction = room.makeAction(RPC_ACTION_NAME);
|
|
44
51
|
return {
|
|
45
52
|
send(message, peerId) {
|
|
46
|
-
return
|
|
53
|
+
return rpcAction.send(message, { target: peerId });
|
|
47
54
|
},
|
|
48
55
|
onMessage(handler) {
|
|
49
|
-
|
|
50
|
-
return () =>
|
|
56
|
+
rpcAction.onMessage = (data, { peerId }) => handler(data, peerId);
|
|
57
|
+
return () => {
|
|
58
|
+
rpcAction.onMessage = null;
|
|
59
|
+
};
|
|
51
60
|
},
|
|
52
61
|
onPeerJoin(handler) {
|
|
53
|
-
room
|
|
54
|
-
return () => void 0;
|
|
62
|
+
return subscribeTrysteroPeerEvents(room, { onJoin: handler });
|
|
55
63
|
},
|
|
56
64
|
onPeerLeave(handler) {
|
|
57
|
-
room
|
|
58
|
-
return () => void 0;
|
|
65
|
+
return subscribeTrysteroPeerEvents(room, { onLeave: handler });
|
|
59
66
|
}
|
|
60
67
|
};
|
|
61
68
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Room } from "@trystero-p2p/nostr";
|
|
2
|
+
type PeerHandler = (peerId: string) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe multiple consumers to Trystero's single peer-event callback slots.
|
|
5
|
+
*
|
|
6
|
+
* Trystero 0.25 exposes `onPeerJoin` and `onPeerLeave` as nullable callback
|
|
7
|
+
* properties. This adapter preserves the library's multi-subscriber transport
|
|
8
|
+
* contract and returns a disposer for each subscription.
|
|
9
|
+
*/
|
|
10
|
+
export declare function subscribeTrysteroPeerEvents(room: Room, handlers: {
|
|
11
|
+
onJoin?: PeerHandler;
|
|
12
|
+
onLeave?: PeerHandler;
|
|
13
|
+
}): () => void;
|
|
14
|
+
export {};
|
|
@@ -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,10 +1,11 @@
|
|
|
1
1
|
import { createInstanceLogFunction, type LogFunction } from "../services/lib/logUtils.js";
|
|
2
2
|
import type { NecessaryServices } from "../interfaces/ServiceModule.js";
|
|
3
|
+
type RemoteSizeReviewMode = "notice" | "dialogue";
|
|
3
4
|
/**
|
|
4
5
|
* Notify when checking remote storage size is not configured.
|
|
5
6
|
* @returns true if the check is passed or user has configured the notification, false to block subsequent processes. (always true).
|
|
6
7
|
*/
|
|
7
|
-
export declare function onNotifyRemoteSizeNotConfiguredFactory(host: NecessaryServices<"appLifecycle" | "API" | "setting", never>, log: ReturnType<typeof createInstanceLogFunction
|
|
8
|
+
export declare function onNotifyRemoteSizeNotConfiguredFactory(host: NecessaryServices<"appLifecycle" | "API" | "setting", never>, log: ReturnType<typeof createInstanceLogFunction>, mode?: RemoteSizeReviewMode): () => Promise<boolean>;
|
|
8
9
|
/**
|
|
9
10
|
* Notify when the remote storage size exceed the threshold.
|
|
10
11
|
* @returns true if the check is passed or user has chosen to ignore the warning, false to block subsequent processes.
|
|
@@ -12,7 +13,7 @@ export declare function onNotifyRemoteSizeNotConfiguredFactory(host: NecessarySe
|
|
|
12
13
|
* @param log
|
|
13
14
|
* @returns
|
|
14
15
|
*/
|
|
15
|
-
export declare function onNotifyRemoteSizeExceedFactory(host: NecessaryServices<"API" | "setting" | "replicator", "rebuilder">, log: ReturnType<typeof createInstanceLogFunction
|
|
16
|
+
export declare function onNotifyRemoteSizeExceedFactory(host: NecessaryServices<"API" | "setting" | "replicator", "rebuilder">, log: ReturnType<typeof createInstanceLogFunction>, mode?: RemoteSizeReviewMode): () => Promise<boolean>;
|
|
16
17
|
/**
|
|
17
18
|
* Scan the remote storage size and notify if it is not configured or exceed the threshold.
|
|
18
19
|
* @param host The necessary services required for the operation.
|
|
@@ -20,9 +21,10 @@ export declare function onNotifyRemoteSizeExceedFactory(host: NecessaryServices<
|
|
|
20
21
|
* @param resetThreshold Whether to reset the notification threshold before scanning. This is useful when you want to force the notification to show up again.
|
|
21
22
|
* @returns A promise that resolves to true if all checks pass or user has configured the notification.
|
|
22
23
|
*/
|
|
23
|
-
export declare function scanAllStat(host: NecessaryServices<"API" | "setting" | "replicator" | "appLifecycle", "rebuilder">, log: LogFunction, resetThreshold?: boolean): Promise<boolean>;
|
|
24
|
+
export declare function scanAllStat(host: NecessaryServices<"API" | "setting" | "replicator" | "appLifecycle", "rebuilder">, log: LogFunction, resetThreshold?: boolean, mode?: RemoteSizeReviewMode): Promise<boolean>;
|
|
24
25
|
/**
|
|
25
26
|
* Associate the remote storage size check feature with the app lifecycle events.
|
|
26
27
|
* @param host
|
|
27
28
|
*/
|
|
28
29
|
export declare function useCheckRemoteSize(host: NecessaryServices<"API" | "setting" | "replicator" | "appLifecycle", "rebuilder">): void;
|
|
30
|
+
export {};
|
|
@@ -2,7 +2,17 @@ import { sizeToHumanReadable } from "octagonal-wheels/number";
|
|
|
2
2
|
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../common/types.js";
|
|
3
3
|
import { createInstanceLogFunction } from "../services/lib/logUtils.js";
|
|
4
4
|
import { EVENT_REQUEST_CHECK_REMOTE_SIZE } from "../events/coreEvents.js";
|
|
5
|
-
|
|
5
|
+
import { fireAndForget } from "../common/utils.js";
|
|
6
|
+
const REMOTE_SIZE_NOTICE_DURATION_MS = 5 * 60 * 1e3;
|
|
7
|
+
function configureReviewAnchor(anchor, label, review) {
|
|
8
|
+
anchor.href = "#";
|
|
9
|
+
anchor.textContent = label;
|
|
10
|
+
anchor.addEventListener("click", (event) => {
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
fireAndForget(review);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function onNotifyRemoteSizeNotConfiguredFactory(host, log, mode = "notice") {
|
|
6
16
|
return async () => {
|
|
7
17
|
const translate = host.services.context.translate;
|
|
8
18
|
log(translate("moduleCheckRemoteSize.logCheckingStorageSizes"), LOG_LEVEL_VERBOSE);
|
|
@@ -15,13 +25,25 @@ function onNotifyRemoteSizeNotConfiguredFactory(host, log) {
|
|
|
15
25
|
const ANSWER_800 = translate("moduleCheckRemoteSize.option800MB");
|
|
16
26
|
const ANSWER_2000 = translate("moduleCheckRemoteSize.option2GB");
|
|
17
27
|
const ASK_ME_NEXT_TIME = translate("moduleCheckRemoteSize.optionAskMeLater");
|
|
28
|
+
if (mode === "notice") {
|
|
29
|
+
host.services.API.confirm.askInPopup(
|
|
30
|
+
"remote-size-not-configured",
|
|
31
|
+
translate("moduleCheckRemoteSize.noticeNotConfigured"),
|
|
32
|
+
(anchor) => configureReviewAnchor(
|
|
33
|
+
anchor,
|
|
34
|
+
translate("moduleCheckRemoteSize.optionReview"),
|
|
35
|
+
() => onNotifyRemoteSizeNotConfiguredFactory(host, log, "dialogue")()
|
|
36
|
+
),
|
|
37
|
+
REMOTE_SIZE_NOTICE_DURATION_MS
|
|
38
|
+
);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
18
41
|
const ret = await host.services.API.confirm.askSelectStringDialogue(
|
|
19
42
|
message,
|
|
20
43
|
[ANSWER_0, ANSWER_800, ANSWER_2000, ASK_ME_NEXT_TIME],
|
|
21
44
|
{
|
|
22
45
|
defaultAction: ASK_ME_NEXT_TIME,
|
|
23
|
-
title: translate("moduleCheckRemoteSize.titleDatabaseSizeNotify")
|
|
24
|
-
timeout: 40
|
|
46
|
+
title: translate("moduleCheckRemoteSize.titleDatabaseSizeNotify")
|
|
25
47
|
}
|
|
26
48
|
);
|
|
27
49
|
if (ret == ANSWER_0) {
|
|
@@ -49,7 +71,7 @@ function onNotifyRemoteSizeNotConfiguredFactory(host, log) {
|
|
|
49
71
|
return true;
|
|
50
72
|
};
|
|
51
73
|
}
|
|
52
|
-
function onNotifyRemoteSizeExceedFactory(host, log) {
|
|
74
|
+
function onNotifyRemoteSizeExceedFactory(host, log, mode = "notice") {
|
|
53
75
|
return async () => {
|
|
54
76
|
const translate = host.services.context.translate;
|
|
55
77
|
const settings = host.services.setting.currentSettings();
|
|
@@ -89,13 +111,28 @@ function onNotifyRemoteSizeExceedFactory(host, log) {
|
|
|
89
111
|
});
|
|
90
112
|
const ANSWER_REBUILD = translate("moduleCheckRemoteSize.optionRebuildAll");
|
|
91
113
|
const ANSWER_IGNORE = translate("moduleCheckRemoteSize.optionDismiss");
|
|
114
|
+
if (mode === "notice") {
|
|
115
|
+
host.services.API.confirm.askInPopup(
|
|
116
|
+
"remote-size-exceeded",
|
|
117
|
+
translate("moduleCheckRemoteSize.noticeExceeded", {
|
|
118
|
+
measuredSize: sizeToHumanReadable(estimatedSize),
|
|
119
|
+
notifySize: sizeToHumanReadable(maxSize)
|
|
120
|
+
}),
|
|
121
|
+
(anchor) => configureReviewAnchor(
|
|
122
|
+
anchor,
|
|
123
|
+
translate("moduleCheckRemoteSize.optionReview"),
|
|
124
|
+
() => onNotifyRemoteSizeExceedFactory(host, log, "dialogue")()
|
|
125
|
+
),
|
|
126
|
+
REMOTE_SIZE_NOTICE_DURATION_MS
|
|
127
|
+
);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
92
130
|
const ret = await host.services.API.confirm.askSelectStringDialogue(
|
|
93
131
|
message,
|
|
94
132
|
[ANSWER_ENLARGE_LIMIT, ANSWER_REBUILD, ANSWER_IGNORE],
|
|
95
133
|
{
|
|
96
134
|
defaultAction: ANSWER_IGNORE,
|
|
97
|
-
title: translate("moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded")
|
|
98
|
-
timeout: 60
|
|
135
|
+
title: translate("moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded")
|
|
99
136
|
}
|
|
100
137
|
);
|
|
101
138
|
if (ret == ANSWER_REBUILD) {
|
|
@@ -142,7 +179,7 @@ function onNotifyRemoteSizeExceedFactory(host, log) {
|
|
|
142
179
|
return true;
|
|
143
180
|
};
|
|
144
181
|
}
|
|
145
|
-
async function scanAllStat(host, log, resetThreshold = false) {
|
|
182
|
+
async function scanAllStat(host, log, resetThreshold = false, mode = "notice") {
|
|
146
183
|
if (resetThreshold) {
|
|
147
184
|
await host.services.setting.applyPartial(
|
|
148
185
|
{
|
|
@@ -151,8 +188,8 @@ async function scanAllStat(host, log, resetThreshold = false) {
|
|
|
151
188
|
true
|
|
152
189
|
);
|
|
153
190
|
}
|
|
154
|
-
const onNotifyNotConfigured = onNotifyRemoteSizeNotConfiguredFactory(host, log);
|
|
155
|
-
const onNotifyExceed = onNotifyRemoteSizeExceedFactory(host, log);
|
|
191
|
+
const onNotifyNotConfigured = onNotifyRemoteSizeNotConfiguredFactory(host, log, mode);
|
|
192
|
+
const onNotifyExceed = onNotifyRemoteSizeExceedFactory(host, log, mode);
|
|
156
193
|
return await onNotifyNotConfigured() && await onNotifyExceed();
|
|
157
194
|
}
|
|
158
195
|
function useCheckRemoteSize(host) {
|
|
@@ -163,10 +200,13 @@ function useCheckRemoteSize(host) {
|
|
|
163
200
|
id: "livesync-reset-remote-size-threshold-and-check",
|
|
164
201
|
name: "Reset notification threshold and check the remote database usage",
|
|
165
202
|
callback: async () => {
|
|
166
|
-
await scanAllStat(host, log, true);
|
|
203
|
+
await scanAllStat(host, log, true, "dialogue");
|
|
167
204
|
}
|
|
168
205
|
});
|
|
169
|
-
host.services.context.events.onEvent(
|
|
206
|
+
host.services.context.events.onEvent(
|
|
207
|
+
EVENT_REQUEST_CHECK_REMOTE_SIZE,
|
|
208
|
+
() => scanAllStat(host, log, true, "dialogue")
|
|
209
|
+
);
|
|
170
210
|
return Promise.resolve(true);
|
|
171
211
|
});
|
|
172
212
|
}
|
|
@@ -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
|
};
|
|
@@ -48,10 +48,6 @@ export declare class ServiceRebuilder extends ServiceModuleBase<ServiceRebuilder
|
|
|
48
48
|
constructor(services: ServiceRebuilderDependencies);
|
|
49
49
|
$performRebuildDB(method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"): Promise<void>;
|
|
50
50
|
informOptionalFeatures(): Promise<void>;
|
|
51
|
-
askUsingOptionalFeature(opt: {
|
|
52
|
-
enableFetch?: boolean;
|
|
53
|
-
enableOverwrite?: boolean;
|
|
54
|
-
}): Promise<void>;
|
|
55
51
|
rebuildRemote(): Promise<void>;
|
|
56
52
|
private performRemoteRebuild;
|
|
57
53
|
$rebuildRemote(): Promise<void>;
|
|
@@ -60,8 +56,9 @@ export declare class ServiceRebuilder extends ServiceModuleBase<ServiceRebuilder
|
|
|
60
56
|
$rebuildEverything(): Promise<void>;
|
|
61
57
|
$fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean): Promise<void>;
|
|
62
58
|
$fetchLocalDBFast(autoResume: boolean): Promise<void>;
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
private scheduleInitialisation;
|
|
60
|
+
scheduleRebuild(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
|
|
61
|
+
scheduleFetch(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
|
|
65
62
|
private _tryResetRemoteDatabase;
|
|
66
63
|
private _onResetLocalDatabase;
|
|
67
64
|
suspendAllSync(): Promise<void>;
|
|
@@ -67,14 +67,6 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
67
67
|
["OK"]
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
|
-
async askUsingOptionalFeature(opt) {
|
|
71
|
-
if (await this.UI.confirm.askYesNoDialog(
|
|
72
|
-
"Do you want to enable extra features? If you are new to Self-hosted LiveSync, try the core feature first!",
|
|
73
|
-
{ title: "Enable extra features", defaultOption: "No", timeout: 15 }
|
|
74
|
-
) == "yes") {
|
|
75
|
-
await this.setting.suggestOptionalFeatures(opt);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
70
|
async rebuildRemote() {
|
|
79
71
|
await this.replicator.runBoundedRemoteActivity(() => this.performRemoteRebuild(), {
|
|
80
72
|
label: "rebuild-remote"
|
|
@@ -95,7 +87,7 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
95
87
|
await delay(1e3);
|
|
96
88
|
await this.replication.replicateAllToRemote(true);
|
|
97
89
|
await delay(1e3);
|
|
98
|
-
await this.replication.replicateAllToRemote(true
|
|
90
|
+
await this.replication.replicateAllToRemote(true);
|
|
99
91
|
}
|
|
100
92
|
$rebuildRemote() {
|
|
101
93
|
return this.rebuildRemote();
|
|
@@ -123,7 +115,7 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
123
115
|
await delay(1e3);
|
|
124
116
|
await this.replication.replicateAllToRemote(true);
|
|
125
117
|
await delay(1e3);
|
|
126
|
-
await this.replication.replicateAllToRemote(true
|
|
118
|
+
await this.replication.replicateAllToRemote(true);
|
|
127
119
|
}
|
|
128
120
|
$rebuildEverything() {
|
|
129
121
|
return this.rebuildEverything();
|
|
@@ -134,23 +126,35 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
134
126
|
$fetchLocalDBFast(autoResume) {
|
|
135
127
|
return this.fetchLocalDBFast(autoResume);
|
|
136
128
|
}
|
|
137
|
-
async
|
|
129
|
+
async scheduleInitialisation(flag, prepareBeforeRestart) {
|
|
138
130
|
try {
|
|
139
|
-
await this.storageAccess.writeFileAuto(
|
|
131
|
+
await this.storageAccess.writeFileAuto(flag, "");
|
|
140
132
|
} catch (ex) {
|
|
141
|
-
this._log(`Could not create ${
|
|
133
|
+
this._log(`Could not create ${flag}`, LOG_LEVEL_NOTICE);
|
|
142
134
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
135
|
+
return false;
|
|
143
136
|
}
|
|
144
|
-
this.appLifecycle.
|
|
145
|
-
}
|
|
146
|
-
async scheduleFetch() {
|
|
137
|
+
this.appLifecycle.setSuspended(true);
|
|
147
138
|
try {
|
|
148
|
-
await
|
|
139
|
+
await prepareBeforeRestart?.();
|
|
149
140
|
} catch (ex) {
|
|
150
|
-
|
|
151
|
-
|
|
141
|
+
try {
|
|
142
|
+
await this.storageAccess.delete(flag, true);
|
|
143
|
+
} catch (cleanupError) {
|
|
144
|
+
this._log(`Could not remove ${flag} after restart preparation failed`, LOG_LEVEL_NOTICE);
|
|
145
|
+
this._log(cleanupError, LOG_LEVEL_VERBOSE);
|
|
146
|
+
}
|
|
147
|
+
this.appLifecycle.setSuspended(false);
|
|
148
|
+
throw ex;
|
|
152
149
|
}
|
|
153
150
|
this.appLifecycle.performRestart();
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
scheduleRebuild(prepareBeforeRestart) {
|
|
154
|
+
return this.scheduleInitialisation(FlagFilesHumanReadable.REBUILD_ALL, prepareBeforeRestart);
|
|
155
|
+
}
|
|
156
|
+
scheduleFetch(prepareBeforeRestart) {
|
|
157
|
+
return this.scheduleInitialisation(FlagFilesHumanReadable.FETCH_ALL, prepareBeforeRestart);
|
|
154
158
|
}
|
|
155
159
|
async _tryResetRemoteDatabase() {
|
|
156
160
|
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>;
|
|
@@ -127,7 +128,7 @@ export interface IReplicationService {
|
|
|
127
128
|
databaseQueueCount: ReactiveSource<number>;
|
|
128
129
|
storageApplyingCount: ReactiveSource<number>;
|
|
129
130
|
replicationResultCount: ReactiveSource<number>;
|
|
130
|
-
replicateAllToRemote(showingNotice?: boolean
|
|
131
|
+
replicateAllToRemote(showingNotice?: boolean): Promise<boolean>;
|
|
131
132
|
replicateAllFromRemote(showingNotice?: boolean): Promise<boolean>;
|
|
132
133
|
markLocked(lockByClean?: boolean): Promise<void>;
|
|
133
134
|
markUnlocked(): Promise<void>;
|
|
@@ -188,18 +189,24 @@ export interface ISettingService {
|
|
|
188
189
|
onRealiseSetting(): Promise<boolean>;
|
|
189
190
|
suspendAllSync(): Promise<boolean>;
|
|
190
191
|
suspendExtraSync(): Promise<boolean>;
|
|
191
|
-
suggestOptionalFeatures(opt: {
|
|
192
|
-
enableFetch?: boolean;
|
|
193
|
-
enableOverwrite?: boolean;
|
|
194
|
-
}): Promise<boolean>;
|
|
195
192
|
enableOptionalFeature(mode: keyof OPTIONAL_SYNC_FEATURES): Promise<boolean>;
|
|
196
193
|
clearUsedPassphrase(): void;
|
|
197
194
|
decryptSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings>;
|
|
198
195
|
adjustSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings>;
|
|
199
196
|
loadSettings(): Promise<void>;
|
|
197
|
+
getSettingsMigrationState(): SettingsMigrationState | undefined;
|
|
200
198
|
getDeviceAndVaultName(): string;
|
|
201
199
|
setDeviceAndVaultName(name: string): void;
|
|
202
200
|
saveDeviceAndVaultName(): void;
|
|
201
|
+
/**
|
|
202
|
+
* Read host-provided, device-local configuration by its exact key.
|
|
203
|
+
* This storage is not part of the synchronised settings document.
|
|
204
|
+
*/
|
|
205
|
+
getDeviceLocalConfig(key: string): string | null;
|
|
206
|
+
/** Store host-provided, device-local configuration by its exact key. */
|
|
207
|
+
setDeviceLocalConfig(key: string, value: string): void;
|
|
208
|
+
/** Delete host-provided, device-local configuration by its exact key. */
|
|
209
|
+
deleteDeviceLocalConfig(key: string): void;
|
|
203
210
|
onBeforeSaveSettingData(nextSettings: ObsidianLiveSyncSettings, previousSettings: ObsidianLiveSyncSettings): Promise<(Partial<ObsidianLiveSyncSettings> | void)[]>;
|
|
204
211
|
saveSettingData(): Promise<void>;
|
|
205
212
|
currentSettings(): ObsidianLiveSyncSettings;
|