@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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ObsidianLiveSyncSettings } from "../../common/types.js";
|
|
2
|
+
import { type ContinuousReplicationRequest, type ReplicationFailureRequest, type ReplicationOutcome, type ReplicationReadinessRequirements, type UnattendedOneShotRequest, type UserInitiatedOneShotRequest } from "../../replication/ReplicatorProvider.js";
|
|
3
|
+
import type { IReplicatorService } from "./IService.js";
|
|
4
|
+
/** ReplicationService collaborators required to execute typed provider roles. */
|
|
5
|
+
export interface TypedReplicationCoordinatorDependencies {
|
|
6
|
+
readonly replicatorService: Pick<IReplicatorService, "acquireActiveReplicatorContext" | "hasActiveReplicator" | "runWithActiveReplicatorContext" | "runFiniteReplicationActivity">;
|
|
7
|
+
readonly currentSettings: () => ObsidianLiveSyncSettings;
|
|
8
|
+
readonly checkReadiness: (showMessage: boolean, readiness: ReplicationReadinessRequirements) => Promise<boolean>;
|
|
9
|
+
readonly handleFailure: (request: ReplicationFailureRequest) => Promise<boolean>;
|
|
10
|
+
/**
|
|
11
|
+
* Advance the host's finite-attempt clock after readiness admits dispatch.
|
|
12
|
+
*
|
|
13
|
+
* This includes a provider-declared blocked capability, because the legacy
|
|
14
|
+
* service advanced its event rate-limit clock at the same boundary.
|
|
15
|
+
*/
|
|
16
|
+
readonly recordFiniteAttempt: () => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Execute typed provider capabilities against one captured active context.
|
|
20
|
+
*
|
|
21
|
+
* The coordinator owns capability selection, readiness dispatch, finite
|
|
22
|
+
* activity accounting, and failure notification. It does not own event rate
|
|
23
|
+
* limiting, legacy Replicator calls, or the active Replicator lifecycle.
|
|
24
|
+
*
|
|
25
|
+
* Each role is deliberately expressed as a separate control flow. Readiness,
|
|
26
|
+
* interaction authority, activity accounting, and failure handling differ by
|
|
27
|
+
* role, so a flag-driven capability table would conceal lifecycle decisions
|
|
28
|
+
* which callers and reviewers need to see explicitly.
|
|
29
|
+
*/
|
|
30
|
+
export declare class TypedReplicationCoordinator {
|
|
31
|
+
private readonly dependencies;
|
|
32
|
+
private oneShotInProgress;
|
|
33
|
+
constructor(dependencies: TypedReplicationCoordinatorDependencies);
|
|
34
|
+
/**
|
|
35
|
+
* Execute a user-initiated finite role with the request's interaction authority.
|
|
36
|
+
*
|
|
37
|
+
* The active context is acquired atomically, readiness runs before
|
|
38
|
+
* capability dispatch, and the settings snapshot is captured before the
|
|
39
|
+
* finite activity begins. A failed result invokes recovery only after that
|
|
40
|
+
* activity has settled.
|
|
41
|
+
*/
|
|
42
|
+
runUserInitiated(request: UserInitiatedOneShotRequest): Promise<ReplicationOutcome>;
|
|
43
|
+
/**
|
|
44
|
+
* Execute an unattended finite role without granting interaction authority.
|
|
45
|
+
*
|
|
46
|
+
* Requests carrying interaction authority are rejected before context
|
|
47
|
+
* acquisition. Accepted requests otherwise retain the same readiness,
|
|
48
|
+
* settings-snapshot, activity, and recovery ordering as user work.
|
|
49
|
+
*/
|
|
50
|
+
runUnattended(request: UnattendedOneShotRequest): Promise<ReplicationOutcome>;
|
|
51
|
+
/**
|
|
52
|
+
* Start the provider's continuous role outside finite activity accounting.
|
|
53
|
+
*
|
|
54
|
+
* Capability applicability is established before readiness, because an
|
|
55
|
+
* inapplicable long-lived role must not trigger unrelated preparation or
|
|
56
|
+
* diagnostics. After readiness, the short startup call is admitted against
|
|
57
|
+
* that exact publication. The provider owns the registered long-lived task
|
|
58
|
+
* after startup settles; it does not retain this admission for its lifetime.
|
|
59
|
+
* Startup failures become typed outcomes without invoking the finite-operation
|
|
60
|
+
* recovery handler.
|
|
61
|
+
*/
|
|
62
|
+
startContinuous(request: ContinuousReplicationRequest): Promise<ReplicationOutcome>;
|
|
63
|
+
/**
|
|
64
|
+
* Request transfer cancellation on one atomically acquired active context.
|
|
65
|
+
*
|
|
66
|
+
* Cancellation deliberately bypasses readiness, activity accounting, and
|
|
67
|
+
* failure-recovery handlers: it is an operator control over existing work,
|
|
68
|
+
* not a request to begin new remote work.
|
|
69
|
+
*/
|
|
70
|
+
stopActiveTransfer(): Promise<ReplicationOutcome>;
|
|
71
|
+
/**
|
|
72
|
+
* Admit one complete typed OneShot attempt for this service instance.
|
|
73
|
+
*
|
|
74
|
+
* Ownership is recorded before the operation reaches its first await and is
|
|
75
|
+
* retained through readiness, provider work, activity settlement, and
|
|
76
|
+
* failure handling. Later requests are neither queued nor joined: they
|
|
77
|
+
* settle immediately so repeated UI actions cannot replay after the owner.
|
|
78
|
+
*/
|
|
79
|
+
private runOneShot;
|
|
80
|
+
private finishFiniteAttempt;
|
|
81
|
+
private acquireContext;
|
|
82
|
+
private acquireReadyContext;
|
|
83
|
+
private runFiniteActivity;
|
|
84
|
+
/**
|
|
85
|
+
* Reserve the readiness-tested publication for provider dispatch.
|
|
86
|
+
*
|
|
87
|
+
* Readiness and settings capture intentionally happen before admission. If
|
|
88
|
+
* an ownership transition completes in between, the newly admitted context
|
|
89
|
+
* is rejected instead of running work prepared for an older publication.
|
|
90
|
+
* Failure recovery runs only after the reservation has been released.
|
|
91
|
+
*/
|
|
92
|
+
private runAdmittedFiniteActivity;
|
|
93
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CAPABILITY_SUPPORT_KINDS,
|
|
3
|
+
NO_INTERACTION,
|
|
4
|
+
REPLICATION_PROGRESS_PRESENTATIONS,
|
|
5
|
+
isActiveReplicatorContextBoundToSetting,
|
|
6
|
+
replicationBlocked,
|
|
7
|
+
replicationFailed
|
|
8
|
+
} from "../../replication/ReplicatorProvider.js";
|
|
9
|
+
import { asCopy } from "../../common/utils.object.js";
|
|
10
|
+
class TypedReplicationCoordinator {
|
|
11
|
+
constructor(dependencies) {
|
|
12
|
+
this.dependencies = dependencies;
|
|
13
|
+
}
|
|
14
|
+
dependencies;
|
|
15
|
+
oneShotInProgress = false;
|
|
16
|
+
/**
|
|
17
|
+
* Execute a user-initiated finite role with the request's interaction authority.
|
|
18
|
+
*
|
|
19
|
+
* The active context is acquired atomically, readiness runs before
|
|
20
|
+
* capability dispatch, and the settings snapshot is captured before the
|
|
21
|
+
* finite activity begins. A failed result invokes recovery only after that
|
|
22
|
+
* activity has settled.
|
|
23
|
+
*/
|
|
24
|
+
async runUserInitiated(request) {
|
|
25
|
+
return await this.runOneShot(async () => {
|
|
26
|
+
const showProgress = request.progressPresentation === REPLICATION_PROGRESS_PRESENTATIONS.NOTICE;
|
|
27
|
+
const ready = await this.acquireReadyContext(showProgress);
|
|
28
|
+
if ("status" in ready) return ready;
|
|
29
|
+
const capability = ready.provider.userInitiatedOneShot;
|
|
30
|
+
if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
|
31
|
+
return this.finishFiniteAttempt(replicationBlocked(capability.reason));
|
|
32
|
+
}
|
|
33
|
+
const settings = Object.freeze(asCopy(this.dependencies.currentSettings()));
|
|
34
|
+
return this.finishFiniteAttempt(
|
|
35
|
+
await this.runAdmittedFiniteActivity(
|
|
36
|
+
ready,
|
|
37
|
+
(context) => capability.run(context.replicator, settings, request),
|
|
38
|
+
settings,
|
|
39
|
+
request.interaction,
|
|
40
|
+
request.progressPresentation
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Execute an unattended finite role without granting interaction authority.
|
|
47
|
+
*
|
|
48
|
+
* Requests carrying interaction authority are rejected before context
|
|
49
|
+
* acquisition. Accepted requests otherwise retain the same readiness,
|
|
50
|
+
* settings-snapshot, activity, and recovery ordering as user work.
|
|
51
|
+
*/
|
|
52
|
+
async runUnattended(request) {
|
|
53
|
+
if (request.interaction.kind !== NO_INTERACTION.kind) {
|
|
54
|
+
return replicationBlocked("interaction-required");
|
|
55
|
+
}
|
|
56
|
+
return await this.runOneShot(async () => {
|
|
57
|
+
const ready = await this.acquireReadyContext(false);
|
|
58
|
+
if ("status" in ready) return ready;
|
|
59
|
+
const capability = ready.provider.unattendedOneShot;
|
|
60
|
+
if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
|
61
|
+
return this.finishFiniteAttempt(replicationBlocked(capability.reason));
|
|
62
|
+
}
|
|
63
|
+
const settings = Object.freeze(asCopy(this.dependencies.currentSettings()));
|
|
64
|
+
return this.finishFiniteAttempt(
|
|
65
|
+
await this.runAdmittedFiniteActivity(
|
|
66
|
+
ready,
|
|
67
|
+
(context) => capability.run(context.replicator, settings, request),
|
|
68
|
+
settings,
|
|
69
|
+
NO_INTERACTION,
|
|
70
|
+
REPLICATION_PROGRESS_PRESENTATIONS.QUIET
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Start the provider's continuous role outside finite activity accounting.
|
|
77
|
+
*
|
|
78
|
+
* Capability applicability is established before readiness, because an
|
|
79
|
+
* inapplicable long-lived role must not trigger unrelated preparation or
|
|
80
|
+
* diagnostics. After readiness, the short startup call is admitted against
|
|
81
|
+
* that exact publication. The provider owns the registered long-lived task
|
|
82
|
+
* after startup settles; it does not retain this admission for its lifetime.
|
|
83
|
+
* Startup failures become typed outcomes without invoking the finite-operation
|
|
84
|
+
* recovery handler.
|
|
85
|
+
*/
|
|
86
|
+
async startContinuous(request) {
|
|
87
|
+
if (request.interaction.kind !== NO_INTERACTION.kind) {
|
|
88
|
+
return replicationBlocked("interaction-required");
|
|
89
|
+
}
|
|
90
|
+
const context = await this.acquireContext();
|
|
91
|
+
if ("status" in context) return context;
|
|
92
|
+
const capability = context.provider.continuous;
|
|
93
|
+
if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
|
94
|
+
return replicationBlocked(capability.reason);
|
|
95
|
+
}
|
|
96
|
+
const ready = await this.acquireReadyContext(false, context);
|
|
97
|
+
if ("status" in ready) return ready;
|
|
98
|
+
const setting = Object.freeze(asCopy(this.dependencies.currentSettings()));
|
|
99
|
+
const admitted = await this.dependencies.replicatorService.runWithActiveReplicatorContext(
|
|
100
|
+
async (activeContext) => {
|
|
101
|
+
if (activeContext !== ready || !isActiveReplicatorContextBoundToSetting(activeContext, setting)) {
|
|
102
|
+
return replicationBlocked("not-ready");
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return await capability.run(activeContext.replicator, setting, request);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return replicationFailed(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
return admitted ?? replicationBlocked("no-active-replicator");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Request transfer cancellation on one atomically acquired active context.
|
|
115
|
+
*
|
|
116
|
+
* Cancellation deliberately bypasses readiness, activity accounting, and
|
|
117
|
+
* failure-recovery handlers: it is an operator control over existing work,
|
|
118
|
+
* not a request to begin new remote work.
|
|
119
|
+
*/
|
|
120
|
+
async stopActiveTransfer() {
|
|
121
|
+
const context = await this.acquireContext();
|
|
122
|
+
if ("status" in context) return context;
|
|
123
|
+
const capability = context.provider.stopActiveTransfer;
|
|
124
|
+
if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
|
125
|
+
return replicationBlocked(capability.reason);
|
|
126
|
+
}
|
|
127
|
+
const admitted = await this.dependencies.replicatorService.runWithActiveReplicatorContext(
|
|
128
|
+
async (activeContext) => {
|
|
129
|
+
if (activeContext !== context) return replicationBlocked("not-ready");
|
|
130
|
+
try {
|
|
131
|
+
return await capability.run(activeContext.replicator);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return replicationFailed(error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
return admitted ?? replicationBlocked("no-active-replicator");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Admit one complete typed OneShot attempt for this service instance.
|
|
141
|
+
*
|
|
142
|
+
* Ownership is recorded before the operation reaches its first await and is
|
|
143
|
+
* retained through readiness, provider work, activity settlement, and
|
|
144
|
+
* failure handling. Later requests are neither queued nor joined: they
|
|
145
|
+
* settle immediately so repeated UI actions cannot replay after the owner.
|
|
146
|
+
*/
|
|
147
|
+
async runOneShot(run) {
|
|
148
|
+
if (this.oneShotInProgress) return replicationBlocked("replication-in-progress");
|
|
149
|
+
this.oneShotInProgress = true;
|
|
150
|
+
try {
|
|
151
|
+
return await run();
|
|
152
|
+
} finally {
|
|
153
|
+
this.oneShotInProgress = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
finishFiniteAttempt(outcome) {
|
|
157
|
+
this.dependencies.recordFiniteAttempt();
|
|
158
|
+
return outcome;
|
|
159
|
+
}
|
|
160
|
+
async acquireContext() {
|
|
161
|
+
const context = await this.dependencies.replicatorService.acquireActiveReplicatorContext();
|
|
162
|
+
if (context) return context;
|
|
163
|
+
return this.dependencies.replicatorService.hasActiveReplicator() ? replicationBlocked("provider-not-composed") : replicationBlocked("no-active-replicator");
|
|
164
|
+
}
|
|
165
|
+
async acquireReadyContext(showMessage, contextOverride) {
|
|
166
|
+
const context = contextOverride ?? await this.acquireContext();
|
|
167
|
+
if ("status" in context) return context;
|
|
168
|
+
if (!await this.dependencies.checkReadiness(showMessage, context.provider.readiness)) {
|
|
169
|
+
return replicationBlocked("not-ready");
|
|
170
|
+
}
|
|
171
|
+
return context;
|
|
172
|
+
}
|
|
173
|
+
async runFiniteActivity(run) {
|
|
174
|
+
try {
|
|
175
|
+
return await this.dependencies.replicatorService.runFiniteReplicationActivity(run, {
|
|
176
|
+
label: "replication"
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return replicationFailed(error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Reserve the readiness-tested publication for provider dispatch.
|
|
184
|
+
*
|
|
185
|
+
* Readiness and settings capture intentionally happen before admission. If
|
|
186
|
+
* an ownership transition completes in between, the newly admitted context
|
|
187
|
+
* is rejected instead of running work prepared for an older publication.
|
|
188
|
+
* Failure recovery runs only after the reservation has been released.
|
|
189
|
+
*/
|
|
190
|
+
async runAdmittedFiniteActivity(expectedContext, run, setting, interaction, progressPresentation) {
|
|
191
|
+
const admitted = await this.dependencies.replicatorService.runWithActiveReplicatorContext((context) => {
|
|
192
|
+
if (context !== expectedContext || !isActiveReplicatorContextBoundToSetting(context, setting)) {
|
|
193
|
+
return replicationBlocked("not-ready");
|
|
194
|
+
}
|
|
195
|
+
return this.runFiniteActivity(() => run(context));
|
|
196
|
+
});
|
|
197
|
+
const result = admitted ?? replicationBlocked("no-active-replicator");
|
|
198
|
+
if (result.status === "failed") {
|
|
199
|
+
await this.dependencies.handleFailure(
|
|
200
|
+
Object.freeze({
|
|
201
|
+
context: expectedContext,
|
|
202
|
+
setting,
|
|
203
|
+
outcome: result,
|
|
204
|
+
progressPresentation,
|
|
205
|
+
interaction
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
export {
|
|
213
|
+
TypedReplicationCoordinator
|
|
214
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReplicatorInstance } from "../../replication/ReplicatorInstance.js";
|
|
2
|
+
import type { ActiveReplicatorContext, ReplicatorProviderDefinition } from "../../replication/ReplicatorProvider.js";
|
|
3
|
+
/** Atomic service publication used as both the active view and generation identity. */
|
|
4
|
+
export interface ActiveReplicatorPublication {
|
|
5
|
+
readonly replicator: ReplicatorInstance;
|
|
6
|
+
readonly replicatorType: string;
|
|
7
|
+
readonly context: ActiveReplicatorContext | undefined;
|
|
8
|
+
}
|
|
9
|
+
/** One admitted use of an exact active publication. */
|
|
10
|
+
export interface ActiveReplicatorReservation {
|
|
11
|
+
readonly context: ActiveReplicatorContext;
|
|
12
|
+
/** Release this reservation. Repeated calls have no effect. */
|
|
13
|
+
release(): void;
|
|
14
|
+
}
|
|
15
|
+
/** Private service-side control over one fenced publication. */
|
|
16
|
+
export interface ActiveReplicatorRetirement {
|
|
17
|
+
readonly publication: ActiveReplicatorPublication;
|
|
18
|
+
/** Wait until every use admitted before the fence has settled. */
|
|
19
|
+
waitForDemandSettlement(): Promise<void>;
|
|
20
|
+
/** Mark physical close complete. Repeated calls have no effect. */
|
|
21
|
+
complete(): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Keep active publication, admission, and quiescing retirement as one state.
|
|
25
|
+
*
|
|
26
|
+
* The publication object itself is the generation identity. Retirement first
|
|
27
|
+
* removes it from `current`, then waits only for reservations captured from
|
|
28
|
+
* that object. A replacement cannot be published until physical retirement is
|
|
29
|
+
* explicitly completed.
|
|
30
|
+
*/
|
|
31
|
+
export declare class ActiveReplicatorState {
|
|
32
|
+
private activeOwner;
|
|
33
|
+
private retiringOwner;
|
|
34
|
+
private retirement;
|
|
35
|
+
get current(): ActiveReplicatorPublication | undefined;
|
|
36
|
+
publish(provider: ReplicatorProviderDefinition | undefined, replicator: ReplicatorInstance, replicatorType: string, configurationIdentity: string | undefined): void;
|
|
37
|
+
/** Reserve the current typed publication, or reject admission while quiescing. */
|
|
38
|
+
reserve(): ActiveReplicatorReservation | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Fence the current publication and begin its explicit quiescing transition.
|
|
41
|
+
*
|
|
42
|
+
* Completion is separate from demand settlement so callers cannot publish a
|
|
43
|
+
* replacement between draining admitted work and physically closing the old
|
|
44
|
+
* Replicator.
|
|
45
|
+
*/
|
|
46
|
+
beginRetirement(): ActiveReplicatorRetirement | undefined;
|
|
47
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
class ActiveReplicatorPublicationOwner {
|
|
2
|
+
constructor(publication) {
|
|
3
|
+
this.publication = publication;
|
|
4
|
+
}
|
|
5
|
+
publication;
|
|
6
|
+
accepting = true;
|
|
7
|
+
demandCount = 0;
|
|
8
|
+
demandSettlement;
|
|
9
|
+
settleDemand;
|
|
10
|
+
reserve() {
|
|
11
|
+
const context = this.publication.context;
|
|
12
|
+
if (!this.accepting || !context) {
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
this.demandCount++;
|
|
16
|
+
let released = false;
|
|
17
|
+
return {
|
|
18
|
+
context,
|
|
19
|
+
release: () => {
|
|
20
|
+
if (released) return;
|
|
21
|
+
released = true;
|
|
22
|
+
this.demandCount--;
|
|
23
|
+
if (this.demandCount === 0) {
|
|
24
|
+
this.settleDemand?.();
|
|
25
|
+
this.settleDemand = void 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
stopAccepting() {
|
|
31
|
+
this.accepting = false;
|
|
32
|
+
}
|
|
33
|
+
waitForDemandSettlement() {
|
|
34
|
+
if (this.demandCount === 0) {
|
|
35
|
+
return Promise.resolve();
|
|
36
|
+
}
|
|
37
|
+
this.demandSettlement ??= new Promise((resolve) => {
|
|
38
|
+
this.settleDemand = resolve;
|
|
39
|
+
});
|
|
40
|
+
return this.demandSettlement;
|
|
41
|
+
}
|
|
42
|
+
get hasDemand() {
|
|
43
|
+
return this.demandCount !== 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
class ActiveReplicatorState {
|
|
47
|
+
activeOwner;
|
|
48
|
+
retiringOwner;
|
|
49
|
+
retirement;
|
|
50
|
+
get current() {
|
|
51
|
+
return this.activeOwner?.publication;
|
|
52
|
+
}
|
|
53
|
+
publish(provider, replicator, replicatorType, configurationIdentity) {
|
|
54
|
+
if (this.activeOwner) {
|
|
55
|
+
throw new Error("Cannot replace an active Replicator without beginning retirement.");
|
|
56
|
+
}
|
|
57
|
+
if (this.retiringOwner) {
|
|
58
|
+
throw new Error("Cannot publish an active Replicator before retirement has completed.");
|
|
59
|
+
}
|
|
60
|
+
this.activeOwner = new ActiveReplicatorPublicationOwner({
|
|
61
|
+
replicator,
|
|
62
|
+
replicatorType,
|
|
63
|
+
context: provider && configurationIdentity !== void 0 ? { provider, replicator, configurationIdentity } : void 0
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Reserve the current typed publication, or reject admission while quiescing. */
|
|
67
|
+
reserve() {
|
|
68
|
+
return this.activeOwner?.reserve();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fence the current publication and begin its explicit quiescing transition.
|
|
72
|
+
*
|
|
73
|
+
* Completion is separate from demand settlement so callers cannot publish a
|
|
74
|
+
* replacement between draining admitted work and physically closing the old
|
|
75
|
+
* Replicator.
|
|
76
|
+
*/
|
|
77
|
+
beginRetirement() {
|
|
78
|
+
if (this.retirement) return this.retirement;
|
|
79
|
+
const owner = this.activeOwner;
|
|
80
|
+
if (!owner) return void 0;
|
|
81
|
+
owner.stopAccepting();
|
|
82
|
+
this.activeOwner = void 0;
|
|
83
|
+
this.retiringOwner = owner;
|
|
84
|
+
let completed = false;
|
|
85
|
+
const retirement = {
|
|
86
|
+
publication: owner.publication,
|
|
87
|
+
waitForDemandSettlement: () => owner.waitForDemandSettlement(),
|
|
88
|
+
complete: () => {
|
|
89
|
+
if (completed) return;
|
|
90
|
+
if (owner.hasDemand) {
|
|
91
|
+
throw new Error("Cannot complete active Replicator retirement while admitted work remains.");
|
|
92
|
+
}
|
|
93
|
+
if (this.retiringOwner !== owner) {
|
|
94
|
+
throw new Error("Active Replicator retirement no longer owns the quiescing state.");
|
|
95
|
+
}
|
|
96
|
+
completed = true;
|
|
97
|
+
this.retiringOwner = void 0;
|
|
98
|
+
this.retirement = void 0;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
this.retirement = retirement;
|
|
102
|
+
return retirement;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
ActiveReplicatorState
|
|
107
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RemoteDBSettings } from "../../common/types.js";
|
|
2
|
+
import { type CentralRemoteAdministrationRequest, type CentralRemoteAdministrationResult } from "../../replication/CentralRemoteAdministration.js";
|
|
3
|
+
import { type ActiveReplicatorContext } from "../../replication/ReplicatorProvider.js";
|
|
4
|
+
/** Stop, then dispatch central-remote administration through the atomically captured provider. */
|
|
5
|
+
export declare function runCentralRemoteAdministrationWithContext(context: ActiveReplicatorContext | undefined, setting: RemoteDBSettings, request: CentralRemoteAdministrationRequest): Promise<CentralRemoteAdministrationResult>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS,
|
|
3
|
+
centralRemoteAdministrationVerificationFailed
|
|
4
|
+
} from "../../replication/CentralRemoteAdministration.js";
|
|
5
|
+
import { CAPABILITY_SUPPORT_KINDS } from "../../replication/ProviderCapability.js";
|
|
6
|
+
import {
|
|
7
|
+
isActiveReplicatorContextBoundToSetting
|
|
8
|
+
} from "../../replication/ReplicatorProvider.js";
|
|
9
|
+
async function stopActiveTransfer(context) {
|
|
10
|
+
const capability = context.provider.stopActiveTransfer;
|
|
11
|
+
if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
|
12
|
+
throw new Error("The active Replicator does not support the stop required for central remote administration.");
|
|
13
|
+
}
|
|
14
|
+
const outcome = await capability.run(context.replicator);
|
|
15
|
+
if (outcome.status === "completed") return;
|
|
16
|
+
if (outcome.status === "failed") throw outcome.error;
|
|
17
|
+
throw new Error("The active Replicator transfer did not stop before central remote administration.");
|
|
18
|
+
}
|
|
19
|
+
async function runCentralRemoteAdministrationWithContext(context, setting, request) {
|
|
20
|
+
if (!context) {
|
|
21
|
+
return centralRemoteAdministrationVerificationFailed(
|
|
22
|
+
CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.NO_ACTIVE_REPLICATOR
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (!isActiveReplicatorContextBoundToSetting(context, setting)) {
|
|
26
|
+
return centralRemoteAdministrationVerificationFailed(
|
|
27
|
+
CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.ACTIVE_CONFIGURATION_MISMATCH
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const capability = context.provider.centralRemoteAdministration;
|
|
31
|
+
if (!capability) {
|
|
32
|
+
return centralRemoteAdministrationVerificationFailed(
|
|
33
|
+
CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.CAPABILITY_NOT_APPLICABLE
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
|
37
|
+
return centralRemoteAdministrationVerificationFailed(capability.reason);
|
|
38
|
+
}
|
|
39
|
+
await stopActiveTransfer(context);
|
|
40
|
+
return await capability.run(context.replicator, setting, request);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
runCentralRemoteAdministrationWithContext
|
|
44
|
+
};
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type
|
|
1
|
+
import type { ReplicatorInstance } from "../../replication/ReplicatorInstance.js";
|
|
2
|
+
import { type MultipleHandlerFunction } from "../lib/HandlerUtils.js";
|
|
3
|
+
import type { IReplicatorService, ReplicatorFactoryCallback } from "./IService.js";
|
|
3
4
|
import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
|
|
4
5
|
import type { SettingService } from "./SettingService.js";
|
|
5
6
|
import type { AppLifecycleService } from "./AppLifecycleService.js";
|
|
6
7
|
import { UnresolvedErrorManager } from "./UnresolvedErrorManager.js";
|
|
7
8
|
import type { DatabaseEventService } from "./DatabaseEventService.js";
|
|
8
9
|
import { type AsyncActivityOptions, type AsyncActivityRunner } from "../../interfaces/AsyncActivityRunner.js";
|
|
10
|
+
import type { ActiveReplicatorContext, ReplicatorProviderDefinitionMap } from "../../replication/ReplicatorProvider.js";
|
|
11
|
+
import type { RemoteDBSettings } from "../../common/types.js";
|
|
12
|
+
import type { RemoteResourceKind, RemoteResourceMap } from "../../replication/RemoteResource.js";
|
|
13
|
+
import type { CentralRemoteAdministrationRequest, CentralRemoteAdministrationResult } from "../../replication/CentralRemoteAdministration.js";
|
|
9
14
|
export interface ReplicatorServiceDependencies {
|
|
10
15
|
settingService: SettingService;
|
|
11
16
|
appLifecycleService: AppLifecycleService;
|
|
@@ -14,7 +19,13 @@ export interface ReplicatorServiceDependencies {
|
|
|
14
19
|
registerLifecycleHandlers?: boolean;
|
|
15
20
|
}
|
|
16
21
|
/**
|
|
17
|
-
*
|
|
22
|
+
* Own the active Replicator publication and its serialised lifecycle transitions.
|
|
23
|
+
*
|
|
24
|
+
* This service composes provider definitions, retires active instances when
|
|
25
|
+
* their configuration identity changes, and publishes the provider and
|
|
26
|
+
* Replicator atomically. Remote administration is dispatched through the
|
|
27
|
+
* admitted active context; `ReplicationService` owns synchronisation
|
|
28
|
+
* readiness and replication dispatch.
|
|
18
29
|
*/
|
|
19
30
|
export declare abstract class ReplicatorService<T extends ServiceContext = ServiceContext> extends ServiceBase<T> implements IReplicatorService {
|
|
20
31
|
protected dependencies: ReplicatorServiceDependencies;
|
|
@@ -23,8 +34,9 @@ export declare abstract class ReplicatorService<T extends ServiceContext = Servi
|
|
|
23
34
|
_log: (msg: unknown, level?: import("octagonal-wheels/common/logger").LOG_LEVEL, key?: string) => void;
|
|
24
35
|
private settingService;
|
|
25
36
|
private databaseEventService;
|
|
26
|
-
private
|
|
27
|
-
private
|
|
37
|
+
private readonly _activeReplicatorState;
|
|
38
|
+
private _providerDefinitions;
|
|
39
|
+
private _transition;
|
|
28
40
|
private appLifecycleService;
|
|
29
41
|
_unresolvedErrorManager: UnresolvedErrorManager;
|
|
30
42
|
constructor(context: T, dependencies: ReplicatorServiceDependencies);
|
|
@@ -36,26 +48,75 @@ export declare abstract class ReplicatorService<T extends ServiceContext = Servi
|
|
|
36
48
|
/** Runs one finite replication and exposes its document-delivery lifetime. */
|
|
37
49
|
runFiniteReplicationActivity<TValue>(task: () => TValue | PromiseLike<TValue>, options?: AsyncActivityOptions): Promise<TValue>;
|
|
38
50
|
private runRemoteActivity;
|
|
51
|
+
/** Request a reversible transfer stop without retiring the active publication. */
|
|
39
52
|
private suspendReplication;
|
|
40
53
|
private reinitialiseReplicator;
|
|
41
|
-
|
|
54
|
+
/** Request transfer cancellation for either a reversible pause or terminal retirement. */
|
|
55
|
+
private requestPublicationTransferStop;
|
|
56
|
+
/** Fence new work, request cancellation, and drain only this publication's admitted tasks. */
|
|
57
|
+
private quiesceActiveReplicator;
|
|
58
|
+
/** Close one drained publication and release its private retirement state. */
|
|
59
|
+
private closeRetiringReplicator;
|
|
60
|
+
private disposeReplicatorNow;
|
|
42
61
|
private discardFailedReplicator;
|
|
62
|
+
private enqueueTransition;
|
|
43
63
|
private _initialiseReplicator;
|
|
64
|
+
private getProviderDefinition;
|
|
65
|
+
private createReplicator;
|
|
66
|
+
private createReplicatorForSetting;
|
|
67
|
+
private isCurrentProviderConfiguration;
|
|
68
|
+
private publishActiveReplicator;
|
|
69
|
+
private initialiseReplicatorNow;
|
|
44
70
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
71
|
+
* Fence new work, drain admitted work, and close the active Replicator.
|
|
72
|
+
*
|
|
73
|
+
* The command is serialised with every other ownership transition. Callers
|
|
74
|
+
* must not await it from inside `runWithActiveReplicatorContext()`, because
|
|
75
|
+
* retirement waits for that admitted callback to settle.
|
|
47
76
|
*/
|
|
48
|
-
|
|
77
|
+
onCloseActiveReplication(): Promise<boolean>;
|
|
49
78
|
/**
|
|
50
79
|
* Get a new replicator instance based on the provided settings.
|
|
51
80
|
*/
|
|
52
|
-
readonly
|
|
53
|
-
readonly
|
|
81
|
+
private readonly legacyGetNewReplicator;
|
|
82
|
+
readonly getNewReplicator: MultipleHandlerFunction<ReplicatorFactoryCallback>;
|
|
83
|
+
registerReplicatorProviderDefinitions(definitions: ReplicatorProviderDefinitionMap): void;
|
|
54
84
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
85
|
+
* Return a non-owning synchronous view for lifecycle diagnostics and focused tests.
|
|
86
|
+
*
|
|
87
|
+
* Production work must use an admitted acquisition or callback boundary so that
|
|
88
|
+
* the context cannot be retired while it is in use.
|
|
57
89
|
*/
|
|
58
|
-
|
|
90
|
+
protected inspectActiveReplicatorContext(): ActiveReplicatorContext | undefined;
|
|
91
|
+
createRemoteResource<TKind extends RemoteResourceKind>(kind: TKind, setting: RemoteDBSettings): Promise<RemoteResourceMap[TKind] | undefined>;
|
|
92
|
+
/** Apply and verify one provider-declared action before permitting a later switch. */
|
|
93
|
+
runCentralRemoteAdministration(request: CentralRemoteAdministrationRequest): Promise<CentralRemoteAdministrationResult>;
|
|
94
|
+
acquireActiveReplicatorContext(): Promise<ActiveReplicatorContext | undefined>;
|
|
95
|
+
/**
|
|
96
|
+
* Admit one callback against the publication ordered before later transitions.
|
|
97
|
+
*
|
|
98
|
+
* Release never enters the transition queue. The callback must not initiate
|
|
99
|
+
* or await a settings, database, or Replicator lifecycle transition which
|
|
100
|
+
* would wait for this same reservation.
|
|
101
|
+
*/
|
|
102
|
+
runWithActiveReplicatorContext<TResult>(task: (context: ActiveReplicatorContext) => TResult | PromiseLike<TResult>): Promise<TResult | undefined>;
|
|
103
|
+
readonly onBeforeReplicatorPublication: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<() => Promise<boolean>>;
|
|
104
|
+
/**
|
|
105
|
+
* Return the legacy unreserved view of the active Replicator.
|
|
106
|
+
*
|
|
107
|
+
* Missing active state retains its established Notice-level diagnostic.
|
|
108
|
+
* New work must acquire or admit an `ActiveReplicatorContext` instead.
|
|
109
|
+
*/
|
|
110
|
+
getActiveReplicator(): ReplicatorInstance | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* Classify whether a compatibility active Replicator exists without
|
|
113
|
+
* acquiring or returning it.
|
|
114
|
+
*
|
|
115
|
+
* This side-effect-free, non-owning predicate exists only for
|
|
116
|
+
* compatibility-state classification. Work must acquire or admit an
|
|
117
|
+
* `ActiveReplicatorContext` before using a Replicator.
|
|
118
|
+
*/
|
|
119
|
+
hasActiveReplicator(): boolean;
|
|
59
120
|
replicationStatics: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource<{
|
|
60
121
|
sent: number;
|
|
61
122
|
arrived: number;
|