@vrtmrz/livesync-commonlib 0.1.16 → 0.1.18

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 (37) hide show
  1. package/dist/common/ConnectionString.js +9 -1
  2. package/dist/common/models/setting.const.d.ts +18 -0
  3. package/dist/common/models/setting.const.defaults.js +10 -1
  4. package/dist/common/models/setting.const.js +12 -0
  5. package/dist/common/models/setting.const.qr.js +3 -1
  6. package/dist/common/models/setting.p2p.d.ts +21 -0
  7. package/dist/common/models/setting.p2p.js +35 -0
  8. package/dist/common/models/setting.type.d.ts +11 -1
  9. package/dist/common/models/shared.definition.configNames.js +8 -0
  10. package/dist/common/models/tweak.definition.d.ts +2 -0
  11. package/dist/common/settingConstants.d.ts +2 -0
  12. package/dist/common/types.d.ts +5 -3
  13. package/dist/common/types.js +17 -1
  14. package/dist/common/utils.js +4 -1
  15. package/dist/interfaces/DatabaseFileAccess.d.ts +10 -0
  16. package/dist/managers/EntryManager/EntryManager.d.ts +1 -0
  17. package/dist/managers/EntryManager/EntryManager.js +4 -0
  18. package/dist/managers/EntryManager/EntryManagerImpls.d.ts +7 -0
  19. package/dist/managers/EntryManager/EntryManagerImpls.js +21 -3
  20. package/dist/pouchdb/LiveSyncLocalDB.d.ts +1 -0
  21. package/dist/pouchdb/LiveSyncLocalDB.js +3 -0
  22. package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +8 -0
  23. package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +36 -8
  24. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +2 -2
  25. package/dist/rpc/transports/TrysteroTransport.d.ts +2 -0
  26. package/dist/rpc/transports/TrysteroTransport.js +18 -2
  27. package/dist/rpc/transports/trysteroUtils.js +12 -1
  28. package/dist/serviceModules/ServiceDatabaseFileAccessBase.d.ts +1 -0
  29. package/dist/serviceModules/ServiceDatabaseFileAccessBase.js +22 -4
  30. package/dist/worker/bgWorker.js +1 -1
  31. package/docs/conflict-resolution.md +2 -0
  32. package/docs/development.md +1 -1
  33. package/docs/p2p-transport-lifecycle.md +4 -2
  34. package/docs/proven-in-use.md +3 -1
  35. package/docs/releasing.md +2 -0
  36. package/docs/remote-configurations.md +2 -0
  37. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { normaliseP2PConnectionPath, normaliseP2PMaxWirePayloadBytes } from "./models/setting.p2p.js";
1
2
  const PROXY_SCHEME = "https";
2
3
  function parseSlsUri(uriString) {
3
4
  const match = uriString.match(/^sls\+([^:]+):(.*)$/);
@@ -155,7 +156,9 @@ class ConnectionStringParser {
155
156
  P2P_AutoBroadcast: searchParams.get("autoBroadcast") === "true",
156
157
  P2P_turnServers: searchParams.get("turnServers") || "",
157
158
  P2P_turnUsername: searchParams.get("turnUser") || "",
158
- P2P_turnCredential: searchParams.get("turnPass") || ""
159
+ P2P_turnCredential: searchParams.get("turnPass") || "",
160
+ P2P_maxWirePayloadBytes: normaliseP2PMaxWirePayloadBytes(Number(searchParams.get("maxWirePayloadBytes"))),
161
+ P2P_connectionPath: normaliseP2PConnectionPath(searchParams.get("connectionPath"))
159
162
  };
160
163
  }
161
164
  static serializeP2P(settings) {
@@ -168,6 +171,11 @@ class ConnectionStringParser {
168
171
  if (settings.P2P_turnServers) searchParams.set("turnServers", settings.P2P_turnServers);
169
172
  if (settings.P2P_turnUsername) searchParams.set("turnUser", settings.P2P_turnUsername);
170
173
  if (settings.P2P_turnCredential) searchParams.set("turnPass", settings.P2P_turnCredential);
174
+ searchParams.set(
175
+ "maxWirePayloadBytes",
176
+ String(normaliseP2PMaxWirePayloadBytes(settings.P2P_maxWirePayloadBytes))
177
+ );
178
+ searchParams.set("connectionPath", normaliseP2PConnectionPath(settings.P2P_connectionPath));
171
179
  const credentials = settings.P2P_passphrase ? `:${encodeURIComponent(settings.P2P_passphrase)}@` : "";
172
180
  const host = encodeURIComponent(settings.P2P_roomID);
173
181
  const query = searchParams.toString() ? `?${searchParams.toString()}` : "";
@@ -9,6 +9,24 @@ export declare const RemoteTypes: {
9
9
  export declare const REMOTE_COUCHDB: "";
10
10
  export declare const REMOTE_MINIO: "MINIO";
11
11
  export declare const REMOTE_P2P: "ONLY_P2P";
12
+ /** ICE route policies supported by a P2P connection profile. */
13
+ export declare const P2PConnectionPaths: {
14
+ readonly Automatic: "automatic";
15
+ readonly Relay: "relay";
16
+ };
17
+ export type P2PConnectionPath = (typeof P2PConnectionPaths)[keyof typeof P2PConnectionPaths];
18
+ /**
19
+ * User-facing message-size presets for the P2P RPC transport.
20
+ *
21
+ * These values limit Commonlib wire payloads before Trystero applies its own
22
+ * framing. They are not file Chunk sizes or network MTUs.
23
+ */
24
+ export declare const P2PMessageSizePresets: {
25
+ readonly Standard: 15360;
26
+ readonly Reduced: 2048;
27
+ readonly Conservative: 1024;
28
+ readonly MaximumCompatibility: 800;
29
+ };
12
30
  export declare const E2EEAlgorithmNames: {
13
31
  readonly "": "V1: Legacy";
14
32
  readonly v2: "V2: AES-256-GCM With HKDF";
@@ -1,4 +1,11 @@
1
- import { ChunkAlgorithms, CURRENT_SETTING_VERSION, E2EEAlgorithms, REMOTE_COUCHDB } from "./setting.const.js";
1
+ import {
2
+ ChunkAlgorithms,
3
+ CURRENT_SETTING_VERSION,
4
+ E2EEAlgorithms,
5
+ P2PConnectionPaths,
6
+ P2PMessageSizePresets,
7
+ REMOTE_COUCHDB
8
+ } from "./setting.const.js";
2
9
  import { PREFERRED_BASE } from "./setting.const.preferred.js";
3
10
  import { AutoAccepting } from "./setting.type.js";
4
11
  const P2P_DEFAULT_SETTINGS = {
@@ -21,6 +28,8 @@ const P2P_DEFAULT_SETTINGS = {
21
28
  P2P_turnServers: "",
22
29
  P2P_turnUsername: "",
23
30
  P2P_turnCredential: "",
31
+ P2P_maxWirePayloadBytes: P2PMessageSizePresets.Standard,
32
+ P2P_connectionPath: P2PConnectionPaths.Automatic,
24
33
  P2P_useDiagRTC: false
25
34
  };
26
35
  const SETTINGS_SCHEMA_DEFAULTS = {
@@ -9,6 +9,16 @@ const RemoteTypes = {
9
9
  const REMOTE_COUCHDB = RemoteTypes.REMOTE_COUCHDB;
10
10
  const REMOTE_MINIO = RemoteTypes.REMOTE_MINIO;
11
11
  const REMOTE_P2P = RemoteTypes.REMOTE_P2P;
12
+ const P2PConnectionPaths = {
13
+ Automatic: "automatic",
14
+ Relay: "relay"
15
+ };
16
+ const P2PMessageSizePresets = {
17
+ Standard: 15360,
18
+ Reduced: 2048,
19
+ Conservative: 1024,
20
+ MaximumCompatibility: 800
21
+ };
12
22
  const E2EEAlgorithmNames = {
13
23
  "": "V1: Legacy",
14
24
  v2: "V2: AES-256-GCM With HKDF",
@@ -59,6 +69,8 @@ export {
59
69
  MODE_SELECTIVE,
60
70
  MODE_SHINY,
61
71
  NetworkWarningStyles,
72
+ P2PConnectionPaths,
73
+ P2PMessageSizePresets,
62
74
  REMOTE_COUCHDB,
63
75
  REMOTE_MINIO,
64
76
  REMOTE_P2P,
@@ -168,8 +168,10 @@ const KeyIndexOfSettings = {
168
168
  P2P_ActiveRemoteConfigurationId: 159,
169
169
  autoAcceptCompatibleTweak: 160,
170
170
  tweakModified: -1,
171
- P2P_useDiagRTC: -1
171
+ P2P_useDiagRTC: -1,
172
172
  // Do not encode into the QR Code.
173
+ P2P_maxWirePayloadBytes: 163,
174
+ P2P_connectionPath: 164
173
175
  };
174
176
  export {
175
177
  KeyIndexOfSettings
@@ -0,0 +1,21 @@
1
+ import { type P2PConnectionPath } from "./setting.const.js";
2
+ /**
3
+ * Return a safe outgoing RPC wire-payload bound for Trystero.
4
+ *
5
+ * Existing profiles can omit this value. Invalid, excessively small, and
6
+ * excessively large values retain the established 15,360-byte behaviour.
7
+ */
8
+ export declare function normaliseP2PMaxWirePayloadBytes(value: unknown): number;
9
+ /** Return a recognised P2P connection path, defaulting legacy values to automatic selection. */
10
+ export declare function normaliseP2PConnectionPath(value: unknown): P2PConnectionPath;
11
+ /** Split the stored comma-separated TURN server list without changing its entries. */
12
+ export declare function splitP2PTurnServerUrls(value: string): string[];
13
+ /**
14
+ * Report whether a value is a minimally valid TURN or TURN-over-TLS URL.
15
+ *
16
+ * Full allocation validation remains the responsibility of WebRTC. This
17
+ * check only prevents relay-only mode when no TURN endpoint can be attempted.
18
+ */
19
+ export declare function isValidP2PTurnServerUrl(value: string): boolean;
20
+ /** Report whether a stored TURN server list contains at least one usable TURN URL. */
21
+ export declare function hasValidP2PTurnServerUrl(value: string): boolean;
@@ -0,0 +1,35 @@
1
+ import { P2PConnectionPaths, P2PMessageSizePresets } from "./setting.const.js";
2
+ function normaliseP2PMaxWirePayloadBytes(value) {
3
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < P2PMessageSizePresets.MaximumCompatibility || value > P2PMessageSizePresets.Standard) {
4
+ return P2PMessageSizePresets.Standard;
5
+ }
6
+ return value;
7
+ }
8
+ function normaliseP2PConnectionPath(value) {
9
+ return value === P2PConnectionPaths.Relay ? P2PConnectionPaths.Relay : P2PConnectionPaths.Automatic;
10
+ }
11
+ function splitP2PTurnServerUrls(value) {
12
+ return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
13
+ }
14
+ function isValidP2PTurnServerUrl(value) {
15
+ const candidate = value.trim();
16
+ if (!candidate || /\s/.test(candidate)) return false;
17
+ try {
18
+ const parsed = new URL(candidate);
19
+ const protocol = parsed.protocol.toLowerCase();
20
+ if (protocol !== "turn:" && protocol !== "turns:") return false;
21
+ return (parsed.hostname || parsed.pathname).length > 0;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ function hasValidP2PTurnServerUrl(value) {
27
+ return splitP2PTurnServerUrls(value).some(isValidP2PTurnServerUrl);
28
+ }
29
+ export {
30
+ hasValidP2PTurnServerUrl,
31
+ isValidP2PTurnServerUrl,
32
+ normaliseP2PConnectionPath,
33
+ normaliseP2PMaxWirePayloadBytes,
34
+ splitP2PTurnServerUrls
35
+ };
@@ -1,4 +1,4 @@
1
- import type { ChunkAlgorithms, E2EEAlgorithms, HashAlgorithms, MODE_AUTOMATIC, MODE_PAUSED, MODE_SELECTIVE, MODE_SHINY, RemoteTypes } from "./setting.const.js";
1
+ import type { ChunkAlgorithms, E2EEAlgorithms, HashAlgorithms, MODE_AUTOMATIC, MODE_PAUSED, MODE_SELECTIVE, MODE_SHINY, P2PConnectionPath, RemoteTypes } from "./setting.const.js";
2
2
  import type { CustomRegExpSourceList } from "./shared.type.util.js";
3
3
  import type { JWTAlgorithm } from "./auth.type.js";
4
4
  /** Display languages recognised by the current LiveSync settings schema. */
@@ -528,6 +528,16 @@ export interface P2PConnectionInfo {
528
528
  * The TURN credential (password, secret, etc...) for the P2P connection.
529
529
  */
530
530
  P2P_turnCredential: string;
531
+ /**
532
+ * Maximum serialised RPC wire payload sent through Trystero before
533
+ * Commonlib splits it. Applies to outgoing messages.
534
+ */
535
+ P2P_maxWirePayloadBytes?: number;
536
+ /**
537
+ * ICE route policy for this P2P profile. Relay-only mode is effective only
538
+ * when the profile contains at least one valid TURN URL.
539
+ */
540
+ P2P_connectionPath?: P2PConnectionPath;
531
541
  /**
532
542
  * Use Diagnostic Wrapper for RTCPeerConnection to collect statistics.
533
543
  */
@@ -152,6 +152,14 @@ const configurationNames = {
152
152
  desc: "The credential/password for the TURN servers.",
153
153
  isHidden: true
154
154
  },
155
+ P2P_maxWirePayloadBytes: {
156
+ name: "P2P Message Size",
157
+ desc: "The maximum outgoing RPC wire payload before Commonlib splits it for the P2P transport."
158
+ },
159
+ P2P_connectionPath: {
160
+ name: "Connection Path",
161
+ desc: "Select the WebRTC route automatically or require a configured TURN relay."
162
+ },
155
163
  useOnlyLocalChunk: {
156
164
  name: "Use Only Local Chunks",
157
165
  desc: "If enabled, the plugin will not attempt to connect to the remote database even if the chunk was not found locally.",
@@ -186,6 +186,8 @@ export declare const TweakValuesTemplate: {
186
186
  P2P_turnServers?: string;
187
187
  P2P_turnUsername?: string;
188
188
  P2P_turnCredential?: string;
189
+ P2P_maxWirePayloadBytes?: number;
190
+ P2P_connectionPath?: import("./setting.const.js").P2PConnectionPath;
189
191
  P2P_useDiagRTC?: boolean;
190
192
  };
191
193
  export type TweakValues = Partial<typeof TweakValuesTemplate>;
@@ -196,6 +196,8 @@ export declare const AllSettingDefault: {
196
196
  P2P_turnServers: string;
197
197
  P2P_turnUsername: string;
198
198
  P2P_turnCredential: string;
199
+ P2P_maxWirePayloadBytes?: number;
200
+ P2P_connectionPath?: import("./types.js").P2PConnectionPath;
199
201
  P2P_useDiagRTC?: boolean;
200
202
  };
201
203
  export type AllSettings = ObsidianLiveSyncSettings & OnDialogSettings;
@@ -8,7 +8,8 @@ import { ChunkTypes, EntryTypes, MILESTONE_DOCID, NODEINFO_DOCID, NoteTypes, SYN
8
8
  import { AUTO_MERGED, CANCELLED, LEAVE_TO_SUBSEQUENT, MISSING_OR_ERROR, NOT_CONFLICTED, TIME_ARGUMENT_INFINITY } from "./models/shared.const.symbols.js";
9
9
  import { IDPrefixes, LEAF_WAIT_ONLY_REMOTE, LEAF_WAIT_TIMEOUT, LEAF_WAIT_TIMEOUT_SEQUENTIAL_REPLICATOR, MAX_DOC_SIZE, MAX_DOC_SIZE_BIN, PREFIX_CHUNK, PREFIX_ENCRYPTED_CHUNK, PREFIX_OBFUSCATED, RECENT_MODIFIED_DOCS_QTY, REPLICATION_BUSY_TIMEOUT, SALT_OF_ID, SALT_OF_PASSPHRASE, SEED_MURMURHASH, VER } from "./models/shared.const.behabiour.js";
10
10
  import { AutoAccepting, type BucketSyncSetting, type ChunkSplitterVersion, type ConfigPassphraseStore, type CouchDBConnection, type E2EEAlgorithm, type EncryptionSettings, type HashAlgorithm, type HasSettings, type LocalDBSettings, type ObsidianLiveSyncSettings, type P2PConnectionInfo, type P2PSyncSetting, type PluginSyncSettingEntry, type RemoteDBSettings, type RemoteType, type RemoteTypeSettings, type SYNC_MODE } from "./models/setting.type.js";
11
- import { ChunkAlgorithmNames, ChunkAlgorithms, CURRENT_SETTING_VERSION, E2EEAlgorithmNames, E2EEAlgorithms, HashAlgorithms, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, RemoteTypes, SETTING_VERSION_INITIAL, SETTING_VERSION_SUPPORT_CASE_INSENSITIVE, MODE_AUTOMATIC, MODE_PAUSED, MODE_SELECTIVE, MODE_SHINY } from "./models/setting.const.js";
11
+ import { ChunkAlgorithmNames, ChunkAlgorithms, CURRENT_SETTING_VERSION, E2EEAlgorithmNames, E2EEAlgorithms, HashAlgorithms, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, RemoteTypes, SETTING_VERSION_INITIAL, SETTING_VERSION_SUPPORT_CASE_INSENSITIVE, MODE_AUTOMATIC, MODE_PAUSED, MODE_SELECTIVE, MODE_SHINY, P2PConnectionPaths, P2PMessageSizePresets, type P2PConnectionPath } from "./models/setting.const.js";
12
+ import { hasValidP2PTurnServerUrl, isValidP2PTurnServerUrl, normaliseP2PConnectionPath, normaliseP2PMaxWirePayloadBytes, splitP2PTurnServerUrls } from "./models/setting.p2p.js";
12
13
  import { PREFERRED_BASE, PREFERRED_JOURNAL_SYNC, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED } from "./models/setting.const.preferred.js";
13
14
  import { P2P_DEFAULT_SETTINGS, DEFAULT_SETTINGS, NEW_VAULT_SETTINGS, SETTINGS_SCHEMA_DEFAULTS, createNewVaultSettings } from "./models/setting.const.defaults.js";
14
15
  import { prepareSettingsForLoad, SettingsMigrationReviewCodes, type PreparedSettings, type SettingsMigrationReviewCode, type SettingsMigrationReviewReason, type SettingsMigrationState } from "./models/setting.lifecycle.js";
@@ -34,8 +35,9 @@ export { MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED, MODE_SHINY, type SYNC_MODE
34
35
  export { type PluginSyncSettingEntry };
35
36
  export { SETTING_VERSION_INITIAL, SETTING_VERSION_SUPPORT_CASE_INSENSITIVE, CURRENT_SETTING_VERSION };
36
37
  export type { BucketSyncSetting, LocalDBSettings };
37
- export { RemoteTypes, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, type RemoteType, AutoAccepting };
38
- export type { P2PConnectionInfo, P2PSyncSetting };
38
+ export { RemoteTypes, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, P2PConnectionPaths, P2PMessageSizePresets, type RemoteType, AutoAccepting, };
39
+ export type { P2PConnectionInfo, P2PConnectionPath, P2PSyncSetting };
40
+ export { hasValidP2PTurnServerUrl, isValidP2PTurnServerUrl, normaliseP2PConnectionPath, normaliseP2PMaxWirePayloadBytes, splitP2PTurnServerUrls, };
39
41
  export { P2P_DEFAULT_SETTINGS };
40
42
  export type { RemoteTypeSettings };
41
43
  export { E2EEAlgorithmNames, E2EEAlgorithms, type E2EEAlgorithm };
@@ -59,8 +59,17 @@ import {
59
59
  MODE_AUTOMATIC,
60
60
  MODE_PAUSED,
61
61
  MODE_SELECTIVE,
62
- MODE_SHINY
62
+ MODE_SHINY,
63
+ P2PConnectionPaths,
64
+ P2PMessageSizePresets
63
65
  } from "./models/setting.const.js";
66
+ import {
67
+ hasValidP2PTurnServerUrl,
68
+ isValidP2PTurnServerUrl,
69
+ normaliseP2PConnectionPath,
70
+ normaliseP2PMaxWirePayloadBytes,
71
+ splitP2PTurnServerUrls
72
+ } from "./models/setting.p2p.js";
64
73
  import {
65
74
  PREFERRED_BASE,
66
75
  PREFERRED_JOURNAL_SYNC,
@@ -181,6 +190,8 @@ export {
181
190
  NODEINFO_DOCID,
182
191
  NOT_CONFLICTED,
183
192
  NoteTypes,
193
+ P2PConnectionPaths,
194
+ P2PMessageSizePresets,
184
195
  P2P_DEFAULT_SETTINGS,
185
196
  PREFERRED_BASE,
186
197
  PREFERRED_JOURNAL_SYNC,
@@ -225,7 +236,12 @@ export {
225
236
  configURIBaseQR,
226
237
  configurationNames,
227
238
  createNewVaultSettings,
239
+ hasValidP2PTurnServerUrl,
228
240
  isMetaEntry,
241
+ isValidP2PTurnServerUrl,
242
+ normaliseP2PConnectionPath,
243
+ normaliseP2PMaxWirePayloadBytes,
229
244
  prepareSettingsForLoad,
245
+ splitP2PTurnServerUrls,
230
246
  statusDisplay
231
247
  };
@@ -7,6 +7,7 @@ import {
7
7
  PREFIX_OBFUSCATED,
8
8
  SYNCINFO_ID
9
9
  } from "./types.js";
10
+ import { normaliseP2PConnectionPath, normaliseP2PMaxWirePayloadBytes } from "./models/setting.p2p.js";
10
11
  import { isErrorOfMissingDoc } from "../pouchdb/utils_couchdb.js";
11
12
  import { replaceAll, replaceAllPairs } from "octagonal-wheels/string";
12
13
  import { concatUInt8Array } from "octagonal-wheels/binary";
@@ -459,7 +460,9 @@ function pickP2PSyncSettings(setting) {
459
460
  P2P_DevicePeerName: setting.P2P_DevicePeerName || "",
460
461
  P2P_turnServers: setting.P2P_turnServers,
461
462
  P2P_turnUsername: setting.P2P_turnUsername,
462
- P2P_turnCredential: setting.P2P_turnCredential
463
+ P2P_turnCredential: setting.P2P_turnCredential,
464
+ P2P_maxWirePayloadBytes: normaliseP2PMaxWirePayloadBytes(setting.P2P_maxWirePayloadBytes),
465
+ P2P_connectionPath: normaliseP2PConnectionPath(setting.P2P_connectionPath)
463
466
  };
464
467
  }
465
468
  function wrapByDefault(func, onError) {
@@ -4,6 +4,16 @@ export interface DatabaseFileAccess {
4
4
  store: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise<boolean>;
5
5
  /** Store a file as a child of an exact revision and return the created revision. */
6
6
  storeWithBaseRevision: (file: UXFileInfo, baseRevision: string | undefined, skipCheck?: boolean) => Promise<string | false>;
7
+ /**
8
+ * Store a file as a child of an exact revision only while that revision remains a live leaf.
9
+ *
10
+ * Unlike {@link storeWithBaseRevision}, this method does not force a branch below an obsolete
11
+ * revision. 'Live leaf' means any current revision-tree leaf, including a non-winning conflict
12
+ * leaf or a logical-deletion leaf. In particular, it returns `false` when another writer has
13
+ * already advanced the supplied base; ordinary target and Chunk validation can also refuse the
14
+ * write without creating a Metadata successor.
15
+ */
16
+ storeWithLiveBaseRevision: (file: UXFileInfo, baseRevision: string, skipCheck?: boolean) => Promise<string | false>;
7
17
  storeAsConflictedRevision: (file: UXFileInfo, currentRev: string, skipCheck?: boolean) => Promise<boolean>;
8
18
  /** Preserve unknown storage content as a conflict and return its exact revision. */
9
19
  storeAsConflictedRevisionWithResult: (file: UXFileInfo, currentRev: string, skipCheck?: boolean) => Promise<string | false>;
@@ -35,4 +35,5 @@ export declare class EntryManager {
35
35
  deleteDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise<boolean>;
36
36
  storeDeletionAtRevision(path: FilePathWithPrefix | FilePath, baseRevision: string): Promise<false | PouchDB.Core.Response>;
37
37
  putDBEntry(note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise<false | PouchDB.Core.Response>;
38
+ putDBEntryWithLiveBaseRevision(note: SavingEntry, baseRevision: string, onlyChunks?: boolean): Promise<false | PouchDB.Core.Response>;
38
39
  }
@@ -9,6 +9,7 @@ import {
9
9
  isTargetFile,
10
10
  prepareChunk,
11
11
  putDBEntry,
12
+ putDBEntryWithLiveBaseRevision,
12
13
  storeDeletionByPathAtRevision
13
14
  } from "./EntryManagerImpls.js";
14
15
  class EntryManager {
@@ -71,6 +72,9 @@ class EntryManager {
71
72
  async putDBEntry(note, onlyChunks, conflictBaseRev) {
72
73
  return await putDBEntry(this.serviceHost, this, note, onlyChunks, conflictBaseRev);
73
74
  }
75
+ async putDBEntryWithLiveBaseRevision(note, baseRevision, onlyChunks) {
76
+ return await putDBEntryWithLiveBaseRevision(this.serviceHost, this, note, baseRevision, onlyChunks);
77
+ }
74
78
  }
75
79
  export {
76
80
  EntryManager
@@ -13,6 +13,13 @@ type Managers = {
13
13
  type NecessaryManagers<T extends keyof Managers> = Pick<Managers, T>;
14
14
  export declare function createChunks(managers: NecessaryManagers<"chunkManager" | "hashManager" | "splitter">, dispFilename: string, note: SavingEntry): Promise<false | DocumentID[]>;
15
15
  export declare function putDBEntry(host: NecessaryServicesInterfaces<"path" | "setting", never>, managers: NecessaryManagers<"localDatabase" | "chunkManager" | "hashManager" | "splitter">, note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise<false | PouchDB.Core.Response>;
16
+ /**
17
+ * Store an entry below an exact revision only while that revision remains a live leaf.
18
+ *
19
+ * PouchDB's ordinary MVCC write is the authority for this check. A 409 response means that the
20
+ * base has already been advanced, so the caller receives `false` and can retry from fresh state.
21
+ */
22
+ export declare function putDBEntryWithLiveBaseRevision(host: NecessaryServicesInterfaces<"path" | "setting", never>, managers: NecessaryManagers<"localDatabase" | "chunkManager" | "hashManager" | "splitter">, note: SavingEntry, baseRevision: string, onlyChunks?: boolean): Promise<false | PouchDB.Core.Response>;
16
23
  export declare function isTargetFile(host: NecessaryServicesInterfaces<"setting", never>, filenameSrc: string): boolean;
17
24
  export declare function prepareChunk({ chunkManager, hashManager }: NecessaryManagers<"chunkManager" | "hashManager">, piece: string): Promise<GeneratedChunk>;
18
25
  export declare function getDBEntryMetaByPath(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase }: NecessaryManagers<"localDatabase">, path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, includeDeleted?: boolean): Promise<false | LoadedEntry>;
@@ -110,6 +110,23 @@ async function createChunks(managers, dispFilename, note) {
110
110
  return chunks;
111
111
  }
112
112
  async function putDBEntry(host, managers, note, onlyChunks, conflictBaseRev) {
113
+ const revisionTarget = conflictBaseRev ? { mode: "force-base", baseRevision: conflictBaseRev } : { mode: "latest" };
114
+ return await putDBEntryInternal(host, managers, note, onlyChunks, revisionTarget);
115
+ }
116
+ async function putDBEntryWithLiveBaseRevision(host, managers, note, baseRevision, onlyChunks) {
117
+ try {
118
+ return await putDBEntryInternal(host, managers, note, onlyChunks, {
119
+ mode: "live-base",
120
+ baseRevision
121
+ });
122
+ } catch (ex) {
123
+ if (ex && typeof ex === "object" && "status" in ex && ex.status === 409) {
124
+ return false;
125
+ }
126
+ throw ex;
127
+ }
128
+ }
129
+ async function putDBEntryInternal(host, managers, note, onlyChunks, revisionTarget) {
113
130
  const { localDatabase, chunkManager, splitter } = managers;
114
131
  const filename = host.services.path.id2path(note._id, note);
115
132
  const dispFilename = stripAllPrefixes(filename);
@@ -146,8 +163,8 @@ async function putDBEntry(host, managers, note, onlyChunks, conflictBaseRev) {
146
163
  eden: {}
147
164
  };
148
165
  return await serialized("file:" + filename, async () => {
149
- if (conflictBaseRev) {
150
- newDoc._rev = conflictBaseRev;
166
+ if (revisionTarget.mode !== "latest") {
167
+ newDoc._rev = revisionTarget.baseRevision;
151
168
  } else {
152
169
  try {
153
170
  const old = await localDatabase.get(newDoc._id);
@@ -159,7 +176,7 @@ async function putDBEntry(host, managers, note, onlyChunks, conflictBaseRev) {
159
176
  }
160
177
  }
161
178
  }
162
- const r = await localDatabase.put(newDoc, { force: true });
179
+ const r = revisionTarget.mode === "live-base" ? await localDatabase.put(newDoc) : await localDatabase.put(newDoc, { force: true });
163
180
  if (r.ok) {
164
181
  return r;
165
182
  } else {
@@ -523,5 +540,6 @@ export {
523
540
  isTargetFile,
524
541
  prepareChunk,
525
542
  putDBEntry,
543
+ putDBEntryWithLiveBaseRevision,
526
544
  storeDeletionByPathAtRevision
527
545
  };
@@ -192,6 +192,7 @@ export declare class LiveSyncLocalDB {
192
192
  deleteDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise<boolean>;
193
193
  storeDeletionAtRevision(path: FilePathWithPrefix | FilePath, baseRevision: string): Promise<false | PouchDB.Core.Response>;
194
194
  putDBEntry(note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise<false | PouchDB.Core.Response>;
195
+ putDBEntryWithLiveBaseRevision(note: SavingEntry, baseRevision: string, onlyChunks?: boolean): Promise<false | PouchDB.Core.Response>;
195
196
  getConflictedDoc(path: FilePathWithPrefix, rev: string): Promise<false | diff_result_leaf>;
196
197
  tryAutoMerge(path: FilePathWithPrefix, enableMarkdownAutoMerge: boolean): AutoMergeResult;
197
198
  }
@@ -370,6 +370,9 @@ class LiveSyncLocalDB {
370
370
  async putDBEntry(note, onlyChunks, conflictBaseRev) {
371
371
  return await this.managers.entryManager.putDBEntry(note, onlyChunks, conflictBaseRev);
372
372
  }
373
+ async putDBEntryWithLiveBaseRevision(note, baseRevision, onlyChunks) {
374
+ return await this.managers.entryManager.putDBEntryWithLiveBaseRevision(note, baseRevision, onlyChunks);
375
+ }
373
376
  async getConflictedDoc(path, rev) {
374
377
  return await this.managers.conflictManager.getConflictedDoc(path, rev);
375
378
  }
@@ -20,6 +20,7 @@ export declare class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicat
20
20
  private _replicator?;
21
21
  private _lifecycleOperation;
22
22
  private _shouldBeOpen;
23
+ private _activeTransportCompatibilitySignature?;
23
24
  get openReplicationUI(): (showResult: boolean) => Promise<boolean | void>;
24
25
  get rawReplicator(): TrysteroReplicator;
25
26
  get rawHost(): P2PHost;
@@ -27,6 +28,13 @@ export declare class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicat
27
28
  terminateSync(): void;
28
29
  private _buildEnv;
29
30
  private _enqueueLifecycleOperation;
31
+ /**
32
+ * Capture settings which are fixed when the active P2P transport joins its room.
33
+ * A later open request can remain idempotent only while these effective values match.
34
+ */
35
+ private _getTransportCompatibilitySignature;
36
+ /** Close and forget the transport without changing the requested lifecycle state. */
37
+ private _closeTransport;
30
38
  open(): Promise<void>;
31
39
  close(): Promise<void>;
32
40
  closeReplication(): void;
@@ -16,11 +16,18 @@ import {
16
16
  P2PHost
17
17
  } from "./TrysteroReplicatorP2PServer.js";
18
18
  import { delay } from "octagonal-wheels/promises";
19
+ import {
20
+ hasValidP2PTurnServerUrl,
21
+ normaliseP2PConnectionPath,
22
+ normaliseP2PMaxWirePayloadBytes
23
+ } from "../../common/models/setting.p2p.js";
24
+ import { P2PConnectionPaths } from "../../common/models/setting.const.js";
19
25
  class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
20
26
  _p2pHost;
21
27
  _replicator;
22
28
  _lifecycleOperation = Promise.resolve();
23
29
  _shouldBeOpen = false;
30
+ _activeTransportCompatibilitySignature;
24
31
  get openReplicationUI() {
25
32
  return this.env.openReplicationUI;
26
33
  }
@@ -81,6 +88,26 @@ class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
81
88
  this._lifecycleOperation = queued.catch(() => void 0);
82
89
  return queued;
83
90
  }
91
+ /**
92
+ * Capture settings which are fixed when the active P2P transport joins its room.
93
+ * A later open request can remain idempotent only while these effective values match.
94
+ */
95
+ _getTransportCompatibilitySignature() {
96
+ const settings = this.env.services.setting.currentSettings();
97
+ const configuredPath = normaliseP2PConnectionPath(settings.P2P_connectionPath);
98
+ const effectivePath = configuredPath === P2PConnectionPaths.Relay && hasValidP2PTurnServerUrl(settings.P2P_turnServers ?? "") ? P2PConnectionPaths.Relay : P2PConnectionPaths.Automatic;
99
+ return `${normaliseP2PMaxWirePayloadBytes(settings.P2P_maxWirePayloadBytes)}:${effectivePath}`;
100
+ }
101
+ /** Close and forget the transport without changing the requested lifecycle state. */
102
+ async _closeTransport() {
103
+ if (this._replicator) {
104
+ this._replicator.disableBroadcastChanges();
105
+ await this._replicator.close();
106
+ this._replicator = void 0;
107
+ }
108
+ this._p2pHost = void 0;
109
+ this._activeTransportCompatibilitySignature = void 0;
110
+ }
84
111
  async open() {
85
112
  if (!this.env.services.setting.currentSettings().P2P_Enabled) {
86
113
  Logger(this.translate("P2P.NotEnabled"), LOG_LEVEL_NOTICE);
@@ -89,9 +116,13 @@ class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
89
116
  this._shouldBeOpen = true;
90
117
  await this._enqueueLifecycleOperation(async () => {
91
118
  if (!this._shouldBeOpen) return;
119
+ const compatibilitySignature = this._getTransportCompatibilitySignature();
92
120
  if (this._replicator && this._p2pHost?.isServing) {
93
- Logger("P2P replicator is already open.");
94
- return;
121
+ if (this._activeTransportCompatibilitySignature === compatibilitySignature) {
122
+ Logger("P2P replicator is already open.");
123
+ return;
124
+ }
125
+ await this._closeTransport();
95
126
  }
96
127
  try {
97
128
  const env = this._buildEnv();
@@ -100,23 +131,20 @@ class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
100
131
  this._p2pHost = host;
101
132
  this._replicator = replicator;
102
133
  await replicator.open();
134
+ this._activeTransportCompatibilitySignature = compatibilitySignature;
103
135
  } catch (e) {
104
136
  Logger(e instanceof Error ? e.message : "Error while opening P2P connection", LOG_LEVEL_NOTICE);
105
137
  Logger(e, LOG_LEVEL_VERBOSE);
106
138
  this._p2pHost = void 0;
107
139
  this._replicator = void 0;
140
+ this._activeTransportCompatibilitySignature = void 0;
108
141
  }
109
142
  });
110
143
  }
111
144
  async close() {
112
145
  this._shouldBeOpen = false;
113
146
  await this._enqueueLifecycleOperation(async () => {
114
- if (this._replicator) {
115
- this._replicator.disableBroadcastChanges();
116
- await this._replicator.close();
117
- this._replicator = void 0;
118
- }
119
- this._p2pHost = void 0;
147
+ await this._closeTransport();
120
148
  });
121
149
  }
122
150
  closeReplication() {
@@ -13,7 +13,7 @@ import { EVENT_PLATFORM_UNLOADED } from "../../events/coreEvents.js";
13
13
  import { shareRunningResult } from "octagonal-wheels/concurrency/lock_v2";
14
14
  import { Computed } from "octagonal-wheels/dataobject/Computed";
15
15
  import { RpcRoom } from "../../rpc/index.js";
16
- import { TRYSTERO_RPC_DEFAULTS } from "../../rpc/transports/TrysteroTransport.js";
16
+ import { resolveTrysteroRpcOptions } from "../../rpc/transports/TrysteroTransport.js";
17
17
  import { subscribeTrysteroPeerEvents } from "../../rpc/transports/trysteroRoomEvents.js";
18
18
  import { toRpcMethodName } from "./rpcCompat.js";
19
19
  import { generateJoinRoomOptions } from "../../rpc/transports/trysteroUtils.js";
@@ -379,7 +379,7 @@ You can chose as follows:
379
379
  };
380
380
  this._rpcRoom?.close();
381
381
  this._rpcRoom = new RpcRoom({
382
- ...TRYSTERO_RPC_DEFAULTS,
382
+ ...resolveTrysteroRpcOptions(this.settings),
383
383
  transport,
384
384
  canAcceptRequest: async (peerId, method) => {
385
385
  if (method === toRpcMethodName("!reqAuth")) return true;
@@ -68,6 +68,8 @@ export declare const TRYSTERO_RPC_DEFAULTS: {
68
68
  /** SCTP guarantees delivery; retransmission is a last-resort safety net. */
69
69
  readonly chunkMissingRetryMs: 150;
70
70
  };
71
+ /** Resolve the RpcRoom defaults contributed by one P2P connection profile. */
72
+ export declare function resolveTrysteroRpcOptions(settings: Pick<P2PConnectionInfo, "P2P_maxWirePayloadBytes">): Required<Pick<RpcRoomOptions, "maxWirePayloadBytes" | "chunkMissingRetryMs">>;
71
73
  /**
72
74
  * Wrap an already-joined Trystero `Room` as a `TransportAdapter` for `RpcRoom`.
73
75
  *
@@ -2,6 +2,7 @@ import { joinRoom, selfId } from "@trystero-p2p/nostr";
2
2
  import { RpcRoom } from "../RpcRoom.js";
3
3
  import { exposeDB } from "../pouchdb/RpcPouchDBServer.js";
4
4
  import { RpcPouchDBProxy } from "../pouchdb/RpcPouchDBProxy.js";
5
+ import { normaliseP2PMaxWirePayloadBytes } from "../../common/models/setting.p2p.js";
5
6
  import { ConnectionStringParser } from "../../common/ConnectionString.js";
6
7
  import { compatGlobal } from "../../common/coreEnvFunctions.js";
7
8
  import { generateJoinRoomOptions } from "./trysteroUtils.js";
@@ -46,6 +47,12 @@ const TRYSTERO_RPC_DEFAULTS = {
46
47
  /** SCTP guarantees delivery; retransmission is a last-resort safety net. */
47
48
  chunkMissingRetryMs: 150
48
49
  };
50
+ function resolveTrysteroRpcOptions(settings) {
51
+ return {
52
+ ...TRYSTERO_RPC_DEFAULTS,
53
+ maxWirePayloadBytes: normaliseP2PMaxWirePayloadBytes(settings.P2P_maxWirePayloadBytes)
54
+ };
55
+ }
49
56
  function wrapTrysteroRoom(room) {
50
57
  const rpcAction = room.makeAction(RPC_ACTION_NAME);
51
58
  return {
@@ -93,7 +100,11 @@ function joinTrysteroRoomFromUrl(url) {
93
100
  }
94
101
  function serveTrysteroDB(settings, db, ns = "pdb", options) {
95
102
  const roomHandle = joinTrysteroRoom(settings);
96
- const rpcRoom = new RpcRoom({ ...TRYSTERO_RPC_DEFAULTS, transport: roomHandle.transport, ...options });
103
+ const rpcRoom = new RpcRoom({
104
+ ...resolveTrysteroRpcOptions(settings),
105
+ transport: roomHandle.transport,
106
+ ...options
107
+ });
97
108
  exposeDB(rpcRoom, db, ns);
98
109
  return {
99
110
  peerId: roomHandle.peerId,
@@ -107,7 +118,11 @@ function serveTrysteroDB(settings, db, ns = "pdb", options) {
107
118
  }
108
119
  function connectTrysteroDBClient(settings, serverPeerId, dbName, ns = "pdb", options) {
109
120
  const roomHandle = joinTrysteroRoom(settings);
110
- const rpcRoom = new RpcRoom({ ...TRYSTERO_RPC_DEFAULTS, transport: roomHandle.transport, ...options });
121
+ const rpcRoom = new RpcRoom({
122
+ ...resolveTrysteroRpcOptions(settings),
123
+ transport: roomHandle.transport,
124
+ ...options
125
+ });
111
126
  const proxy = new RpcPouchDBProxy(rpcRoom.session(serverPeerId), dbName, ns);
112
127
  return {
113
128
  proxy,
@@ -136,6 +151,7 @@ export {
136
151
  connectTrysteroDBClient,
137
152
  joinTrysteroRoom,
138
153
  joinTrysteroRoomFromUrl,
154
+ resolveTrysteroRpcOptions,
139
155
  serveTrysteroDB,
140
156
  wrapTrysteroRoom
141
157
  };