@vrtmrz/livesync-commonlib 0.1.17 → 0.1.19-rc.0

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 (46) hide show
  1. package/dist/API/DirectFileManipulatorV2.js +3 -1
  2. package/dist/common/ConnectionString.js +9 -1
  3. package/dist/common/models/fileaccess.type.d.ts +7 -0
  4. package/dist/common/models/setting.const.d.ts +18 -0
  5. package/dist/common/models/setting.const.defaults.js +10 -1
  6. package/dist/common/models/setting.const.js +12 -0
  7. package/dist/common/models/setting.const.qr.js +3 -1
  8. package/dist/common/models/setting.p2p.d.ts +21 -0
  9. package/dist/common/models/setting.p2p.js +35 -0
  10. package/dist/common/models/setting.type.d.ts +11 -1
  11. package/dist/common/models/shared.definition.configNames.js +8 -0
  12. package/dist/common/models/tweak.definition.d.ts +2 -0
  13. package/dist/common/settingConstants.d.ts +2 -0
  14. package/dist/common/types.d.ts +5 -3
  15. package/dist/common/types.js +17 -1
  16. package/dist/common/utils.js +4 -1
  17. package/dist/interfaces/DatabaseRebuilder.d.ts +4 -2
  18. package/dist/managers/StorageEventManager.d.ts +16 -5
  19. package/dist/managers/StorageEventManager.js +46 -8
  20. package/dist/pouchdb/LiveSyncLocalDB.d.ts +7 -1
  21. package/dist/pouchdb/LiveSyncLocalDB.js +120 -38
  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/serviceFeatures/prepareDatabaseForUse.js +18 -15
  29. package/dist/serviceModules/Rebuilder.d.ts +27 -4
  30. package/dist/serviceModules/Rebuilder.js +113 -35
  31. package/dist/serviceModules/ServiceFileHandlerBase.d.ts +17 -0
  32. package/dist/serviceModules/ServiceFileHandlerBase.js +145 -9
  33. package/dist/services/base/DatabaseService.d.ts +11 -3
  34. package/dist/services/base/DatabaseService.js +63 -7
  35. package/dist/services/base/IService.d.ts +1 -0
  36. package/dist/services/base/ReplicationService.d.ts +16 -0
  37. package/dist/services/base/ReplicationService.js +36 -4
  38. package/dist/services/base/ReplicatorService.d.ts +1 -0
  39. package/dist/services/base/ReplicatorService.js +32 -13
  40. package/dist/worker/bgWorker.js +1 -1
  41. package/docs/development.md +2 -0
  42. package/docs/p2p-transport-lifecycle.md +4 -2
  43. package/docs/proven-in-use.md +5 -1
  44. package/docs/releasing.md +24 -0
  45. package/docs/remote-configurations.md +2 -0
  46. package/package.json +1 -1
@@ -48,7 +48,9 @@ class DirectFileManipulator {
48
48
  async init() {
49
49
  try {
50
50
  await this.services.appLifecycle.onReady();
51
- await this.liveSyncLocalDB.initializeDatabase();
51
+ if (!await this.liveSyncLocalDB.initializeDatabase()) {
52
+ throw new Error("Direct database initialisation was rejected.");
53
+ }
52
54
  this.liveSyncLocalDB.refreshSettings();
53
55
  this.ready.resolve();
54
56
  } catch (error) {
@@ -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()}` : "";
@@ -59,6 +59,13 @@ export type FileEventItem = {
59
59
  type: FileEventType;
60
60
  args: FileEventArgs;
61
61
  key: string;
62
+ /**
63
+ * The event was loaded from the persisted storage-operation snapshot.
64
+ *
65
+ * A restored event records pending operation intent, not current storage
66
+ * state. File handlers must revalidate it before applying side effects.
67
+ */
68
+ restoredFromPreviousRuntime?: true;
62
69
  skipBatchWait?: boolean;
63
70
  cancelled?: boolean;
64
71
  batched?: boolean;
@@ -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) {
@@ -17,7 +17,9 @@ export interface Rebuilder {
17
17
  /** See {@link scheduleRebuild}; this variant writes the Fetch flag. */
18
18
  scheduleFetch(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
19
19
  /**
20
- * Declares the finish of the rebuild process and unlock remote, resume reflecting the changes.
20
+ * Complete the final reflection scan and checks owned by a fetch, then establish application readiness.
21
+ *
22
+ * @returns `true` when every required completion phase succeeded; otherwise, `false`.
21
23
  */
22
- finishRebuild(): Promise<void>;
24
+ finishRebuild(): Promise<boolean>;
23
25
  }
@@ -21,6 +21,9 @@ type FileEventItemSentinelFlush = {
21
21
  type: typeof TYPE_SENTINEL_FLUSH;
22
22
  };
23
23
  export type FileEventItemSentinel = FileEventItemSentinelFlush;
24
+ type RunQueuedEventsOptions = {
25
+ waitForProcessing?: boolean;
26
+ };
24
27
  export interface StorageEventManagerBaseDependencies {
25
28
  setting: SettingService;
26
29
  vaultService: IVaultService;
@@ -61,13 +64,11 @@ export declare abstract class StorageEventManagerBase<TAdapter extends IStorageE
61
64
  protected updateStatus(): void;
62
65
  /**
63
66
  * Snapshot restoration promise.
64
- * Snapshot will be restored before starting to watch vault changes.
65
- * In designed time, this has been called from Initialisation process, which has been implemented on `ModuleInitializerFile.ts`.
67
+ * It resolves after the restored file operations have finished, before Vault watching begins.
66
68
  */
67
69
  snapShotRestored: Promise<void> | null;
68
70
  /**
69
- * Restore the previous snapshot if exists.
70
- * @returns
71
+ * Restore and complete the previous storage-event snapshot when it exists.
71
72
  */
72
73
  restoreState(): Promise<void>;
73
74
  appendQueue(params: FileEvent[], ctx?: unknown): Promise<void>;
@@ -111,7 +112,17 @@ export declare abstract class StorageEventManagerBase<TAdapter extends IStorageE
111
112
  processFileEvent(fei: FileEventItem): Promise<void>;
112
113
  _takeSnapshot(): Promise<void>;
113
114
  _restoreFromSnapshot(): Promise<void>;
114
- protected runQueuedEvents(): Promise<void>;
115
+ /**
116
+ * Dispatch buffered storage events.
117
+ *
118
+ * Ordinary watcher events retain the existing fire-and-forget behaviour.
119
+ * Snapshot restoration requests completion so its caller can use the replay as
120
+ * a lifecycle boundary. In that mode, a sentinel also waits for every operation
121
+ * dispatched before it, preserving the ordering represented by the snapshot.
122
+ * An individual restored operation failure is logged but does not prevent the
123
+ * following Offline Scanner from reconciling the resulting current state.
124
+ */
125
+ protected runQueuedEvents(options?: RunQueuedEventsOptions): Promise<void>;
115
126
  protected processingCount: number;
116
127
  protected requestProcessQueue(fei: FileEventItem): Promise<void>;
117
128
  isWaiting(filename: FilePath): boolean;
@@ -72,13 +72,11 @@ class StorageEventManagerBase extends StorageEventManager {
72
72
  }
73
73
  /**
74
74
  * Snapshot restoration promise.
75
- * Snapshot will be restored before starting to watch vault changes.
76
- * In designed time, this has been called from Initialisation process, which has been implemented on `ModuleInitializerFile.ts`.
75
+ * It resolves after the restored file operations have finished, before Vault watching begins.
77
76
  */
78
77
  snapShotRestored = null;
79
78
  /**
80
- * Restore the previous snapshot if exists.
81
- * @returns
79
+ * Restore and complete the previous storage-event snapshot when it exists.
82
80
  */
83
81
  restoreState() {
84
82
  this.snapShotRestored = this._restoreFromSnapshot();
@@ -371,15 +369,44 @@ class StorageEventManagerBase extends StorageEventManager {
371
369
  const snapShot = await this._loadSnapshot();
372
370
  if (snapShot && Array.isArray(snapShot) && snapShot.length > 0) {
373
371
  this._log(`Restoring storage operation snapshot: ${snapShot.length} items`, LOG_LEVEL_VERBOSE);
374
- this.bufferedQueuedItems = snapShot.map((e) => ({ ...e, skipBatchWait: true }));
372
+ this.bufferedQueuedItems = snapShot.map(
373
+ (event) => event.type === TYPE_SENTINEL_FLUSH ? event : { ...event, restoredFromPreviousRuntime: true, skipBatchWait: true }
374
+ );
375
375
  this.updateStatus();
376
- await this.runQueuedEvents();
376
+ await this.runQueuedEvents({ waitForProcessing: true });
377
377
  } else {
378
378
  this._log(`No snapshot to restore`, LOG_LEVEL_VERBOSE);
379
379
  }
380
380
  }
381
- runQueuedEvents() {
381
+ /**
382
+ * Dispatch buffered storage events.
383
+ *
384
+ * Ordinary watcher events retain the existing fire-and-forget behaviour.
385
+ * Snapshot restoration requests completion so its caller can use the replay as
386
+ * a lifecycle boundary. In that mode, a sentinel also waits for every operation
387
+ * dispatched before it, preserving the ordering represented by the snapshot.
388
+ * An individual restored operation failure is logged but does not prevent the
389
+ * following Offline Scanner from reconciling the resulting current state.
390
+ */
391
+ runQueuedEvents(options = {}) {
392
+ const waitForProcessing = options.waitForProcessing ?? false;
382
393
  return skipIfDuplicated("storage-event-manager-run-queued-events", async () => {
394
+ let dispatched = [];
395
+ const waitForDispatched = async () => {
396
+ if (dispatched.length === 0) return;
397
+ const pending = dispatched;
398
+ dispatched = [];
399
+ const results = await Promise.allSettled(pending.map(({ processing }) => processing));
400
+ for (const [index, result] of results.entries()) {
401
+ if (result.status === "rejected") {
402
+ this._log(
403
+ `Restored storage operation failed for ${pending[index].path}; the Offline Scanner will reconcile the current state`,
404
+ LOG_LEVEL_NOTICE
405
+ );
406
+ this._log(result.reason, LOG_LEVEL_VERBOSE);
407
+ }
408
+ }
409
+ };
383
410
  do {
384
411
  if (this.bufferedQueuedItems.length === 0) {
385
412
  break;
@@ -392,12 +419,23 @@ class StorageEventManagerBase extends StorageEventManager {
392
419
  this.updateStatus();
393
420
  if (fei.type === TYPE_SENTINEL_FLUSH) {
394
421
  this._log(`Waiting for idle`, LOG_LEVEL_VERBOSE);
422
+ if (waitForProcessing) {
423
+ await waitForDispatched();
424
+ }
395
425
  await this.waitForIdle();
396
426
  this.updateStatus();
397
427
  continue;
398
428
  }
399
- fireAndForget(() => this.processFileEvent(fei));
429
+ const processing = this.processFileEvent(fei);
430
+ if (waitForProcessing) {
431
+ dispatched.push({ path: fei.args.file.path, processing });
432
+ } else {
433
+ fireAndForget(processing);
434
+ }
400
435
  } while (this.bufferedQueuedItems.length > 0);
436
+ if (waitForProcessing) {
437
+ await waitForDispatched();
438
+ }
401
439
  });
402
440
  }
403
441
  processingCount = 0;
@@ -46,11 +46,17 @@ export declare class LiveSyncLocalDB {
46
46
  env: LiveSyncLocalDBEnv;
47
47
  clearCaches(): void;
48
48
  _prepareHashFunctions(): Promise<void>;
49
- onunload(): void;
50
49
  refreshSettings(): void;
51
50
  offRemoteChunkFetchedHandler?: ReturnType<LiveSyncEventHub["onEvent"]>;
51
+ private databaseCloseCleanup?;
52
+ private databaseUnloadNotification?;
52
53
  constructor(dbname: string, env: LiveSyncLocalDBEnv);
54
+ /** Notify dependent services at most once for each physical database lifecycle. */
55
+ private notifyDatabaseUnload;
56
+ onunload(): void;
53
57
  close(): Promise<void>;
58
+ private teardownDatabaseDependencies;
59
+ private rollbackFailedInitialisation;
54
60
  onNewLeaf(chunk: EntryLeaf): void;
55
61
  initializeDatabase(): Promise<boolean>;
56
62
  /**