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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +90 -20
  2. package/dist/API/DirectFileManipulatorV2.d.ts +3 -0
  3. package/dist/common/configForDoc.d.ts +1 -0
  4. package/dist/common/configForDoc.js +24 -7
  5. package/dist/common/models/setting.const.defaults.d.ts +19 -0
  6. package/dist/common/models/setting.const.defaults.js +21 -2
  7. package/dist/common/models/setting.const.preferred.js +1 -2
  8. package/dist/common/models/setting.lifecycle.d.ts +33 -0
  9. package/dist/common/models/setting.lifecycle.js +121 -0
  10. package/dist/common/models/setting.type.d.ts +10 -4
  11. package/dist/common/models/shared.definition.configNames.js +3 -3
  12. package/dist/common/models/tweak.definition.d.ts +1 -1
  13. package/dist/common/models/tweak.definition.js +1 -6
  14. package/dist/common/settingConstants.d.ts +3 -2
  15. package/dist/common/settingConstants.js +6 -6
  16. package/dist/common/types.d.ts +4 -2
  17. package/dist/common/types.js +16 -1
  18. package/dist/context.d.ts +2 -1
  19. package/dist/context.js +4 -0
  20. package/dist/interfaces/Confirm.d.ts +10 -2
  21. package/dist/interfaces/DatabaseFileAccess.d.ts +8 -0
  22. package/dist/interfaces/DatabaseRebuilder.d.ts +12 -2
  23. package/dist/interfaces/FileReflectionProvenance.d.ts +32 -0
  24. package/dist/interfaces/FileReflectionProvenance.js +25 -0
  25. package/dist/managers/ConflictManager.js +42 -9
  26. package/dist/managers/EntryManager/EntryManager.d.ts +1 -0
  27. package/dist/managers/EntryManager/EntryManager.js +5 -1
  28. package/dist/managers/EntryManager/EntryManagerImpls.d.ts +1 -0
  29. package/dist/managers/EntryManager/EntryManagerImpls.js +29 -1
  30. package/dist/platform/node/storage.d.ts +10 -1
  31. package/dist/platform/node/storage.js +86 -25
  32. package/dist/pouchdb/LiveSyncLocalDB.d.ts +1 -0
  33. package/dist/pouchdb/LiveSyncLocalDB.js +3 -0
  34. package/dist/remoteConfigurations.d.ts +9 -0
  35. package/dist/remoteConfigurations.js +14 -0
  36. package/dist/replication/LiveSyncAbstractReplicator.d.ts +3 -3
  37. package/dist/replication/couchdb/LiveSyncReplicator.d.ts +0 -1
  38. package/dist/replication/couchdb/LiveSyncReplicator.js +0 -3
  39. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +0 -1
  40. package/dist/replication/journal/LiveSyncJournalReplicator.js +0 -3
  41. package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +5 -3
  42. package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +42 -30
  43. package/dist/replication/trystero/P2PReplicatorCore.d.ts +2 -6
  44. package/dist/replication/trystero/P2PReplicatorCore.js +16 -86
  45. package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -1
  46. package/dist/replication/trystero/TrysteroReplicator.js +10 -4
  47. package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +4 -5
  48. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +22 -30
  49. package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -2
  50. package/dist/replication/trystero/addP2PEventHandlers.d.ts +4 -2
  51. package/dist/replication/trystero/addP2PEventHandlers.js +9 -10
  52. package/dist/replication/trystero/types.d.ts +2 -0
  53. package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +1 -1
  54. package/dist/replication/trystero/useP2PReplicatorCommands.js +3 -1
  55. package/dist/replication/trystero/useP2PReplicatorFeature.js +25 -15
  56. package/dist/rpc/RpcRoom.d.ts +1 -1
  57. package/dist/rpc/RpcRoom.js +22 -16
  58. package/dist/rpc/transports/TrysteroTransport.d.ts +2 -0
  59. package/dist/rpc/transports/TrysteroTransport.js +24 -17
  60. package/dist/rpc/transports/trysteroRoomEvents.d.ts +14 -0
  61. package/dist/rpc/transports/trysteroRoomEvents.js +34 -0
  62. package/dist/serviceFeatures/checkRemoteSize.d.ts +5 -3
  63. package/dist/serviceFeatures/checkRemoteSize.js +51 -11
  64. package/dist/serviceFeatures/remoteConfig.d.ts +37 -1
  65. package/dist/serviceFeatures/remoteConfig.js +90 -0
  66. package/dist/serviceModules/Rebuilder.d.ts +3 -6
  67. package/dist/serviceModules/Rebuilder.js +31 -22
  68. package/dist/serviceModules/ServiceDatabaseFileAccessBase.d.ts +5 -0
  69. package/dist/serviceModules/ServiceDatabaseFileAccessBase.js +69 -13
  70. package/dist/serviceModules/ServiceFileHandlerBase.d.ts +20 -0
  71. package/dist/serviceModules/ServiceFileHandlerBase.js +224 -40
  72. package/dist/services/BrowserServices.d.ts +2 -0
  73. package/dist/services/BrowserServices.js +4 -1
  74. package/dist/services/HeadlessServices.d.ts +2 -0
  75. package/dist/services/HeadlessServices.js +2 -1
  76. package/dist/services/base/CommonlibMessages.d.ts +318 -0
  77. package/dist/services/base/CommonlibMessages.js +313 -0
  78. package/dist/services/base/IService.d.ts +13 -6
  79. package/dist/services/base/KeyValueDBService.d.ts +1 -1
  80. package/dist/services/base/KeyValueDBService.js +3 -1
  81. package/dist/services/base/MessageTranslator.d.ts +5 -2
  82. package/dist/services/base/MessageTranslator.js +21 -0
  83. package/dist/services/base/ReplicationService.d.ts +4 -2
  84. package/dist/services/base/ReplicationService.js +11 -16
  85. package/dist/services/base/ServiceBase.d.ts +1 -1
  86. package/dist/services/base/ServiceBase.js +2 -4
  87. package/dist/services/base/SettingService.d.ts +17 -9
  88. package/dist/services/base/SettingService.js +38 -19
  89. package/dist/services/implements/headless/HeadlessAPIService.d.ts +3 -3
  90. package/dist/services/implements/headless/HeadlessAPIService.js +2 -2
  91. package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +3 -3
  92. package/dist/settings.d.ts +10 -0
  93. package/dist/settings.js +32 -0
  94. package/dist/worker/bgWorker.js +1 -1
  95. package/docs/conflict-resolution.md +161 -0
  96. package/docs/development.md +106 -7
  97. package/docs/p2p-transport-lifecycle.md +62 -0
  98. package/docs/platform-storage.md +4 -0
  99. package/docs/proven-in-use.md +65 -0
  100. package/docs/releasing.md +26 -6
  101. package/docs/remote-configurations.md +63 -0
  102. package/docs/settings-lifecycle.md +85 -0
  103. package/package.json +17 -27
  104. package/dist/common/i18n.d.ts +0 -18
  105. package/dist/common/i18n.js +0 -7193
  106. package/dist/common/messages/combinedMessages.dev.d.ts +0 -7
  107. package/dist/common/messages/combinedMessages.dev.js +0 -44
  108. package/dist/common/messages/combinedMessages.prod.d.ts +0 -9264
  109. package/dist/common/messages/combinedMessages.prod.js +0 -9271
  110. package/dist/common/messages/de.d.ts +0 -297
  111. package/dist/common/messages/de.js +0 -7
  112. package/dist/common/messages/def.d.ts +0 -1119
  113. package/dist/common/messages/def.js +0 -7
  114. package/dist/common/messages/es.d.ts +0 -689
  115. package/dist/common/messages/es.js +0 -7
  116. package/dist/common/messages/fr.d.ts +0 -582
  117. package/dist/common/messages/fr.js +0 -7
  118. package/dist/common/messages/he.d.ts +0 -583
  119. package/dist/common/messages/he.js +0 -7
  120. package/dist/common/messages/ja.d.ts +0 -838
  121. package/dist/common/messages/ja.js +0 -7
  122. package/dist/common/messages/ko.d.ts +0 -800
  123. package/dist/common/messages/ko.js +0 -7
  124. package/dist/common/messages/ru.d.ts +0 -858
  125. package/dist/common/messages/ru.js +0 -7
  126. package/dist/common/messages/zh-tw.d.ts +0 -384
  127. package/dist/common/messages/zh-tw.js +0 -7
  128. package/dist/common/messages/zh.d.ts +0 -1093
  129. package/dist/common/messages/zh.js +0 -7
  130. package/dist/common/messagesJson/de.json +0 -295
  131. package/dist/common/messagesJson/en.json +0 -1117
  132. package/dist/common/messagesJson/es.json +0 -687
  133. package/dist/common/messagesJson/fr.json +0 -580
  134. package/dist/common/messagesJson/he.json +0 -581
  135. package/dist/common/messagesJson/ja.json +0 -836
  136. package/dist/common/messagesJson/ko.json +0 -798
  137. package/dist/common/messagesJson/ru.json +0 -856
  138. package/dist/common/messagesJson/zh-tw.json +0 -382
  139. package/dist/common/messagesJson/zh.json +0 -1091
  140. package/dist/common/rosetta.d.ts +0 -42
  141. package/dist/common/rosetta.js +0 -57
@@ -1,30 +1,29 @@
1
1
  import { EVENT_DATABASE_REBUILT, EVENT_PLATFORM_UNLOADED, EVENT_SETTING_SAVED } from "../../events/coreEvents.js";
2
2
  import { EVENT_ADVERTISEMENT_RECEIVED, EVENT_DEVICE_LEAVED, EVENT_REQUEST_STATUS } from "./TrysteroReplicatorP2PServer.js";
3
- function addP2PEventHandlers(instance, events) {
3
+ function addP2PEventHandlers(source, events) {
4
+ const current = () => typeof source === "function" ? source() : source;
4
5
  events.onEvent(EVENT_ADVERTISEMENT_RECEIVED, (peer) => {
5
- void instance.onNewPeer(peer);
6
+ void current().onNewPeer(peer);
6
7
  });
7
8
  events.onEvent(EVENT_DEVICE_LEAVED, (peerId) => {
8
- instance.onPeerLeaved(peerId);
9
+ current().onPeerLeaved(peerId);
9
10
  });
10
11
  events.onEvent(EVENT_REQUEST_STATUS, () => {
11
- instance.requestStatus();
12
+ current().requestStatus();
12
13
  });
13
14
  events.onEvent(EVENT_DATABASE_REBUILT, async () => {
14
- await instance.open();
15
+ await current().open();
15
16
  });
16
17
  events.onEvent(EVENT_PLATFORM_UNLOADED, () => {
17
- void instance.close();
18
+ void current().close();
18
19
  });
19
20
  events.onEvent(EVENT_SETTING_SAVED, async (settings) => {
20
- const isOpen = instance.isServing ?? instance.server?.isServing ?? false;
21
+ const instance = current();
21
22
  if (settings.P2P_Enabled && settings.P2P_AutoStart) {
22
23
  await instance.open();
23
24
  return;
24
25
  }
25
- if (isOpen) {
26
- await instance.close();
27
- }
26
+ await instance.close();
28
27
  });
29
28
  }
30
29
  async function openP2PReplicator(instance) {
@@ -85,6 +85,8 @@ export interface ReplicatorHostEnv extends ReplicatorHost {
85
85
  db: PouchDB.Database<EntryDoc>;
86
86
  simpleStore: SimpleStore<unknown>;
87
87
  runFiniteReplicationActivity?: AsyncActivityRunner["run"];
88
+ /** Lightweight, repeatable host policy checked before ordinary P2P replication starts. */
89
+ canStartOrdinaryReplication?(showMessage?: boolean): Promise<boolean>;
88
90
  processReplicatedDocs(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): void | Promise<void>;
89
91
  }
90
92
  export type Advertisement = {
@@ -4,4 +4,4 @@ import type { UseP2PReplicatorResult } from "./UseP2PReplicatorResult.js";
4
4
  * ServiceFeature: Registers event handlers for P2P replication and manages the lifecycle of a LiveSyncTrysteroReplicator instance.
5
5
  * @param host
6
6
  */
7
- export declare function useP2PReplicatorCommands(host: NecessaryServices<"API" | "setting", never>, { replicator }: UseP2PReplicatorResult): void;
7
+ export declare function useP2PReplicatorCommands(host: NecessaryServices<"API" | "setting", never>, result: UseP2PReplicatorResult): void;
@@ -1,9 +1,10 @@
1
1
  import { Logger, LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
2
- function useP2PReplicatorCommands(host, { replicator }) {
2
+ function useP2PReplicatorCommands(host, result) {
3
3
  host.services.API.addCommand({
4
4
  id: "p2p-establish-connection",
5
5
  name: "P2P Sync : Connect to the Signalling Server",
6
6
  checkCallback: (isChecking) => {
7
+ const replicator = result.replicator;
7
8
  if (!replicator) return false;
8
9
  if (isChecking) return !(replicator.server?.isServing ?? false);
9
10
  void replicator.open();
@@ -13,6 +14,7 @@ function useP2PReplicatorCommands(host, { replicator }) {
13
14
  id: "p2p-close-connection",
14
15
  name: "P2P Sync : Disconnect from the Signalling Server",
15
16
  checkCallback: (isChecking) => {
17
+ const replicator = result.replicator;
16
18
  if (!replicator) return false;
17
19
  if (isChecking) return replicator.server?.isServing ?? false;
18
20
  Logger("Closing P2P Connection", LOG_LEVEL_NOTICE);
@@ -8,6 +8,7 @@ function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFa
8
8
  let replicator = new LiveSyncTrysteroReplicator({
9
9
  services: host.services
10
10
  });
11
+ let replacementPromise;
11
12
  if (openReplicationUIFactory) {
12
13
  replicator.env.openReplicationUI = openReplicationUIFactory(replicator);
13
14
  }
@@ -19,27 +20,36 @@ function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFa
19
20
  return replicator;
20
21
  }
21
22
  };
22
- addP2PEventHandlers(activeReplicator.replicator, host.services.context.events);
23
+ addP2PEventHandlers(() => activeReplicator.replicator, host.services.context.events);
23
24
  host.services.replicator.getNewReplicator.addHandler(
24
25
  async (settingOverride = {}) => {
25
26
  const settings = { ...host.services.setting.currentSettings(), ...settingOverride };
26
27
  if (settings.remoteType == REMOTE_P2P) {
27
- const existingReplicator = replicator;
28
+ if (replacementPromise) return await replacementPromise;
29
+ const operation = (async () => {
30
+ const existingReplicator = replicator;
31
+ try {
32
+ await existingReplicator?.close();
33
+ } catch (e) {
34
+ Logger(`Error closing existing p2p replicator`);
35
+ Logger(e, LOG_LEVEL_VERBOSE);
36
+ }
37
+ const newReplicator = new LiveSyncTrysteroReplicator({ services: host.services });
38
+ if (openReplicationUIFactory) {
39
+ newReplicator.env.openReplicationUI = openReplicationUIFactory(newReplicator);
40
+ }
41
+ if (openRebuildUIFactory) {
42
+ newReplicator.env.openRebuildUI = openRebuildUIFactory(newReplicator);
43
+ }
44
+ replicator = newReplicator;
45
+ return replicator;
46
+ })();
47
+ replacementPromise = operation;
28
48
  try {
29
- await existingReplicator?.close();
30
- } catch (e) {
31
- Logger(`Error closing existing p2p replicator`);
32
- Logger(e, LOG_LEVEL_VERBOSE);
49
+ return await operation;
50
+ } finally {
51
+ if (replacementPromise === operation) replacementPromise = void 0;
33
52
  }
34
- const newReplicator = new LiveSyncTrysteroReplicator({ services: host.services });
35
- if (openReplicationUIFactory) {
36
- newReplicator.env.openReplicationUI = openReplicationUIFactory(newReplicator);
37
- }
38
- if (openRebuildUIFactory) {
39
- newReplicator.env.openRebuildUI = openRebuildUIFactory(newReplicator);
40
- }
41
- replicator = newReplicator;
42
- return replicator;
43
53
  }
44
54
  return void 0;
45
55
  }
@@ -10,7 +10,7 @@ export declare class RpcRoom {
10
10
  private incomingChunkMap;
11
11
  private incomingChunkTimers;
12
12
  private peerVersion;
13
- private disposer;
13
+ private disposers;
14
14
  constructor(options: RpcRoomOptions);
15
15
  close(): void;
16
16
  session(peerId: string): RpcSession;
@@ -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
- disposer;
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.disposer = this.options.transport.onMessage((msg, peerId) => {
34
- void this.onWireMessage(msg, peerId);
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.options.transport.onPeerJoin((peerId) => {
38
- void this.sendEnvelope(peerId, {
39
- kind: "handshake",
40
- versionMajor: RPC_VERSION_MAJOR,
41
- versionMinor: RPC_VERSION_MINOR
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.options.transport.onPeerLeave((peerId) => {
47
- this.sessions.delete(peerId);
48
- this.peerVersion.delete(peerId);
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.disposer?.();
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 [sendAd, arrivedAd] = room.makeAction(AD_ACTION_NAME);
14
- const sendAdvertisement = (toPeerId) => sendAd(localAd, toPeerId).then(() => void 0);
15
- room.onPeerJoin((peerId) => {
16
- void sendAdvertisement(peerId);
17
- });
18
- room.onPeerLeave((peerId) => {
19
- peerMap.delete(peerId);
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
- arrivedAd((data, peerId) => {
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 [sendRpc, arrivedRpc] = room.makeAction(RPC_ACTION_NAME);
50
+ const rpcAction = room.makeAction(RPC_ACTION_NAME);
44
51
  return {
45
52
  send(message, peerId) {
46
- return sendRpc(message, peerId).then(() => void 0);
53
+ return rpcAction.send(message, { target: peerId });
47
54
  },
48
55
  onMessage(handler) {
49
- arrivedRpc((data, peerId) => handler(data, peerId));
50
- return () => void 0;
56
+ rpcAction.onMessage = (data, { peerId }) => handler(data, peerId);
57
+ return () => {
58
+ rpcAction.onMessage = null;
59
+ };
51
60
  },
52
61
  onPeerJoin(handler) {
53
- room.onPeerJoin((peerId) => handler(peerId));
54
- return () => void 0;
62
+ return subscribeTrysteroPeerEvents(room, { onJoin: handler });
55
63
  },
56
64
  onPeerLeave(handler) {
57
- room.onPeerLeave((peerId) => handler(peerId));
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>): () => Promise<boolean>;
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>): () => Promise<boolean>;
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
- function onNotifyRemoteSizeNotConfiguredFactory(host, log) {
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(EVENT_REQUEST_CHECK_REMOTE_SIZE, () => scanAllStat(host, log, true));
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 { ObsidianLiveSyncSettings } from "../common/models/setting.type.js";
2
+ import { type RemoteConfigurationResult } from "../common/ConnectionString.js";
3
+ import type { ObsidianLiveSyncSettings, RemoteConfiguration } from "../common/models/setting.type.js";
3
4
  import type { NecessaryServices } from "../interfaces/ServiceModule.js";
4
5
  export type RemoteConfigHost = NecessaryServices<"setting" | "UI" | "replication" | "control" | "appLifecycle" | "API", never>;
5
6
  export declare function migrateLegacyRemoteConfigurationsInPlace(settings: ObsidianLiveSyncSettings, log?: (message: string, level?: LOG_LEVEL) => void): boolean;
@@ -8,6 +9,41 @@ export declare function migrateLegacyRemoteConfigurationsInPlace(settings: Obsid
8
9
  * @returns A unique string identifier.
9
10
  */
10
11
  export declare function createRemoteConfigurationId(): string;
12
+ export type SerializableRemoteConfigurationType = Exclude<RemoteConfigurationResult["type"], "webdav">;
13
+ export interface UpsertRemoteConfigurationOptions {
14
+ /**
15
+ * Reuse this identifier to update a known profile. Omit it to allocate a new opaque identifier.
16
+ */
17
+ id?: string;
18
+ /**
19
+ * User-visible name. Omit it to derive a descriptive name from the connection settings.
20
+ */
21
+ name?: string;
22
+ /**
23
+ * Select this profile as the main remote and project it onto the compatibility fields.
24
+ */
25
+ activate?: boolean;
26
+ /**
27
+ * Select this profile for P2P features without changing the main remote selection.
28
+ */
29
+ activateForP2P?: boolean;
30
+ }
31
+ /**
32
+ * Suggest a concise display name from a serialisable remote configuration.
33
+ *
34
+ * The name is presentation only. Callers must continue to use the profile ID for identity and
35
+ * `activeConfigurationId` for the selected main remote.
36
+ */
37
+ export declare function suggestRemoteConfigurationName(configuration: RemoteConfigurationResult): string;
38
+ /**
39
+ * Create or update a multiple-remote profile from the corresponding compatibility fields.
40
+ *
41
+ * This mutates `settings`. Passing an existing `id` intentionally replaces that profile; omitting
42
+ * it always allocates a new opaque ID and preserves all existing profiles. Generated display names
43
+ * are made unique for readability, but names never act as identifiers. The stored connection URI is
44
+ * plaintext at this boundary; `SettingService` applies configured at-rest encryption when saving it.
45
+ */
46
+ export declare function upsertRemoteConfigurationInPlace(settings: ObsidianLiveSyncSettings, type: SerializableRemoteConfigurationType, options?: UpsertRemoteConfigurationOptions): RemoteConfiguration;
11
47
  /**
12
48
  * Keep compatibility for users who were already using P2P as their main active remote.
13
49
  */
@@ -51,6 +51,94 @@ function migrateLegacyRemoteConfigurationsInPlace(settings, log) {
51
51
  function createRemoteConfigurationId() {
52
52
  return `remote-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
53
53
  }
54
+ function toRemoteConfigurationResult(type, settings) {
55
+ if (type === "couchdb") {
56
+ return { type, settings };
57
+ }
58
+ if (type === "s3") {
59
+ return { type, settings };
60
+ }
61
+ return { type, settings };
62
+ }
63
+ function suggestRemoteConfigurationName(configuration) {
64
+ if (configuration.type === "couchdb") {
65
+ try {
66
+ const host = new URL(configuration.settings.couchDB_URI).host;
67
+ return host ? `CouchDB ${host}` : "CouchDB remote";
68
+ } catch {
69
+ return "CouchDB remote";
70
+ }
71
+ }
72
+ if (configuration.type === "s3") {
73
+ const bucket = configuration.settings.bucket.trim();
74
+ if (bucket) {
75
+ return `S3 ${bucket}`;
76
+ }
77
+ try {
78
+ const host = new URL(configuration.settings.endpoint).host;
79
+ return host ? `S3 ${host}` : "Object Storage remote";
80
+ } catch {
81
+ return "Object Storage remote";
82
+ }
83
+ }
84
+ if (configuration.type === "p2p") {
85
+ const room = configuration.settings.P2P_roomID.trim();
86
+ return room ? `P2P ${room}` : "P2P remote";
87
+ }
88
+ return "Remote configuration";
89
+ }
90
+ function allocateRemoteConfigurationId(configurations) {
91
+ let id = createRemoteConfigurationId();
92
+ while (configurations[id]) {
93
+ id = createRemoteConfigurationId();
94
+ }
95
+ return id;
96
+ }
97
+ function allocateRemoteConfigurationName(configurations, baseName, updatingId) {
98
+ const usedNames = new Set(
99
+ Object.values(configurations).filter((configuration) => configuration.id !== updatingId).map((configuration) => configuration.name)
100
+ );
101
+ if (!usedNames.has(baseName)) {
102
+ return baseName;
103
+ }
104
+ let suffix = 2;
105
+ while (usedNames.has(`${baseName} (${suffix})`)) {
106
+ suffix += 1;
107
+ }
108
+ return `${baseName} (${suffix})`;
109
+ }
110
+ function upsertRemoteConfigurationInPlace(settings, type, options = {}) {
111
+ if (options.activateForP2P && type !== "p2p") {
112
+ throw new Error("Only a P2P remote configuration can be selected for P2P features.");
113
+ }
114
+ const configurations = settings.remoteConfigurations ?? {};
115
+ const requestedId = options.id?.trim();
116
+ const id = requestedId || allocateRemoteConfigurationId(configurations);
117
+ const existing = configurations[id];
118
+ const serialisable = toRemoteConfigurationResult(type, settings);
119
+ const suggestedName = suggestRemoteConfigurationName(serialisable);
120
+ const requestedName = options.name?.trim();
121
+ const name = requestedName || existing?.name || allocateRemoteConfigurationName(configurations, suggestedName, existing?.id);
122
+ const configuration = {
123
+ id,
124
+ name,
125
+ uri: ConnectionStringParser.serialize(serialisable),
126
+ isEncrypted: false
127
+ };
128
+ settings.remoteConfigurations ??= configurations;
129
+ configurations[id] = configuration;
130
+ if (options.activate) {
131
+ if (!activateRemoteConfiguration(settings, id)) {
132
+ throw new Error(`Failed to activate remote configuration '${id}'.`);
133
+ }
134
+ }
135
+ if (options.activateForP2P) {
136
+ if (!activateP2PRemoteConfiguration(settings, id)) {
137
+ throw new Error(`Failed to activate P2P remote configuration '${id}'.`);
138
+ }
139
+ }
140
+ return configuration;
141
+ }
54
142
  function migrateP2PActiveRemoteConfigurationIdInPlace(settings) {
55
143
  if ((settings.P2P_ActiveRemoteConfigurationId ?? "").trim() !== "") {
56
144
  return false;
@@ -242,6 +330,8 @@ export {
242
330
  migrateLegacyRemoteConfigurationsInPlace,
243
331
  migrateP2PActiveRemoteConfigurationIdInPlace,
244
332
  migrateToMultipleRemoteConfigurations,
333
+ suggestRemoteConfigurationName,
334
+ upsertRemoteConfigurationInPlace,
245
335
  useRemoteConfiguration,
246
336
  useRemoteConfigurationMigration
247
337
  };