@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
@@ -1,15 +1,21 @@
1
- import { handlers } from "../lib/HandlerUtils.js";
1
+ import { firstResultFunction, handlers } from "../lib/HandlerUtils.js";
2
2
  import { ServiceBase } from "./ServiceBase.js";
3
3
  import { createInstanceLogFunction } from "../lib/logUtils.js";
4
4
  import { UnresolvedErrorManager } from "./UnresolvedErrorManager.js";
5
5
  import { yieldMicrotask } from "octagonal-wheels/promises";
6
6
  import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../common/logger.js";
7
- import { RemoteTypes } from "../../common/types.js";
8
7
  import { DEFAULT_REPLICATION_STATICS } from "../../common/models/shared.definition.js";
9
8
  import { reactiveSource } from "octagonal-wheels/dataobject/reactive";
10
9
  import {
11
10
  runWithOptionalActivity
12
11
  } from "../../interfaces/AsyncActivityRunner.js";
12
+ import { CAPABILITY_SUPPORT_KINDS } from "../../replication/ProviderCapability.js";
13
+ import {
14
+ ActiveReplicatorState
15
+ } from "./ReplicatorService.activeReplicatorState.js";
16
+ import { resolveRemoteResource } from "./ReplicatorService.remoteResourceResolver.js";
17
+ import { runCentralRemoteAdministrationWithContext } from "./ReplicatorService.centralRemoteAdministration.js";
18
+ import { asCopy } from "../../common/utils.object.js";
13
19
  class ReplicatorService extends ServiceBase {
14
20
  constructor(context, dependencies) {
15
21
  super(context);
@@ -23,11 +29,11 @@ class ReplicatorService extends ServiceBase {
23
29
  this.databaseEventService = dependencies.databaseEventService;
24
30
  if (dependencies.registerLifecycleHandlers ?? true) {
25
31
  this.settingService.onRealiseSetting.addHandler(this._initialiseReplicator.bind(this));
26
- this.databaseEventService.onResetDatabase.addHandler(this.disposeReplicator.bind(this));
27
- this.databaseEventService.onDatabaseInitialisation.addHandler(this.disposeReplicator.bind(this));
32
+ this.databaseEventService.onDatabaseInitialisation.addHandler(this.onCloseActiveReplication.bind(this));
28
33
  this.databaseEventService.onDatabaseInitialised.addHandler(this.reinitialiseReplicator.bind(this));
29
34
  this.databaseEventService.onDatabaseHasReady.addHandler(this.reinitialiseReplicator.bind(this));
30
35
  this.appLifecycleService.onSuspending.addHandler(this.suspendReplication.bind(this));
36
+ this.appLifecycleService.onUnload.addHandler(this.onCloseActiveReplication.bind(this));
31
37
  }
32
38
  }
33
39
  dependencies;
@@ -36,8 +42,9 @@ class ReplicatorService extends ServiceBase {
36
42
  _log = createInstanceLogFunction("ReplicatorService");
37
43
  settingService;
38
44
  databaseEventService;
39
- _activeReplicator;
40
- _replicatorType;
45
+ _activeReplicatorState = new ActiveReplicatorState();
46
+ _providerDefinitions = /* @__PURE__ */ new Map();
47
+ _transition = Promise.resolve();
41
48
  appLifecycleService;
42
49
  _unresolvedErrorManager;
43
50
  /**
@@ -65,32 +72,68 @@ class ReplicatorService extends ServiceBase {
65
72
  this.boundedRemoteActivityCount.value--;
66
73
  }
67
74
  }
68
- suspendReplication() {
69
- const activeReplicator = this._activeReplicator;
70
- if (activeReplicator) {
71
- activeReplicator.closeReplication();
72
- }
73
- return Promise.resolve(true);
75
+ /** Request a reversible transfer stop without retiring the active publication. */
76
+ async suspendReplication() {
77
+ return await this.enqueueTransition(async () => {
78
+ const publication = this._activeReplicatorState.current;
79
+ if (publication) {
80
+ await this.requestPublicationTransferStop(publication, "suspension");
81
+ }
82
+ return true;
83
+ });
74
84
  }
75
85
  async reinitialiseReplicator() {
76
- await this.disposeReplicator();
77
- await yieldMicrotask();
78
- return this._initialiseReplicator();
86
+ return await this.enqueueTransition(async () => {
87
+ await this.disposeReplicatorNow();
88
+ await yieldMicrotask();
89
+ return await this.initialiseReplicatorNow();
90
+ });
79
91
  }
80
- async disposeReplicator() {
81
- this._log("Detect database reset, closing active replicator if exists.");
82
- if (this._activeReplicator) {
83
- await Promise.resolve(this._activeReplicator.closeReplication());
92
+ /** Request transfer cancellation for either a reversible pause or terminal retirement. */
93
+ async requestPublicationTransferStop(publication, reason) {
94
+ try {
95
+ const capability = publication.context?.provider.stopActiveTransfer;
96
+ if (capability?.kind === CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
97
+ const outcome = await capability.run(publication.replicator);
98
+ if (outcome.status !== "completed") {
99
+ this._log(
100
+ `Active Replicator transfer stop settled as '${outcome.status}' during ${reason}.`,
101
+ LOG_LEVEL_VERBOSE
102
+ );
103
+ }
104
+ return;
105
+ }
106
+ if (!publication.context) {
107
+ await Promise.resolve(publication.replicator.terminateSync());
108
+ }
109
+ } catch (error) {
110
+ this._log(`Failed to request active Replicator transfer cancellation during ${reason}.`, LOG_LEVEL_VERBOSE);
111
+ this._log(error, LOG_LEVEL_VERBOSE);
84
112
  }
85
- this._activeReplicator = void 0;
86
- this._replicatorType = void 0;
113
+ }
114
+ /** Fence new work, request cancellation, and drain only this publication's admitted tasks. */
115
+ async quiesceActiveReplicator() {
116
+ const retirement = this._activeReplicatorState.beginRetirement();
117
+ if (!retirement) return void 0;
118
+ await this.requestPublicationTransferStop(retirement.publication, "retirement");
119
+ await retirement.waitForDemandSettlement();
120
+ return retirement;
121
+ }
122
+ /** Close one drained publication and release its private retirement state. */
123
+ async closeRetiringReplicator(retirement) {
124
+ await Promise.resolve(retirement.publication.replicator.closeReplication());
125
+ retirement.complete();
126
+ }
127
+ async disposeReplicatorNow(reason) {
128
+ const retirement = await this.quiesceActiveReplicator();
129
+ if (!retirement) return true;
130
+ this._log(
131
+ reason === "configuration transition" ? "Configuration changed, closing active replicator." : "Closing active replicator."
132
+ );
133
+ await this.closeRetiringReplicator(retirement);
87
134
  return true;
88
135
  }
89
136
  async discardFailedReplicator(replicator) {
90
- if (this._activeReplicator === replicator) {
91
- this._activeReplicator = void 0;
92
- this._replicatorType = void 0;
93
- }
94
137
  try {
95
138
  await Promise.resolve(replicator.closeReplication());
96
139
  } catch (error) {
@@ -98,44 +141,82 @@ class ReplicatorService extends ServiceBase {
98
141
  this._log(error, LOG_LEVEL_VERBOSE);
99
142
  }
100
143
  }
144
+ enqueueTransition(operation) {
145
+ const transition = this._transition.then(operation, operation);
146
+ this._transition = transition.then(
147
+ () => void 0,
148
+ () => void 0
149
+ );
150
+ return transition;
151
+ }
101
152
  async _initialiseReplicator() {
153
+ return await this.enqueueTransition(() => this.initialiseReplicatorNow());
154
+ }
155
+ getProviderDefinition(setting) {
156
+ return this._providerDefinitions.get(setting.remoteType);
157
+ }
158
+ async createReplicator(settingOverride = {}) {
159
+ const currentSettings = this.settingService.currentSettings();
160
+ const setting = { ...currentSettings, ...settingOverride };
161
+ return await this.createReplicatorForSetting(setting, settingOverride);
162
+ }
163
+ async createReplicatorForSetting(setting, legacySettingOverride = {}) {
164
+ const provider = this.getProviderDefinition(setting);
165
+ if (provider) {
166
+ return await provider.create(setting);
167
+ }
168
+ return await this.legacyGetNewReplicator(legacySettingOverride);
169
+ }
170
+ isCurrentProviderConfiguration(provider, configurationIdentity) {
171
+ const currentSetting = this.settingService.currentSettings();
172
+ return !!currentSetting && this.getProviderDefinition(currentSetting) === provider && provider.isConfigured(currentSetting) && provider.configurationIdentity(currentSetting) === configurationIdentity;
173
+ }
174
+ publishActiveReplicator(provider, replicator, replicatorType, configurationIdentity) {
175
+ this._activeReplicatorState.publish(provider, replicator, replicatorType, configurationIdentity);
176
+ }
177
+ async initialiseReplicatorNow() {
102
178
  const message = this.context.translate("Replicator.Message.InitialiseFatalError");
103
179
  const setting = this.settingService.currentSettings();
104
180
  if (!setting) {
105
- this._activeReplicator = void 0;
106
- this._replicatorType = void 0;
181
+ await this.disposeReplicatorNow("configuration transition");
107
182
  this._unresolvedErrorManager.clearError(message);
108
183
  return true;
109
184
  }
110
185
  const replicatorType = setting.remoteType;
111
- const isCouchDBConfigured = replicatorType === RemoteTypes.REMOTE_COUCHDB && !!setting.couchDB_URI?.trim() && !!setting.couchDB_DBNAME?.trim();
112
- const isMinioConfigured = replicatorType === RemoteTypes.REMOTE_MINIO && !!setting.endpoint?.trim() && !!setting.bucket?.trim();
113
- const isP2PEnabled = replicatorType === RemoteTypes.REMOTE_P2P && setting.P2P_Enabled;
114
- const hasReplicatorConfig = isCouchDBConfigured || isMinioConfigured || isP2PEnabled;
186
+ const provider = this.getProviderDefinition(setting);
187
+ let hasReplicatorConfig;
188
+ let configurationIdentity;
189
+ try {
190
+ hasReplicatorConfig = provider ? provider.isConfigured(setting) : this._providerDefinitions.size === 0;
191
+ configurationIdentity = provider && hasReplicatorConfig ? provider.configurationIdentity(setting) : void 0;
192
+ } catch (error) {
193
+ await this.disposeReplicatorNow("configuration transition");
194
+ this._unresolvedErrorManager.showError(message, LOG_LEVEL_NOTICE);
195
+ throw error;
196
+ }
115
197
  if (!hasReplicatorConfig) {
116
- this._activeReplicator = void 0;
117
- this._replicatorType = void 0;
198
+ await this.disposeReplicatorNow("configuration transition");
118
199
  this._unresolvedErrorManager.clearError(message);
119
200
  this._log("No remote replicator configuration found. Skipping replicator initialisation.");
120
201
  return true;
121
202
  }
122
- if (replicatorType === this._replicatorType && this._activeReplicator) {
203
+ const activePublication = this._activeReplicatorState.current;
204
+ const isSameProvider = replicatorType === activePublication?.replicatorType && (!provider || activePublication.context?.provider === provider);
205
+ if (isSameProvider && (!provider || configurationIdentity === activePublication?.context?.configurationIdentity)) {
123
206
  this._unresolvedErrorManager.clearError(message);
124
207
  this._log("Active replicator has been kept", LOG_LEVEL_VERBOSE);
125
208
  return true;
126
209
  } else {
127
210
  this._log("Acquiring new replicator");
128
- const newReplicator = await this.getNewReplicator();
211
+ await this.disposeReplicatorNow("configuration transition");
212
+ if (activePublication) {
213
+ this._log("Active replicator closed", LOG_LEVEL_VERBOSE);
214
+ }
215
+ const newReplicator = await this.createReplicatorForSetting(setting);
129
216
  if (!newReplicator) {
130
217
  this._unresolvedErrorManager.showError(message, LOG_LEVEL_NOTICE);
131
218
  return false;
132
219
  }
133
- if (this._activeReplicator) {
134
- await Promise.resolve(this._activeReplicator.closeReplication());
135
- this._log("Active replicator closed", LOG_LEVEL_VERBOSE);
136
- }
137
- this._activeReplicator = newReplicator;
138
- this._replicatorType = replicatorType;
139
220
  this.replicationStatics.value = { ...DEFAULT_REPLICATION_STATICS };
140
221
  await yieldMicrotask();
141
222
  try {
@@ -145,45 +226,143 @@ class ReplicatorService extends ServiceBase {
145
226
  this._unresolvedErrorManager.showError(message, LOG_LEVEL_NOTICE);
146
227
  return false;
147
228
  }
148
- if (!await this.onReplicatorInitialised()) {
149
- this._log("Failed to initialise the replicator, onReplicatorInitialised reported some problems.");
229
+ if (!await this.onBeforeReplicatorPublication()) {
230
+ this._log(
231
+ "Failed to initialise the replicator, onBeforeReplicatorPublication reported some problems."
232
+ );
150
233
  await this.discardFailedReplicator(newReplicator);
151
234
  this._unresolvedErrorManager.showError(message, LOG_LEVEL_NOTICE);
152
235
  return false;
153
236
  }
237
+ if (provider && (configurationIdentity === void 0 || !this.isCurrentProviderConfiguration(provider, configurationIdentity))) {
238
+ await this.discardFailedReplicator(newReplicator);
239
+ this._unresolvedErrorManager.clearError(message);
240
+ this._log(
241
+ "Discarded a replicator whose configuration changed before publication.",
242
+ LOG_LEVEL_VERBOSE
243
+ );
244
+ return true;
245
+ }
154
246
  } catch (error) {
155
247
  await this.discardFailedReplicator(newReplicator);
156
248
  this._unresolvedErrorManager.showError(message, LOG_LEVEL_NOTICE);
157
249
  throw error;
158
250
  }
159
- const remoteTypeDisplay = replicatorType === RemoteTypes.REMOTE_COUCHDB ? "CouchDB" : replicatorType;
251
+ this.publishActiveReplicator(provider, newReplicator, replicatorType, configurationIdentity);
252
+ const remoteTypeDisplay = provider?.diagnosticName ?? (replicatorType || "CouchDB");
160
253
  this._log(`Replicator (${remoteTypeDisplay}) initialised and activated`, LOG_LEVEL_VERBOSE);
161
254
  this._unresolvedErrorManager.clearError(message);
162
255
  return true;
163
256
  }
164
257
  }
165
258
  /**
166
- * Close the active replication if any.
167
- * Not used currently.
259
+ * Fence new work, drain admitted work, and close the active Replicator.
260
+ *
261
+ * The command is serialised with every other ownership transition. Callers
262
+ * must not await it from inside `runWithActiveReplicatorContext()`, because
263
+ * retirement waits for that admitted callback to settle.
168
264
  */
169
- onCloseActiveReplication = handlers().anySuccess("onCloseActiveReplication");
265
+ async onCloseActiveReplication() {
266
+ return await this.enqueueTransition(() => this.disposeReplicatorNow());
267
+ }
170
268
  /**
171
269
  * Get a new replicator instance based on the provided settings.
172
270
  */
173
- getNewReplicator = handlers().firstResult("getNewReplicator");
174
- onReplicatorInitialised = handlers().bailFirstFailure("onReplicatorInitialised");
271
+ legacyGetNewReplicator = firstResultFunction("getNewReplicator");
272
+ getNewReplicator = Object.assign(
273
+ async (settingOverride = {}) => await this.createReplicator(settingOverride),
274
+ {
275
+ addHandler: (callback) => this.legacyGetNewReplicator.addHandler(callback),
276
+ removeHandler: (callback) => this.legacyGetNewReplicator.removeHandler(callback)
277
+ }
278
+ );
279
+ registerReplicatorProviderDefinitions(definitions) {
280
+ for (const [kind, definition] of definitions) {
281
+ const existing = this._providerDefinitions.get(kind);
282
+ if (existing && existing !== definition) {
283
+ throw new Error(`Replicator provider '${kind}' is already composed.`);
284
+ }
285
+ this._providerDefinitions.set(kind, definition);
286
+ }
287
+ }
288
+ /**
289
+ * Return a non-owning synchronous view for lifecycle diagnostics and focused tests.
290
+ *
291
+ * Production work must use an admitted acquisition or callback boundary so that
292
+ * the context cannot be retired while it is in use.
293
+ */
294
+ inspectActiveReplicatorContext() {
295
+ return this._activeReplicatorState.current?.context;
296
+ }
297
+ async createRemoteResource(kind, setting) {
298
+ const provider = this.getProviderDefinition(setting);
299
+ if (!provider || !provider.isConfigured(setting)) {
300
+ return void 0;
301
+ }
302
+ return await resolveRemoteResource(provider.remoteResources, kind, setting);
303
+ }
304
+ /** Apply and verify one provider-declared action before permitting a later switch. */
305
+ async runCentralRemoteAdministration(request) {
306
+ const setting = asCopy(this.settingService.currentSettings());
307
+ const result = await this.runWithActiveReplicatorContext(
308
+ (context) => runCentralRemoteAdministrationWithContext(context, setting, request)
309
+ );
310
+ return result ?? await runCentralRemoteAdministrationWithContext(void 0, setting, request);
311
+ }
312
+ async acquireActiveReplicatorContext() {
313
+ while (true) {
314
+ const transition = this._transition;
315
+ await transition;
316
+ if (transition === this._transition) {
317
+ return this._activeReplicatorState.current?.context;
318
+ }
319
+ }
320
+ }
321
+ /**
322
+ * Admit one callback against the publication ordered before later transitions.
323
+ *
324
+ * Release never enters the transition queue. The callback must not initiate
325
+ * or await a settings, database, or Replicator lifecycle transition which
326
+ * would wait for this same reservation.
327
+ */
328
+ async runWithActiveReplicatorContext(task) {
329
+ const reservation = await this.enqueueTransition(async () => this._activeReplicatorState.reserve());
330
+ if (!reservation) return void 0;
331
+ try {
332
+ return await task(reservation.context);
333
+ } finally {
334
+ reservation.release();
335
+ }
336
+ }
337
+ onBeforeReplicatorPublication = handlers().bailFirstFailure(
338
+ "onBeforeReplicatorPublication"
339
+ );
175
340
  /**
176
- * Get the currently active replicator instance.
177
- * If no active replicator, return undefined but that is the fatal situation (on Obsidian).
341
+ * Return the legacy unreserved view of the active Replicator.
342
+ *
343
+ * Missing active state retains its established Notice-level diagnostic.
344
+ * New work must acquire or admit an `ActiveReplicatorContext` instead.
178
345
  */
179
346
  getActiveReplicator() {
180
347
  const message = "No replicator has been activated or has not been initialised yet.";
181
- if (!this._activeReplicator) {
348
+ const activeReplicator = this._activeReplicatorState.current?.replicator;
349
+ if (!activeReplicator) {
182
350
  this._unresolvedErrorManager.showError(message, LOG_LEVEL_NOTICE);
183
351
  return void 0;
184
352
  }
185
353
  this._unresolvedErrorManager.clearError(message);
186
- return this._activeReplicator;
354
+ return activeReplicator;
355
+ }
356
+ /**
357
+ * Classify whether a compatibility active Replicator exists without
358
+ * acquiring or returning it.
359
+ *
360
+ * This side-effect-free, non-owning predicate exists only for
361
+ * compatibility-state classification. Work must acquire or admit an
362
+ * `ActiveReplicatorContext` before using a Replicator.
363
+ */
364
+ hasActiveReplicator() {
365
+ return this._activeReplicatorState.current !== void 0;
187
366
  }
188
367
  replicationStatics = reactiveSource({ ...DEFAULT_REPLICATION_STATICS });
189
368
  }
@@ -0,0 +1,4 @@
1
+ import type { RemoteDBSettings } from "../../common/types.js";
2
+ import type { RemoteResourceCapabilities, RemoteResourceKind, RemoteResourceMap } from "../../replication/RemoteResource.js";
3
+ /** Resolve one ReplicatorService-owned finite resource without adding a method per role. */
4
+ export declare function resolveRemoteResource<TKind extends RemoteResourceKind>(capabilities: RemoteResourceCapabilities, kind: TKind, setting: RemoteDBSettings): Promise<RemoteResourceMap[TKind] | undefined>;
@@ -0,0 +1,11 @@
1
+ import { CAPABILITY_SUPPORT_KINDS } from "../../replication/ProviderCapability.js";
2
+ async function resolveRemoteResource(capabilities, kind, setting) {
3
+ const capability = capabilities[kind];
4
+ if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
5
+ return void 0;
6
+ }
7
+ return await capability.run(setting);
8
+ }
9
+ export {
10
+ resolveRemoteResource
11
+ };
@@ -1,4 +1,4 @@
1
- import type { RemoteDBSettings, RemotePreferredTweakResult, TweakValues } from "../../common/types.js";
1
+ import type { ObsidianLiveSyncSettings, RemoteDBSettings, RemotePreferredTweakResult, TweakValues } from "../../common/types.js";
2
2
  import type { ITweakValueService } from "./IService.js";
3
3
  import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
4
4
  /**
@@ -18,8 +18,9 @@ export declare abstract class TweakValueService<T extends ServiceContext = Servi
18
18
  /**
19
19
  * Ask the user to resolve any mismatched tweak values.
20
20
  * @param preferredSource The preferred tweak values to resolve against.
21
+ * @param updatePreferredRemote Optional exact-context writer supplied by the failed operation.
21
22
  */
22
- abstract askResolvingMismatched(preferredSource: TweakValues): Promise<"OK" | "CHECKAGAIN" | "IGNORE">;
23
+ abstract askResolvingMismatched(preferredSource: TweakValues, updatePreferredRemote?: (setting: ObsidianLiveSyncSettings) => Promise<boolean>): Promise<"OK" | "CHECKAGAIN" | "IGNORE">;
23
24
  /**
24
25
  * Check and ask the user to use the remote configuration.
25
26
  * @param settings The remote database settings to connect.
@@ -3,7 +3,7 @@ import { TweakValueService } from "../../base/TweakValueService.js";
3
3
  export declare class InjectableTweakValueService<T extends ServiceContext> extends TweakValueService<T> {
4
4
  fetchRemotePreferred: import("../../lib/HandlerUtils.js").HandlerFunction<(trialSetting: import("../../../settings.js").RemoteDBSettings) => Promise<import("../../../common/types.js").RemotePreferredTweakResult>, unknown>;
5
5
  checkAndAskResolvingMismatched: import("../../lib/HandlerUtils.js").HandlerFunction<(preferred: Partial<import("../../../common/types.js").TweakValues>) => Promise<[import("../../../common/types.js").TweakValues | boolean, boolean]>, unknown>;
6
- askResolvingMismatched: import("../../lib/HandlerUtils.js").HandlerFunction<(preferredSource: import("../../../common/types.js").TweakValues) => Promise<"OK" | "CHECKAGAIN" | "IGNORE">, unknown>;
6
+ askResolvingMismatched: import("../../lib/HandlerUtils.js").HandlerFunction<(preferredSource: import("../../../common/types.js").TweakValues, updatePreferredRemote?: (setting: import("../../../settings.js").ObsidianLiveSyncSettings) => Promise<boolean>) => Promise<"OK" | "CHECKAGAIN" | "IGNORE">, unknown>;
7
7
  checkAndAskUseRemoteConfiguration: import("../../lib/HandlerUtils.js").HandlerFunction<(settings: import("../../../settings.js").RemoteDBSettings) => Promise<{
8
8
  result: false | import("../../../common/types.js").TweakValues;
9
9
  requireFetch: boolean;
@@ -182,6 +182,22 @@ export declare class AllHandler<T extends unknown[]> extends BooleanHandlerBase<
182
182
  */
183
183
  invoke(...args: T): Promise<boolean>;
184
184
  }
185
+ /**
186
+ * A handler that invokes every added handler sequentially and reports whether
187
+ * all of them succeeded.
188
+ *
189
+ * Unlike {@link AllHandler}, a failure does not prevent later cleanup handlers
190
+ * from settling.
191
+ */
192
+ export declare class SettledAllHandler<T extends unknown[]> extends BooleanHandlerBase<T> {
193
+ /**
194
+ * Invokes every handler in order, treating a false result or an exception
195
+ * as an aggregate failure.
196
+ * @param args The arguments to pass to the handlers.
197
+ * @returns True only when every handler succeeds.
198
+ */
199
+ invoke(...args: T): Promise<boolean>;
200
+ }
185
201
  /**
186
202
  * A handler that invokes all added handler functions in parallel and returns true only if all return true.
187
203
  */
@@ -316,6 +332,7 @@ export interface MultiBinderInstance<T extends unknown[], U> extends InvokableHa
316
332
  export interface BooleanMultiBinderInstance<T extends unknown[]> extends InvokableBooleanHandler<T>, MultiRegisterHandler<T, boolean> {
317
333
  }
318
334
  export declare function allFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
335
+ export declare function allSettledFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
319
336
  export declare function bailFirstFailureFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
320
337
  export declare function allParallelFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
321
338
  export declare function anySuccessFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
@@ -333,6 +350,13 @@ export declare function handlers<T extends object>(): {
333
350
  * @returns
334
351
  */
335
352
  all<K extends FunctionKeys<T>>(name: K): BooleanMultipleHandlerFunction<Extract<T[K], (...args: any[]) => Promise<boolean>>>;
353
+ /**
354
+ * Create a handler that invokes every added handler sequentially and
355
+ * returns true only if all of them succeed.
356
+ * @param name
357
+ * @returns
358
+ */
359
+ allSettled<K extends FunctionKeys<T>>(name: K): BooleanMultipleHandlerFunction<Extract<T[K], (...args: any[]) => Promise<boolean>>>;
336
360
  /**
337
361
  * Create a handler that invokes all added handler functions in parallel and returns true only if all return true.
338
362
  * @param name
@@ -207,6 +207,31 @@ class AllHandler extends BooleanHandlerBase {
207
207
  return true;
208
208
  }
209
209
  }
210
+ class SettledAllHandler extends BooleanHandlerBase {
211
+ /**
212
+ * Invokes every handler in order, treating a false result or an exception
213
+ * as an aggregate failure.
214
+ * @param args The arguments to pass to the handlers.
215
+ * @returns True only when every handler succeeds.
216
+ */
217
+ async invoke(...args) {
218
+ let succeeded = true;
219
+ const callbacks = [...this._callbacks];
220
+ for (const callback of callbacks) {
221
+ try {
222
+ const result = await Promise.resolve(callback(...args));
223
+ if (result === false) {
224
+ succeeded = false;
225
+ }
226
+ } catch (error) {
227
+ Logger(`SettledAllHandler ${this._name} treated error as failure!`, LOG_LEVEL_VERBOSE);
228
+ Logger(error, LOG_LEVEL_VERBOSE);
229
+ succeeded = false;
230
+ }
231
+ }
232
+ return succeeded;
233
+ }
234
+ }
210
235
  class ParallelAllHandler extends BooleanHandlerBase {
211
236
  /**
212
237
  * Invoke all handlers in parallel
@@ -288,6 +313,10 @@ function allFunction(name) {
288
313
  const handler = new AllHandler(name ?? "handleAllFunc");
289
314
  return getMultipleBound(handler);
290
315
  }
316
+ function allSettledFunction(name) {
317
+ const handler = new SettledAllHandler(name ?? "allSettledFunction");
318
+ return getMultipleBound(handler);
319
+ }
291
320
  function bailFirstFailureFunction(name) {
292
321
  const handler = new AllHandler(name ?? "bailFirstFailureFunction");
293
322
  return getMultipleBound(handler);
@@ -336,6 +365,15 @@ function handlers() {
336
365
  all(name) {
337
366
  return allFunction(String(name));
338
367
  },
368
+ /**
369
+ * Create a handler that invokes every added handler sequentially and
370
+ * returns true only if all of them succeed.
371
+ * @param name
372
+ * @returns
373
+ */
374
+ allSettled(name) {
375
+ return allSettledFunction(String(name));
376
+ },
339
377
  /**
340
378
  * Create a handler that invokes all added handler functions in parallel and returns true only if all return true.
341
379
  * @param name
@@ -400,8 +438,10 @@ export {
400
438
  LazyBinder,
401
439
  MultiBinder,
402
440
  ParallelAllHandler,
441
+ SettledAllHandler,
403
442
  allFunction,
404
443
  allParallelFunction,
444
+ allSettledFunction,
405
445
  anySuccessFunction,
406
446
  bailFirstFailureFunction,
407
447
  bindableFunction,
@@ -6,7 +6,8 @@ This document is for Commonlib developers. Package consumers should begin with t
6
6
 
7
7
  The source tree is larger than the supported package surface. Consumers may import only paths in the generated package export map:
8
8
 
9
- - `src/index.ts`, `src/context.ts`, `src/settings.ts`, `src/remoteConfigurations.ts`, `src/platform/browser/index.ts`, and `src/platform/node/index.ts` define the focused entries;
9
+ - `src/index.ts`, `src/context.ts`, `src/settings.ts`, `src/remoteConfigurations.ts`, `src/p2p/index.ts`, `src/platform/browser/index.ts`, and `src/platform/node/index.ts` define the focused entries;
10
+ - `src/replication/index.ts` defines the focused provider-capability and interaction contract for hosts which compose replication;
10
11
  - `src/rpc/index.ts` defines a transitional entry for the existing LiveSync P2P composition, not a stable Commonlib 1.0 contract;
11
12
  - `_tools/build-package.mjs` compiles those entries and creates the publishable manifest under `.package`;
12
13
  - `docs/migration/downstream-imports.json` is the reviewed inventory from which explicit `compat/*` exports are generated;
@@ -16,6 +17,29 @@ The source tree is larger than the supported package surface. Consumers may impo
16
17
 
17
18
  Files elsewhere under `src` are implementation details unless a focused entry re-exports them or the compatibility inventory names them. Do not use the source layout, TypeScript path aliases, or the presence of generated declarations as evidence of a supported consumer import.
18
19
 
20
+ ## Service composition and state ownership
21
+
22
+ Commonlib uses four related forms of composition. Choose between them by ownership and dependency direction, not merely by whether an implementation has mutable state.
23
+
24
+ - A **Service** is a long-lived contract in the Service Hub. Add a handler to an existing Service when the behaviour is a simple extension of that contract.
25
+ - A **serviceFeature** is a typed composition function. It accepts only the Services and ServiceModules named by `NecessaryServices`, then registers handlers, commands, lifecycle bindings, or other host glue. It is not added to a runtime registry. A serviceFeature may return a focused view or controller, and may retain bounded private state when that state belongs only to the composed feature.
26
+ - A **ServiceModule** is a long-lived stateful or resource-owning object constructed by the host and shared through the typed `ServiceModules` record. Use one when several consumers need the same operational capability or lifecycle, such as storage access, file handling, or database rebuilding. State alone does not make a ServiceModule necessary.
27
+ - An **AbstractModule** is the legacy application module layer. Existing modules may continue to own application behaviour, but new composition should not acquire the broad core merely to gain lifecycle access or shared dependencies.
28
+
29
+ When a feature needs state, separate the component which owns that state, its transitions, and its invariants from the code which connects it to application lifecycle events and downstream effects. Give the stateful component narrow collaborators or callbacks rather than the complete Service Hub or application core. Keep application-level handler registration and host-specific presentation in the surrounding serviceFeature. A handler or effect which is part of the state contract, such as querying unresolved errors or reporting their first occurrence, may remain inside the stateful component when its input and output ports are explicit and bounded.
30
+
31
+ Existing features illustrate this direction:
32
+
33
+ - `targetFilter.ts` keeps each cache or readiness gate inside the factory which owns the corresponding predicate. `useTargetFilters` composes those predicates and registers them in the required order.
34
+ - `prepareDatabaseForUse.ts` keeps the initialisation sequence independently testable. `usePrepareDatabaseForUse` constructs its error manager and binds the operation to the database lifecycle.
35
+ - the P2P composition keeps durable policy and room-session transitions in focused state owners, while `useP2PReplicatorFeature` connects those owners to Services and exposes narrow consumer views.
36
+
37
+ Older code does not apply this boundary consistently. Improve it when changing the affected ownership or lifecycle; do not perform an unrelated mechanical conversion merely to change the abstraction name.
38
+
39
+ Use interaction-based, London School unit tests at a serviceFeature boundary. Verify which collaborator is called, in which order, what is not called after a failure, and which handler receives the composed operation. Test the focused state owner separately for its transitions and invariants. If a unit test requires a broad core fixture, a deep chain of mocks, import-order substitution, or knowledge of unrelated services, stop and review the responsibility and dependency boundary before adding more test machinery. Difficulty writing a clean interaction test is a design-review signal, not a reason to expose more internals.
40
+
41
+ See [Service feature composition](service-feature-composition.md) for the decision table, current examples, state-ownership rules, legacy Module boundary, and London School testing guidance.
42
+
19
43
  ## Local validation
20
44
 
21
45
  For a complete local package gate, run:
@@ -71,19 +95,28 @@ The one-shot CouchDB connectivity preflight is the first bounded consumer. It ap
71
95
 
72
96
  The active local database identity, settings-selected reset sequence, and lifecycle event phases are specified in [Local database lifecycle](database-lifecycle.md). Extend that document and the focused service and rebuild tests whenever database selection, reset ownership, event ordering, or failure propagation changes.
73
97
 
98
+ ### Replication control boundaries
99
+
100
+ `ReplicationService` remains the host-facing façade for handler registration, legacy entry points, mutable counters, event rate limiting, full transfers, and rebuild maintenance. It delegates ordered readiness evaluation and typed provider dispatch to focused collaborators without moving the handler objects, changing their priority semantics, or taking active Replicator ownership from `ReplicatorService`.
101
+
102
+ `evaluateReadiness` is a domain-neutral, ordered evaluator. Its input states the diagnostic purpose and supplies named conditions; applicable conditions run sequentially, the first rejection short-circuits later work, condition-specific rejection reporting completes before return, and callback exceptions propagate unchanged. `createReplicationReadinessEvaluator` supplies the Replication-specific condition list, including the conditional central-remote preparation gate. Add a new readiness requirement as a named condition with focused ordering and short-circuit tests rather than extending one compound boolean expression.
103
+
104
+ `TypedReplicationCoordinator` keeps user-initiated, unattended, continuous, and stop roles as explicit control flows. Their interaction authority, readiness position, activity accounting, and failure handling differ, so these roles must not be collapsed into a flag-driven capability table. Share only mechanics whose lifecycle meaning is identical, such as atomic context acquisition and finite activity accounting, and retain a focused sequencing test for each role.
105
+
74
106
  ### P2P composition ownership
75
107
 
76
- `useP2PReplicatorFeature` is the sole owner of the active `LiveSyncTrysteroReplicator` and its lifecycle bindings. It creates or replaces the outer replicator when `ReplicatorService` requests one, closes the previous instance before replacement, shares an in-flight replacement between concurrent callers, and returns a stable result object whose `replicator` property resolves the current instance.
108
+ `useP2PReplicatorFeature` composes one stable P2P service owner, one room-session owner, and their lifecycle bindings. The service exposes focused views, including owner-arbitrated connection-probe admission, over that same state. When `ReplicatorService` requests a P2P Replicator, the provider returns a fresh non-owning adapter. Replacing or disposing that active adapter does not cancel service-owned work or retire the room; the explicit stop capability and transport lifecycle view own those distinct operations. Database reset and explicit close reach the stable service through their pre-destruction lifecycle hooks, so non-ownership by the active adapter cannot leave a room using an unavailable local database.
109
+
110
+ Every composed provider projects its effective connection settings to an opaque configuration identity. A changed identity replaces the active Replicator; there is no same-instance rebind branch. The projection follows provider-owned resource binding rather than the stored Setup URI grammar, and it remains private because it can contain credentials. `ReplicatorService` serialises those transitions, clears the old context before retirement, rechecks the candidate identity after asynchronous initialisation, and publishes the provider and Replicator together. Typed consumers use `acquireActiveReplicatorContext()` when they only need to wait for a queued transition, and `runWithActiveReplicatorContext()` when work must reserve the exact publication against later retirement.
77
111
 
78
- Consumers must preserve that result object and read `result.replicator` at the point of use. Do not destructure the property into a command, event handler, view, or other long-lived closure: a database reinitialisation or remote-type transition can close and replace the captured instance, and calling `open()` on that obsolete outer object can create a second connection outside the active service state.
112
+ Consumers must request only the focused view which they need. Commands which connect or disconnect consume `transportLifecycle`; P2P Setup consumes `connectionProbe`; peer lists consume `peerDirectory`; RTC inspection consumes `diagnostics`. Ordinary consumers must not receive a raw room, raw host, or concrete Replicator. The result's `replicator` property is a deprecated compatibility façade for panes which have not yet migrated, not the active provider adapter.
79
113
 
80
114
  The similarly named compositions have narrower roles:
81
115
 
82
- - `useP2PReplicatorCommands` adds host commands and resolves the current instance when a command is checked or invoked;
83
- - host UI features add views, status presentation, and injected peer-selection callbacks without owning the replicator lifecycle; and
84
- - the deprecated `useP2PReplicator` entry is a compatibility composition which delegates ownership to `useP2PReplicatorFeature`.
116
+ - `useP2PReplicatorCommands` adds host commands against the transport lifecycle view;
117
+ - host UI features add views, status presentation, and injected peer-selection callbacks without owning the replicator lifecycle.
85
118
 
86
- When registering P2P event handlers for a replaceable instance, pass a provider to `addP2PEventHandlers`. Passing a fixed instance remains supported only for compositions whose instance cannot change. Extend the focused feature, command, event-hub, and compatibility-wrapper tests whenever this ownership boundary changes.
119
+ The compatibility event bridge delegates lifecycle and status requests to the stable service while remaining panes migrate. Peer arrival and departure are handled inside their originating room session; the global peer events remain observational and are not routed into the current session. Do not add another consumer to the compatibility bridge. Extend the focused feature, command, session-fencing, and event-hub tests whenever this ownership boundary changes.
87
120
 
88
121
  The physical room, WebRTC peer, and relay-socket boundaries are documented in [P2P transport lifecycle](p2p-transport-lifecycle.md). In particular, normal shutdown leaves the Trystero room without closing raw peer connections directly.
89
122