@vrtmrz/livesync-commonlib 0.1.44 → 0.2.1
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 +80 -4
- package/dist/API/DirectFileManipulatorV2.d.ts +2 -0
- package/dist/API/DirectFileManipulatorV2.js +3 -0
- package/dist/API/processSetting.d.ts +1 -0
- package/dist/common/configForDoc.d.ts +1 -0
- package/dist/common/configForDoc.js +11 -1
- package/dist/common/models/db.definition.d.ts +1 -0
- package/dist/common/models/setting.const.defaults.js +1 -0
- package/dist/common/models/setting.const.preferred.js +1 -3
- package/dist/common/models/setting.const.qr.js +1 -0
- package/dist/common/models/setting.policy.d.ts +4 -0
- package/dist/common/models/setting.policy.js +1 -0
- package/dist/common/models/setting.type.d.ts +2 -0
- package/dist/common/models/shared.definition.d.ts +20 -0
- package/dist/common/models/tweak.definition.d.ts +1 -0
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/common/settingConstants.js +6 -2
- package/dist/interfaces/FileHandler.d.ts +5 -1
- package/dist/interfaces/StorageEventManager.d.ts +5 -0
- package/dist/interfaces/StorageEventManager.js +7 -0
- package/dist/managers/EntryManager/EntryManagerImpls.js +3 -2
- package/dist/managers/StorageEventManager.d.ts +19 -10
- package/dist/managers/StorageEventManager.js +259 -189
- package/dist/pouchdb/LiveSyncDBFunctions.js +4 -0
- package/dist/pouchdb/StreamingFetch.d.ts +5 -1
- package/dist/pouchdb/StreamingFetch.js +529 -65
- package/dist/replication/LiveSyncAbstractReplicator.d.ts +2 -0
- package/dist/replication/LiveSyncAbstractReplicator.js +5 -2
- package/dist/replication/RemoteResource.d.ts +23 -2
- package/dist/replication/RemoteResource.js +6 -2
- package/dist/replication/couchdb/LiveSyncReplicator.d.ts +14 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +171 -3
- package/dist/replication/couchdb/fastForward.d.ts +19 -0
- package/dist/replication/couchdb/fastForward.js +119 -0
- package/dist/replication/couchdb/replicationProgress.d.ts +6 -0
- package/dist/replication/couchdb/replicationProgress.js +105 -0
- package/dist/replication/index.d.ts +4 -1
- package/dist/replication/joinedDevices.d.ts +25 -0
- package/dist/replication/joinedDevices.js +123 -0
- package/dist/replication/journal/JournalSyncCore.d.ts +2 -1
- package/dist/replication/journal/JournalSyncCore.js +11 -1
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +4 -0
- package/dist/replication/journal/LiveSyncJournalReplicator.js +56 -1
- package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +3 -0
- package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -1
- package/dist/replication/journal/objectstore/MinioStorageAdapter.js +27 -1
- package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +1 -1
- package/dist/replication/trystero/P2PRoomSessionOwner.js +27 -14
- package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
- package/dist/serviceFeatures/offlineScanner.js +19 -7
- package/dist/serviceModules/Rebuilder.js +41 -15
- package/dist/serviceModules/ServiceFileAccessBase.js +5 -0
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +8 -5
- package/dist/serviceModules/ServiceFileHandlerBase.js +75 -38
- package/dist/services/BrowserServices.d.ts +4 -2
- package/dist/services/BrowserServices.js +2 -5
- package/dist/services/HeadlessServices.d.ts +3 -0
- package/dist/services/HeadlessServices.js +2 -5
- package/dist/services/ServiceHub.d.ts +4 -4
- package/dist/services/base/AppLifecycleService.js +1 -1
- package/dist/services/base/ControlService.js +6 -2
- package/dist/services/base/FileProcessingService.d.ts +7 -1
- package/dist/services/base/FileProcessingService.js +9 -1
- package/dist/services/base/IService.d.ts +19 -19
- package/dist/services/base/ReplicatorService.d.ts +1 -0
- package/dist/services/base/ReplicatorService.remoteResourceResolver.js +1 -1
- package/dist/services/base/SettingService.d.ts +8 -18
- package/dist/services/base/SettingService.js +28 -46
- package/dist/services/implements/base/SvelteDialog.d.ts +2 -2
- package/dist/services/implements/base/UIService.d.ts +2 -0
- package/dist/services/implements/base/UIService.js +5 -0
- package/dist/services/implements/injectable/InjectableServiceHub.d.ts +4 -4
- package/dist/services/implements/injectable/InjectableServices.d.ts +2 -2
- package/dist/services/implements/injectable/InjectableSettingService.d.ts +0 -3
- package/dist/services/implements/injectable/InjectableSettingService.js +0 -11
- package/dist/services/lib/HandlerUtils.d.ts +60 -0
- package/dist/services/lib/HandlerUtils.js +46 -0
- package/dist/string_and_binary/chunks.js +7 -5
- package/dist/uiCommands.d.ts +26 -0
- package/dist/uiCommands.js +57 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/development.md +15 -3
- package/docs/offline-scanner.md +15 -13
- package/docs/proven-in-use.md +38 -4
- package/docs/releasing.md +20 -13
- package/docs/settings-lifecycle.md +39 -5
- package/docs/storage-events-and-reflection.md +109 -10
- package/package.json +13 -18
- package/dist/services/base/ConfigService.d.ts +0 -7
- package/dist/services/base/ConfigService.js +0 -6
- package/dist/services/implements/browser/ConfigServiceBrowserCompat.d.ts +0 -16
- package/dist/services/implements/browser/ConfigServiceBrowserCompat.js +0 -23
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReplicationStatics } from "../common/models/shared.definition.js";
|
|
1
2
|
import { type EntryDoc, type DatabaseConnectingStatus, type RemoteDBSettings, type TweakValues, type RemotePreferredTweakResult, type NodeData } from "../common/types.js";
|
|
2
3
|
import type { RequiredServices } from "../interfaces/ServiceModule.js";
|
|
3
4
|
import type { TranslationParameters } from "../services/base/MessageTranslator.js";
|
|
@@ -62,6 +63,7 @@ export declare abstract class LiveSyncAbstractReplicator implements ReplicatorIn
|
|
|
62
63
|
constructor(env: LiveSyncReplicatorEnv);
|
|
63
64
|
abstract terminateSync(): void | Promise<void>;
|
|
64
65
|
abstract openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, ignoreCleanLock: boolean): Promise<void | boolean>;
|
|
66
|
+
protected getReplicationStatics(): ReplicationStatics;
|
|
65
67
|
updateInfo: () => void;
|
|
66
68
|
abstract tryConnectRemote(setting: RemoteDBSettings, showResult?: boolean): Promise<boolean>;
|
|
67
69
|
abstract replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise<boolean>;
|
|
@@ -75,8 +75,8 @@ class LiveSyncAbstractReplicator {
|
|
|
75
75
|
constructor(env) {
|
|
76
76
|
this.env = env;
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
getReplicationStatics() {
|
|
79
|
+
return {
|
|
80
80
|
sent: this.docSent,
|
|
81
81
|
arrived: this.docArrived,
|
|
82
82
|
maxPullSeq: this.maxPullSeq,
|
|
@@ -85,6 +85,9 @@ class LiveSyncAbstractReplicator {
|
|
|
85
85
|
lastSyncPushSeq: this.lastSyncPushSeq,
|
|
86
86
|
syncStatus: this.syncStatus
|
|
87
87
|
};
|
|
88
|
+
}
|
|
89
|
+
updateInfo = () => {
|
|
90
|
+
this.env.services.replicator.replicationStatics.value = this.getReplicationStatics();
|
|
88
91
|
};
|
|
89
92
|
}
|
|
90
93
|
export {
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { RemoteDBSettings, RemotePreferredTweakResult } from "../common/types.js";
|
|
2
|
+
import type { JoinedDevicesSnapshot, ForgetJoinedDeviceResult } from "./joinedDevices.js";
|
|
2
3
|
import type { RemoteDBStatus } from "./LiveSyncAbstractReplicator.js";
|
|
3
4
|
import { type CapabilitySupport } from "./ProviderCapability.js";
|
|
5
|
+
import type { InitialFetchFastForward } from "./couchdb/fastForward.js";
|
|
4
6
|
/** Stable machine identifiers for finite provider-owned remote resources. */
|
|
5
7
|
export declare const REMOTE_RESOURCE_KINDS: Readonly<{
|
|
6
8
|
readonly CONNECTION: "connection";
|
|
9
|
+
readonly JOINED_DEVICES: "joined-devices";
|
|
7
10
|
readonly PREFERRED_TWEAK: "preferred-tweak";
|
|
8
11
|
readonly SECURITY_SEED: "security-seed";
|
|
9
12
|
readonly SYNCHRONISATION_INFORMATION: "synchronisation-information";
|
|
13
|
+
readonly INITIAL_FETCH_FAST_FORWARD: "initial-fetch-fast-forward";
|
|
10
14
|
}>;
|
|
11
15
|
export type RemoteResourceKind = (typeof REMOTE_RESOURCE_KINDS)[keyof typeof REMOTE_RESOURCE_KINDS];
|
|
12
16
|
/** Options understood by a provider's finite remote-connection resource. */
|
|
@@ -64,22 +68,39 @@ export interface SynchronisationInformationResource {
|
|
|
64
68
|
check(): Promise<boolean>;
|
|
65
69
|
dispose(): Promise<void>;
|
|
66
70
|
}
|
|
71
|
+
/** Checkpoint handoff owned exclusively by one fresh Fast Fetch. */
|
|
72
|
+
export interface InitialFetchFastForwardResource {
|
|
73
|
+
prepare(): Promise<InitialFetchFastForward | undefined>;
|
|
74
|
+
dispose(): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
/** An owned reader and editor of device records for one remote settings snapshot. */
|
|
77
|
+
export interface JoinedDevicesResource {
|
|
78
|
+
read(): Promise<JoinedDevicesSnapshot>;
|
|
79
|
+
forget(deviceId: string): Promise<ForgetJoinedDeviceResult>;
|
|
80
|
+
dispose(): Promise<void>;
|
|
81
|
+
}
|
|
67
82
|
export type RemoteResourceMap = {
|
|
83
|
+
readonly [REMOTE_RESOURCE_KINDS.JOINED_DEVICES]: JoinedDevicesResource;
|
|
68
84
|
readonly [REMOTE_RESOURCE_KINDS.CONNECTION]: RemoteConnectionProbe;
|
|
69
85
|
readonly [REMOTE_RESOURCE_KINDS.PREFERRED_TWEAK]: PreferredTweakProbe;
|
|
70
86
|
readonly [REMOTE_RESOURCE_KINDS.SECURITY_SEED]: SecuritySeedResource;
|
|
71
87
|
readonly [REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION]: SynchronisationInformationResource;
|
|
88
|
+
readonly [REMOTE_RESOURCE_KINDS.INITIAL_FETCH_FAST_FORWARD]: InitialFetchFastForwardResource;
|
|
72
89
|
};
|
|
73
90
|
export type RemoteResourceFactoryMap = {
|
|
74
91
|
readonly [K in RemoteResourceKind]: (setting: RemoteDBSettings) => Promise<RemoteResourceMap[K]>;
|
|
75
92
|
};
|
|
76
|
-
/**
|
|
93
|
+
/** Resource support catalogue; older hosts may omit the initial-fetch optimisation. */
|
|
77
94
|
export type RemoteResourceCapabilities = {
|
|
78
|
-
readonly [K in RemoteResourceKind]: CapabilitySupport<RemoteResourceFactoryMap[K]>;
|
|
95
|
+
readonly [K in Exclude<RemoteResourceKind, typeof REMOTE_RESOURCE_KINDS.INITIAL_FETCH_FAST_FORWARD>]: CapabilitySupport<RemoteResourceFactoryMap[K]>;
|
|
96
|
+
} & {
|
|
97
|
+
readonly [REMOTE_RESOURCE_KINDS.INITIAL_FETCH_FAST_FORWARD]?: CapabilitySupport<RemoteResourceFactoryMap[typeof REMOTE_RESOURCE_KINDS.INITIAL_FETCH_FAST_FORWARD]>;
|
|
79
98
|
};
|
|
80
99
|
/** Explicit catalogue for providers which own no finite remote resources. */
|
|
81
100
|
export declare const NO_REMOTE_RESOURCE_CAPABILITIES: RemoteResourceCapabilities;
|
|
82
101
|
export type ConnectionProbeFactory = RemoteResourceFactoryMap[typeof REMOTE_RESOURCE_KINDS.CONNECTION];
|
|
83
102
|
export type PreferredTweakProbeFactory = RemoteResourceFactoryMap[typeof REMOTE_RESOURCE_KINDS.PREFERRED_TWEAK];
|
|
84
103
|
export type SecuritySeedResourceFactory = RemoteResourceFactoryMap[typeof REMOTE_RESOURCE_KINDS.SECURITY_SEED];
|
|
104
|
+
export type InitialFetchFastForwardResourceFactory = RemoteResourceFactoryMap[typeof REMOTE_RESOURCE_KINDS.INITIAL_FETCH_FAST_FORWARD];
|
|
85
105
|
export type SynchronisationInformationResourceFactory = RemoteResourceFactoryMap[typeof REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION];
|
|
106
|
+
export type JoinedDevicesResourceFactory = RemoteResourceFactoryMap[typeof REMOTE_RESOURCE_KINDS.JOINED_DEVICES];
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { CAPABILITY_NOT_APPLICABLE } from "./ProviderCapability.js";
|
|
2
2
|
const REMOTE_RESOURCE_KINDS = Object.freeze({
|
|
3
3
|
CONNECTION: "connection",
|
|
4
|
+
JOINED_DEVICES: "joined-devices",
|
|
4
5
|
PREFERRED_TWEAK: "preferred-tweak",
|
|
5
6
|
SECURITY_SEED: "security-seed",
|
|
6
|
-
SYNCHRONISATION_INFORMATION: "synchronisation-information"
|
|
7
|
+
SYNCHRONISATION_INFORMATION: "synchronisation-information",
|
|
8
|
+
INITIAL_FETCH_FAST_FORWARD: "initial-fetch-fast-forward"
|
|
7
9
|
});
|
|
8
10
|
const NO_REMOTE_RESOURCE_CAPABILITIES = Object.freeze({
|
|
9
11
|
[REMOTE_RESOURCE_KINDS.CONNECTION]: CAPABILITY_NOT_APPLICABLE,
|
|
12
|
+
[REMOTE_RESOURCE_KINDS.JOINED_DEVICES]: CAPABILITY_NOT_APPLICABLE,
|
|
10
13
|
[REMOTE_RESOURCE_KINDS.PREFERRED_TWEAK]: CAPABILITY_NOT_APPLICABLE,
|
|
11
14
|
[REMOTE_RESOURCE_KINDS.SECURITY_SEED]: CAPABILITY_NOT_APPLICABLE,
|
|
12
|
-
[REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION]: CAPABILITY_NOT_APPLICABLE
|
|
15
|
+
[REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION]: CAPABILITY_NOT_APPLICABLE,
|
|
16
|
+
[REMOTE_RESOURCE_KINDS.INITIAL_FETCH_FAST_FORWARD]: CAPABILITY_NOT_APPLICABLE
|
|
13
17
|
});
|
|
14
18
|
export {
|
|
15
19
|
NO_REMOTE_RESOURCE_CAPABILITIES,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { ReplicationStatics } from "../../common/models/shared.definition.js";
|
|
1
2
|
import { type EntryDoc, type RemoteDBSettings, type EntryLeaf, type SyncParameters, type DatabaseEntry, type NodeData, type RemotePreferredTweakResult } from "../../common/types.js";
|
|
3
|
+
import type { InitialFetchFastForwardResource } from "../RemoteResource.js";
|
|
2
4
|
import { LiveSyncAbstractReplicator, type LiveSyncReplicatorEnv, type RemoteDBStatus } from "../LiveSyncAbstractReplicator.js";
|
|
3
5
|
import type { OwnedCouchDBConnection, RemoteConnectionOpenOptions } from "../../services/base/RemoteConnection.js";
|
|
4
6
|
import type { IPathService } from "../../services/base/IService.js";
|
|
5
7
|
import { type CentralCompatibilityDecisionRecorder } from "../CentralCompatibility.js";
|
|
6
8
|
import { type ReplicationOutcome } from "../ReplicatorProvider.js";
|
|
9
|
+
import { type ForgetJoinedDeviceResult, type JoinedDevicesSnapshot } from "../joinedDevices.js";
|
|
7
10
|
export type { OwnedCouchDBConnection, RemoteConnectionOpenOptions } from "../../services/base/RemoteConnection.js";
|
|
8
11
|
/**
|
|
9
12
|
* Compatibility constructor environment for the CouchDB Replicator facade.
|
|
@@ -43,8 +46,15 @@ export declare class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicato
|
|
|
43
46
|
private continuousTask;
|
|
44
47
|
private continuousStopWaiter;
|
|
45
48
|
private continuousStopRequested;
|
|
49
|
+
private continuousPhase?;
|
|
50
|
+
private couchDBProgress?;
|
|
51
|
+
protected getReplicationStatics(): ReplicationStatics;
|
|
52
|
+
private publishPreparationPhase;
|
|
53
|
+
private finishReplicationProgress;
|
|
46
54
|
isMobile(): boolean;
|
|
47
55
|
constructor(env: LiveSyncCouchDBReplicatorEnv);
|
|
56
|
+
/** Own the checkpoint handoff for one fresh, suspended Fast Fetch only. */
|
|
57
|
+
createInitialFetchFastForwardResource(setting: RemoteDBSettings): InitialFetchFastForwardResource;
|
|
48
58
|
getInitialSyncParameters(setting: RemoteDBSettings): Promise<SyncParameters>;
|
|
49
59
|
getSyncParameters(setting: RemoteDBSettings): Promise<SyncParameters>;
|
|
50
60
|
putSyncParameters(setting: RemoteDBSettings, params: SyncParameters): Promise<boolean>;
|
|
@@ -152,6 +162,10 @@ export declare class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicato
|
|
|
152
162
|
compactRemote(setting: RemoteDBSettings): Promise<boolean>;
|
|
153
163
|
getRemoteStatus(setting: RemoteDBSettings): Promise<RemoteDBStatus | false>;
|
|
154
164
|
countCompromisedChunks(setting?: RemoteDBSettings): Promise<number | boolean>;
|
|
165
|
+
private connectForJoinedDevices;
|
|
166
|
+
private getOwnJoinedDeviceId;
|
|
167
|
+
getJoinedDevices(setting: RemoteDBSettings): Promise<JoinedDevicesSnapshot>;
|
|
168
|
+
forgetJoinedDevice(setting: RemoteDBSettings, deviceId: string): Promise<ForgetJoinedDeviceResult>;
|
|
155
169
|
getConnectedDeviceList(setting?: RemoteDBSettings): Promise<false | {
|
|
156
170
|
node_info: Record<string, NodeData>;
|
|
157
171
|
accepted_nodes: string[];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { observeReplicationProgress } from "./replicationProgress.js";
|
|
1
2
|
import {
|
|
2
3
|
VER,
|
|
3
4
|
MILESTONE_DOCID,
|
|
5
|
+
NODEINFO_DOCID,
|
|
4
6
|
REPLICATION_BUSY_TIMEOUT,
|
|
5
7
|
LOG_LEVEL_INFO,
|
|
6
8
|
LOG_LEVEL_NOTICE,
|
|
@@ -26,6 +28,7 @@ import { requiredRemoteFeatures, usesEncryptedInternalMetadata } from "../../pou
|
|
|
26
28
|
import { assessRemoteDocumentIds } from "../../pouchdb/remoteIdCompatibility.js";
|
|
27
29
|
import { isErrorOfMissingDoc } from "../../pouchdb/utils_couchdb.js";
|
|
28
30
|
import { preprocessOutgoing } from "../../pouchdb/encryption.js";
|
|
31
|
+
import { prepareInitialFetchFastForward } from "./fastForward.js";
|
|
29
32
|
import { ensureDatabaseIsCompatible, getEffectiveTweakValues } from "../../pouchdb/LiveSyncDBFunctions.js";
|
|
30
33
|
import {
|
|
31
34
|
LiveSyncAbstractReplicator
|
|
@@ -54,6 +57,10 @@ import {
|
|
|
54
57
|
replicationBlocked,
|
|
55
58
|
replicationFailed
|
|
56
59
|
} from "../ReplicatorProvider.js";
|
|
60
|
+
import {
|
|
61
|
+
forgetJoinedDevice as forgetJoinedDeviceFromStore,
|
|
62
|
+
projectJoinedDevices
|
|
63
|
+
} from "../joinedDevices.js";
|
|
57
64
|
const currentVersionRange = {
|
|
58
65
|
min: 0,
|
|
59
66
|
max: 2400,
|
|
@@ -116,6 +123,24 @@ class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator {
|
|
|
116
123
|
continuousTask;
|
|
117
124
|
continuousStopWaiter;
|
|
118
125
|
continuousStopRequested = false;
|
|
126
|
+
continuousPhase;
|
|
127
|
+
couchDBProgress;
|
|
128
|
+
getReplicationStatics() {
|
|
129
|
+
return {
|
|
130
|
+
...super.getReplicationStatics(),
|
|
131
|
+
...this.couchDBProgress ? { couchDB: this.couchDBProgress } : {}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
publishPreparationPhase(phase) {
|
|
135
|
+
this.continuousPhase = phase;
|
|
136
|
+
this.couchDBProgress = { phase };
|
|
137
|
+
this.updateInfo();
|
|
138
|
+
}
|
|
139
|
+
finishReplicationProgress() {
|
|
140
|
+
if (!this.couchDBProgress || this.couchDBProgress.phase === "idle") return;
|
|
141
|
+
this.couchDBProgress = { ...this.couchDBProgress, phase: "idle" };
|
|
142
|
+
this.updateInfo();
|
|
143
|
+
}
|
|
119
144
|
isMobile() {
|
|
120
145
|
return this.env.services.API.isMobile();
|
|
121
146
|
}
|
|
@@ -123,6 +148,59 @@ class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator {
|
|
|
123
148
|
super(env);
|
|
124
149
|
this.env = env;
|
|
125
150
|
}
|
|
151
|
+
/** Own the checkpoint handoff for one fresh, suspended Fast Fetch only. */
|
|
152
|
+
createInitialFetchFastForwardResource(setting) {
|
|
153
|
+
const snapshot = { ...setting };
|
|
154
|
+
const identity = (value) => JSON.stringify([
|
|
155
|
+
value.couchDB_URI,
|
|
156
|
+
value.couchDB_DBNAME,
|
|
157
|
+
value.couchDB_USER,
|
|
158
|
+
value.couchDB_PASSWORD,
|
|
159
|
+
value.couchDB_CustomHeaders,
|
|
160
|
+
value.useJWT,
|
|
161
|
+
value.jwtAlgorithm,
|
|
162
|
+
value.jwtKey,
|
|
163
|
+
value.jwtExpDuration,
|
|
164
|
+
value.jwtKid,
|
|
165
|
+
value.jwtSub,
|
|
166
|
+
value.encrypt,
|
|
167
|
+
value.passphrase,
|
|
168
|
+
value.E2EEAlgorithm,
|
|
169
|
+
value.enableCompression,
|
|
170
|
+
value.pathNormalizationVersion,
|
|
171
|
+
value.readChunksOnline,
|
|
172
|
+
value.useRequestAPI
|
|
173
|
+
]);
|
|
174
|
+
const binding = identity(snapshot);
|
|
175
|
+
let connection;
|
|
176
|
+
let prepared = false;
|
|
177
|
+
let disposed = false;
|
|
178
|
+
return {
|
|
179
|
+
prepare: async () => {
|
|
180
|
+
if (prepared || disposed) return void 0;
|
|
181
|
+
prepared = true;
|
|
182
|
+
if (this.currentSettings.doNotSuspendOnFetching || snapshot.useRequestAPI) return void 0;
|
|
183
|
+
const local = this.rawDatabase;
|
|
184
|
+
const isCurrent = () => !disposed && this.rawDatabase === local && this.currentSettings.suspendFileWatching && this.currentSettings.suspendParseReplicationResult && identity(this.currentSettings) === binding;
|
|
185
|
+
if (!isCurrent()) return void 0;
|
|
186
|
+
const openedConnection = await this.openOwnedConnection(snapshot);
|
|
187
|
+
if (disposed) {
|
|
188
|
+
await openedConnection.close();
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
connection = openedConnection;
|
|
192
|
+
return await prepareInitialFetchFastForward(local, connection.db, {
|
|
193
|
+
push: {},
|
|
194
|
+
...snapshot.readChunksOnline ? { pull: { ...selectorOnDemandPull } } : {}
|
|
195
|
+
}, isCurrent);
|
|
196
|
+
},
|
|
197
|
+
dispose: async () => {
|
|
198
|
+
if (disposed) return;
|
|
199
|
+
disposed = true;
|
|
200
|
+
if (connection) await connection.close();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
126
204
|
getInitialSyncParameters(setting) {
|
|
127
205
|
return Promise.resolve({
|
|
128
206
|
...DEFAULT_SYNC_PARAMETERS,
|
|
@@ -182,6 +260,7 @@ class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator {
|
|
|
182
260
|
this.continuousStopRequested = true;
|
|
183
261
|
}
|
|
184
262
|
this.abortController();
|
|
263
|
+
this.finishReplicationProgress();
|
|
185
264
|
if (!task) {
|
|
186
265
|
return Promise.resolve();
|
|
187
266
|
}
|
|
@@ -293,6 +372,17 @@ class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator {
|
|
|
293
372
|
this.controller.abort();
|
|
294
373
|
}
|
|
295
374
|
this.controller = controller;
|
|
375
|
+
const progress = observeReplicationProgress(
|
|
376
|
+
syncHandler,
|
|
377
|
+
syncMode,
|
|
378
|
+
this.continuousPhase ?? "one-shot",
|
|
379
|
+
controller.signal,
|
|
380
|
+
(snapshot) => {
|
|
381
|
+
if (this.controller !== controller) return;
|
|
382
|
+
this.couchDBProgress = snapshot;
|
|
383
|
+
this.updateInfo();
|
|
384
|
+
}
|
|
385
|
+
);
|
|
296
386
|
const gen = genReplication(syncHandler, controller.signal);
|
|
297
387
|
try {
|
|
298
388
|
for await (const [type, e] of gen) {
|
|
@@ -302,6 +392,7 @@ class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator {
|
|
|
302
392
|
return "FAILED";
|
|
303
393
|
}
|
|
304
394
|
releaser();
|
|
395
|
+
if (controller.signal.aborted || this.controller !== controller) break;
|
|
305
396
|
switch (type) {
|
|
306
397
|
case "change":
|
|
307
398
|
if ("direction" in e) {
|
|
@@ -381,8 +472,9 @@ class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator {
|
|
|
381
472
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
|
382
473
|
return "FAILED";
|
|
383
474
|
} finally {
|
|
384
|
-
|
|
385
|
-
|
|
475
|
+
progress.dispose();
|
|
476
|
+
controller.abort();
|
|
477
|
+
if (this.controller === controller) this.controller = void 0;
|
|
386
478
|
}
|
|
387
479
|
}
|
|
388
480
|
getEmptyMaxEntry(remoteID) {
|
|
@@ -925,7 +1017,8 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
925
1017
|
plugin_version: this.env.services.API.getPluginVersion(),
|
|
926
1018
|
vault_name: this.env.services.vault.vaultName(),
|
|
927
1019
|
device_name: this.env.services.vault.getVaultName(),
|
|
928
|
-
progress
|
|
1020
|
+
progress,
|
|
1021
|
+
platform: this.env.services.API.getPlatform()
|
|
929
1022
|
};
|
|
930
1023
|
let tweakAssessment;
|
|
931
1024
|
const ensure = await ensureDatabaseIsCompatible(
|
|
@@ -1058,6 +1151,8 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1058
1151
|
if (this.continuousTask !== task) {
|
|
1059
1152
|
return;
|
|
1060
1153
|
}
|
|
1154
|
+
this.finishReplicationProgress();
|
|
1155
|
+
this.continuousPhase = void 0;
|
|
1061
1156
|
this.continuousTask = void 0;
|
|
1062
1157
|
this.continuousStopWaiter = void 0;
|
|
1063
1158
|
this.continuousStopRequested = false;
|
|
@@ -1071,6 +1166,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1071
1166
|
);
|
|
1072
1167
|
return false;
|
|
1073
1168
|
}
|
|
1169
|
+
this.publishPreparationPhase("catch-up");
|
|
1074
1170
|
const localDB = this.rawDatabase;
|
|
1075
1171
|
Logger(this.translate("liveSyncReplicator.beforeLiveSync"));
|
|
1076
1172
|
const caughtUp = await this.env.services.replicator.runFiniteReplicationActivity(
|
|
@@ -1086,6 +1182,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1086
1182
|
{ label: "replication" }
|
|
1087
1183
|
);
|
|
1088
1184
|
if (caughtUp && !this.continuousStopRequested) {
|
|
1185
|
+
this.publishPreparationPhase("continuous");
|
|
1089
1186
|
Logger(this.translate("liveSyncReplicator.liveSyncBegin"));
|
|
1090
1187
|
const ret = await this.checkReplicationConnectivity(setting, true, false, showResult);
|
|
1091
1188
|
if (ret === false || this.continuousStopRequested) {
|
|
@@ -1608,6 +1705,77 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
1608
1705
|
}
|
|
1609
1706
|
return await this.withRemoteConnection(dbRet, countCompromisedChunks);
|
|
1610
1707
|
}
|
|
1708
|
+
async connectForJoinedDevices(setting) {
|
|
1709
|
+
let result;
|
|
1710
|
+
try {
|
|
1711
|
+
result = await this.connectRemoteCouchDBWithSetting(setting, this.isMobile(), false);
|
|
1712
|
+
} catch {
|
|
1713
|
+
throw new Error("Could not connect to remote CouchDB for joined-device management.");
|
|
1714
|
+
}
|
|
1715
|
+
if (typeof result === "string") {
|
|
1716
|
+
throw new Error("Could not connect to remote CouchDB for joined-device management.");
|
|
1717
|
+
}
|
|
1718
|
+
return result;
|
|
1719
|
+
}
|
|
1720
|
+
async getOwnJoinedDeviceId() {
|
|
1721
|
+
const nodeInfo = await this.rawDatabase.get(NODEINFO_DOCID);
|
|
1722
|
+
const nodeId = nodeInfo?.nodeid;
|
|
1723
|
+
if (typeof nodeId !== "string" || nodeId.trim().length === 0) {
|
|
1724
|
+
throw new Error("The local node identity is unavailable.");
|
|
1725
|
+
}
|
|
1726
|
+
return nodeId;
|
|
1727
|
+
}
|
|
1728
|
+
async getJoinedDevices(setting) {
|
|
1729
|
+
const connection = await this.connectForJoinedDevices(setting);
|
|
1730
|
+
return await this.withRemoteConnection(connection, async (db) => {
|
|
1731
|
+
let milestone;
|
|
1732
|
+
try {
|
|
1733
|
+
milestone = await db.get(MILESTONE_DOCID);
|
|
1734
|
+
} catch (ex) {
|
|
1735
|
+
if (!isErrorOfMissingDoc(ex)) throw ex;
|
|
1736
|
+
}
|
|
1737
|
+
return projectJoinedDevices(milestone);
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
async forgetJoinedDevice(setting, deviceId) {
|
|
1741
|
+
const ownDeviceId = await this.getOwnJoinedDeviceId();
|
|
1742
|
+
const connection = await this.connectForJoinedDevices(setting);
|
|
1743
|
+
return await this.withRemoteConnection(connection, async (db) => {
|
|
1744
|
+
const store = {
|
|
1745
|
+
read: async () => {
|
|
1746
|
+
let milestone;
|
|
1747
|
+
try {
|
|
1748
|
+
milestone = await db.get(MILESTONE_DOCID);
|
|
1749
|
+
} catch (ex) {
|
|
1750
|
+
if (isErrorOfMissingDoc(ex)) return void 0;
|
|
1751
|
+
throw ex;
|
|
1752
|
+
}
|
|
1753
|
+
return { milestone, revision: milestone._rev };
|
|
1754
|
+
},
|
|
1755
|
+
write: async (milestone, revision) => {
|
|
1756
|
+
try {
|
|
1757
|
+
await db.put({ ...milestone, _rev: revision });
|
|
1758
|
+
} catch (ex) {
|
|
1759
|
+
if (ex && typeof ex === "object" && ex.status === 409) {
|
|
1760
|
+
return "conflict";
|
|
1761
|
+
}
|
|
1762
|
+
throw ex;
|
|
1763
|
+
}
|
|
1764
|
+
let verifiedMilestone;
|
|
1765
|
+
try {
|
|
1766
|
+
verifiedMilestone = await db.get(MILESTONE_DOCID);
|
|
1767
|
+
} catch {
|
|
1768
|
+
throw new Error("Could not verify joined-device removal.");
|
|
1769
|
+
}
|
|
1770
|
+
if (projectJoinedDevices(verifiedMilestone).devices.some((device) => device.id === deviceId)) {
|
|
1771
|
+
throw new Error("Could not verify joined-device removal.");
|
|
1772
|
+
}
|
|
1773
|
+
return "updated";
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
return await forgetJoinedDeviceFromStore(store, deviceId, ownDeviceId);
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1611
1779
|
async getConnectedDeviceList(setting = this.currentSettings) {
|
|
1612
1780
|
const dbRet = await this.connectRemoteCouchDBWithSetting(setting, this.isMobile(), true);
|
|
1613
1781
|
if (typeof dbRet === "string") {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EntryDoc } from "../../common/types.js";
|
|
2
|
+
type Sequence = string | number;
|
|
3
|
+
/** Evidence retained only for the currently running, fresh Fast Fetch. */
|
|
4
|
+
export interface InitialFetchFastForward {
|
|
5
|
+
recordStoredRevisions(revisions: readonly {
|
|
6
|
+
id: string;
|
|
7
|
+
rev: string;
|
|
8
|
+
}[]): void;
|
|
9
|
+
recordCheckpoint(sequence: Sequence): void;
|
|
10
|
+
fastForward(): Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Prepare ordinary checkpoint pairs before a fresh import into an empty database.
|
|
14
|
+
* The importer must remain the sole writer until fastForward settles.
|
|
15
|
+
* The remote database must remain in place throughout the import and handoff:
|
|
16
|
+
* PouchDB's HTTP ID identifies a server and database name, not its incarnation.
|
|
17
|
+
*/
|
|
18
|
+
export declare function prepareInitialFetchFastForward(localDatabase: PouchDB.Database<EntryDoc>, remoteDatabase: PouchDB.Database<EntryDoc>, options: PouchDB.Replication.SyncOptions, isCurrent: () => boolean): Promise<InitialFetchFastForward | undefined>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import generateReplicationId from "pouchdb-generate-replication-id";
|
|
2
|
+
import Checkpointer from "pouchdb-checkpointer";
|
|
3
|
+
import { uuid } from "pouchdb-utils";
|
|
4
|
+
const MAX_TRACKED_REVISIONS = 1e5;
|
|
5
|
+
async function prepareInitialFetchFastForward(localDatabase, remoteDatabase, options, isCurrent) {
|
|
6
|
+
const local = localDatabase;
|
|
7
|
+
const remote = remoteDatabase;
|
|
8
|
+
if ((await local.info()).doc_count !== 0 || !isCurrent()) return void 0;
|
|
9
|
+
const localId = await local.id();
|
|
10
|
+
const remoteId = await remote.id();
|
|
11
|
+
const pairs = [
|
|
12
|
+
{ source: local, target: remote, options: { ...options, ...options.push } },
|
|
13
|
+
{ source: remote, target: local, options: { ...options, ...options.pull } }
|
|
14
|
+
];
|
|
15
|
+
const checkpoints = [];
|
|
16
|
+
for (const pair of pairs) {
|
|
17
|
+
const id = await generateReplicationId(pair.source, pair.target, pair.options);
|
|
18
|
+
for (const database of [local, remote]) {
|
|
19
|
+
try {
|
|
20
|
+
await database.get(id);
|
|
21
|
+
return void 0;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error.status !== 404) throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const session = uuid();
|
|
27
|
+
await new Checkpointer(pair.source, pair.target, id, { cancelled: false }).writeCheckpoint(0, session);
|
|
28
|
+
checkpoints.push({
|
|
29
|
+
id,
|
|
30
|
+
session,
|
|
31
|
+
localRevision: (await local.get(id))._rev,
|
|
32
|
+
remoteRevision: (await remote.get(id))._rev
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const imported = /* @__PURE__ */ new Set();
|
|
36
|
+
let withinBudget = true;
|
|
37
|
+
let lastSequence;
|
|
38
|
+
let consumed = false;
|
|
39
|
+
const key = (id, rev) => JSON.stringify([id, rev]);
|
|
40
|
+
return {
|
|
41
|
+
recordStoredRevisions(revisions) {
|
|
42
|
+
if (!withinBudget) return;
|
|
43
|
+
for (const revision of revisions) {
|
|
44
|
+
imported.add(key(revision.id, revision.rev));
|
|
45
|
+
if (imported.size > MAX_TRACKED_REVISIONS) {
|
|
46
|
+
imported.clear();
|
|
47
|
+
withinBudget = false;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
recordCheckpoint(sequence) {
|
|
53
|
+
lastSequence = sequence;
|
|
54
|
+
},
|
|
55
|
+
async fastForward() {
|
|
56
|
+
if (consumed) return false;
|
|
57
|
+
consumed = true;
|
|
58
|
+
if (!withinBudget || lastSequence === void 0 || !isCurrent()) return false;
|
|
59
|
+
if (await local.id() !== localId || await remote.id() !== remoteId) return false;
|
|
60
|
+
const localSequence = (await local.info()).update_seq;
|
|
61
|
+
let since = 0;
|
|
62
|
+
while (true) {
|
|
63
|
+
const changes = await local.changes({
|
|
64
|
+
since,
|
|
65
|
+
limit: 1e3,
|
|
66
|
+
style: "all_docs",
|
|
67
|
+
return_docs: true
|
|
68
|
+
});
|
|
69
|
+
for (const change of changes.results) {
|
|
70
|
+
for (const revision of change.changes) {
|
|
71
|
+
if (!imported.has(key(change.id, revision.rev))) return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (changes.results.length < 1e3) break;
|
|
75
|
+
since = changes.last_seq;
|
|
76
|
+
}
|
|
77
|
+
if (!isCurrent() || (await local.info()).update_seq !== localSequence) return false;
|
|
78
|
+
const guard = (database, id, expectedRevision, session) => ({
|
|
79
|
+
adapter: database.adapter,
|
|
80
|
+
async get(requestedId) {
|
|
81
|
+
let document;
|
|
82
|
+
try {
|
|
83
|
+
document = await database.get(requestedId);
|
|
84
|
+
} catch (cause) {
|
|
85
|
+
throw new Error("The initial-fetch checkpoint is no longer available.", { cause });
|
|
86
|
+
}
|
|
87
|
+
if (requestedId !== id || document._rev !== expectedRevision || document.session_id !== session) {
|
|
88
|
+
throw new Error("The initial-fetch checkpoint changed before fast-forwarding.");
|
|
89
|
+
}
|
|
90
|
+
return document;
|
|
91
|
+
},
|
|
92
|
+
async put(document) {
|
|
93
|
+
if (document._id !== id || document._rev !== expectedRevision) {
|
|
94
|
+
throw new Error("The initial-fetch checkpoint no longer matches its import.");
|
|
95
|
+
}
|
|
96
|
+
const response = await database.put(document);
|
|
97
|
+
expectedRevision = response.rev;
|
|
98
|
+
return response;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
for (const [index, checkpoint] of checkpoints.entries()) {
|
|
102
|
+
if (!isCurrent() || (await local.info()).update_seq !== localSequence) return false;
|
|
103
|
+
const guardedLocal = guard(local, checkpoint.id, checkpoint.localRevision, checkpoint.session);
|
|
104
|
+
const guardedRemote = guard(remote, checkpoint.id, checkpoint.remoteRevision, checkpoint.session);
|
|
105
|
+
const source = index === 0 ? guardedLocal : guardedRemote;
|
|
106
|
+
const target = index === 0 ? guardedRemote : guardedLocal;
|
|
107
|
+
await new Checkpointer(source, target, checkpoint.id, { cancelled: false }).writeCheckpoint(
|
|
108
|
+
index === 0 ? localSequence : lastSequence,
|
|
109
|
+
checkpoint.session
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
imported.clear();
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export {
|
|
118
|
+
prepareInitialFetchFastForward
|
|
119
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EntryDoc } from "../../common/types.js";
|
|
2
|
+
import type { CouchDBReplicationPhase, CouchDBReplicationProgress } from "../../common/models/shared.definition.js";
|
|
3
|
+
/** Observe one handler without adding its high-volume checkpoint events to the replication inbox. */
|
|
4
|
+
export declare function observeReplicationProgress(handler: PouchDB.Replication.Sync<EntryDoc> | PouchDB.Replication.Replication<EntryDoc>, mode: "sync" | "pullOnly" | "pushOnly", phase: Exclude<CouchDBReplicationPhase, "idle">, signal: AbortSignal, publish: (progress: CouchDBReplicationProgress) => void): {
|
|
5
|
+
dispose(): void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { compatGlobal } from "../../common/coreEnvFunctions.js";
|
|
2
|
+
const PROGRESS_PUBLICATION_INTERVAL_MS = 200;
|
|
3
|
+
function asEmitter(value) {
|
|
4
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
5
|
+
if (!("on" in value) || typeof value.on !== "function") return void 0;
|
|
6
|
+
if (!("removeListener" in value) || typeof value.removeListener !== "function") return void 0;
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function isSequence(value) {
|
|
10
|
+
return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
11
|
+
}
|
|
12
|
+
function observeReplicationProgress(handler, mode, phase, signal, publish) {
|
|
13
|
+
const progress = {};
|
|
14
|
+
const detach = [];
|
|
15
|
+
let timer;
|
|
16
|
+
let dirty = false;
|
|
17
|
+
let disposed = false;
|
|
18
|
+
let currentPhase = phase;
|
|
19
|
+
const cancelTimer = () => {
|
|
20
|
+
if (timer === void 0) return;
|
|
21
|
+
compatGlobal.clearTimeout(timer);
|
|
22
|
+
timer = void 0;
|
|
23
|
+
};
|
|
24
|
+
const flush = () => {
|
|
25
|
+
cancelTimer();
|
|
26
|
+
if (!dirty) return;
|
|
27
|
+
dirty = false;
|
|
28
|
+
publish({
|
|
29
|
+
phase: currentPhase,
|
|
30
|
+
...progress.pull ? { pull: { ...progress.pull } } : {},
|
|
31
|
+
...progress.push ? { push: { ...progress.push } } : {}
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
const schedule = () => {
|
|
35
|
+
dirty = true;
|
|
36
|
+
timer ??= compatGlobal.setTimeout(flush, PROGRESS_PUBLICATION_INTERVAL_MS);
|
|
37
|
+
};
|
|
38
|
+
const listen = (emitter, event, listener) => {
|
|
39
|
+
emitter.on(event, listener);
|
|
40
|
+
detach.push(() => emitter.removeListener(event, listener));
|
|
41
|
+
};
|
|
42
|
+
const observeDirection = (direction, emitter) => {
|
|
43
|
+
if (!emitter) return;
|
|
44
|
+
const value = { checkedChanges: 0, completedBatches: 0, checking: false };
|
|
45
|
+
progress[direction] = value;
|
|
46
|
+
listen(emitter, "checkpoint", (event) => {
|
|
47
|
+
if (disposed || signal.aborted || typeof event !== "object" || event === null) return;
|
|
48
|
+
if ("revs_diff" in event && typeof event.revs_diff === "object" && event.revs_diff !== null && "id" in event.revs_diff && typeof event.revs_diff.id === "string") {
|
|
49
|
+
value.checkedChanges++;
|
|
50
|
+
value.checking = true;
|
|
51
|
+
} else if ("start_next_batch" in event && isSequence(event.start_next_batch)) {
|
|
52
|
+
value.checking = true;
|
|
53
|
+
} else if ("checkpoint" in event && isSequence(event.checkpoint)) {
|
|
54
|
+
value.completedBatches++;
|
|
55
|
+
value.lastSeq = event.checkpoint;
|
|
56
|
+
} else {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
schedule();
|
|
60
|
+
});
|
|
61
|
+
const stopChecking = () => {
|
|
62
|
+
if (disposed || signal.aborted) return;
|
|
63
|
+
if (value.checking) {
|
|
64
|
+
value.checking = false;
|
|
65
|
+
dirty = true;
|
|
66
|
+
}
|
|
67
|
+
flush();
|
|
68
|
+
};
|
|
69
|
+
listen(emitter, "change", stopChecking);
|
|
70
|
+
listen(emitter, "paused", stopChecking);
|
|
71
|
+
};
|
|
72
|
+
const dispose = () => {
|
|
73
|
+
if (disposed) return;
|
|
74
|
+
disposed = true;
|
|
75
|
+
cancelTimer();
|
|
76
|
+
for (const remove of detach) remove();
|
|
77
|
+
signal.removeEventListener("abort", dispose);
|
|
78
|
+
currentPhase = "idle";
|
|
79
|
+
if (progress.pull) progress.pull.checking = false;
|
|
80
|
+
if (progress.push) progress.push.checking = false;
|
|
81
|
+
dirty = true;
|
|
82
|
+
flush();
|
|
83
|
+
};
|
|
84
|
+
if (mode === "sync") {
|
|
85
|
+
const duplex = handler;
|
|
86
|
+
observeDirection("pull", asEmitter(duplex.pull));
|
|
87
|
+
observeDirection("push", asEmitter(duplex.push));
|
|
88
|
+
} else {
|
|
89
|
+
observeDirection(mode === "pullOnly" ? "pull" : "push", asEmitter(handler));
|
|
90
|
+
}
|
|
91
|
+
const outer = asEmitter(handler);
|
|
92
|
+
if (outer) {
|
|
93
|
+
listen(outer, "complete", dispose);
|
|
94
|
+
listen(outer, "error", dispose);
|
|
95
|
+
listen(outer, "denied", dispose);
|
|
96
|
+
}
|
|
97
|
+
signal.addEventListener("abort", dispose, { once: true });
|
|
98
|
+
dirty = true;
|
|
99
|
+
if (signal.aborted) dispose();
|
|
100
|
+
else flush();
|
|
101
|
+
return { dispose };
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
observeReplicationProgress
|
|
105
|
+
};
|