@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.
- package/README.md +5 -1
- package/dist/common/models/setting.p2p.d.ts +2 -0
- package/dist/common/models/setting.p2p.js +5 -0
- package/dist/interfaces/ServiceModule.d.ts +13 -4
- package/dist/managers/ChunkFetcher.js +10 -0
- package/dist/p2p/P2PService.d.ts +123 -0
- package/dist/p2p/P2PService.js +271 -0
- package/dist/p2p/index.d.ts +8 -0
- package/dist/p2p/index.js +8 -0
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +3 -0
- package/dist/pouchdb/LiveSyncLocalDB.js +23 -3
- package/dist/pouchdb/ReplicatorShim.d.ts +43 -4
- package/dist/pouchdb/ReplicatorShim.js +78 -5
- package/dist/replication/CentralCompatibility.d.ts +38 -0
- package/dist/replication/CentralCompatibility.js +39 -0
- package/dist/replication/CentralRemoteAdministration.d.ts +84 -0
- package/dist/replication/CentralRemoteAdministration.js +71 -0
- package/dist/replication/LiveSyncAbstractReplicator.d.ts +19 -6
- package/dist/replication/ProviderCapability.d.ts +32 -0
- package/dist/replication/ProviderCapability.js +27 -0
- package/dist/replication/RemoteResource.d.ts +85 -0
- package/dist/replication/RemoteResource.js +17 -0
- package/dist/replication/ReplicatorInstance.d.ts +23 -0
- package/dist/replication/ReplicatorProvider.d.ts +262 -0
- package/dist/replication/ReplicatorProvider.js +150 -0
- package/dist/replication/couchdb/LiveSyncReplicator.d.ts +54 -8
- package/dist/replication/couchdb/LiveSyncReplicator.js +411 -158
- package/dist/replication/index.d.ts +10 -0
- package/dist/replication/index.js +72 -0
- package/dist/replication/journal/JournalSyncCore.d.ts +2 -0
- package/dist/replication/journal/JournalSyncCore.js +18 -5
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +38 -10
- package/dist/replication/journal/LiveSyncJournalReplicator.js +271 -63
- package/dist/replication/journal/LiveSyncJournalReplicatorEnv.d.ts +8 -0
- package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +2 -0
- package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -0
- package/dist/replication/journal/objectstore/MinioStorageAdapter.js +10 -1
- package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +12 -40
- package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +52 -152
- package/dist/replication/trystero/P2PAutomationCoordinator.d.ts +20 -0
- package/dist/replication/trystero/P2PAutomationCoordinator.js +43 -0
- package/dist/replication/trystero/P2PConnectionProbeAdmission.d.ts +32 -0
- package/dist/replication/trystero/P2PConnectionProbeAdmission.js +10 -0
- package/dist/replication/trystero/P2PReplicationWire.d.ts +31 -0
- package/dist/replication/trystero/P2PReplicationWire.js +50 -0
- package/dist/replication/trystero/P2PRoomSession.d.ts +32 -0
- package/dist/replication/trystero/P2PRoomSession.js +101 -0
- package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +61 -0
- package/dist/replication/trystero/P2PRoomSessionOwner.js +249 -0
- package/dist/replication/trystero/TrysteroReplicator.d.ts +88 -36
- package/dist/replication/trystero/TrysteroReplicator.js +273 -113
- package/dist/replication/trystero/TrysteroReplicatorP2PClient.d.ts +7 -4
- package/dist/replication/trystero/TrysteroReplicatorP2PClient.js +21 -14
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +26 -3
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +84 -39
- package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -6
- package/dist/replication/trystero/addP2PEventHandlers.d.ts +8 -12
- package/dist/replication/trystero/addP2PEventHandlers.js +19 -19
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.d.ts +9 -0
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +30 -0
- package/dist/replication/trystero/types.d.ts +18 -0
- package/dist/replication/trystero/types.js +5 -1
- package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +3 -3
- package/dist/replication/trystero/useP2PReplicatorCommands.js +6 -8
- package/dist/replication/trystero/useP2PReplicatorFeature.d.ts +16 -7
- package/dist/replication/trystero/useP2PReplicatorFeature.js +73 -55
- package/dist/rpc/RpcRoom.d.ts +15 -1
- package/dist/rpc/RpcRoom.js +176 -41
- package/dist/rpc/RpcSession.d.ts +3 -1
- package/dist/rpc/RpcSession.js +2 -2
- package/dist/rpc/index.d.ts +1 -1
- package/dist/rpc/transports/trysteroUtils.js +2 -1
- package/dist/rpc/types.d.ts +26 -1
- package/dist/serviceFeatures/checkRemoteSize.js +5 -2
- package/dist/serviceModules/Rebuilder.js +66 -56
- package/dist/services/base/AppLifecycleService.d.ts +4 -0
- package/dist/services/base/AppLifecycleService.js +4 -0
- package/dist/services/base/ControlService.js +0 -4
- package/dist/services/base/DatabaseEventService.js +1 -1
- package/dist/services/base/IService.d.ts +105 -8
- package/dist/services/base/ReadinessEvaluator.d.ts +40 -0
- package/dist/services/base/ReadinessEvaluator.js +16 -0
- package/dist/services/base/RemoteConnection.d.ts +3 -0
- package/dist/services/base/RemoteService.js +2 -2
- package/dist/services/base/ReplicationService.d.ts +45 -10
- package/dist/services/base/ReplicationService.js +250 -91
- package/dist/services/base/ReplicationService.readiness.d.ts +52 -0
- package/dist/services/base/ReplicationService.readiness.js +87 -0
- package/dist/services/base/ReplicationService.typedReplication.d.ts +93 -0
- package/dist/services/base/ReplicationService.typedReplication.js +214 -0
- package/dist/services/base/ReplicatorService.activeReplicatorState.d.ts +47 -0
- package/dist/services/base/ReplicatorService.activeReplicatorState.js +107 -0
- package/dist/services/base/ReplicatorService.centralRemoteAdministration.d.ts +5 -0
- package/dist/services/base/ReplicatorService.centralRemoteAdministration.js +44 -0
- package/dist/services/base/ReplicatorService.d.ts +75 -14
- package/dist/services/base/ReplicatorService.js +232 -53
- package/dist/services/base/ReplicatorService.remoteResourceResolver.d.ts +4 -0
- package/dist/services/base/ReplicatorService.remoteResourceResolver.js +11 -0
- package/dist/services/base/TweakValueService.d.ts +3 -2
- package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +1 -1
- package/dist/services/lib/HandlerUtils.d.ts +24 -0
- package/dist/services/lib/HandlerUtils.js +40 -0
- package/docs/development.md +40 -7
- package/docs/p2p-transport-lifecycle.md +135 -11
- package/docs/proven-in-use.md +5 -1
- package/docs/remote-configurations.md +4 -0
- package/docs/service-feature-composition.md +250 -0
- package/package.json +17 -1
- package/dist/replication/trystero/P2PReplicatorBase.d.ts +0 -19
- package/dist/replication/trystero/P2PReplicatorCore.d.ts +0 -8
- package/dist/replication/trystero/P2PReplicatorCore.js +0 -56
- package/dist/replication/trystero/TrysteroReplicatorP2PConnection.d.ts +0 -2
- package/dist/replication/trystero/TrysteroReplicatorP2PConnection.js +0 -4
- /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.
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
return await this.enqueueTransition(async () => {
|
|
87
|
+
await this.disposeReplicatorNow();
|
|
88
|
+
await yieldMicrotask();
|
|
89
|
+
return await this.initialiseReplicatorNow();
|
|
90
|
+
});
|
|
79
91
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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.
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
149
|
-
this._log(
|
|
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
|
-
|
|
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
|
-
*
|
|
167
|
-
*
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
*
|
|
177
|
-
*
|
|
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
|
-
|
|
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
|
|
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,
|
package/docs/development.md
CHANGED
|
@@ -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`
|
|
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
|
|
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
|
|
83
|
-
- host UI features add views, status presentation, and injected peer-selection callbacks without owning the replicator lifecycle
|
|
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
|
-
|
|
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
|
|