@vrtmrz/livesync-commonlib 0.1.19-rc.0 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +5 -1
  2. package/dist/common/models/setting.p2p.d.ts +2 -0
  3. package/dist/common/models/setting.p2p.js +5 -0
  4. package/dist/interfaces/ServiceModule.d.ts +13 -4
  5. package/dist/managers/ChunkFetcher.js +10 -0
  6. package/dist/p2p/P2PService.d.ts +123 -0
  7. package/dist/p2p/P2PService.js +271 -0
  8. package/dist/p2p/index.d.ts +8 -0
  9. package/dist/p2p/index.js +8 -0
  10. package/dist/pouchdb/LiveSyncLocalDB.d.ts +3 -0
  11. package/dist/pouchdb/LiveSyncLocalDB.js +23 -3
  12. package/dist/pouchdb/ReplicatorShim.d.ts +43 -4
  13. package/dist/pouchdb/ReplicatorShim.js +78 -5
  14. package/dist/replication/CentralCompatibility.d.ts +38 -0
  15. package/dist/replication/CentralCompatibility.js +39 -0
  16. package/dist/replication/CentralRemoteAdministration.d.ts +84 -0
  17. package/dist/replication/CentralRemoteAdministration.js +71 -0
  18. package/dist/replication/LiveSyncAbstractReplicator.d.ts +19 -6
  19. package/dist/replication/ProviderCapability.d.ts +32 -0
  20. package/dist/replication/ProviderCapability.js +27 -0
  21. package/dist/replication/RemoteResource.d.ts +85 -0
  22. package/dist/replication/RemoteResource.js +17 -0
  23. package/dist/replication/ReplicatorInstance.d.ts +23 -0
  24. package/dist/replication/ReplicatorProvider.d.ts +249 -0
  25. package/dist/replication/ReplicatorProvider.js +145 -0
  26. package/dist/replication/couchdb/LiveSyncReplicator.d.ts +54 -8
  27. package/dist/replication/couchdb/LiveSyncReplicator.js +411 -158
  28. package/dist/replication/index.d.ts +10 -0
  29. package/dist/replication/index.js +70 -0
  30. package/dist/replication/journal/JournalSyncCore.d.ts +2 -0
  31. package/dist/replication/journal/JournalSyncCore.js +18 -5
  32. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +38 -10
  33. package/dist/replication/journal/LiveSyncJournalReplicator.js +271 -63
  34. package/dist/replication/journal/LiveSyncJournalReplicatorEnv.d.ts +8 -0
  35. package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +2 -0
  36. package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -0
  37. package/dist/replication/journal/objectstore/MinioStorageAdapter.js +10 -1
  38. package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +12 -40
  39. package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +52 -152
  40. package/dist/replication/trystero/P2PAutomationCoordinator.d.ts +20 -0
  41. package/dist/replication/trystero/P2PAutomationCoordinator.js +43 -0
  42. package/dist/replication/trystero/P2PConnectionProbeAdmission.d.ts +32 -0
  43. package/dist/replication/trystero/P2PConnectionProbeAdmission.js +10 -0
  44. package/dist/replication/trystero/P2PReplicationWire.d.ts +31 -0
  45. package/dist/replication/trystero/P2PReplicationWire.js +50 -0
  46. package/dist/replication/trystero/P2PRoomSession.d.ts +32 -0
  47. package/dist/replication/trystero/P2PRoomSession.js +101 -0
  48. package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +61 -0
  49. package/dist/replication/trystero/P2PRoomSessionOwner.js +249 -0
  50. package/dist/replication/trystero/TrysteroReplicator.d.ts +88 -36
  51. package/dist/replication/trystero/TrysteroReplicator.js +273 -113
  52. package/dist/replication/trystero/TrysteroReplicatorP2PClient.d.ts +7 -4
  53. package/dist/replication/trystero/TrysteroReplicatorP2PClient.js +21 -14
  54. package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +26 -3
  55. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +84 -39
  56. package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -6
  57. package/dist/replication/trystero/addP2PEventHandlers.d.ts +8 -12
  58. package/dist/replication/trystero/addP2PEventHandlers.js +19 -19
  59. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.d.ts +9 -0
  60. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +30 -0
  61. package/dist/replication/trystero/types.d.ts +18 -0
  62. package/dist/replication/trystero/types.js +5 -1
  63. package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +3 -3
  64. package/dist/replication/trystero/useP2PReplicatorCommands.js +6 -8
  65. package/dist/replication/trystero/useP2PReplicatorFeature.d.ts +16 -7
  66. package/dist/replication/trystero/useP2PReplicatorFeature.js +72 -55
  67. package/dist/rpc/RpcRoom.d.ts +15 -1
  68. package/dist/rpc/RpcRoom.js +176 -41
  69. package/dist/rpc/RpcSession.d.ts +3 -1
  70. package/dist/rpc/RpcSession.js +2 -2
  71. package/dist/rpc/index.d.ts +1 -1
  72. package/dist/rpc/transports/trysteroUtils.js +2 -1
  73. package/dist/rpc/types.d.ts +26 -1
  74. package/dist/serviceFeatures/checkRemoteSize.js +5 -2
  75. package/dist/serviceModules/Rebuilder.js +66 -56
  76. package/dist/services/base/AppLifecycleService.d.ts +4 -0
  77. package/dist/services/base/AppLifecycleService.js +4 -0
  78. package/dist/services/base/ControlService.js +0 -4
  79. package/dist/services/base/DatabaseEventService.js +1 -1
  80. package/dist/services/base/IService.d.ts +105 -8
  81. package/dist/services/base/ReadinessEvaluator.d.ts +40 -0
  82. package/dist/services/base/ReadinessEvaluator.js +16 -0
  83. package/dist/services/base/RemoteConnection.d.ts +3 -0
  84. package/dist/services/base/RemoteService.js +2 -2
  85. package/dist/services/base/ReplicationService.d.ts +45 -10
  86. package/dist/services/base/ReplicationService.js +248 -91
  87. package/dist/services/base/ReplicationService.readiness.d.ts +52 -0
  88. package/dist/services/base/ReplicationService.readiness.js +87 -0
  89. package/dist/services/base/ReplicationService.typedReplication.d.ts +83 -0
  90. package/dist/services/base/ReplicationService.typedReplication.js +191 -0
  91. package/dist/services/base/ReplicatorService.activeReplicatorState.d.ts +47 -0
  92. package/dist/services/base/ReplicatorService.activeReplicatorState.js +107 -0
  93. package/dist/services/base/ReplicatorService.centralRemoteAdministration.d.ts +5 -0
  94. package/dist/services/base/ReplicatorService.centralRemoteAdministration.js +44 -0
  95. package/dist/services/base/ReplicatorService.d.ts +75 -14
  96. package/dist/services/base/ReplicatorService.js +232 -53
  97. package/dist/services/base/ReplicatorService.remoteResourceResolver.d.ts +4 -0
  98. package/dist/services/base/ReplicatorService.remoteResourceResolver.js +11 -0
  99. package/dist/services/base/TweakValueService.d.ts +3 -2
  100. package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +1 -1
  101. package/dist/services/lib/HandlerUtils.d.ts +24 -0
  102. package/dist/services/lib/HandlerUtils.js +40 -0
  103. package/docs/development.md +40 -7
  104. package/docs/p2p-transport-lifecycle.md +135 -11
  105. package/docs/proven-in-use.md +5 -1
  106. package/docs/remote-configurations.md +4 -0
  107. package/docs/service-feature-composition.md +250 -0
  108. package/package.json +17 -1
  109. package/dist/replication/trystero/P2PReplicatorBase.d.ts +0 -19
  110. package/dist/replication/trystero/P2PReplicatorCore.d.ts +0 -8
  111. package/dist/replication/trystero/P2PReplicatorCore.js +0 -56
  112. package/dist/replication/trystero/TrysteroReplicatorP2PConnection.d.ts +0 -2
  113. package/dist/replication/trystero/TrysteroReplicatorP2PConnection.js +0 -4
  114. /package/dist/replication/{trystero/P2PReplicatorBase.js → ReplicatorInstance.js} +0 -0
@@ -14,7 +14,24 @@ import { ensureRemoteIsCompatible } from "../../pouchdb/LiveSyncDBFunctions.js";
14
14
  import { extractObject } from "../../common/utils.js";
15
15
  import { clearHandlers } from "../SyncParamsHandler.js";
16
16
  import { JournalStorageReadStatuses } from "./objectstore/JournalStorageAdapter.js";
17
+ import {
18
+ CENTRAL_COMPATIBILITY_ACCEPTED,
19
+ CENTRAL_COMPATIBILITY_NOT_ASSESSED,
20
+ CENTRAL_COMPATIBILITY_REJECTION_REASONS,
21
+ centralCompatibilityRejected,
22
+ centralCompatibilityRecoveryHint
23
+ } from "../CentralCompatibility.js";
24
+ import {
25
+ outcomeFromFiniteOpenReplication,
26
+ replicationFailed
27
+ } from "../ReplicatorProvider.js";
17
28
  const MILSTONE_DOCID = "_00000000-milestone.json";
29
+ async function readRemoteMilestone(client) {
30
+ const result = await client.downloadJsonWithResult(MILSTONE_DOCID);
31
+ if (result.status === JournalStorageReadStatuses.AVAILABLE) return result.value;
32
+ if (result.status === JournalStorageReadStatuses.NOT_FOUND) return void 0;
33
+ throw result.error;
34
+ }
18
35
  const currentVersionRange = {
19
36
  min: 0,
20
37
  max: 2,
@@ -28,11 +45,18 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
28
45
  return this.env.services.keyValueDB.simpleStore;
29
46
  }
30
47
  _client;
48
+ /** Whether this instance has entered a Journal transfer since its last close. */
49
+ hasEnteredReplication = false;
50
+ /** Transfers whose settlement must be observed by an admitted Stop request. */
51
+ activeJournalTransfers;
52
+ /** Shared settlement for repeated Stop requests at the same boundary. */
53
+ journalTransferStopSettlement;
54
+ /** Monotonic fence which prevents a preflight crossing a later Stop boundary. */
55
+ journalTransferStopGeneration = 0;
31
56
  async getReplicationPBKDF2Salt(setting, refresh) {
32
- return await this.client.getReplicationPBKDF2Salt(refresh);
57
+ return await this.setupJournalSyncClient(setting).getReplicationPBKDF2Salt(refresh);
33
58
  }
34
- setupJournalSyncClient() {
35
- const settings = this.currentSettings;
59
+ setupJournalSyncClient(settings = this.currentSettings) {
36
60
  if (this._client) {
37
61
  this._client.applyNewConfig(settings, this.simpleStore, this.env);
38
62
  } else {
@@ -45,13 +69,17 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
45
69
  }
46
70
  return this._client;
47
71
  }
48
- async ensureBucketIsCompatible(deviceNodeID, currentVersionRange2) {
49
- const downloadedMilestone = await this.client.downloadJson(MILSTONE_DOCID);
50
- const cPointInfo = await this.client.getCheckpointInfo();
72
+ async ensureBucketIsCompatible(deviceNodeID, currentVersionRange2, setting = this.currentSettings, client = this.setupJournalSyncClient(setting)) {
73
+ const milestoneResult = await client.downloadJsonWithResult(MILSTONE_DOCID);
74
+ if (milestoneResult.status === JournalStorageReadStatuses.UNAVAILABLE) {
75
+ throw milestoneResult.error;
76
+ }
77
+ const downloadedMilestone = milestoneResult.status === JournalStorageReadStatuses.AVAILABLE ? milestoneResult.value : false;
78
+ const cPointInfo = await client.getCheckpointInfo();
51
79
  const progress = [...cPointInfo?.receivedFiles || []].sort().pop() || "";
52
80
  return await ensureRemoteIsCompatible(
53
81
  downloadedMilestone,
54
- this.currentSettings,
82
+ setting,
55
83
  deviceNodeID,
56
84
  currentVersionRange2,
57
85
  {
@@ -62,7 +90,9 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
62
90
  progress
63
91
  },
64
92
  async (info) => {
65
- await this.client.uploadJson(MILSTONE_DOCID, info);
93
+ if (!await client.uploadJson(MILSTONE_DOCID, info)) {
94
+ throw new Error("Could not upload remote milestone");
95
+ }
66
96
  }
67
97
  );
68
98
  }
@@ -74,40 +104,175 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
74
104
  Logger(`Database updated from ${from} to ${to}`, LOG_LEVEL_NOTICE);
75
105
  return Promise.resolve(true);
76
106
  }
107
+ /**
108
+ * Run one Replicator-owned Journal transfer after any earlier Stop boundary.
109
+ *
110
+ * The client owns transfer mechanics, while this Replicator owns the
111
+ * settlement required by its active-transfer cancellation contract.
112
+ */
113
+ runJournalTransfer(run) {
114
+ const stopGeneration = this.journalTransferStopGeneration;
115
+ let resolveTask;
116
+ let rejectTask;
117
+ const task = new Promise((resolve, reject) => {
118
+ resolveTask = resolve;
119
+ rejectTask = reject;
120
+ });
121
+ const activeTransfers = this.activeJournalTransfers ??= /* @__PURE__ */ new Set();
122
+ activeTransfers.add(task);
123
+ const release = () => activeTransfers.delete(task);
124
+ void task.then(release, release);
125
+ void (async () => {
126
+ const stopping = this.journalTransferStopSettlement;
127
+ if (stopping) {
128
+ await stopping;
129
+ }
130
+ return await run(stopGeneration);
131
+ })().then(resolveTask, rejectTask);
132
+ return task;
133
+ }
134
+ /** Request client cancellation and await the transfers admitted before this Stop boundary. */
77
135
  terminateSync() {
78
- this.client.requestStop();
136
+ this.journalTransferStopGeneration += 1;
137
+ const client = this._client;
138
+ client?.requestStop();
139
+ if (this.journalTransferStopSettlement) {
140
+ return this.journalTransferStopSettlement;
141
+ }
142
+ const activeTransfers = [...this.activeJournalTransfers ?? []];
143
+ if (activeTransfers.length === 0) return Promise.resolve();
144
+ let settlement;
145
+ settlement = Promise.allSettled(activeTransfers).then(() => {
146
+ if (this.journalTransferStopSettlement === settlement) {
147
+ this.journalTransferStopSettlement = void 0;
148
+ }
149
+ });
150
+ this.journalTransferStopSettlement = settlement;
151
+ return settlement;
79
152
  }
80
- async openReplication(setting, _, showResult, ignoreCleanLock = false) {
81
- if (!await this.checkReplicationConnectivity(false, ignoreCleanLock, showResult)) return false;
82
- await this.client.sync(showResult);
83
- return true;
153
+ async openReplication(setting, _, showResult, ignoreCleanLock = false, recordCompatibilityDecision) {
154
+ return await this.runJournalTransfer(async (stopGeneration) => {
155
+ const client = this.setupJournalSyncClient(setting);
156
+ stopGeneration = this.journalTransferStopGeneration;
157
+ if (!await this.checkReplicationConnectivity(
158
+ false,
159
+ ignoreCleanLock,
160
+ showResult,
161
+ setting,
162
+ client,
163
+ recordCompatibilityDecision
164
+ )) {
165
+ return false;
166
+ }
167
+ if (stopGeneration !== this.journalTransferStopGeneration) return false;
168
+ this.hasEnteredReplication = true;
169
+ return await client.sync(showResult);
170
+ });
171
+ }
172
+ /**
173
+ * Run one finite Journal attempt against one settings-bound borrowed client.
174
+ *
175
+ * Only a rejection observed by this attempt is projected into recovery;
176
+ * unavailable storage and later transport failures do not reuse old state.
177
+ */
178
+ async openOneShotReplicationWithOutcome(setting, showResult, ignoreCleanLock = false) {
179
+ let decision = CENTRAL_COMPATIBILITY_NOT_ASSESSED;
180
+ const recordDecision = (next) => {
181
+ decision = next;
182
+ };
183
+ try {
184
+ const result = await this.openReplication(setting, false, showResult, ignoreCleanLock, recordDecision);
185
+ return outcomeFromFiniteOpenReplication(result, centralCompatibilityRecoveryHint(decision));
186
+ } catch (error) {
187
+ return replicationFailed(error, centralCompatibilityRecoveryHint(decision));
188
+ }
189
+ }
190
+ async runDirectionalReplication(setting, showingNotice, transfer, recordCompatibilityDecision) {
191
+ return await this.runJournalTransfer(async (stopGeneration) => {
192
+ const client = this.setupJournalSyncClient(setting);
193
+ stopGeneration = this.journalTransferStopGeneration;
194
+ if (!await this.checkReplicationConnectivity(
195
+ false,
196
+ false,
197
+ !!showingNotice,
198
+ setting,
199
+ client,
200
+ recordCompatibilityDecision
201
+ )) {
202
+ return false;
203
+ }
204
+ if (stopGeneration !== this.journalTransferStopGeneration) return false;
205
+ this.hasEnteredReplication = true;
206
+ return await transfer(client);
207
+ });
208
+ }
209
+ /** Capture only the compatibility decision made by this exact borrowed client. */
210
+ async runDirectionalReplicationWithOutcome(setting, showingNotice, transfer) {
211
+ let decision = CENTRAL_COMPATIBILITY_NOT_ASSESSED;
212
+ const recordDecision = (next) => {
213
+ decision = next;
214
+ };
215
+ try {
216
+ const result = await this.runDirectionalReplication(setting, showingNotice, transfer, recordDecision);
217
+ return outcomeFromFiniteOpenReplication(result, centralCompatibilityRecoveryHint(decision));
218
+ } catch (error) {
219
+ return replicationFailed(error, centralCompatibilityRecoveryHint(decision));
220
+ }
84
221
  }
85
222
  async replicateAllToServer(setting, showingNotice) {
86
- if (!await this.checkReplicationConnectivity(false, false, !!showingNotice)) return false;
87
- return await this.client.sendLocalJournal(showingNotice);
223
+ return await this.runDirectionalReplication(
224
+ setting,
225
+ showingNotice,
226
+ (client) => client.sendLocalJournal(showingNotice)
227
+ );
228
+ }
229
+ async replicateAllToServerWithOutcome(setting, showingNotice) {
230
+ return await this.runDirectionalReplicationWithOutcome(
231
+ setting,
232
+ showingNotice,
233
+ (client) => client.sendLocalJournal(showingNotice)
234
+ );
88
235
  }
89
236
  async replicateAllFromServer(setting, showingNotice) {
90
- if (!await this.checkReplicationConnectivity(false, false, !!showingNotice)) return false;
91
- return await this.client.receiveRemoteJournal(showingNotice);
237
+ return await this.runDirectionalReplication(
238
+ setting,
239
+ showingNotice,
240
+ (client) => client.receiveRemoteJournal(showingNotice)
241
+ );
92
242
  }
93
- async checkReplicationConnectivity(skipCheck, ignoreCleanLock = false, showMessage = false) {
94
- if (!await this.client.isAvailable()) {
243
+ async replicateAllFromServerWithOutcome(setting, showingNotice) {
244
+ return await this.runDirectionalReplicationWithOutcome(
245
+ setting,
246
+ showingNotice,
247
+ (client) => client.receiveRemoteJournal(showingNotice)
248
+ );
249
+ }
250
+ async checkReplicationConnectivity(skipCheck, ignoreCleanLock = false, showMessage = false, setting = this.currentSettings, client = this.setupJournalSyncClient(setting), recordCompatibilityDecision) {
251
+ recordCompatibilityDecision?.(CENTRAL_COMPATIBILITY_NOT_ASSESSED);
252
+ if (!await client.isAvailable()) {
95
253
  return false;
96
254
  }
97
255
  if (!skipCheck) {
98
- await this.client.ensureCheckpointCachesAreFresh();
256
+ await client.ensureCheckpointCachesAreFresh();
99
257
  this.remoteCleaned = false;
100
258
  this.remoteLocked = false;
101
259
  this.remoteLockedAndDeviceNotAccepted = false;
102
260
  this.tweakSettingsMismatched = false;
103
- const ensure = await this.ensureBucketIsCompatible(this.nodeid, currentVersionRange);
261
+ this.preferredTweakValue = void 0;
262
+ const ensure = await this.ensureBucketIsCompatible(this.nodeid, currentVersionRange, setting, client);
104
263
  if (ensure == "INCOMPATIBLE") {
264
+ recordCompatibilityDecision?.(
265
+ centralCompatibilityRejected(CENTRAL_COMPATIBILITY_REJECTION_REASONS.INCOMPATIBLE_VERSION)
266
+ );
105
267
  Logger(
106
268
  "The remote database has no compatibility with the running version. Please upgrade the plugin.",
107
269
  LOG_LEVEL_NOTICE
108
270
  );
109
271
  return false;
110
272
  } else if (ensure == "NODE_LOCKED") {
273
+ recordCompatibilityDecision?.(
274
+ centralCompatibilityRejected(CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_LOCKED)
275
+ );
111
276
  Logger(
112
277
  "The remote database has been rebuilt or corrupted since we have synchronized last time. Fetch rebuilt DB, explicit unlocking or chunk clean-up is required.",
113
278
  LOG_LEVEL_NOTICE
@@ -121,6 +286,9 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
121
286
  if (ignoreCleanLock) {
122
287
  this.remoteLocked = true;
123
288
  } else {
289
+ recordCompatibilityDecision?.(
290
+ centralCompatibilityRejected(CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_CLEANED)
291
+ );
124
292
  Logger(
125
293
  "The remote database has been cleaned up. Fetch rebuilt DB, explicit unlocking or chunk clean-up is required.",
126
294
  LOG_LEVEL_NOTICE
@@ -132,40 +300,52 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
132
300
  }
133
301
  } else if (ensure == "OK") {
134
302
  } else if (ensure[0] == "MISMATCHED") {
303
+ recordCompatibilityDecision?.(
304
+ centralCompatibilityRejected(CENTRAL_COMPATIBILITY_REJECTION_REASONS.TWEAK_MISMATCH, ensure[1])
305
+ );
135
306
  Logger(this.translate("liveSyncReplicator.mismatchedTweakDetected"), LOG_LEVEL_NOTICE);
136
307
  this.tweakSettingsMismatched = true;
137
308
  this.preferredTweakValue = ensure[1];
138
309
  return false;
139
310
  }
311
+ recordCompatibilityDecision?.(CENTRAL_COMPATIBILITY_ACCEPTED);
140
312
  }
141
313
  return true;
142
314
  }
143
- async fetchRemoteChunks(missingChunks, showResult) {
144
- return Promise.resolve([]);
145
- }
146
315
  closeReplication() {
147
- this.client.requestStop();
316
+ const reportClosure = this.hasEnteredReplication;
317
+ this.hasEnteredReplication = false;
318
+ const client = this._client;
319
+ this._client = void 0;
320
+ client?.dispose();
148
321
  this.syncStatus = "CLOSED";
149
- Logger("Replication closed");
322
+ if (reportClosure) {
323
+ Logger("Replication closed");
324
+ }
150
325
  this.updateInfo();
151
326
  }
152
327
  async tryResetRemoteDatabase(setting) {
153
328
  this.closeReplication();
154
329
  try {
155
- await this.client.resetBucket();
330
+ if (!await this.client.resetBucket()) {
331
+ throw new Error("Could not reset remote bucket");
332
+ }
156
333
  clearHandlers();
157
334
  Logger("Remote Bucket Cleared", LOG_LEVEL_NOTICE);
158
335
  await this.tryCreateRemoteDatabase(setting);
159
336
  } catch (ex) {
160
337
  Logger("Something happened on Remote Bucket Clear", LOG_LEVEL_NOTICE);
161
338
  Logger(ex, LOG_LEVEL_NOTICE);
339
+ throw ex;
162
340
  }
163
341
  }
164
342
  async tryCreateRemoteDatabase(setting) {
165
343
  this.closeReplication();
166
344
  Logger("Remote Database Created or Connected", LOG_LEVEL_NOTICE);
167
345
  clearHandlers();
168
- await this.ensurePBKDF2Salt(setting, true, false);
346
+ if (!await this.ensurePBKDF2Salt(setting, true, false)) {
347
+ throw new Error("Could not ensure PBKDF2 salt (Security Seed)");
348
+ }
169
349
  return await Promise.resolve();
170
350
  }
171
351
  async markRemoteLocked(setting, locked, lockByClean) {
@@ -180,9 +360,10 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
180
360
  node_info: {},
181
361
  tweak_values: {}
182
362
  };
363
+ const client = this.setupJournalSyncClient(setting);
183
364
  const remoteMilestone = {
184
365
  ...defInitPoint,
185
- ...await this.client.downloadJson(MILSTONE_DOCID) || {}
366
+ ...await readRemoteMilestone(client) ?? {}
186
367
  };
187
368
  remoteMilestone.node_chunk_info = { ...defInitPoint.node_chunk_info, ...remoteMilestone.node_chunk_info };
188
369
  remoteMilestone.accepted_nodes = [this.nodeid];
@@ -193,7 +374,9 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
193
374
  } else {
194
375
  Logger("Unlock remote bucket to prevent data corruption", LOG_LEVEL_NOTICE);
195
376
  }
196
- await this.client.uploadJson(MILSTONE_DOCID, remoteMilestone);
377
+ if (!await client.uploadJson(MILSTONE_DOCID, remoteMilestone)) {
378
+ throw new Error("Could not upload remote milestone");
379
+ }
197
380
  }
198
381
  async markRemoteResolved(setting) {
199
382
  const defInitPoint = {
@@ -206,14 +389,17 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
206
389
  node_info: {},
207
390
  tweak_values: {}
208
391
  };
392
+ const client = this.setupJournalSyncClient(setting);
209
393
  const remoteMilestone = {
210
394
  ...defInitPoint,
211
- ...await this.client.downloadJson(MILSTONE_DOCID) || {}
395
+ ...await readRemoteMilestone(client) ?? {}
212
396
  };
213
397
  remoteMilestone.node_chunk_info = { ...defInitPoint.node_chunk_info, ...remoteMilestone.node_chunk_info };
214
398
  remoteMilestone.accepted_nodes = Array.from(/* @__PURE__ */ new Set([...remoteMilestone.accepted_nodes, this.nodeid]));
215
399
  Logger("Mark this device as 'resolved'.", LOG_LEVEL_NOTICE);
216
- await this.client.uploadJson(MILSTONE_DOCID, remoteMilestone);
400
+ if (!await client.uploadJson(MILSTONE_DOCID, remoteMilestone)) {
401
+ throw new Error("Could not upload remote milestone");
402
+ }
217
403
  }
218
404
  async tryConnectRemote(setting, showResult = true) {
219
405
  const endpoint = setting.endpoint;
@@ -227,6 +413,8 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
227
413
  ${ex.message}`, LOG_LEVEL_NOTICE);
228
414
  Logger(ex, LOG_LEVEL_NOTICE);
229
415
  return false;
416
+ } finally {
417
+ testClient.dispose();
230
418
  }
231
419
  }
232
420
  async resetRemoteTweakSettings(setting) {
@@ -244,54 +432,74 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
244
432
  }
245
433
  }
246
434
  async setPreferredRemoteTweakSettings(setting) {
435
+ const trialClient = new JournalSyncCore(
436
+ setting,
437
+ this.simpleStore,
438
+ this.env,
439
+ new MinioStorageAdapter(setting, this.env)
440
+ );
247
441
  try {
248
- const remoteMilestone = await this.client.downloadJson(MILSTONE_DOCID);
442
+ const remoteMilestone = await trialClient.downloadJson(MILSTONE_DOCID);
249
443
  if (!remoteMilestone) {
250
444
  throw new Error("Missing remote milestone");
251
445
  }
252
446
  remoteMilestone.tweak_values[DEVICE_ID_PREFERRED] = extractObject(TweakValuesTemplate, {
253
447
  ...setting
254
448
  });
255
- Logger(`tweak values on the remote database have been cleared`, LOG_LEVEL_VERBOSE);
256
- await this.client.uploadJson(MILSTONE_DOCID, remoteMilestone);
449
+ Logger(`Preferred tweak values have been registered`, LOG_LEVEL_VERBOSE);
450
+ if (!await trialClient.uploadJson(MILSTONE_DOCID, remoteMilestone)) {
451
+ throw new Error("Could not upload remote milestone");
452
+ }
257
453
  } catch (ex) {
258
- Logger(`Could not retrieve remote milestone`, LOG_LEVEL_NOTICE);
454
+ Logger(`Could not update remote preferred tweak values`, LOG_LEVEL_NOTICE);
259
455
  throw ex;
456
+ } finally {
457
+ trialClient.dispose();
260
458
  }
261
459
  }
262
- async getRemotePreferredTweakValues(_setting) {
263
- const result = await this.client.downloadJsonWithResult(MILSTONE_DOCID);
264
- if (result.status === JournalStorageReadStatuses.NOT_FOUND) {
265
- return {
266
- status: RemotePreferredTweakStatuses.NOT_CONFIGURED,
267
- reason: RemotePreferredTweakNotConfiguredReasons.MILESTONE_MISSING
268
- };
269
- }
270
- if (result.status === JournalStorageReadStatuses.UNAVAILABLE) {
271
- return {
272
- status: RemotePreferredTweakStatuses.UNAVAILABLE,
273
- error: result.error
274
- };
275
- }
276
- const preferred = result.value.tweak_values?.[DEVICE_ID_PREFERRED];
277
- if (!preferred) {
460
+ async getRemotePreferredTweakValues(setting) {
461
+ const trialClient = new JournalSyncCore(
462
+ setting,
463
+ this.simpleStore,
464
+ this.env,
465
+ new MinioStorageAdapter(setting, this.env)
466
+ );
467
+ try {
468
+ const result = await trialClient.downloadJsonWithResult(MILSTONE_DOCID);
469
+ if (result.status === JournalStorageReadStatuses.NOT_FOUND) {
470
+ return {
471
+ status: RemotePreferredTweakStatuses.NOT_CONFIGURED,
472
+ reason: RemotePreferredTweakNotConfiguredReasons.MILESTONE_MISSING
473
+ };
474
+ }
475
+ if (result.status === JournalStorageReadStatuses.UNAVAILABLE) {
476
+ return {
477
+ status: RemotePreferredTweakStatuses.UNAVAILABLE,
478
+ error: result.error
479
+ };
480
+ }
481
+ const preferred = result.value.tweak_values?.[DEVICE_ID_PREFERRED];
482
+ if (!preferred) {
483
+ return {
484
+ status: RemotePreferredTweakStatuses.NOT_CONFIGURED,
485
+ reason: RemotePreferredTweakNotConfiguredReasons.PREFERRED_VALUES_MISSING
486
+ };
487
+ }
278
488
  return {
279
- status: RemotePreferredTweakStatuses.NOT_CONFIGURED,
280
- reason: RemotePreferredTweakNotConfiguredReasons.PREFERRED_VALUES_MISSING
489
+ status: RemotePreferredTweakStatuses.AVAILABLE,
490
+ values: preferred
281
491
  };
492
+ } finally {
493
+ trialClient.dispose();
282
494
  }
283
- return {
284
- status: RemotePreferredTweakStatuses.AVAILABLE,
285
- values: preferred
286
- };
287
495
  }
288
496
  async getRemoteStatus(setting) {
289
497
  const testClient = new MinioStorageAdapter(setting, this.env);
290
- return await testClient.getUsage();
291
- }
292
- countCompromisedChunks() {
293
- Logger(`Bucket Sync Replicator cannot count compromised chunks`, LOG_LEVEL_VERBOSE);
294
- return Promise.resolve(0);
498
+ try {
499
+ return await testClient.getUsage();
500
+ } finally {
501
+ testClient.dispose();
502
+ }
295
503
  }
296
504
  getConnectedDeviceList(setting) {
297
505
  return Promise.resolve(false);
@@ -1,3 +1,11 @@
1
1
  import type { LiveSyncReplicatorEnv } from "../LiveSyncAbstractReplicator.js";
2
+ /**
3
+ * Compatibility name for the Journal Replicator constructor environment.
4
+ *
5
+ * Journal currently adds no requirements to {@link LiveSyncReplicatorEnv}.
6
+ * The distinct exported interface preserves the existing constructor contract
7
+ * and an extension point; shared host composition should depend on the base
8
+ * environment unless it specifically constructs a Journal Replicator.
9
+ */
2
10
  export interface LiveSyncJournalReplicatorEnv extends LiveSyncReplicatorEnv {
3
11
  }
@@ -15,6 +15,8 @@ export type JournalStorageReadResult<T> = {
15
15
  error: unknown;
16
16
  };
17
17
  export interface IJournalStorage {
18
+ /** Release transport resources retained by this adapter, when any. */
19
+ dispose?(): void;
18
20
  upload(key: string, data: Uint8Array, mime: string): Promise<boolean>;
19
21
  download(key: string, ignoreCache?: boolean): Promise<Uint8Array | false>;
20
22
  downloadWithResult(key: string, ignoreCache?: boolean): Promise<JournalStorageReadResult<Uint8Array>>;
@@ -10,6 +10,8 @@ export declare class MinioStorageAdapter implements IJournalStorage {
10
10
  constructor(settings: BucketSyncSetting, env: LiveSyncJournalReplicatorEnv);
11
11
  private runTrackedRequest;
12
12
  applyNewConfig(settings: BucketSyncSetting): void;
13
+ /** Release the SDK client owned by this adapter. Repeated calls are safe. */
14
+ dispose(): void;
13
15
  get customHeaders(): [string, string][];
14
16
  _getClient(): S3;
15
17
  upload(key: string, data: Uint8Array, mime: string): Promise<boolean>;
@@ -28,8 +28,14 @@ class MinioStorageAdapter {
28
28
  return await runWithTrackedPhysicalRequest(this._env.services.API, task);
29
29
  }
30
30
  applyNewConfig(settings) {
31
+ this.dispose();
31
32
  this._settings = settings;
33
+ }
34
+ /** Release the SDK client owned by this adapter. Repeated calls are safe. */
35
+ dispose() {
36
+ const instance = this._instance;
32
37
  this._instance = void 0;
38
+ instance?.destroy();
33
39
  }
34
40
  get customHeaders() {
35
41
  return this._settings.bucketCustomHeaders.length == 0 ? [] : Object.entries(parseHeaderValues(this._settings.bucketCustomHeaders));
@@ -118,7 +124,10 @@ class MinioStorageAdapter {
118
124
  const cmd = new GetObjectCommand({
119
125
  Bucket: this._settings.bucket,
120
126
  Key: `${this._settings.bucketPrefix}${key}`,
121
- ...ignoreCache ? { ResponseCacheControl: "no-cache" } : {}
127
+ // The SDK serialises this response override as a signed query parameter.
128
+ // Control-object reads use `no-store`: `no-cache` may retain the old
129
+ // response and serve it after a successful PUT in the same renderer.
130
+ ...ignoreCache ? { ResponseCacheControl: "no-store" } : {}
122
131
  });
123
132
  try {
124
133
  return await this.runTrackedRequest(async () => {
@@ -1,8 +1,9 @@
1
1
  import { type RemoteDBSettings, type EntryLeaf, type RemotePreferredTweakResult, type LOG_LEVEL, type NodeData } from "../../common/types.js";
2
2
  import { LiveSyncAbstractReplicator, type LiveSyncReplicatorEnv, type RemoteDBStatus } from "../LiveSyncAbstractReplicator.js";
3
3
  import { TrysteroReplicator } from "./TrysteroReplicator.js";
4
- import { P2PHost, type AcceptanceDecision, type RevokeAcceptanceDecision } from "./TrysteroReplicatorP2PServer.js";
4
+ import { type AcceptanceDecision, type RevokeAcceptanceDecision } from "./TrysteroReplicatorP2PServer.js";
5
5
  import type { Advertisement } from "./types.js";
6
+ import { type P2PRoomSessionAccess } from "./P2PRoomSessionOwner.js";
6
7
  export interface LiveSyncTrysteroReplicatorEnv extends LiveSyncReplicatorEnv {
7
8
  /**
8
9
  * Injected by the host platform (e.g. Obsidian) to show a UI for peer selection.
@@ -16,60 +17,31 @@ export interface LiveSyncTrysteroReplicatorEnv extends LiveSyncReplicatorEnv {
16
17
  openRebuildUI?: (showResult: boolean) => Promise<boolean | void>;
17
18
  }
18
19
  export declare class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator {
19
- private _p2pHost?;
20
- private _replicator?;
21
- private _lifecycleOperation;
22
- private _shouldBeOpen;
23
- private _activeTransportCompatibilitySignature?;
20
+ private readonly sessionOwner;
21
+ /** Peer lifecycle is fenced inside the room session rather than this facade. */
22
+ readonly handlesPeerEventsWithinSession = true;
24
23
  get openReplicationUI(): (showResult: boolean) => Promise<boolean | void>;
25
24
  get rawReplicator(): TrysteroReplicator;
26
- get rawHost(): P2PHost;
25
+ get rawHost(): import("./TrysteroReplicatorP2PServer.js").TrysteroReplicatorP2PServer;
27
26
  getReplicationPBKDF2Salt(_setting: RemoteDBSettings, _refresh?: boolean): Promise<Uint8Array<ArrayBuffer>>;
28
27
  terminateSync(): void;
29
- private _buildEnv;
30
- private _enqueueLifecycleOperation;
31
- /**
32
- * Capture settings which are fixed when the active P2P transport joins its room.
33
- * A later open request can remain idempotent only while these effective values match.
34
- */
35
- private _getTransportCompatibilitySignature;
36
- /** Close and forget the transport without changing the requested lifecycle state. */
37
- private _closeTransport;
38
28
  open(): Promise<void>;
39
29
  close(): Promise<void>;
40
30
  closeReplication(): void;
41
- get server(): P2PHost;
31
+ get server(): import("./TrysteroReplicatorP2PServer.js").TrysteroReplicatorP2PServer;
42
32
  get knownAdvertisements(): Advertisement[];
43
33
  enableBroadcastChanges(): void;
44
34
  disableBroadcastChanges(): void;
45
35
  requestStatus(): void;
46
36
  onNewPeer(peer: Advertisement): Promise<void>;
47
37
  onPeerLeaved(peerId: string): void;
48
- replicateFromCommand(showResult?: boolean): Promise<void>;
49
- replicateFrom(peerId: string, showNotice?: boolean, skipOrdinaryReplicationPolicy?: boolean): Promise<{
50
- error: any;
51
- ok?: undefined;
52
- } | {
53
- ok: boolean;
54
- error?: undefined;
55
- }>;
56
- requestSynchroniseToPeer(peerId: string): Promise<{
57
- error: any;
58
- ok?: undefined;
59
- } | {
60
- ok: boolean;
61
- error?: undefined;
62
- }>;
38
+ replicateFromCommand(showResult?: boolean): Promise<boolean>;
39
+ replicateFrom(peerId: string, showNotice?: boolean, skipOrdinaryReplicationPolicy?: boolean): Promise<import("./TrysteroReplicator.js").P2PReplicationResult>;
40
+ requestSynchroniseToPeer(peerId: string): Promise<import("./TrysteroReplicator.js").P2PReplicationResult>;
63
41
  getRemoteConfig(peerId: string): Promise<false | import("../../common/types.js").ObsidianLiveSyncSettings>;
64
42
  watchPeer(peerId: string): void;
65
43
  unwatchPeer(peerId: string): void;
66
- sync(peerId: string, showNotice?: boolean): Promise<{
67
- error: any;
68
- ok?: undefined;
69
- } | {
70
- ok: boolean;
71
- error?: undefined;
72
- }>;
44
+ sync(peerId: string, showNotice?: boolean): Promise<import("./TrysteroReplicator.js").P2PReplicationResult>;
73
45
  setOnSetup(): void;
74
46
  clearOnSetup(): void;
75
47
  makeDecision(decision: AcceptanceDecision): Promise<void>;
@@ -96,5 +68,5 @@ export declare class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicat
96
68
  accepted_nodes: string[];
97
69
  }>;
98
70
  env: LiveSyncTrysteroReplicatorEnv;
99
- constructor(env: LiveSyncTrysteroReplicatorEnv);
71
+ constructor(env: LiveSyncTrysteroReplicatorEnv, sessionOwner?: P2PRoomSessionAccess);
100
72
  }