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

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 (114) hide show
  1. package/README.md +5 -1
  2. package/dist/common/models/setting.p2p.d.ts +2 -0
  3. package/dist/common/models/setting.p2p.js +5 -0
  4. package/dist/interfaces/ServiceModule.d.ts +13 -4
  5. package/dist/managers/ChunkFetcher.js +10 -0
  6. package/dist/p2p/P2PService.d.ts +123 -0
  7. package/dist/p2p/P2PService.js +271 -0
  8. package/dist/p2p/index.d.ts +8 -0
  9. package/dist/p2p/index.js +8 -0
  10. package/dist/pouchdb/LiveSyncLocalDB.d.ts +3 -0
  11. package/dist/pouchdb/LiveSyncLocalDB.js +23 -3
  12. package/dist/pouchdb/ReplicatorShim.d.ts +43 -4
  13. package/dist/pouchdb/ReplicatorShim.js +78 -5
  14. package/dist/replication/CentralCompatibility.d.ts +38 -0
  15. package/dist/replication/CentralCompatibility.js +39 -0
  16. package/dist/replication/CentralRemoteAdministration.d.ts +84 -0
  17. package/dist/replication/CentralRemoteAdministration.js +71 -0
  18. package/dist/replication/LiveSyncAbstractReplicator.d.ts +19 -6
  19. package/dist/replication/ProviderCapability.d.ts +32 -0
  20. package/dist/replication/ProviderCapability.js +27 -0
  21. package/dist/replication/RemoteResource.d.ts +85 -0
  22. package/dist/replication/RemoteResource.js +17 -0
  23. package/dist/replication/ReplicatorInstance.d.ts +23 -0
  24. package/dist/replication/ReplicatorProvider.d.ts +249 -0
  25. package/dist/replication/ReplicatorProvider.js +145 -0
  26. package/dist/replication/couchdb/LiveSyncReplicator.d.ts +54 -8
  27. package/dist/replication/couchdb/LiveSyncReplicator.js +411 -158
  28. package/dist/replication/index.d.ts +10 -0
  29. package/dist/replication/index.js +70 -0
  30. package/dist/replication/journal/JournalSyncCore.d.ts +2 -0
  31. package/dist/replication/journal/JournalSyncCore.js +18 -5
  32. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +38 -10
  33. package/dist/replication/journal/LiveSyncJournalReplicator.js +271 -63
  34. package/dist/replication/journal/LiveSyncJournalReplicatorEnv.d.ts +8 -0
  35. package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +2 -0
  36. package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -0
  37. package/dist/replication/journal/objectstore/MinioStorageAdapter.js +10 -1
  38. package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +12 -40
  39. package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +52 -152
  40. package/dist/replication/trystero/P2PAutomationCoordinator.d.ts +20 -0
  41. package/dist/replication/trystero/P2PAutomationCoordinator.js +43 -0
  42. package/dist/replication/trystero/P2PConnectionProbeAdmission.d.ts +32 -0
  43. package/dist/replication/trystero/P2PConnectionProbeAdmission.js +10 -0
  44. package/dist/replication/trystero/P2PReplicationWire.d.ts +31 -0
  45. package/dist/replication/trystero/P2PReplicationWire.js +50 -0
  46. package/dist/replication/trystero/P2PRoomSession.d.ts +32 -0
  47. package/dist/replication/trystero/P2PRoomSession.js +101 -0
  48. package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +61 -0
  49. package/dist/replication/trystero/P2PRoomSessionOwner.js +249 -0
  50. package/dist/replication/trystero/TrysteroReplicator.d.ts +88 -36
  51. package/dist/replication/trystero/TrysteroReplicator.js +273 -113
  52. package/dist/replication/trystero/TrysteroReplicatorP2PClient.d.ts +7 -4
  53. package/dist/replication/trystero/TrysteroReplicatorP2PClient.js +21 -14
  54. package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +26 -3
  55. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +84 -39
  56. package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -6
  57. package/dist/replication/trystero/addP2PEventHandlers.d.ts +8 -12
  58. package/dist/replication/trystero/addP2PEventHandlers.js +19 -19
  59. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.d.ts +9 -0
  60. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +30 -0
  61. package/dist/replication/trystero/types.d.ts +18 -0
  62. package/dist/replication/trystero/types.js +5 -1
  63. package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +3 -3
  64. package/dist/replication/trystero/useP2PReplicatorCommands.js +6 -8
  65. package/dist/replication/trystero/useP2PReplicatorFeature.d.ts +16 -7
  66. package/dist/replication/trystero/useP2PReplicatorFeature.js +72 -55
  67. package/dist/rpc/RpcRoom.d.ts +15 -1
  68. package/dist/rpc/RpcRoom.js +176 -41
  69. package/dist/rpc/RpcSession.d.ts +3 -1
  70. package/dist/rpc/RpcSession.js +2 -2
  71. package/dist/rpc/index.d.ts +1 -1
  72. package/dist/rpc/transports/trysteroUtils.js +2 -1
  73. package/dist/rpc/types.d.ts +26 -1
  74. package/dist/serviceFeatures/checkRemoteSize.js +5 -2
  75. package/dist/serviceModules/Rebuilder.js +66 -56
  76. package/dist/services/base/AppLifecycleService.d.ts +4 -0
  77. package/dist/services/base/AppLifecycleService.js +4 -0
  78. package/dist/services/base/ControlService.js +0 -4
  79. package/dist/services/base/DatabaseEventService.js +1 -1
  80. package/dist/services/base/IService.d.ts +105 -8
  81. package/dist/services/base/ReadinessEvaluator.d.ts +40 -0
  82. package/dist/services/base/ReadinessEvaluator.js +16 -0
  83. package/dist/services/base/RemoteConnection.d.ts +3 -0
  84. package/dist/services/base/RemoteService.js +2 -2
  85. package/dist/services/base/ReplicationService.d.ts +45 -10
  86. package/dist/services/base/ReplicationService.js +248 -91
  87. package/dist/services/base/ReplicationService.readiness.d.ts +52 -0
  88. package/dist/services/base/ReplicationService.readiness.js +87 -0
  89. package/dist/services/base/ReplicationService.typedReplication.d.ts +83 -0
  90. package/dist/services/base/ReplicationService.typedReplication.js +191 -0
  91. package/dist/services/base/ReplicatorService.activeReplicatorState.d.ts +47 -0
  92. package/dist/services/base/ReplicatorService.activeReplicatorState.js +107 -0
  93. package/dist/services/base/ReplicatorService.centralRemoteAdministration.d.ts +5 -0
  94. package/dist/services/base/ReplicatorService.centralRemoteAdministration.js +44 -0
  95. package/dist/services/base/ReplicatorService.d.ts +75 -14
  96. package/dist/services/base/ReplicatorService.js +232 -53
  97. package/dist/services/base/ReplicatorService.remoteResourceResolver.d.ts +4 -0
  98. package/dist/services/base/ReplicatorService.remoteResourceResolver.js +11 -0
  99. package/dist/services/base/TweakValueService.d.ts +3 -2
  100. package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +1 -1
  101. package/dist/services/lib/HandlerUtils.d.ts +24 -0
  102. package/dist/services/lib/HandlerUtils.js +40 -0
  103. package/docs/development.md +40 -7
  104. package/docs/p2p-transport-lifecycle.md +135 -11
  105. package/docs/proven-in-use.md +5 -1
  106. package/docs/remote-configurations.md +4 -0
  107. package/docs/service-feature-composition.md +250 -0
  108. package/package.json +17 -1
  109. package/dist/replication/trystero/P2PReplicatorBase.d.ts +0 -19
  110. package/dist/replication/trystero/P2PReplicatorCore.d.ts +0 -8
  111. package/dist/replication/trystero/P2PReplicatorCore.js +0 -56
  112. package/dist/replication/trystero/TrysteroReplicatorP2PConnection.d.ts +0 -2
  113. package/dist/replication/trystero/TrysteroReplicatorP2PConnection.js +0 -4
  114. /package/dist/replication/{trystero/P2PReplicatorBase.js → ReplicatorInstance.js} +0 -0
@@ -9,6 +9,29 @@ export type RpcErrorShape = {
9
9
  message: string;
10
10
  details?: JsonLike;
11
11
  };
12
+ /**
13
+ * Options which control one outbound RPC invocation.
14
+ *
15
+ * The numeric timeout overload remains supported by {@link RpcSession.call}
16
+ * and {@link RpcRoom.invoke}; use this object when a caller also needs an
17
+ * abort signal.
18
+ */
19
+ export type RpcCallOptions = {
20
+ readonly timeoutMs?: number;
21
+ readonly signal?: AbortSignal;
22
+ };
23
+ /**
24
+ * Context supplied to handlers registered through the cancellation-aware
25
+ * registration API.
26
+ *
27
+ * Cancellation is cooperative: the handler must observe `signal.aborted` or
28
+ * otherwise pass the signal to an abort-capable operation. The RPC layer still
29
+ * suppresses a response after cancellation when a handler does not stop
30
+ * immediately.
31
+ */
32
+ export type RpcRequestContext = {
33
+ readonly signal: AbortSignal;
34
+ };
12
35
  export type RpcRequestEnvelope = {
13
36
  kind: "request";
14
37
  requestId: string;
@@ -66,10 +89,12 @@ export type RpcRoomOptions = {
66
89
  transport: TransportAdapter;
67
90
  maxWirePayloadBytes?: number;
68
91
  chunkMissingRetryMs?: number;
69
- canAcceptRequest?: (peerId: string, method: string) => boolean | Promise<boolean>;
92
+ canAcceptRequest?: (peerId: string, method: string, context: RpcRequestContext) => boolean | Promise<boolean>;
70
93
  onProtocolWarning?: (message: string, peerId?: string) => void;
71
94
  };
72
95
  export type RpcMethodHandler<T extends JsonLike[], U> = (peerId: string, ...args: T) => U | Promise<U>;
96
+ /** Handler which can stop cooperative work when its request is cancelled. */
97
+ export type RpcCancellationAwareMethodHandler<T extends JsonLike[], U> = (context: RpcRequestContext, peerId: string, ...args: T) => U | Promise<U>;
73
98
  export type RpcRegisterOptions = {
74
99
  serial?: boolean;
75
100
  };
@@ -4,6 +4,9 @@ import { createInstanceLogFunction } from "../services/lib/logUtils.js";
4
4
  import { EVENT_REQUEST_CHECK_REMOTE_SIZE } from "../events/coreEvents.js";
5
5
  import { fireAndForget } from "../common/utils.js";
6
6
  const REMOTE_SIZE_NOTICE_DURATION_MS = 5 * 60 * 1e3;
7
+ function canReadRemoteStatus(replicator) {
8
+ return replicator !== void 0 && "getRemoteStatus" in replicator && typeof replicator.getRemoteStatus === "function";
9
+ }
7
10
  function configureReviewAnchor(anchor, label, review) {
8
11
  anchor.href = "#";
9
12
  anchor.textContent = label;
@@ -84,13 +87,13 @@ function onNotifyRemoteSizeExceedFactory(host, log, mode = "notice") {
84
87
  return true;
85
88
  }
86
89
  const replicator = host.services.replicator.getActiveReplicator();
87
- const remoteStat = await replicator?.getRemoteStatus(host.services.setting.currentSettings());
90
+ const remoteStat = canReadRemoteStatus(replicator) ? await replicator.getRemoteStatus(settings) : false;
88
91
  if (!remoteStat) {
89
92
  log("Failed to get remote status, skipping remote size exceed check.", LOG_LEVEL_INFO);
90
93
  return true;
91
94
  }
92
95
  const estimatedSize = remoteStat.estimatedSize;
93
- if (estimatedSize) {
96
+ if (estimatedSize !== void 0) {
94
97
  const maxSize = settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024;
95
98
  if (estimatedSize <= maxSize) {
96
99
  log(
@@ -10,8 +10,12 @@ import { getConfiguredFunctionsForEncryption } from "../pouchdb/encryption.js";
10
10
  import { AuthorizationHeaderGenerator, generateCredentialObject } from "../replication/httplib.js";
11
11
  import { parseHeaderValues } from "../common/utils.js";
12
12
  import { sizeToHumanReadable } from "octagonal-wheels/number";
13
+ import { REMOTE_RESOURCE_KINDS } from "../replication/index.js";
13
14
  const FAST_FETCH_CHECKPOINT_KEY = "fast-fetch-checkpoint";
14
15
  const FAST_FETCH_RETRY_DELAYS = [2e3, 5e3, 1e4, 2e4];
16
+ function canResetCentralRemote(replicator) {
17
+ return "tryResetRemoteDatabase" in replicator && typeof replicator.tryResetRemoteDatabase === "function";
18
+ }
15
19
  class ServiceRebuilder extends ServiceModuleBase {
16
20
  events;
17
21
  appLifecycle;
@@ -180,6 +184,9 @@ Please enable them from the settings screen after setup is complete.`,
180
184
  this._log("No active replicator found when trying to reset remote database.", LOG_LEVEL_NOTICE);
181
185
  return;
182
186
  }
187
+ if (!canResetCentralRemote(currentReplicator)) {
188
+ throw new Error("The active replicator does not support resetting a central remote database.");
189
+ }
183
190
  await currentReplicator.tryResetRemoteDatabase(settings);
184
191
  }
185
192
  // private async _tryCreateRemoteDatabase(): Promise<void> {
@@ -397,67 +404,70 @@ Are you sure you wish to proceed?`;
397
404
  localDB = this.database.localDatabase.localDatabase;
398
405
  since = "0";
399
406
  }
400
- const replicator = this.replicator.getActiveReplicator() ?? await this.replicator.getNewReplicator();
401
- if (!replicator) {
402
- throw new Error("No active replicator found for fast fetch.");
407
+ const securitySeed = await this.replicator.createRemoteResource(REMOTE_RESOURCE_KINDS.SECURITY_SEED, settings);
408
+ if (!securitySeed) {
409
+ throw new Error("The selected provider cannot supply a Security Seed for Fast Fetch.");
403
410
  }
404
- const salt = () => replicator.getReplicationPBKDF2Salt(settings);
405
- const enc = getConfiguredFunctionsForEncryption(
406
- settings.passphrase,
407
- false,
408
- false,
409
- salt,
410
- settings.E2EEAlgorithm
411
- );
412
- const authHeader = await new AuthorizationHeaderGenerator().getAuthorizationHeader(
413
- generateCredentialObject(settings)
414
- );
415
- const customHeaders = parseHeaderValues(settings.couchDB_CustomHeaders);
416
- for (let attempt = 0; ; attempt++) {
417
- try {
418
- await fetchChangesForInitialSync(
419
- localDB,
420
- remote,
421
- authHeader,
422
- enc.outgoing,
423
- since,
424
- (progress) => {
425
- this._log(
426
- `Fast fetch progress: ${progress.totalValidFetched} / ${progress.docsToFetch}
411
+ try {
412
+ const enc = getConfiguredFunctionsForEncryption(
413
+ settings.passphrase,
414
+ false,
415
+ false,
416
+ () => securitySeed.read(),
417
+ settings.E2EEAlgorithm
418
+ );
419
+ const authHeader = await new AuthorizationHeaderGenerator().getAuthorizationHeader(
420
+ generateCredentialObject(settings)
421
+ );
422
+ const customHeaders = parseHeaderValues(settings.couchDB_CustomHeaders);
423
+ for (let attempt = 0; ; attempt++) {
424
+ try {
425
+ await fetchChangesForInitialSync(
426
+ localDB,
427
+ remote,
428
+ authHeader,
429
+ enc.outgoing,
430
+ since,
431
+ (progress) => {
432
+ this._log(
433
+ `Fast fetch progress: ${progress.totalValidFetched} / ${progress.docsToFetch}
427
434
  Total bytes fetched: ${sizeToHumanReadable(progress.totalBytes)}`,
428
- LOG_LEVEL_NOTICE,
429
- "fetch-init-progress"
430
- );
431
- },
432
- (sequence) => this.saveFastFetchCheckpoint(remote, sequence),
433
- customHeaders
434
- );
435
- break;
436
- } catch (ex) {
437
- if (!isRetryableStreamingFetchFailure(ex) || attempt >= FAST_FETCH_RETRY_DELAYS.length) throw ex;
438
- checkpoint = this.getFastFetchCheckpoint(remote);
439
- since = checkpoint?.sequence ?? since;
440
- this._log(
441
- `Fast fetch interrupted. Retrying from sequence: ${since}`,
442
- LOG_LEVEL_NOTICE,
443
- "fetch-init-resume"
444
- );
445
- await delay(FAST_FETCH_RETRY_DELAYS[attempt]);
435
+ LOG_LEVEL_NOTICE,
436
+ "fetch-init-progress"
437
+ );
438
+ },
439
+ (sequence) => this.saveFastFetchCheckpoint(remote, sequence),
440
+ customHeaders
441
+ );
442
+ break;
443
+ } catch (ex) {
444
+ if (!isRetryableStreamingFetchFailure(ex) || attempt >= FAST_FETCH_RETRY_DELAYS.length) throw ex;
445
+ checkpoint = this.getFastFetchCheckpoint(remote);
446
+ since = checkpoint?.sequence ?? since;
447
+ this._log(
448
+ `Fast fetch interrupted. Retrying from sequence: ${since}`,
449
+ LOG_LEVEL_NOTICE,
450
+ "fetch-init-resume"
451
+ );
452
+ await delay(FAST_FETCH_RETRY_DELAYS[attempt]);
453
+ }
446
454
  }
447
- }
448
- const allDocs = await localDB.allDocs({ include_docs: false });
449
- this._log(
450
- `Fast database fetch completed. Total documents in local database: ${allDocs.total_rows}`,
451
- LOG_LEVEL_NOTICE,
452
- "fetch-init-complete"
453
- );
454
- await this.replication.markResolved();
455
- if (autoResume) {
456
- if (!await this.finishRebuild()) {
457
- throw new Error("Fast Fetch could not be finalised.");
455
+ const allDocs = await localDB.allDocs({ include_docs: false });
456
+ this._log(
457
+ `Fast database fetch completed. Total documents in local database: ${allDocs.total_rows}`,
458
+ LOG_LEVEL_NOTICE,
459
+ "fetch-init-complete"
460
+ );
461
+ await this.replication.markResolved();
462
+ if (autoResume) {
463
+ if (!await this.finishRebuild()) {
464
+ throw new Error("Fast Fetch could not be finalised.");
465
+ }
458
466
  }
467
+ this.clearFastFetchCheckpoint();
468
+ } finally {
469
+ await securitySeed.dispose();
459
470
  }
460
- this.clearFastFetchCheckpoint();
461
471
  }
462
472
  /**
463
473
  * Complete the final rebuild phases and establish application readiness.
@@ -65,6 +65,9 @@ export declare abstract class AppLifecycleService<T extends ServiceContext = Ser
65
65
  readonly onBeforeUnload: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<() => Promise<boolean>>;
66
66
  /**
67
67
  * Event triggered when the plug-in is being unloaded.
68
+ *
69
+ * Resource owners complete terminal retirement here. `ControlService`
70
+ * awaits every handler before it closes the local database.
68
71
  */
69
72
  readonly onUnload: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<() => Promise<boolean>>;
70
73
  /**
@@ -85,6 +88,7 @@ export declare abstract class AppLifecycleService<T extends ServiceContext = Ser
85
88
  abstract scheduleRestart(): void;
86
89
  /**
87
90
  * Event triggered when the application is being suspended (e.g., system sleep).
91
+ * Suspension is reversible and is not equivalent to terminal unload.
88
92
  */
89
93
  readonly onSuspending: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<() => Promise<boolean>>;
90
94
  /**
@@ -62,10 +62,14 @@ class AppLifecycleService extends ServiceBase {
62
62
  onBeforeUnload = handlers().all("onBeforeUnload");
63
63
  /**
64
64
  * Event triggered when the plug-in is being unloaded.
65
+ *
66
+ * Resource owners complete terminal retirement here. `ControlService`
67
+ * awaits every handler before it closes the local database.
65
68
  */
66
69
  onUnload = handlers().all("onUnload");
67
70
  /**
68
71
  * Event triggered when the application is being suspended (e.g., system sleep).
72
+ * Suspension is reversible and is not equivalent to terminal unload.
69
73
  */
70
74
  onSuspending = handlers().bailFirstFailure("onSuspending");
71
75
  /**
@@ -58,10 +58,6 @@ class ControlService extends ServiceBase {
58
58
  this._unloaded = true;
59
59
  const localDatabase = this.services.databaseService.localDatabaseDirect;
60
60
  if (localDatabase != null) {
61
- const activeReplicator = this.services.replicatorService.getActiveReplicator();
62
- if (activeReplicator) {
63
- activeReplicator.closeReplication();
64
- }
65
61
  localDatabase.onunload();
66
62
  await localDatabase.close();
67
63
  }
@@ -8,7 +8,7 @@ class DatabaseEventService extends ServiceBase {
8
8
  /**
9
9
  * Event triggered when the database is about to be closed.
10
10
  */
11
- onCloseDatabase = handlers().all("onCloseDatabase");
11
+ onCloseDatabase = handlers().allSettled("onCloseDatabase");
12
12
  /**
13
13
  * Event triggered when the database is being initialized.
14
14
  */
@@ -2,7 +2,7 @@ import type { FetchHttpHandler } from "@smithy/fetch-http-handler";
2
2
  import { type LOG_LEVEL } from "octagonal-wheels/common/logger";
3
3
  import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, RemotePreferredTweakResult, SettingsMigrationState, TweakValues, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
4
4
  import type { LiveSyncLocalDB } from "../../pouchdb/LiveSyncLocalDB.js";
5
- import type { LiveSyncAbstractReplicator } from "../../replication/LiveSyncAbstractReplicator.js";
5
+ import type { ReplicatorInstance } from "../../replication/ReplicatorInstance.js";
6
6
  import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
7
7
  import type { Confirm } from "../../interfaces/Confirm.js";
8
8
  import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive";
@@ -12,6 +12,10 @@ import type { DatabaseEventService } from "./DatabaseEventService.js";
12
12
  import type { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "../../common/models/shared.const.symbols.js";
13
13
  import type { AsyncActivityOptions } from "../../interfaces/AsyncActivityRunner.js";
14
14
  import type { OwnedCouchDBConnection, RemoteConnectionOpenOptions } from "./RemoteConnection.js";
15
+ import type { ActiveReplicatorContext, ContinuousReplicationRequest, ReplicationAttemptFailure, ReplicationFailureRequest, ReplicatorProviderDefinitionMap, ReplicationOutcome, ReplicationReadinessRequirements, UnattendedOneShotRequest, UserInitiatedOneShotRequest } from "../../replication/ReplicatorProvider.js";
16
+ import type { RemoteResourceKind, RemoteResourceMap } from "../../replication/RemoteResource.js";
17
+ import type { CentralRemoteAdministrationRequest, CentralRemoteAdministrationResult } from "../../replication/CentralRemoteAdministration.js";
18
+ import type { MultipleHandlerFunction } from "../lib/HandlerUtils.js";
15
19
  declare global {
16
20
  interface OPTIONAL_SYNC_FEATURES {
17
21
  DISABLE: "DISABLE";
@@ -99,10 +103,63 @@ export interface IFileProcessingService {
99
103
  onStorageFileEvent(): void;
100
104
  }
101
105
  export interface IReplicatorService {
106
+ /**
107
+ * Fence new work, drain admitted work, and close the active Replicator.
108
+ *
109
+ * Do not await this command from an admitted active-context callback: the
110
+ * retirement command waits for that callback to settle.
111
+ */
102
112
  onCloseActiveReplication(): Promise<boolean>;
103
- onReplicatorInitialised(): Promise<boolean>;
104
- getNewReplicator(settingOverride?: Partial<ObsidianLiveSyncSettings>): Promise<LiveSyncAbstractReplicator | undefined | false>;
105
- getActiveReplicator(): LiveSyncAbstractReplicator | undefined;
113
+ /**
114
+ * Run host preparation after candidate initialisation and before active publication.
115
+ *
116
+ * This hook runs inside the ownership transition. A handler must not call or await
117
+ * `acquireActiveReplicatorContext()` or `runWithActiveReplicatorContext()`, because
118
+ * both wait for that same transition. Work which requires the new active context
119
+ * belongs after publication.
120
+ */
121
+ onBeforeReplicatorPublication(): Promise<boolean>;
122
+ getNewReplicator: ReplicatorFactoryHandler;
123
+ /** Add the fixed provider definitions composed by the current host. */
124
+ registerReplicatorProviderDefinitions(definitions: ReplicatorProviderDefinitionMap): void;
125
+ /** Resolve one provider-owned finite resource without changing active publication. */
126
+ createRemoteResource<TKind extends RemoteResourceKind>(kind: TKind, setting: RemoteDBSettings): Promise<RemoteResourceMap[TKind] | undefined>;
127
+ /** Apply and verify one typed provider-specific central-remote administration action. */
128
+ runCentralRemoteAdministration(request: CentralRemoteAdministrationRequest): Promise<CentralRemoteAdministrationResult>;
129
+ /** Wait for queued ownership transitions, then return the published active context. */
130
+ acquireActiveReplicatorContext(): Promise<ActiveReplicatorContext | undefined>;
131
+ /**
132
+ * Run one task against an exact active publication admitted before later transitions.
133
+ *
134
+ * Invocation is ordered with replacement and disposal. A later
135
+ * transition fences new admission, requests transfer cancellation, waits for
136
+ * admitted tasks, and only then closes the old Replicator. The
137
+ * callback must not initiate or await settings realisation, database
138
+ * replacement, Replicator retirement, or another operation which queues that
139
+ * same lifecycle transition. Stage such recovery after this promise settles.
140
+ * Terminal plug-in unload uses the same retirement drain before the local
141
+ * database closes. Reversible host suspension only requests transfer
142
+ * cancellation and retains the publication, so it is not a retirement fence.
143
+ *
144
+ * The callback is not invoked when no typed publication can be admitted.
145
+ */
146
+ runWithActiveReplicatorContext<TResult>(task: (context: ActiveReplicatorContext) => TResult | PromiseLike<TResult>): Promise<TResult | undefined>;
147
+ /**
148
+ * Return the legacy unreserved view of the active Replicator.
149
+ *
150
+ * Missing active state retains its established diagnostic. New work must
151
+ * acquire or admit an `ActiveReplicatorContext` instead.
152
+ */
153
+ getActiveReplicator(): ReplicatorInstance | undefined;
154
+ /**
155
+ * Return whether a compatibility active Replicator exists without
156
+ * acquiring or returning it.
157
+ *
158
+ * This side-effect-free, non-owning predicate exists only for
159
+ * compatibility-state classification. Work must acquire or admit an
160
+ * `ActiveReplicatorContext` before using a Replicator.
161
+ */
162
+ hasActiveReplicator(): boolean;
106
163
  replicationStatics: ReactiveSource<ReplicationStatics>;
107
164
  /** Number of finite remote operations currently in progress. */
108
165
  boundedRemoteActivityCount: ReactiveSource<number>;
@@ -113,19 +170,59 @@ export interface IReplicatorService {
113
170
  /** Runs finite replication which may place documents in the local database. */
114
171
  runFiniteReplicationActivity<T>(task: () => T | PromiseLike<T>, options?: AsyncActivityOptions): Promise<T>;
115
172
  }
173
+ export type ReplicatorFactoryCallback = (settingOverride?: Partial<ObsidianLiveSyncSettings>) => Promise<ReplicatorInstance | undefined | false>;
174
+ export type ReplicatorFactoryHandler = MultipleHandlerFunction<ReplicatorFactoryCallback>;
116
175
  export interface IReplicationService {
117
176
  processSynchroniseResult(doc: MetaEntry): Promise<boolean>;
118
177
  processOptionalSynchroniseResult(doc: LoadedEntry): Promise<boolean>;
119
178
  processVirtualDocument(docs: PouchDB.Core.ExistingDocument<EntryDoc>): Promise<boolean>;
120
179
  onBeforeReplicate(showMessage: boolean): Promise<boolean>;
121
- checkConnectionFailure(): Promise<boolean | "CHECKAGAIN" | undefined>;
180
+ /** Prepare provider state which exists only for the central remote. */
181
+ onPrepareCentralRemoteReplication(showMessage: boolean): Promise<boolean>;
182
+ checkConnectionFailure(failure: ReplicationAttemptFailure): Promise<boolean | "CHECKAGAIN" | undefined>;
122
183
  /** Lightweight, idempotent policy checks which every replication entry point may invoke. */
123
184
  onCheckReplicationReady(showMessage: boolean): Promise<boolean>;
124
- isReplicationReady(showMessage: boolean): Promise<boolean>;
185
+ /**
186
+ * Evaluate ordered host and provider readiness without acquiring a Replicator.
187
+ *
188
+ * The provider requirements determine whether central-remote preparation
189
+ * applies. A false result means no remote work has been admitted.
190
+ */
191
+ isReplicationReady(showMessage: boolean, readiness?: ReplicationReadinessRequirements): Promise<boolean>;
192
+ /**
193
+ * Run one user-initiated typed capability against an atomically acquired context.
194
+ *
195
+ * Readiness precedes finite activity, and failure recovery receives only
196
+ * the interaction authority carried by the request.
197
+ */
198
+ replicateUserInitiated(request?: UserInitiatedOneShotRequest): Promise<ReplicationOutcome>;
199
+ /**
200
+ * Run one unattended typed capability without granting interaction authority.
201
+ *
202
+ * Readiness precedes finite activity, and failed outcomes reach unattended
203
+ * recovery only after activity accounting has settled.
204
+ */
205
+ replicateUnattended(request: UnattendedOneShotRequest): Promise<ReplicationOutcome>;
206
+ /** Serialise and rate-limit an unattended event-triggered finite capability. */
207
+ replicateUnattendedByEvent(request: UnattendedOneShotRequest): Promise<ReplicationOutcome>;
208
+ /**
209
+ * Start a supported continuous capability after its provider-specific readiness.
210
+ *
211
+ * Applicability is checked before readiness. Continuous work is not wrapped
212
+ * as finite activity and does not use the finite-operation recovery handler.
213
+ */
214
+ startContinuous(request: ContinuousReplicationRequest): Promise<ReplicationOutcome>;
215
+ /**
216
+ * Stop finite transfer work on the current typed active Replicator.
217
+ *
218
+ * This operator action bypasses readiness, finite activity accounting, and
219
+ * failure-recovery handlers.
220
+ */
221
+ stopActiveTransfer(): Promise<ReplicationOutcome>;
125
222
  performReplication(showMessage?: boolean): Promise<boolean | void>;
126
223
  replicate(showMessage?: boolean): Promise<boolean | void>;
127
224
  replicateByEvent(showMessage?: boolean): Promise<boolean | void>;
128
- onReplicationFailed(showMessage?: boolean): Promise<boolean>;
225
+ onReplicationFailed(request: ReplicationFailureRequest): Promise<boolean>;
129
226
  parseSynchroniseResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<boolean>;
130
227
  databaseQueueCount: ReactiveSource<number>;
131
228
  storageApplyingCount: ReactiveSource<number>;
@@ -238,7 +335,7 @@ export interface ISettingService {
238
335
  export interface ITweakValueService {
239
336
  fetchRemotePreferred(trialSetting: RemoteDBSettings): Promise<RemotePreferredTweakResult>;
240
337
  checkAndAskResolvingMismatched(preferred: Partial<TweakValues>): Promise<[TweakValues | boolean, boolean]>;
241
- askResolvingMismatched(preferredSource: TweakValues): Promise<"OK" | "CHECKAGAIN" | "IGNORE">;
338
+ askResolvingMismatched(preferredSource: TweakValues, updatePreferredRemote?: (setting: ObsidianLiveSyncSettings) => Promise<boolean>): Promise<"OK" | "CHECKAGAIN" | "IGNORE">;
242
339
  checkAndAskUseRemoteConfiguration(settings: RemoteDBSettings): Promise<{
243
340
  result: false | TweakValues;
244
341
  requireFetch: boolean;
@@ -0,0 +1,40 @@
1
+ /** One ordered condition in a readiness evaluation. */
2
+ export interface ReadinessCondition<TContext> {
3
+ /** Stable diagnostic name for the condition; it is not user-facing text. */
4
+ readonly name: string;
5
+ /**
6
+ * Return whether this condition applies to the current evaluation.
7
+ *
8
+ * Applicability checks must be side-effect-free. Conditions without this
9
+ * callback always apply.
10
+ */
11
+ readonly appliesTo?: (context: TContext) => boolean;
12
+ /** Return whether the condition admits the requested operation. */
13
+ readonly evaluate: (context: TContext) => boolean | Promise<boolean>;
14
+ /** Perform condition-specific reporting after a false result. */
15
+ readonly onRejected?: (context: TContext) => void | Promise<void>;
16
+ }
17
+ /** A purpose and its ordered readiness conditions. */
18
+ export interface ReadinessEvaluationRequest<TContext> {
19
+ /** Stable diagnostic purpose; it is not user-facing text. */
20
+ readonly purpose: string;
21
+ readonly context: TContext;
22
+ readonly conditions: readonly ReadinessCondition<TContext>[];
23
+ }
24
+ /** Structured result from an ordered readiness evaluation. */
25
+ export type ReadinessEvaluationResult = {
26
+ readonly ready: true;
27
+ readonly purpose: string;
28
+ } | {
29
+ readonly ready: false;
30
+ readonly purpose: string;
31
+ readonly rejectedCondition: string;
32
+ };
33
+ /**
34
+ * Evaluate applicable conditions in declaration order until one rejects.
35
+ *
36
+ * Rejection reporting completes before the result is returned. Exceptions
37
+ * from applicability, evaluation, or reporting callbacks propagate unchanged;
38
+ * the evaluator does not reinterpret operational failures as policy results.
39
+ */
40
+ export declare function evaluateReadiness<TContext>(request: ReadinessEvaluationRequest<TContext>): Promise<ReadinessEvaluationResult>;
@@ -0,0 +1,16 @@
1
+ async function evaluateReadiness(request) {
2
+ for (const condition of request.conditions) {
3
+ if (condition.appliesTo && !condition.appliesTo(request.context)) continue;
4
+ if (await condition.evaluate(request.context)) continue;
5
+ await condition.onRejected?.(request.context);
6
+ return {
7
+ ready: false,
8
+ purpose: request.purpose,
9
+ rejectedCondition: condition.name
10
+ };
11
+ }
12
+ return { ready: true, purpose: request.purpose };
13
+ }
14
+ export {
15
+ evaluateReadiness
16
+ };
@@ -1,8 +1,11 @@
1
+ import type { E2EEAlgorithm } from "../../common/types.js";
1
2
  /**
2
3
  * Options which bind requests made through a remote CouchDB connection to the
3
4
  * operation which owns it.
4
5
  */
5
6
  export interface RemoteConnectionOpenOptions {
7
+ /** Encryption algorithm captured with the settings which own this connection. */
8
+ readonly encryptionAlgorithm?: E2EEAlgorithm;
6
9
  /**
7
10
  * Cancels abort-capable requests made through the connection when the
8
11
  * owning operation is cancelled.
@@ -275,7 +275,7 @@ class RemoteService extends ServiceBase {
275
275
  }
276
276
  }
277
277
  };
278
- const setting = this._settingService.currentSettings();
278
+ const encryptionAlgorithm = connectionOptions.encryptionAlgorithm ?? this._settingService.currentSettings().E2EEAlgorithm;
279
279
  let db;
280
280
  try {
281
281
  db = new this._pouchDB(uri, conf);
@@ -297,7 +297,7 @@ class RemoteService extends ServiceBase {
297
297
  replicationFilter(db, compression);
298
298
  disableEncryption();
299
299
  if (passphrase !== "false" && typeof passphrase === "string") {
300
- enableEncryption(db, passphrase, useDynamicIterationCount, false, getPBKDF2Salt, setting.E2EEAlgorithm);
300
+ enableEncryption(db, passphrase, useDynamicIterationCount, false, getPBKDF2Salt, encryptionAlgorithm);
301
301
  }
302
302
  } catch (ex) {
303
303
  await closeAfterConnectionFailure();
@@ -2,7 +2,7 @@ import { type LOG_LEVEL } from "../../common/types.js";
2
2
  import type { IAPIService, IDatabaseService, IFileProcessingService, IReplicationService, IReplicatorService, ISettingService } from "./IService.js";
3
3
  import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
4
4
  import { type LogFunction } from "../lib/logUtils.js";
5
- import type { LiveSyncAbstractReplicator } from "../../replication/LiveSyncAbstractReplicator.js";
5
+ import { type ContinuousReplicationRequest, type ReplicationAttemptFailure, type ReplicationOutcome, type ReplicationReadinessRequirements, type UnattendedOneShotRequest, type UserInitiatedOneShotRequest } from "../../replication/ReplicatorProvider.js";
6
6
  import type { AppLifecycleService } from "./AppLifecycleService.js";
7
7
  export interface ReplicationServiceDependencies {
8
8
  APIService: IAPIService;
@@ -13,10 +13,17 @@ export interface ReplicationServiceDependencies {
13
13
  fileProcessingService: IFileProcessingService;
14
14
  }
15
15
  /**
16
- * The ReplicationService provides methods for managing replication processes.
16
+ * Host-facing replication façade for handlers, legacy entry points, and counters.
17
+ *
18
+ * Ordered readiness evaluation and typed capability dispatch are delegated to
19
+ * focused collaborators. Handler objects remain on this service so existing
20
+ * hosts retain their registration identity, priority ordering, and failure
21
+ * semantics. Active Replicator ownership remains with `ReplicatorService`.
17
22
  */
18
23
  export declare abstract class ReplicationService<T extends ServiceContext = ServiceContext> extends ServiceBase<T> implements IReplicationService {
19
24
  private _unresolvedErrorManager;
25
+ private readonly _evaluateReadiness;
26
+ private readonly _typedReplication;
20
27
  showError(msg: string, max_log_level?: LOG_LEVEL): void;
21
28
  clearErrors(): void;
22
29
  _log: LogFunction;
@@ -48,6 +55,8 @@ export declare abstract class ReplicationService<T extends ServiceContext = Serv
48
55
  * An event triggered before starting replication.
49
56
  */
50
57
  readonly onBeforeReplicate: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(showMessage: boolean) => Promise<boolean>>;
58
+ /** Provider preparation which applies only to the central remote. */
59
+ readonly onPrepareCentralRemoteReplication: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(showMessage: boolean) => Promise<boolean>>;
51
60
  /**
52
61
  * Lightweight, repeatable policy checks shared by every replication entry point.
53
62
  * Handlers must remain idempotent because a high-level replication may cross
@@ -55,11 +64,30 @@ export declare abstract class ReplicationService<T extends ServiceContext = Serv
55
64
  */
56
65
  readonly onCheckReplicationReady: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(showMessage: boolean) => Promise<boolean>>;
57
66
  /**
58
- * Check if the replication is ready to start.
59
- * @param showMessage Whether to show messages to the user.
67
+ * Evaluate the ordered readiness conditions for an operation.
68
+ *
69
+ * This method does not acquire a Replicator or begin activity accounting.
70
+ * Provider requirements select whether central-remote preparation applies.
71
+ *
72
+ * @param showMessage Whether condition-specific diagnostics may be prominent.
73
+ * @param readiness Provider-owned preparation requirements.
60
74
  */
61
- isReplicationReady(showMessage?: boolean): Promise<boolean>;
62
- onReplicationFailed: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(showMessage?: boolean) => Promise<boolean>>;
75
+ isReplicationReady(showMessage?: boolean, readiness?: ReplicationReadinessRequirements): Promise<boolean>;
76
+ onReplicationFailed: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(request: import("../../replication/ReplicatorProvider.js").ReplicationFailureRequest) => Promise<boolean>>;
77
+ replicateUserInitiated(request?: UserInitiatedOneShotRequest): Promise<ReplicationOutcome>;
78
+ replicateUnattended(request: UnattendedOneShotRequest): Promise<ReplicationOutcome>;
79
+ replicateUnattendedByEvent(request: UnattendedOneShotRequest): Promise<ReplicationOutcome>;
80
+ startContinuous(request: ContinuousReplicationRequest): Promise<ReplicationOutcome>;
81
+ /**
82
+ * Stop finite transfer work on the active provider without entering the
83
+ * replication readiness or activity accounting paths.
84
+ *
85
+ * The provider and replicator are obtained together so that a concurrent
86
+ * replacement cannot pair a capability from one provider with a
87
+ * replicator from another. Stopping is an operator action, not a failed
88
+ * replication, so it does not invoke failure-recovery handlers.
89
+ */
90
+ stopActiveTransfer(): Promise<ReplicationOutcome>;
63
91
  private performReplicationRequest;
64
92
  /**
65
93
  * Perform replication and handle a failed result.
@@ -80,13 +108,19 @@ export declare abstract class ReplicationService<T extends ServiceContext = Serv
80
108
  /**
81
109
  * Check if there is a connection failure with the remote database.
82
110
  */
83
- readonly checkConnectionFailure: import("../lib/HandlerUtils.js").MultipleHandlerFunction<() => Promise<boolean | "CHECKAGAIN" | undefined>, unknown>;
111
+ readonly checkConnectionFailure: import("../lib/HandlerUtils.js").MultipleHandlerFunction<(failure: ReplicationAttemptFailure) => Promise<boolean | "CHECKAGAIN" | undefined>, unknown>;
84
112
  databaseQueueCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource<number>;
85
113
  storageApplyingCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource<number>;
86
114
  replicationResultCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource<number>;
87
- getActiveReplicatorFor(usage: string): false | LiveSyncAbstractReplicator;
88
- private performReplicateAllToRemote;
89
- private performReplicateAllFromRemote;
115
+ /**
116
+ * Dispatch one directional full transfer through one admitted publication.
117
+ *
118
+ * Each attempt owns a separate reservation. Compatibility recovery runs
119
+ * between those reservations, and a retry is admitted only when the exact
120
+ * first-attempt context remains active. Both attempts use one detached
121
+ * settings snapshot.
122
+ */
123
+ private performDirectionalReplication;
90
124
  replicateAllToRemote(showingNotice?: boolean): Promise<boolean>;
91
125
  replicateAllFromRemote(showingNotice?: boolean): Promise<boolean>;
92
126
  /**
@@ -103,6 +137,7 @@ export declare abstract class ReplicationService<T extends ServiceContext = Serv
103
137
  * it is not a general application-readiness bypass.
104
138
  */
105
139
  replicateAllFromRemoteForRebuild(showingNotice?: boolean): Promise<boolean>;
140
+ private getActiveReplicatorFor;
106
141
  private _getReplicatorAndPerform;
107
142
  markLocked(lockByClean?: boolean): Promise<void>;
108
143
  markUnlocked(): Promise<void>;