@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.
Files changed (92) hide show
  1. package/README.md +80 -4
  2. package/dist/API/DirectFileManipulatorV2.d.ts +2 -0
  3. package/dist/API/DirectFileManipulatorV2.js +3 -0
  4. package/dist/API/processSetting.d.ts +1 -0
  5. package/dist/common/configForDoc.d.ts +1 -0
  6. package/dist/common/configForDoc.js +11 -1
  7. package/dist/common/models/db.definition.d.ts +1 -0
  8. package/dist/common/models/setting.const.defaults.js +1 -0
  9. package/dist/common/models/setting.const.preferred.js +1 -3
  10. package/dist/common/models/setting.const.qr.js +1 -0
  11. package/dist/common/models/setting.policy.d.ts +4 -0
  12. package/dist/common/models/setting.policy.js +1 -0
  13. package/dist/common/models/setting.type.d.ts +2 -0
  14. package/dist/common/models/shared.definition.d.ts +20 -0
  15. package/dist/common/models/tweak.definition.d.ts +1 -0
  16. package/dist/common/settingConstants.d.ts +1 -0
  17. package/dist/common/settingConstants.js +6 -2
  18. package/dist/interfaces/FileHandler.d.ts +5 -1
  19. package/dist/interfaces/StorageEventManager.d.ts +5 -0
  20. package/dist/interfaces/StorageEventManager.js +7 -0
  21. package/dist/managers/EntryManager/EntryManagerImpls.js +3 -2
  22. package/dist/managers/StorageEventManager.d.ts +19 -10
  23. package/dist/managers/StorageEventManager.js +259 -189
  24. package/dist/pouchdb/LiveSyncDBFunctions.js +4 -0
  25. package/dist/pouchdb/StreamingFetch.d.ts +5 -1
  26. package/dist/pouchdb/StreamingFetch.js +529 -65
  27. package/dist/replication/LiveSyncAbstractReplicator.d.ts +2 -0
  28. package/dist/replication/LiveSyncAbstractReplicator.js +5 -2
  29. package/dist/replication/RemoteResource.d.ts +23 -2
  30. package/dist/replication/RemoteResource.js +6 -2
  31. package/dist/replication/couchdb/LiveSyncReplicator.d.ts +14 -0
  32. package/dist/replication/couchdb/LiveSyncReplicator.js +171 -3
  33. package/dist/replication/couchdb/fastForward.d.ts +19 -0
  34. package/dist/replication/couchdb/fastForward.js +119 -0
  35. package/dist/replication/couchdb/replicationProgress.d.ts +6 -0
  36. package/dist/replication/couchdb/replicationProgress.js +105 -0
  37. package/dist/replication/index.d.ts +4 -1
  38. package/dist/replication/joinedDevices.d.ts +25 -0
  39. package/dist/replication/joinedDevices.js +123 -0
  40. package/dist/replication/journal/JournalSyncCore.d.ts +2 -1
  41. package/dist/replication/journal/JournalSyncCore.js +11 -1
  42. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +4 -0
  43. package/dist/replication/journal/LiveSyncJournalReplicator.js +56 -1
  44. package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +3 -0
  45. package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -1
  46. package/dist/replication/journal/objectstore/MinioStorageAdapter.js +27 -1
  47. package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +1 -1
  48. package/dist/replication/trystero/P2PRoomSessionOwner.js +27 -14
  49. package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
  50. package/dist/serviceFeatures/offlineScanner.js +19 -7
  51. package/dist/serviceModules/Rebuilder.js +41 -15
  52. package/dist/serviceModules/ServiceFileAccessBase.js +5 -0
  53. package/dist/serviceModules/ServiceFileHandlerBase.d.ts +8 -5
  54. package/dist/serviceModules/ServiceFileHandlerBase.js +75 -38
  55. package/dist/services/BrowserServices.d.ts +4 -2
  56. package/dist/services/BrowserServices.js +2 -5
  57. package/dist/services/HeadlessServices.d.ts +3 -0
  58. package/dist/services/HeadlessServices.js +2 -5
  59. package/dist/services/ServiceHub.d.ts +4 -4
  60. package/dist/services/base/AppLifecycleService.js +1 -1
  61. package/dist/services/base/ControlService.js +6 -2
  62. package/dist/services/base/FileProcessingService.d.ts +7 -1
  63. package/dist/services/base/FileProcessingService.js +9 -1
  64. package/dist/services/base/IService.d.ts +19 -19
  65. package/dist/services/base/ReplicatorService.d.ts +1 -0
  66. package/dist/services/base/ReplicatorService.remoteResourceResolver.js +1 -1
  67. package/dist/services/base/SettingService.d.ts +8 -18
  68. package/dist/services/base/SettingService.js +28 -46
  69. package/dist/services/implements/base/SvelteDialog.d.ts +2 -2
  70. package/dist/services/implements/base/UIService.d.ts +2 -0
  71. package/dist/services/implements/base/UIService.js +5 -0
  72. package/dist/services/implements/injectable/InjectableServiceHub.d.ts +4 -4
  73. package/dist/services/implements/injectable/InjectableServices.d.ts +2 -2
  74. package/dist/services/implements/injectable/InjectableSettingService.d.ts +0 -3
  75. package/dist/services/implements/injectable/InjectableSettingService.js +0 -11
  76. package/dist/services/lib/HandlerUtils.d.ts +60 -0
  77. package/dist/services/lib/HandlerUtils.js +46 -0
  78. package/dist/string_and_binary/chunks.js +7 -5
  79. package/dist/uiCommands.d.ts +26 -0
  80. package/dist/uiCommands.js +57 -0
  81. package/dist/worker/bgWorker.js +1 -1
  82. package/docs/development.md +15 -3
  83. package/docs/offline-scanner.md +15 -13
  84. package/docs/proven-in-use.md +38 -4
  85. package/docs/releasing.md +20 -13
  86. package/docs/settings-lifecycle.md +39 -5
  87. package/docs/storage-events-and-reflection.md +109 -10
  88. package/package.json +13 -18
  89. package/dist/services/base/ConfigService.d.ts +0 -7
  90. package/dist/services/base/ConfigService.js +0 -6
  91. package/dist/services/implements/browser/ConfigServiceBrowserCompat.d.ts +0 -16
  92. 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
- updateInfo = () => {
79
- this.env.services.replicator.replicationStatics.value = {
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
- /** Exhaustive resource support catalogue supplied by one provider. */
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
- this.abortController();
385
- this.controller = void 0;
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
+ };