@vrtmrz/livesync-commonlib 0.1.19 → 0.1.21

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 +262 -0
  25. package/dist/replication/ReplicatorProvider.js +150 -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 +72 -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 +73 -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 +250 -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 +93 -0
  90. package/dist/services/base/ReplicationService.typedReplication.js +214 -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
@@ -6,12 +6,92 @@ import {
6
6
  import { handlers } from "../lib/HandlerUtils.js";
7
7
  import { ServiceBase } from "./ServiceBase.js";
8
8
  import { reactiveSource } from "octagonal-wheels/dataobject/reactive";
9
- import { createInstanceLogFunction, MARK_LOG_NETWORK_ERROR } from "../lib/logUtils.js";
9
+ import { createInstanceLogFunction } from "../lib/logUtils.js";
10
+ import {
11
+ CAPABILITY_SUPPORT_KINDS,
12
+ NO_INTERACTION,
13
+ CENTRAL_REMOTE_REPLICATION_READINESS,
14
+ REPLICATION_PROGRESS_PRESENTATIONS,
15
+ USER_INITIATED_REPLICATION_AUTHORITY,
16
+ isActiveReplicatorContextBoundToSetting,
17
+ isReplicationCompleted,
18
+ replicationBlocked,
19
+ replicationFailed,
20
+ outcomeFromFiniteOpenReplication
21
+ } from "../../replication/ReplicatorProvider.js";
10
22
  import { UnresolvedErrorManager } from "./UnresolvedErrorManager.js";
11
23
  import { isLockAcquired, shareRunningResult } from "octagonal-wheels/concurrency/lock";
24
+ import {
25
+ createReplicationReadinessEvaluator
26
+ } from "./ReplicationService.readiness.js";
27
+ import { TypedReplicationCoordinator } from "./ReplicationService.typedReplication.js";
28
+ import { asCopy } from "../../common/utils.object.js";
12
29
  const REPLICATION_ON_EVENT_FORECASTED_TIME = 5e3;
30
+ const DIRECTIONAL_REPLICATION = Object.freeze({
31
+ UPLOAD: "upload",
32
+ DOWNLOAD: "download"
33
+ });
34
+ const DIRECTIONAL_REPLICATION_ADMISSION = Object.freeze({
35
+ ORDINARY: "ordinary",
36
+ EXPLICIT_REBUILD: "explicit-rebuild"
37
+ });
38
+ function canUploadAll(replicator) {
39
+ return "replicateAllToServer" in replicator && typeof replicator.replicateAllToServer === "function";
40
+ }
41
+ function canUploadAllWithOutcome(replicator) {
42
+ return "replicateAllToServerWithOutcome" in replicator && typeof replicator.replicateAllToServerWithOutcome === "function";
43
+ }
44
+ function canDownloadAll(replicator) {
45
+ return "replicateAllFromServer" in replicator && typeof replicator.replicateAllFromServer === "function";
46
+ }
47
+ function canDownloadAllWithOutcome(replicator) {
48
+ return "replicateAllFromServerWithOutcome" in replicator && typeof replicator.replicateAllFromServerWithOutcome === "function";
49
+ }
50
+ function canAdministerLegacyCentralRemote(replicator) {
51
+ return "markRemoteLocked" in replicator && typeof replicator.markRemoteLocked === "function" && "markRemoteResolved" in replicator && typeof replicator.markRemoteResolved === "function";
52
+ }
53
+ async function stopActiveTransferForDirectionalReplication(context) {
54
+ const capability = context.provider.stopActiveTransfer;
55
+ if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
56
+ return replicationBlocked(capability.reason);
57
+ }
58
+ try {
59
+ const outcome = await capability.run(context.replicator);
60
+ return outcome.status === "failed" ? replicationBlocked("not-ready") : outcome;
61
+ } catch {
62
+ return replicationBlocked("not-ready");
63
+ }
64
+ }
65
+ async function runDirectionalReplication(context, setting, direction, showingNotice) {
66
+ if (!isActiveReplicatorContextBoundToSetting(context, setting)) {
67
+ return replicationBlocked("not-ready");
68
+ }
69
+ const stopOutcome = await stopActiveTransferForDirectionalReplication(context);
70
+ if (!isReplicationCompleted(stopOutcome)) return stopOutcome;
71
+ try {
72
+ let result;
73
+ if (direction === DIRECTIONAL_REPLICATION.UPLOAD) {
74
+ if (canUploadAllWithOutcome(context.replicator)) {
75
+ return await context.replicator.replicateAllToServerWithOutcome(setting, showingNotice);
76
+ }
77
+ if (!canUploadAll(context.replicator)) return replicationBlocked("capability-not-applicable");
78
+ result = await context.replicator.replicateAllToServer(setting, showingNotice);
79
+ } else {
80
+ if (canDownloadAllWithOutcome(context.replicator)) {
81
+ return await context.replicator.replicateAllFromServerWithOutcome(setting, showingNotice);
82
+ }
83
+ if (!canDownloadAll(context.replicator)) return replicationBlocked("capability-not-applicable");
84
+ result = await context.replicator.replicateAllFromServer(setting, showingNotice);
85
+ }
86
+ return outcomeFromFiniteOpenReplication(result);
87
+ } catch (error) {
88
+ return replicationFailed(error);
89
+ }
90
+ }
13
91
  class ReplicationService extends ServiceBase {
14
92
  _unresolvedErrorManager;
93
+ _evaluateReadiness;
94
+ _typedReplication;
15
95
  showError(msg, max_log_level = LOG_LEVEL_NOTICE) {
16
96
  this._unresolvedErrorManager.showError(msg, max_log_level);
17
97
  }
@@ -38,6 +118,36 @@ class ReplicationService extends ServiceBase {
38
118
  dependencies.appLifecycleService,
39
119
  this.context.events
40
120
  );
121
+ this._evaluateReadiness = createReplicationReadinessEvaluator({
122
+ gates: {
123
+ isApplicationReady: () => this.appLifecycleService.isReady(),
124
+ runPolicyChecks: (showMessage) => this.onCheckReplicationReady(showMessage),
125
+ currentSettings: () => this.settingService.currentSettings(),
126
+ isCleanupRunning: () => isLockAcquired("cleanup"),
127
+ isOnline: () => this.APIService.isOnline
128
+ },
129
+ preparation: {
130
+ commitPendingFileEvents: () => this.fileProcessing.commitPendingFileEvents(),
131
+ prepareCentralRemote: (showMessage) => this.onPrepareCentralRemoteReplication(showMessage),
132
+ runBeforeReplicate: (showMessage) => this.onBeforeReplicate(showMessage)
133
+ },
134
+ diagnostics: {
135
+ getUnresolvedMessages: () => this.appLifecycleService.getUnresolvedMessages(),
136
+ translate: (key) => this.context.translate(key),
137
+ log: this._log,
138
+ showError: (message, maxLogLevel) => this.showError(message, maxLogLevel),
139
+ clearErrors: () => this.clearErrors()
140
+ }
141
+ });
142
+ this._typedReplication = new TypedReplicationCoordinator({
143
+ replicatorService: this.replicatorService,
144
+ currentSettings: () => this.settingService.currentSettings(),
145
+ checkReadiness: (showMessage, readiness) => this.isReplicationReady(showMessage, readiness),
146
+ handleFailure: (request) => this.onReplicationFailed(request),
147
+ recordFiniteAttempt: () => {
148
+ this.previousReplicated = Date.now();
149
+ }
150
+ });
41
151
  }
42
152
  /**
43
153
  * Process a synchronisation result document.
@@ -62,6 +172,10 @@ class ReplicationService extends ServiceBase {
62
172
  * An event triggered before starting replication.
63
173
  */
64
174
  onBeforeReplicate = handlers().bailFirstFailure("onBeforeReplicate");
175
+ /** Provider preparation which applies only to the central remote. */
176
+ onPrepareCentralRemoteReplication = handlers().bailFirstFailure(
177
+ "onPrepareCentralRemoteReplication"
178
+ );
65
179
  /**
66
180
  * Lightweight, repeatable policy checks shared by every replication entry point.
67
181
  * Handlers must remain idempotent because a high-level replication may cross
@@ -69,47 +183,62 @@ class ReplicationService extends ServiceBase {
69
183
  */
70
184
  onCheckReplicationReady = handlers().bailFirstFailure("onCheckReplicationReady");
71
185
  /**
72
- * Check if the replication is ready to start.
73
- * @param showMessage Whether to show messages to the user.
186
+ * Evaluate the ordered readiness conditions for an operation.
187
+ *
188
+ * This method does not acquire a Replicator or begin activity accounting.
189
+ * Provider requirements select whether central-remote preparation applies.
190
+ *
191
+ * @param showMessage Whether condition-specific diagnostics may be prominent.
192
+ * @param readiness Provider-owned preparation requirements.
74
193
  */
75
- async isReplicationReady(showMessage = false) {
76
- if (!this.appLifecycleService.isReady()) {
77
- this._log(`Not ready`);
78
- return false;
79
- }
80
- if (!await this.onCheckReplicationReady(showMessage)) {
81
- return false;
82
- }
83
- const currentSettings = this.settingService.currentSettings();
84
- if (isLockAcquired("cleanup")) {
85
- this._log(this.context.translate("Replicator.Message.Cleaned"), LOG_LEVEL_NOTICE);
86
- return false;
87
- }
88
- if (currentSettings.versionUpFlash != "") {
89
- this._log(this.context.translate("Replicator.Message.VersionUpFlash"), LOG_LEVEL_NOTICE);
90
- return false;
91
- }
92
- if (!await this.fileProcessing.commitPendingFileEvents()) {
93
- this.showError(this.context.translate("Replicator.Message.Pending"), LOG_LEVEL_NOTICE);
94
- return false;
95
- }
96
- if (!this.APIService.isOnline) {
97
- this.showError("Network is offline", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
98
- return false;
99
- }
100
- if (!await this.onBeforeReplicate(showMessage)) {
101
- const hasNetworkError = (await this.appLifecycleService.getUnresolvedMessages()).flat().some((e) => typeof e == "string" && e.indexOf(MARK_LOG_NETWORK_ERROR) !== -1);
102
- if (!hasNetworkError) {
103
- this.showError(this.context.translate("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
104
- } else {
105
- this._log(this.context.translate("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_INFO);
106
- }
107
- return false;
108
- }
109
- this.clearErrors();
110
- return true;
194
+ async isReplicationReady(showMessage = false, readiness = CENTRAL_REMOTE_REPLICATION_READINESS) {
195
+ return (await this._evaluateReadiness({ showMessage, requirements: readiness })).ready;
111
196
  }
112
197
  onReplicationFailed = handlers().bailFirstFailure("onReplicationFailed");
198
+ async replicateUserInitiated(request = {
199
+ trigger: "manual",
200
+ progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
201
+ interaction: USER_INITIATED_REPLICATION_AUTHORITY
202
+ }) {
203
+ return await this._typedReplication.runUserInitiated(request);
204
+ }
205
+ async replicateUnattended(request) {
206
+ return await this._typedReplication.runUnattended(request);
207
+ }
208
+ replicateUnattendedByEvent(request) {
209
+ return shareRunningResult(`replication`, async () => {
210
+ const currentSettings = this.settingService.currentSettings();
211
+ const least = currentSettings.syncMinimumInterval;
212
+ if (least > 0) {
213
+ const now = Date.now();
214
+ const elapsed = now - this.previousReplicated;
215
+ if (elapsed < least) {
216
+ this._log(
217
+ `Replication triggered by event is rate limited. Elapsed: ${elapsed}ms, Least interval: ${least}ms`,
218
+ LOG_LEVEL_VERBOSE
219
+ );
220
+ return replicationBlocked("rate-limited");
221
+ }
222
+ this.previousReplicated = now + REPLICATION_ON_EVENT_FORECASTED_TIME;
223
+ }
224
+ return await this.replicateUnattended(request);
225
+ });
226
+ }
227
+ async startContinuous(request) {
228
+ return await this._typedReplication.startContinuous(request);
229
+ }
230
+ /**
231
+ * Stop finite transfer work on the active provider without entering the
232
+ * replication readiness or activity accounting paths.
233
+ *
234
+ * The provider and replicator are obtained together so that a concurrent
235
+ * replacement cannot pair a capability from one provider with a
236
+ * replicator from another. Stopping is an operator action, not a failed
237
+ * replication, so it does not invoke failure-recovery handlers.
238
+ */
239
+ async stopActiveTransfer() {
240
+ return await this._typedReplication.stopActiveTransfer();
241
+ }
113
242
  async performReplicationRequest(showMessage) {
114
243
  const activeReplicator = this.replicatorService.getActiveReplicator();
115
244
  if (!activeReplicator) {
@@ -125,7 +254,9 @@ class ReplicationService extends ServiceBase {
125
254
  */
126
255
  async performReplication(showMessage) {
127
256
  const result = await this.performReplicationRequest(showMessage);
128
- if (!result) return await this.onReplicationFailed(showMessage);
257
+ if (!result) {
258
+ return false;
259
+ }
129
260
  return result;
130
261
  }
131
262
  /**
@@ -133,6 +264,13 @@ class ReplicationService extends ServiceBase {
133
264
  * @param showMessage Whether to show messages to the user.
134
265
  */
135
266
  async replicate(showMessage) {
267
+ if (await this.replicatorService.acquireActiveReplicatorContext()) {
268
+ const outcome = showMessage ? await this.replicateUserInitiated() : await this.replicateUnattended({
269
+ trigger: "database-event",
270
+ interaction: NO_INTERACTION
271
+ });
272
+ return outcome.status === "completed";
273
+ }
136
274
  try {
137
275
  const checkBeforeReplicate = await this.isReplicationReady(showMessage);
138
276
  if (!checkBeforeReplicate) return false;
@@ -140,7 +278,9 @@ class ReplicationService extends ServiceBase {
140
278
  () => this.performReplicationRequest(showMessage),
141
279
  { label: "replication" }
142
280
  );
143
- if (!result) return await this.onReplicationFailed(showMessage);
281
+ if (!result) {
282
+ return false;
283
+ }
144
284
  return result;
145
285
  } finally {
146
286
  this.previousReplicated = Date.now();
@@ -151,7 +291,13 @@ class ReplicationService extends ServiceBase {
151
291
  * Start the replication process triggered by an event (e.g., file change).
152
292
  * @param showMessage Whether to show messages to the user.
153
293
  */
154
- replicateByEvent(showMessage) {
294
+ async replicateByEvent(showMessage) {
295
+ if (await this.replicatorService.acquireActiveReplicatorContext()) {
296
+ return await this.replicateUnattendedByEvent({
297
+ trigger: "database-event",
298
+ interaction: NO_INTERACTION
299
+ }).then((outcome) => outcome.status === "completed");
300
+ }
155
301
  return shareRunningResult(`replication`, async () => {
156
302
  const currentSettings = this.settingService.currentSettings();
157
303
  const least = currentSettings.syncMinimumInterval;
@@ -178,51 +324,53 @@ class ReplicationService extends ServiceBase {
178
324
  databaseQueueCount = reactiveSource(0);
179
325
  storageApplyingCount = reactiveSource(0);
180
326
  replicationResultCount = reactiveSource(0);
181
- getActiveReplicatorFor(usage) {
182
- const activeReplicator = this.replicatorService.getActiveReplicator();
183
- if (!activeReplicator) {
184
- this._log(`Active replicator not found during ${usage}`, LOG_LEVEL_NOTICE);
185
- return false;
186
- }
187
- return activeReplicator;
188
- }
189
- async performReplicateAllToRemote(showingNotice) {
190
- if (!await this.onBeforeReplicate(showingNotice)) {
327
+ /**
328
+ * Dispatch one directional full transfer through one admitted publication.
329
+ *
330
+ * Each attempt owns a separate reservation. Compatibility recovery runs
331
+ * between those reservations, and a retry is admitted only when the exact
332
+ * first-attempt context remains active. Both attempts use one detached
333
+ * settings snapshot.
334
+ */
335
+ async performDirectionalReplication(direction, showingNotice, admission = DIRECTIONAL_REPLICATION_ADMISSION.ORDINARY) {
336
+ if (direction === DIRECTIONAL_REPLICATION.UPLOAD && !await this.onBeforeReplicate(showingNotice)) {
191
337
  this._log(this.context.translate("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
192
338
  return false;
193
339
  }
194
- const currentSettings = this.settingService.currentSettings();
195
- const activeReplicator = this.getActiveReplicatorFor("sending data to remote");
196
- if (!activeReplicator) {
197
- return false;
198
- }
199
- const ret = await activeReplicator.replicateAllToServer(currentSettings, showingNotice);
200
- if (ret) return true;
201
- const checkResult = await this.checkConnectionFailure();
202
- if (checkResult == "CHECKAGAIN")
203
- return await activeReplicator.replicateAllToServer(currentSettings, showingNotice);
204
- return !checkResult;
205
- }
206
- async performReplicateAllFromRemote(showingNotice) {
207
- const activeReplicator = this.getActiveReplicatorFor("fetching data from remote");
208
- if (!activeReplicator) {
209
- return false;
340
+ const detachedSetting = asCopy(this.settingService.currentSettings());
341
+ if (admission === DIRECTIONAL_REPLICATION_ADMISSION.EXPLICIT_REBUILD) {
342
+ detachedSetting.versionUpFlash = "";
210
343
  }
211
- const currentSettings = this.settingService.currentSettings();
212
- const ret = await activeReplicator.replicateAllFromServer(currentSettings, showingNotice);
213
- if (ret) return true;
214
- const checkResult = await this.checkConnectionFailure();
215
- if (checkResult == "CHECKAGAIN")
216
- return await activeReplicator.replicateAllFromServer(currentSettings, showingNotice);
217
- return !checkResult;
344
+ const setting = Object.freeze(detachedSetting);
345
+ let expectedContext;
346
+ const run = async () => {
347
+ const admitted = await this.replicatorService.runWithActiveReplicatorContext((context) => {
348
+ if (expectedContext && context !== expectedContext) {
349
+ return replicationBlocked("not-ready");
350
+ }
351
+ expectedContext ??= context;
352
+ return runDirectionalReplication(context, setting, direction, showingNotice);
353
+ });
354
+ if (admitted) return admitted;
355
+ this._log(`Active replicator not found during directional ${direction}`, LOG_LEVEL_NOTICE);
356
+ return replicationBlocked("no-active-replicator");
357
+ };
358
+ const outcome = await run();
359
+ if (isReplicationCompleted(outcome)) return true;
360
+ if (outcome.status !== "failed") return false;
361
+ const failedContext = expectedContext;
362
+ if (!failedContext) return false;
363
+ const failure = Object.freeze({ context: failedContext, setting, outcome });
364
+ const checkResult = await this.checkConnectionFailure(failure);
365
+ return checkResult === "CHECKAGAIN" && isReplicationCompleted(await run());
218
366
  }
219
367
  async replicateAllToRemote(showingNotice = false) {
220
368
  if (!this.appLifecycleService.isReady()) return false;
221
- return await this.performReplicateAllToRemote(showingNotice);
369
+ return await this.performDirectionalReplication(DIRECTIONAL_REPLICATION.UPLOAD, showingNotice);
222
370
  }
223
371
  async replicateAllFromRemote(showingNotice = false) {
224
372
  if (!this.appLifecycleService.isReady()) return false;
225
- return await this.performReplicateAllFromRemote(showingNotice);
373
+ return await this.performDirectionalReplication(DIRECTIONAL_REPLICATION.DOWNLOAD, showingNotice);
226
374
  }
227
375
  /**
228
376
  * Perform a full upload owned by an active rebuild while the physical database is ready.
@@ -235,7 +383,11 @@ class ReplicationService extends ServiceBase {
235
383
  this._log("The selected local database is not ready for the rebuild upload.", LOG_LEVEL_NOTICE);
236
384
  return false;
237
385
  }
238
- return await this.performReplicateAllToRemote(showingNotice);
386
+ return await this.performDirectionalReplication(
387
+ DIRECTIONAL_REPLICATION.UPLOAD,
388
+ showingNotice,
389
+ DIRECTIONAL_REPLICATION_ADMISSION.EXPLICIT_REBUILD
390
+ );
239
391
  }
240
392
  /**
241
393
  * Perform a full download owned by an active rebuild while the physical database is ready.
@@ -248,38 +400,45 @@ class ReplicationService extends ServiceBase {
248
400
  this._log("The selected local database is not ready for the rebuild download.", LOG_LEVEL_NOTICE);
249
401
  return false;
250
402
  }
251
- return await this.performReplicateAllFromRemote(showingNotice);
403
+ return await this.performDirectionalReplication(
404
+ DIRECTIONAL_REPLICATION.DOWNLOAD,
405
+ showingNotice,
406
+ DIRECTIONAL_REPLICATION_ADMISSION.EXPLICIT_REBUILD
407
+ );
408
+ }
409
+ getActiveReplicatorFor(usage) {
410
+ const activeReplicator = this.replicatorService.getActiveReplicator();
411
+ if (!activeReplicator) {
412
+ this._log(`Active replicator not found during ${usage}`, LOG_LEVEL_NOTICE);
413
+ return false;
414
+ }
415
+ return activeReplicator;
252
416
  }
253
417
  _getReplicatorAndPerform(action, perform) {
254
418
  const activeReplicator = this.getActiveReplicatorFor(action);
255
- if (!activeReplicator) {
419
+ if (!activeReplicator) return Promise.resolve();
420
+ if (!canAdministerLegacyCentralRemote(activeReplicator)) {
421
+ this._log(`Active replicator does not support ${action}`, LOG_LEVEL_NOTICE);
256
422
  return Promise.resolve();
257
423
  }
258
- const currentSettings = this.settingService.currentSettings();
259
- return perform(currentSettings, activeReplicator);
424
+ return perform(this.settingService.currentSettings(), activeReplicator);
260
425
  }
261
426
  async markLocked(lockByClean = false) {
262
427
  return await this._getReplicatorAndPerform(
263
428
  "marking remote locked",
264
- async (currentSettings, activeReplicator) => {
265
- return await activeReplicator.markRemoteLocked(currentSettings, true, lockByClean);
266
- }
429
+ async (currentSettings, activeReplicator) => await activeReplicator.markRemoteLocked(currentSettings, true, lockByClean)
267
430
  );
268
431
  }
269
432
  async markUnlocked() {
270
433
  return await this._getReplicatorAndPerform(
271
434
  "marking remote unlocked",
272
- async (currentSettings, activeReplicator) => {
273
- return await activeReplicator.markRemoteLocked(currentSettings, false, false);
274
- }
435
+ async (currentSettings, activeReplicator) => await activeReplicator.markRemoteLocked(currentSettings, false, false)
275
436
  );
276
437
  }
277
438
  async markResolved() {
278
439
  return await this._getReplicatorAndPerform(
279
440
  "marking remote resolved",
280
- async (currentSettings, activeReplicator) => {
281
- return await activeReplicator.markRemoteResolved(currentSettings);
282
- }
441
+ async (currentSettings, activeReplicator) => await activeReplicator.markRemoteResolved(currentSettings)
283
442
  );
284
443
  }
285
444
  }
@@ -0,0 +1,52 @@
1
+ import { type LOG_LEVEL, type ObsidianLiveSyncSettings } from "../../common/types.js";
2
+ import { type ReplicationReadinessRequirements } from "../../replication/ReplicatorProvider.js";
3
+ import { type LogFunction } from "../lib/logUtils.js";
4
+ import type { MessageTranslator } from "./MessageTranslator.js";
5
+ import { type ReadinessEvaluationResult } from "./ReadinessEvaluator.js";
6
+ /** State and policy gates consulted without beginning remote preparation. */
7
+ export interface ReplicationReadinessGates {
8
+ readonly isApplicationReady: () => boolean;
9
+ readonly runPolicyChecks: (showMessage: boolean) => Promise<boolean>;
10
+ readonly currentSettings: () => Pick<ObsidianLiveSyncSettings, "versionUpFlash">;
11
+ readonly isCleanupRunning: () => boolean;
12
+ readonly isOnline: () => boolean;
13
+ }
14
+ /** Ordered operational gates which may release or prepare replication work. */
15
+ export interface ReplicationReadinessPreparation {
16
+ readonly commitPendingFileEvents: () => Promise<boolean>;
17
+ readonly prepareCentralRemote: (showMessage: boolean) => Promise<boolean>;
18
+ readonly runBeforeReplicate: (showMessage: boolean) => Promise<boolean>;
19
+ }
20
+ /** Diagnostic state, presentation, and unresolved-error output for readiness. */
21
+ export interface ReplicationReadinessDiagnostics {
22
+ readonly getUnresolvedMessages: () => Promise<(string | Error)[][]>;
23
+ readonly translate: MessageTranslator;
24
+ readonly log: LogFunction;
25
+ readonly showError: (message: string, maxLogLevel?: LOG_LEVEL) => void;
26
+ readonly clearErrors: () => void;
27
+ }
28
+ /** Role-grouped collaborators for the common replication readiness sequence. */
29
+ export interface ReplicationReadinessDependencies {
30
+ readonly gates: ReplicationReadinessGates;
31
+ readonly preparation: ReplicationReadinessPreparation;
32
+ readonly diagnostics: ReplicationReadinessDiagnostics;
33
+ }
34
+ export interface ReplicationReadinessContext {
35
+ readonly showMessage: boolean;
36
+ readonly requirements: ReplicationReadinessRequirements;
37
+ }
38
+ /** Optional inputs accepted by the host-facing replication readiness evaluator. */
39
+ export interface ReplicationReadinessRequest {
40
+ readonly showMessage?: boolean;
41
+ readonly requirements?: ReplicationReadinessRequirements;
42
+ }
43
+ export type ReplicationReadinessEvaluator = (context?: ReplicationReadinessRequest) => Promise<ReadinessEvaluationResult>;
44
+ /**
45
+ * Compose the ordered readiness conditions shared by replication entry points.
46
+ *
47
+ * The returned evaluator owns readiness decisions and their diagnostics only.
48
+ * It does not acquire a Replicator, run provider capabilities, or account for
49
+ * remote activity. Provider-specific preparation is selected by the supplied
50
+ * requirements while the common conditions retain their declaration order.
51
+ */
52
+ export declare function createReplicationReadinessEvaluator(dependencies: ReplicationReadinessDependencies): ReplicationReadinessEvaluator;
@@ -0,0 +1,87 @@
1
+ import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE } from "../../common/types.js";
2
+ import {
3
+ CENTRAL_REMOTE_REPLICATION_READINESS
4
+ } from "../../replication/ReplicatorProvider.js";
5
+ import { MARK_LOG_NETWORK_ERROR } from "../lib/logUtils.js";
6
+ import { evaluateReadiness } from "./ReadinessEvaluator.js";
7
+ function createReplicationReadinessEvaluator(dependencies) {
8
+ const { diagnostics, gates, preparation } = dependencies;
9
+ const reportPreparationFailure = async () => {
10
+ const hasNetworkError = (await diagnostics.getUnresolvedMessages()).flat().some((entry) => typeof entry == "string" && entry.includes(MARK_LOG_NETWORK_ERROR));
11
+ const message = diagnostics.translate("Replicator.Message.SomeModuleFailed");
12
+ if (hasNetworkError) {
13
+ diagnostics.log(message, LOG_LEVEL_INFO);
14
+ } else {
15
+ diagnostics.showError(message, LOG_LEVEL_NOTICE);
16
+ }
17
+ };
18
+ const conditions = [
19
+ {
20
+ name: "application-ready",
21
+ evaluate: () => {
22
+ if (gates.isApplicationReady()) return true;
23
+ diagnostics.log("Not ready");
24
+ return false;
25
+ }
26
+ },
27
+ {
28
+ name: "replication-policy-approved",
29
+ evaluate: ({ showMessage }) => gates.runPolicyChecks(showMessage)
30
+ },
31
+ {
32
+ name: "local-replication-state-ready",
33
+ evaluate: () => {
34
+ const currentSettings = gates.currentSettings();
35
+ if (gates.isCleanupRunning()) {
36
+ diagnostics.log(diagnostics.translate("Replicator.Message.Cleaned"), LOG_LEVEL_NOTICE);
37
+ return false;
38
+ }
39
+ if (currentSettings.versionUpFlash != "") {
40
+ diagnostics.log(diagnostics.translate("Replicator.Message.VersionUpFlash"), LOG_LEVEL_NOTICE);
41
+ return false;
42
+ }
43
+ return true;
44
+ }
45
+ },
46
+ {
47
+ name: "pending-file-events-committed",
48
+ evaluate: () => preparation.commitPendingFileEvents(),
49
+ onRejected: () => {
50
+ diagnostics.showError(diagnostics.translate("Replicator.Message.Pending"), LOG_LEVEL_NOTICE);
51
+ }
52
+ },
53
+ {
54
+ name: "network-online",
55
+ evaluate: () => gates.isOnline(),
56
+ onRejected: ({ showMessage }) => {
57
+ diagnostics.showError("Network is offline", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
58
+ }
59
+ },
60
+ {
61
+ name: "central-remote-prepared",
62
+ appliesTo: ({ requirements }) => requirements.centralRemotePreparation === "required",
63
+ evaluate: ({ showMessage }) => preparation.prepareCentralRemote(showMessage),
64
+ onRejected: reportPreparationFailure
65
+ },
66
+ {
67
+ name: "general-replication-prepared",
68
+ evaluate: ({ showMessage }) => preparation.runBeforeReplicate(showMessage),
69
+ onRejected: reportPreparationFailure
70
+ }
71
+ ];
72
+ return async (context = {}) => {
73
+ const result = await evaluateReadiness({
74
+ purpose: "replication",
75
+ context: {
76
+ showMessage: context.showMessage ?? false,
77
+ requirements: context.requirements ?? CENTRAL_REMOTE_REPLICATION_READINESS
78
+ },
79
+ conditions
80
+ });
81
+ if (result.ready) diagnostics.clearErrors();
82
+ return result;
83
+ };
84
+ }
85
+ export {
86
+ createReplicationReadinessEvaluator
87
+ };