@vrtmrz/livesync-commonlib 0.2.2 → 0.3.0

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 (81) hide show
  1. package/README.md +86 -1
  2. package/dist/API/DirectFileManipulatorV2.d.ts +7 -1
  3. package/dist/API/DirectFileManipulatorV2.js +46 -67
  4. package/dist/API/DirectFileWatchOwner.d.ts +24 -0
  5. package/dist/API/DirectFileWatchOwner.js +256 -0
  6. package/dist/API/processSetting.js +3 -3
  7. package/dist/ContentSplitter/ContentSplitterRabinKarp.js +2 -17
  8. package/dist/ContentSplitter/ContentSplitterV1.js +9 -19
  9. package/dist/ContentSplitter/ContentSplitterV2.js +9 -19
  10. package/dist/common/models/setting.type.d.ts +1 -1
  11. package/dist/common/models/shared.definition.configNames.js +1 -2
  12. package/dist/common/models/tweak.definition.d.ts +1 -1
  13. package/dist/common/pathNormalization.d.ts +2 -0
  14. package/dist/common/pathNormalization.js +7 -2
  15. package/dist/common/utils.d.ts +33 -3
  16. package/dist/common/utils.js +12 -44
  17. package/dist/common/utils.object.js +2 -1
  18. package/dist/concurrency/keyedPendingWork.d.ts +13 -0
  19. package/dist/concurrency/keyedPendingWork.js +27 -0
  20. package/dist/conflicts/conflictCheckQueue.d.ts +45 -0
  21. package/dist/conflicts/conflictCheckQueue.js +108 -0
  22. package/dist/conflicts.d.ts +3 -0
  23. package/dist/conflicts.js +4 -0
  24. package/dist/diagnostics/integrity.js +11 -12
  25. package/dist/interfaces/ServiceModule.d.ts +42 -24
  26. package/dist/interfaces/ServiceModule.js +4 -3
  27. package/dist/managers/ChunkFetcher.d.ts +3 -0
  28. package/dist/managers/ChunkFetcher.js +20 -13
  29. package/dist/managers/LiveSyncManagers.d.ts +2 -0
  30. package/dist/managers/LiveSyncManagers.js +1 -0
  31. package/dist/managers/StorageEventManager.d.ts +3 -3
  32. package/dist/managers/StorageEventManager.js +15 -6
  33. package/dist/managers/chunkFetchSource.d.ts +14 -0
  34. package/dist/managers/chunkFetchSource.js +12 -0
  35. package/dist/pouchdb/LiveSyncDBFunctions.js +5 -0
  36. package/dist/pouchdb/LiveSyncLocalDB.d.ts +7 -1
  37. package/dist/pouchdb/LiveSyncLocalDB.js +14 -10
  38. package/dist/pouchdb/chunks.d.ts +16 -0
  39. package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
  40. package/dist/pouchdb/pathIdentityCompatibility.js +19 -1
  41. package/dist/pouchdb/pouchdb-browser.js +2 -84
  42. package/dist/pouchdb/pouchdb-http.js +2 -84
  43. package/dist/pouchdb/purgeMulti.d.ts +4 -0
  44. package/dist/pouchdb/purgeMulti.js +89 -0
  45. package/dist/replication/RemoteResource.d.ts +3 -0
  46. package/dist/replication/index.d.ts +2 -0
  47. package/dist/replication/index.js +2 -0
  48. package/dist/replication/journal/JournalSyncCore.d.ts +1 -0
  49. package/dist/replication/journal/JournalSyncCore.js +41 -20
  50. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +11 -3
  51. package/dist/replication/journal/LiveSyncJournalReplicator.js +123 -45
  52. package/dist/replication/trystero/P2PRoomSessionOwner.js +14 -13
  53. package/dist/replication/trystero/TrysteroReplicator.d.ts +7 -5
  54. package/dist/replication/trystero/TrysteroReplicator.js +101 -74
  55. package/dist/replication/trystero/TrysteroReplicator.pathIdentity.d.ts +11 -0
  56. package/dist/replication/trystero/TrysteroReplicator.pathIdentity.js +72 -0
  57. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +3 -2
  58. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +2 -0
  59. package/dist/replication/trystero/peerPatterns.d.ts +1 -0
  60. package/dist/replication/trystero/peerPatterns.js +6 -0
  61. package/dist/replication/trystero/useP2PReplicatorFeature.js +6 -2
  62. package/dist/rpc/RpcRoom.d.ts +5 -1
  63. package/dist/rpc/RpcRoom.js +80 -105
  64. package/dist/serviceFeatures/offlineScanner.d.ts +4 -4
  65. package/dist/serviceFeatures/offlineScanner.js +14 -15
  66. package/dist/serviceFeatures/prepareDatabaseForUse.d.ts +2 -2
  67. package/dist/serviceFeatures/remoteConfig.d.ts +10 -5
  68. package/dist/serviceModules/Rebuilder.d.ts +1 -0
  69. package/dist/serviceModules/Rebuilder.js +33 -5
  70. package/dist/serviceModules/ServiceFileHandlerBase.js +2 -7
  71. package/dist/services/implements/injectable/InjectableConflictService.d.ts +5 -5
  72. package/dist/services/lib/HandlerUtils.d.ts +8 -6
  73. package/dist/services/lib/HandlerUtils.js +14 -0
  74. package/dist/worker/bgWorker.js +1 -1
  75. package/docs/conflict-resolution.md +30 -0
  76. package/docs/development.md +50 -0
  77. package/docs/proven-in-use.md +80 -0
  78. package/docs/releasing.md +4 -0
  79. package/docs/service-feature-composition.md +63 -10
  80. package/docs/unicode-path-identity.md +64 -3
  81. package/package.json +11 -3
@@ -1,4 +1,7 @@
1
1
  import { compatGlobal } from "../common/coreEnvFunctions.js";
2
+ import {
3
+ createDeferredAction
4
+ } from "octagonal-wheels/concurrency/deferredAction";
2
5
  import { IncomingChunkBuffer, estimateBytes, splitIntoChunks } from "./chunking.js";
3
6
  import { asRpcErrorShape, RpcError } from "./errors.js";
4
7
  import { RpcSession } from "./RpcSession.js";
@@ -12,6 +15,10 @@ function newId(prefix) {
12
15
  function validNamespacedMethod(method) {
13
16
  return method.includes(".") || method.includes("/");
14
17
  }
18
+ const scheduleRpcTimeout = (callback, delayMs) => {
19
+ const handle = compatGlobal.setTimeout(callback, delayMs);
20
+ return () => compatGlobal.clearTimeout(handle);
21
+ };
15
22
  class RpcRoom {
16
23
  options;
17
24
  pending = /* @__PURE__ */ new Map();
@@ -21,7 +28,7 @@ class RpcRoom {
21
28
  sessions = /* @__PURE__ */ new Map();
22
29
  outgoingChunkMap = /* @__PURE__ */ new Map();
23
30
  incomingChunkMap = /* @__PURE__ */ new Map();
24
- incomingChunkTimers = /* @__PURE__ */ new Map();
31
+ incomingChunkWaits = /* @__PURE__ */ new Map();
25
32
  peerVersion = /* @__PURE__ */ new Map();
26
33
  disposers = [];
27
34
  closed = false;
@@ -83,8 +90,8 @@ class RpcRoom {
83
90
  this.sessions.clear();
84
91
  this.outgoingChunkMap.clear();
85
92
  this.incomingChunkMap.clear();
86
- this.incomingChunkTimers.forEach((h) => compatGlobal.clearTimeout(h));
87
- this.incomingChunkTimers.clear();
93
+ this.incomingChunkWaits.forEach((wait) => wait.dispose());
94
+ this.incomingChunkWaits.clear();
88
95
  }
89
96
  session(peerId) {
90
97
  const existing = this.sessions.get(peerId);
@@ -125,30 +132,32 @@ class RpcRoom {
125
132
  const requestId = newId("req");
126
133
  let pending;
127
134
  const p = new Promise((resolve, reject) => {
135
+ const signal = options.signal;
136
+ const onAbort = () => {
137
+ const current = this.pending.get(requestId);
138
+ if (!current) return;
139
+ this.rejectPending(requestId, new RpcError("CANCELLED", `RPC cancelled: ${method}`), true);
140
+ };
141
+ const deadline = timeoutMs > 0 ? createDeferredAction(() => {
142
+ this.rejectPending(requestId, new RpcError("TIMEOUT", `RPC timed out: ${method}`), true);
143
+ }, scheduleRpcTimeout) : void 0;
128
144
  pending = {
129
145
  peerId,
130
146
  method,
131
147
  resolve,
132
148
  reject,
133
- signal: options.signal,
149
+ releaseWait: () => {
150
+ deadline?.dispose();
151
+ signal?.removeEventListener("abort", onAbort);
152
+ },
134
153
  requestSent: false,
135
154
  cancelRequested: false
136
155
  };
137
- if (timeoutMs > 0) {
138
- pending.timeoutHandle = compatGlobal.setTimeout(() => {
139
- this.rejectPending(requestId, new RpcError("TIMEOUT", `RPC timed out: ${method}`), true);
140
- }, timeoutMs);
141
- }
156
+ deadline?.schedule(timeoutMs);
142
157
  this.pending.set(requestId, pending);
143
- if (options.signal) {
144
- const onAbort = () => {
145
- const current = this.pending.get(requestId);
146
- if (!current) return;
147
- this.rejectPending(requestId, new RpcError("CANCELLED", `RPC cancelled: ${method}`), true);
148
- };
149
- pending.abortListener = onAbort;
150
- options.signal.addEventListener("abort", onAbort, { once: true });
151
- if (options.signal.aborted) onAbort();
158
+ if (signal) {
159
+ signal.addEventListener("abort", onAbort, { once: true });
160
+ if (signal.aborted) onAbort();
152
161
  }
153
162
  });
154
163
  const sendTask = this.sendEnvelope(peerId, {
@@ -191,12 +200,7 @@ class RpcRoom {
191
200
  const pending = this.pending.get(requestId);
192
201
  if (!pending) return void 0;
193
202
  this.pending.delete(requestId);
194
- if (pending.timeoutHandle !== void 0) {
195
- compatGlobal.clearTimeout(pending.timeoutHandle);
196
- }
197
- if (pending.signal && pending.abortListener) {
198
- pending.signal.removeEventListener("abort", pending.abortListener);
199
- }
203
+ pending.releaseWait();
200
204
  return pending;
201
205
  }
202
206
  rejectPending(requestId, error, notifyRemote = false) {
@@ -229,28 +233,25 @@ class RpcRoom {
229
233
  const chunks = splitIntoChunks(serialized, this.options.maxWirePayloadBytes);
230
234
  this.outgoingChunkMap.set(streamId, { peerId, chunks });
231
235
  for (let i = 0; i < chunks.length; i++) {
232
- await this.options.transport.send(
233
- {
234
- wire: "chunk",
235
- streamId,
236
- index: i,
237
- total: chunks.length,
238
- payload: chunks[i]
239
- },
240
- peerId
241
- );
236
+ await this.sendChunk(peerId, streamId, chunks.length, i, chunks[i]);
242
237
  }
243
238
  }
239
+ sendChunk(peerId, streamId, total, index, payload) {
240
+ return this.options.transport.send({ wire: "chunk", streamId, index, total, payload }, peerId);
241
+ }
242
+ sendErrorResponse(peerId, requestId, error) {
243
+ return this.sendEnvelope(peerId, { kind: "response", requestId, ok: false, error });
244
+ }
244
245
  scheduleMissingAck(streamId, peerId) {
245
- const existing = this.incomingChunkTimers.get(streamId);
246
- if (existing) compatGlobal.clearTimeout(existing);
247
- const handle = compatGlobal.setTimeout(() => {
246
+ this.incomingChunkWaits.get(streamId)?.dispose();
247
+ const wait = createDeferredAction(() => {
248
248
  const state = this.incomingChunkMap.get(streamId);
249
249
  if (!state || state.isComplete()) return;
250
250
  const missing = state.missingIndices();
251
251
  void this.options.transport.send({ wire: "chunk-ack", streamId, missing }, peerId);
252
- }, this.options.chunkMissingRetryMs);
253
- this.incomingChunkTimers.set(streamId, handle);
252
+ }, scheduleRpcTimeout);
253
+ wait.schedule(this.options.chunkMissingRetryMs);
254
+ this.incomingChunkWaits.set(streamId, wait);
254
255
  }
255
256
  async onWireMessage(message, peerId) {
256
257
  if (message.wire === "raw") {
@@ -266,9 +267,8 @@ class RpcRoom {
266
267
  state2.add(message.index, message.payload);
267
268
  this.scheduleMissingAck(message.streamId, peerId);
268
269
  if (state2.isComplete()) {
269
- const timer = this.incomingChunkTimers.get(message.streamId);
270
- if (timer) compatGlobal.clearTimeout(timer);
271
- this.incomingChunkTimers.delete(message.streamId);
270
+ this.incomingChunkWaits.get(message.streamId)?.dispose();
271
+ this.incomingChunkWaits.delete(message.streamId);
272
272
  this.incomingChunkMap.delete(message.streamId);
273
273
  await this.options.transport.send(
274
274
  { wire: "chunk-ack", streamId: message.streamId, missing: [] },
@@ -287,16 +287,7 @@ class RpcRoom {
287
287
  for (const index of message.missing) {
288
288
  const payload = state.chunks[index];
289
289
  if (payload === void 0) continue;
290
- await this.options.transport.send(
291
- {
292
- wire: "chunk",
293
- streamId: message.streamId,
294
- index,
295
- total: state.chunks.length,
296
- payload
297
- },
298
- state.peerId
299
- );
290
+ await this.sendChunk(state.peerId, message.streamId, state.chunks.length, index, payload);
300
291
  }
301
292
  }
302
293
  async onEnvelopePayload(payload, peerId) {
@@ -319,8 +310,8 @@ class RpcRoom {
319
310
  return;
320
311
  }
321
312
  if (envelope.kind === "cancel") {
322
- const ctx2 = this.inboundCalls.get(envelope.requestId);
323
- if (ctx2 && ctx2.peerId === peerId) this.abortInboundCall(envelope.requestId, ctx2);
313
+ const ctx = this.inboundCalls.get(envelope.requestId);
314
+ if (ctx && ctx.peerId === peerId) this.abortInboundCall(envelope.requestId, ctx);
324
315
  return;
325
316
  }
326
317
  if (envelope.kind === "response") {
@@ -335,6 +326,9 @@ class RpcRoom {
335
326
  }
336
327
  return;
337
328
  }
329
+ return this.onRequest(envelope, peerId);
330
+ }
331
+ async onRequest(envelope, peerId) {
338
332
  const controller = new AbortController();
339
333
  const ctx = { controller, signal: controller.signal, peerId };
340
334
  this.inboundCalls.set(envelope.requestId, ctx);
@@ -344,12 +338,7 @@ class RpcRoom {
344
338
  } catch (ex) {
345
339
  this.inboundCalls.delete(envelope.requestId);
346
340
  if (!ctx.signal.aborted && !this.closed) {
347
- await this.sendEnvelope(peerId, {
348
- kind: "response",
349
- requestId: envelope.requestId,
350
- ok: false,
351
- error: asRpcErrorShape(ex)
352
- });
341
+ await this.sendErrorResponse(peerId, envelope.requestId, asRpcErrorShape(ex));
353
342
  }
354
343
  return;
355
344
  }
@@ -364,14 +353,9 @@ class RpcRoom {
364
353
  const version = this.peerVersion.get(peerId);
365
354
  if (version && version.major !== RPC_VERSION_MAJOR) {
366
355
  if (!ctx.signal.aborted && !this.closed) {
367
- await this.sendEnvelope(peerId, {
368
- kind: "response",
369
- requestId: envelope.requestId,
370
- ok: false,
371
- error: {
372
- code: "REMOTE_ERROR",
373
- message: `RPC major mismatch: local=${RPC_VERSION_MAJOR}, remote=${version.major}`
374
- }
356
+ await this.sendErrorResponse(peerId, envelope.requestId, {
357
+ code: "REMOTE_ERROR",
358
+ message: `RPC major mismatch: local=${RPC_VERSION_MAJOR}, remote=${version.major}`
375
359
  });
376
360
  }
377
361
  this.inboundCalls.delete(envelope.requestId);
@@ -386,48 +370,15 @@ class RpcRoom {
386
370
  const method = this.methods.get(envelope.method);
387
371
  if (!method) {
388
372
  if (!ctx.signal.aborted && !this.closed) {
389
- await this.sendEnvelope(peerId, {
390
- kind: "response",
391
- requestId: envelope.requestId,
392
- ok: false,
393
- error: {
394
- code: "REMOTE_ERROR",
395
- message: `Method not found: ${envelope.method}`
396
- }
373
+ await this.sendErrorResponse(peerId, envelope.requestId, {
374
+ code: "REMOTE_ERROR",
375
+ message: `Method not found: ${envelope.method}`
397
376
  });
398
377
  }
399
378
  this.inboundCalls.delete(envelope.requestId);
400
379
  return;
401
380
  }
402
- const runner = async () => {
403
- try {
404
- if (ctx.signal.aborted || this.closed) return;
405
- const data = method.cancellationAware ? await method.handler(
406
- ctx,
407
- peerId,
408
- ...envelope.args
409
- ) : await method.handler(peerId, ...envelope.args);
410
- if (ctx.signal.aborted || this.closed) return;
411
- await this.sendEnvelope(peerId, {
412
- kind: "response",
413
- requestId: envelope.requestId,
414
- ok: true,
415
- data
416
- });
417
- } catch (ex) {
418
- if (ctx.signal.aborted || this.closed) return;
419
- await this.sendEnvelope(peerId, {
420
- kind: "response",
421
- requestId: envelope.requestId,
422
- ok: false,
423
- error: asRpcErrorShape(ex)
424
- });
425
- } finally {
426
- if (this.inboundCalls.get(envelope.requestId) === ctx) {
427
- this.inboundCalls.delete(envelope.requestId);
428
- }
429
- }
430
- };
381
+ const runner = () => this.runMethod(method, envelope, peerId, ctx);
431
382
  if (method.serial) {
432
383
  method.queue = method.queue.then(runner, runner);
433
384
  await method.queue;
@@ -435,6 +386,30 @@ class RpcRoom {
435
386
  await runner();
436
387
  }
437
388
  }
389
+ async runMethod(method, envelope, peerId, ctx) {
390
+ try {
391
+ if (ctx.signal.aborted || this.closed) return;
392
+ const data = method.cancellationAware ? await method.handler(
393
+ ctx,
394
+ peerId,
395
+ ...envelope.args
396
+ ) : await method.handler(peerId, ...envelope.args);
397
+ if (ctx.signal.aborted || this.closed) return;
398
+ await this.sendEnvelope(peerId, {
399
+ kind: "response",
400
+ requestId: envelope.requestId,
401
+ ok: true,
402
+ data
403
+ });
404
+ } catch (ex) {
405
+ if (ctx.signal.aborted || this.closed) return;
406
+ await this.sendErrorResponse(peerId, envelope.requestId, asRpcErrorShape(ex));
407
+ } finally {
408
+ if (this.inboundCalls.get(envelope.requestId) === ctx) {
409
+ this.inboundCalls.delete(envelope.requestId);
410
+ }
411
+ }
412
+ }
438
413
  }
439
414
  export {
440
415
  RpcRoom
@@ -245,7 +245,7 @@ export declare function resolveFilePairAction(state: FilePairState, options: Ful
245
245
  * @param options Full scan options
246
246
  * @returns The scan result for all file pairs
247
247
  */
248
- export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options: FullScanOptions): Promise<VaultScanResult>;
248
+ export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options: FullScanOptions): Promise<VaultScanResult>;
249
249
  export declare function normaliseFullScanOptions(showingNoticeOrOptions: Partial<FullScanOptions> | boolean | undefined, ignoreSuspending?: boolean): FullScanOptions;
250
250
  /**
251
251
  * Remove one path from the Offline Scanner's durable last-seen state.
@@ -264,11 +264,11 @@ export declare function clearOfflineScannerLastSeen(host: NecessaryServices<"set
264
264
  * @param ignoreSuspending Legacy suspension flag used with the notice flag
265
265
  * @returns The scan outcome, including accepted individual file failures
266
266
  */
267
- export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options?: Partial<FullScanOptions>): Promise<VaultScanResult>;
268
- export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, ignoreSuspending?: boolean): Promise<VaultScanResult>;
267
+ export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options?: Partial<FullScanOptions>): Promise<VaultScanResult>;
268
+ export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, ignoreSuspending?: boolean): Promise<VaultScanResult>;
269
269
  /**
270
270
  * Associate the initialiser file feature with the app lifecycle events.
271
271
  * This function binds initialization handlers to the appropriate lifecycle events.
272
272
  * @param host Services container with required dependencies
273
273
  */
274
- export declare function useOfflineScanner(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "keyValueDB" | "replicator", "storageAccess" | "fileHandler">): void;
274
+ export declare function useOfflineScanner(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">): void;
@@ -15,7 +15,7 @@ import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "../common/models/shared.const.
15
15
  import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
16
16
  import { compatGlobal } from "../common/coreEnvFunctions.js";
17
17
  import { ICHeader, ICXHeader, PSCHeader } from "../common/models/fileaccess.const.js";
18
- import { serialized } from "octagonal-wheels/concurrency/lock";
18
+ import { serialized, withLocks } from "octagonal-wheels/concurrency/lock";
19
19
  import { VaultScanResults } from "../services/base/VaultScanResult.js";
20
20
  const FilePairProcessResults = {
21
21
  COMPLETED: "completed",
@@ -218,17 +218,10 @@ function createReaddressedMetadata(source, expectedDocumentId) {
218
218
  }
219
219
  async function serializedByMetadataDocumentIds(documentIds, callback) {
220
220
  const lockKeys = [...new Set(documentIds)].sort().map((documentId) => `processFileEvent-${documentId}`);
221
- const acquire = async (index) => {
222
- const key = lockKeys[index];
223
- if (key === void 0) return await callback();
224
- return await serialized(key, () => acquire(index + 1));
225
- };
226
- return await acquire(0);
221
+ return await withLocks(lockKeys, callback);
227
222
  }
228
223
  function findRepairEntry(entries, actualDocumentId) {
229
- return entries.find(
230
- ({ inspection }) => inspection.diagnostic.actualDocumentId === actualDocumentId
231
- );
224
+ return entries.find(({ inspection }) => inspection.diagnostic.actualDocumentId === actualDocumentId);
232
225
  }
233
226
  async function repairMetadataDocumentIdentity(host, request) {
234
227
  const baseResult = {
@@ -443,6 +436,9 @@ function convertCase(settings, path) {
443
436
  }
444
437
  return normalisedPath.toLowerCase();
445
438
  }
439
+ function createComparisonPathLookup(settings, paths) {
440
+ return Object.fromEntries(paths.map((path) => [convertCase(settings, path), path]));
441
+ }
446
442
  async function collectFilesOnStorage(host, settings, log) {
447
443
  log("Collecting local files on the storage", LOG_LEVEL_VERBOSE);
448
444
  const filesStorageSrc = await host.serviceModules.storageAccess.getFiles();
@@ -455,8 +451,7 @@ async function collectFilesOnStorage(host, settings, log) {
455
451
  }
456
452
  const storageFileNameMap = Object.fromEntries(_filesStorage.map((e) => [e.path, e]));
457
453
  const storageFileNames = Object.keys(storageFileNameMap);
458
- const storageFileNameCapsPair = storageFileNames.map((e) => [e, convertCase(settings, e)]);
459
- const storageFileNameCI2CS = Object.fromEntries(storageFileNameCapsPair.map((e) => [e[1], e[0]]));
454
+ const storageFileNameCI2CS = createComparisonPathLookup(settings, storageFileNames);
460
455
  return { storageFileNameMap, storageFileNames, storageFileNameCI2CS };
461
456
  }
462
457
  async function collectDatabaseFiles(host, settings, log, showingNotice) {
@@ -500,8 +495,7 @@ async function collectDatabaseFiles(host, settings, log, showingNotice) {
500
495
  }
501
496
  const databaseFileNameMap = Object.fromEntries(_DBEntries.map((e) => [getPathFromEntry(host, e), e]));
502
497
  const databaseFileNames = Object.keys(databaseFileNameMap);
503
- const databaseFileNameCapsPair = databaseFileNames.map((e) => [e, convertCase(settings, e)]);
504
- const databaseFileNameCI2CS = Object.fromEntries(databaseFileNameCapsPair.map((e) => [e[1], e[0]]));
498
+ const databaseFileNameCI2CS = createComparisonPathLookup(settings, databaseFileNames);
505
499
  const quarantinedFileNamesLC = new Set(
506
500
  [...unresolvedFileNamesLC].filter((path) => !resolvableFileNamesLC.has(path))
507
501
  );
@@ -750,7 +744,12 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
750
744
  const showingNotice = options.showingNotice ?? false;
751
745
  await loadFileStatus(host);
752
746
  const { storageFileNameMap, storageFileNameCI2CS } = await collectFilesOnStorage(host, settings, log);
753
- const { databaseFileNameMap, databaseFileNameCI2CS, quarantinedFileNamesLC } = await collectDatabaseFiles(host, settings, log, showingNotice);
747
+ const { databaseFileNameMap, databaseFileNameCI2CS, quarantinedFileNamesLC } = await collectDatabaseFiles(
748
+ host,
749
+ settings,
750
+ log,
751
+ showingNotice
752
+ );
754
753
  const pairs = [];
755
754
  for (const fileNameLC of unique([
756
755
  ...Object.keys(storageFileNameCI2CS),
@@ -14,10 +14,10 @@ import { type VaultScanResult, type VaultScanOptions } from "../services/base/Va
14
14
  * @param scanOptions Optional content-check policy; omitted options keep strict checks
15
15
  * @returns The initialisation outcome, including accepted individual file failures
16
16
  */
17
- export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean, scanOptions?: VaultScanOptions): Promise<VaultScanResult>;
17
+ export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "vault" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean, scanOptions?: VaultScanOptions): Promise<VaultScanResult>;
18
18
  /**
19
19
  * Associate the initialiser file feature with the app lifecycle events.
20
20
  * This function binds initialization handlers to the appropriate lifecycle events.
21
21
  * @param host Services container with required dependencies
22
22
  */
23
- export declare function usePrepareDatabaseForUse(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>): void;
23
+ export declare function usePrepareDatabaseForUse(host: NecessaryServices<"API" | "appLifecycle" | "vault" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>): void;
@@ -3,6 +3,10 @@ import { type RemoteConfigurationResult } from "../common/ConnectionString.js";
3
3
  import type { ObsidianLiveSyncSettings, RemoteConfiguration } from "../common/models/setting.type.js";
4
4
  import type { NecessaryServices } from "../interfaces/ServiceModule.js";
5
5
  export type RemoteConfigHost = NecessaryServices<"setting" | "UI" | "replication" | "control" | "appLifecycle" | "API", never>;
6
+ type RemoteConfigMigrationHost = NecessaryServices<"setting" | "API", never>;
7
+ type RemoteConfigMigrationFeatureHost = NecessaryServices<"setting" | "API" | "appLifecycle", never>;
8
+ type RemoteConfigSwitchHost = NecessaryServices<"setting" | "UI" | "control" | "API", never>;
9
+ type RemoteConfigReplicationHost = NecessaryServices<"setting" | "UI" | "replication" | "control" | "API", never>;
6
10
  export declare function migrateLegacyRemoteConfigurationsInPlace(settings: ObsidianLiveSyncSettings, log?: (message: string, level?: LOG_LEVEL) => void): boolean;
7
11
  /**
8
12
  * Generate a unique ID for a new remote configuration.
@@ -54,7 +58,7 @@ export declare function migrateP2PActiveRemoteConfigurationIdInPlace(settings: O
54
58
  /**
55
59
  * Migrates existing flat settings to the new multiple remote configurations list.
56
60
  */
57
- export declare function migrateToMultipleRemoteConfigurations(host: RemoteConfigHost): Promise<boolean>;
61
+ export declare function migrateToMultipleRemoteConfigurations(host: RemoteConfigMigrationHost): Promise<boolean>;
58
62
  /**
59
63
  * Logic to switch the active configuration.
60
64
  */
@@ -67,16 +71,17 @@ export declare function activateP2PRemoteConfiguration(settings: ObsidianLiveSyn
67
71
  /**
68
72
  * Command: Switch active connection
69
73
  */
70
- export declare function commandSwitchActiveRemote(host: RemoteConfigHost): Promise<void>;
74
+ export declare function commandSwitchActiveRemote(host: RemoteConfigSwitchHost): Promise<void>;
71
75
  /**
72
76
  * Command: Sync with a saved connection
73
77
  */
74
- export declare function commandReplicateWithSpecificRemote(host: RemoteConfigHost): Promise<void>;
78
+ export declare function commandReplicateWithSpecificRemote(host: RemoteConfigReplicationHost): Promise<void>;
75
79
  /**
76
80
  * Migration feature to be used during initialisation.
77
81
  */
78
- export declare function useRemoteConfigurationMigration(host: RemoteConfigHost): void;
82
+ export declare function useRemoteConfigurationMigration(host: RemoteConfigMigrationFeatureHost): void;
79
83
  /**
80
84
  * Hook to set up remote configuration features (Commands).
81
85
  */
82
- export declare function useRemoteConfiguration(host: RemoteConfigHost): boolean;
86
+ export declare function useRemoteConfiguration(host: RemoteConfigReplicationHost): boolean;
87
+ export {};
@@ -66,6 +66,7 @@ export declare class ServiceRebuilder extends ServiceModuleBase<ServiceRebuilder
66
66
  private scheduleInitialisation;
67
67
  scheduleRebuild(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
68
68
  scheduleFetch(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
69
+ private markRemoteLockedForRebuild;
69
70
  private _tryResetRemoteDatabase;
70
71
  private _onResetLocalDatabase;
71
72
  suspendAllSync(): Promise<void>;
@@ -26,6 +26,12 @@ const FAST_FETCH_RETRY_DELAYS = [2e3, 5e3, 1e4, 2e4];
26
26
  function canResetCentralRemote(replicator) {
27
27
  return "tryResetRemoteDatabase" in replicator && typeof replicator.tryResetRemoteDatabase === "function";
28
28
  }
29
+ function canCheckJournalPathIdentity(replicator) {
30
+ return "assertRemotePathIdentity" in replicator && typeof replicator.assertRemotePathIdentity === "function";
31
+ }
32
+ function canLockJournalForRebuild(replicator) {
33
+ return canResetCentralRemote(replicator) && "markRemoteLockedForRebuild" in replicator && typeof replicator.markRemoteLockedForRebuild === "function";
34
+ }
29
35
  class ServiceRebuilder extends ServiceModuleBase {
30
36
  events;
31
37
  appLifecycle;
@@ -100,7 +106,7 @@ Please enable them from the settings screen after setup is complete.`,
100
106
  notifyThresholdOfRemoteStorageSize: DEFAULT_SETTINGS.notifyThresholdOfRemoteStorageSize
101
107
  });
102
108
  await this.control.applySettings();
103
- await this.replication.markLocked();
109
+ await this.markRemoteLockedForRebuild();
104
110
  await this._tryResetRemoteDatabase();
105
111
  await this.replication.markLocked();
106
112
  await delay(500);
@@ -145,7 +151,7 @@ Please enable them from the settings screen after setup is complete.`,
145
151
  }
146
152
  return;
147
153
  }
148
- await this.replication.markLocked();
154
+ await this.markRemoteLockedForRebuild();
149
155
  await this._tryResetRemoteDatabase();
150
156
  await this.replication.markLocked();
151
157
  await delay(500);
@@ -170,9 +176,6 @@ Please enable them from the settings screen after setup is complete.`,
170
176
  await assertDatabasePathIdentity(this.database.localDatabase.localDatabase, settings);
171
177
  }
172
178
  if (getPathNormalizationVersion(settings) === 0) return;
173
- if (settings.remoteType !== REMOTE_COUCHDB) {
174
- throw new Error("NFC path identity is supported only with CouchDB. No database has been reset.");
175
- }
176
179
  const paths = await this.storageAccess.getFileNames();
177
180
  const selected = [];
178
181
  const databaseReady = this.database.isDatabaseReady();
@@ -191,6 +194,19 @@ Please enable them from the settings screen after setup is complete.`,
191
194
  }
192
195
  async checkRemotePathIdentityBeforeFetch() {
193
196
  const settings = this.setting.currentSettings();
197
+ if (settings.remoteType === REMOTE_MINIO) {
198
+ const trial = await this.replicator.getNewReplicator(settings);
199
+ if (!trial) throw new Error("Could not create a Journal replicator for the Fetch identity check.");
200
+ try {
201
+ if (!canCheckJournalPathIdentity(trial)) {
202
+ throw new Error("The Journal replicator does not support the Fetch identity check.");
203
+ }
204
+ await trial.assertRemotePathIdentity(settings);
205
+ } finally {
206
+ await trial.closeReplication();
207
+ }
208
+ return;
209
+ }
194
210
  if (settings.remoteType !== REMOTE_COUCHDB) return;
195
211
  const remote = settings.couchDB_URI.replace(/\/+$/, "") + (settings.couchDB_DBNAME === "" ? "" : "/" + settings.couchDB_DBNAME);
196
212
  const connection = await this.remote.connect(
@@ -262,6 +278,18 @@ Please enable them from the settings screen after setup is complete.`,
262
278
  scheduleFetch(prepareBeforeRestart) {
263
279
  return this.scheduleInitialisation(FlagFilesHumanReadable.FETCH_ALL, prepareBeforeRestart);
264
280
  }
281
+ async markRemoteLockedForRebuild() {
282
+ const settings = this.setting.currentSettings();
283
+ if (settings.remoteType !== REMOTE_MINIO) {
284
+ await this.replication.markLocked();
285
+ return;
286
+ }
287
+ const replicator = this.replicator.getActiveReplicator();
288
+ if (!replicator || !canLockJournalForRebuild(replicator)) {
289
+ throw new Error("The active Journal replicator does not support safe reconstruction.");
290
+ }
291
+ await replicator.markRemoteLockedForRebuild(settings);
292
+ }
265
293
  async _tryResetRemoteDatabase() {
266
294
  const currentReplicator = this.replicator.getActiveReplicator();
267
295
  const settings = this.setting.currentSettings();
@@ -1,5 +1,5 @@
1
1
  import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
2
- import { serialized } from "octagonal-wheels/concurrency/lock";
2
+ import { withLocks } from "octagonal-wheels/concurrency/lock";
3
3
  import {
4
4
  createBlob,
5
5
  getDocDataAsArray,
@@ -15,11 +15,6 @@ import { getStoragePathFromUXFileInfo } from "../common/typeUtils.js";
15
15
  import { EVEN } from "../common/models/shared.const.symbols.js";
16
16
  import { tryGetFilePath } from "../common/utils.doc.js";
17
17
  import { getPathNormalizationVersion, normalisePathForIdentity } from "../common/pathNormalization.js";
18
- async function serializedByKeys(keys, callback) {
19
- const [key, ...remainingKeys] = keys;
20
- if (key === void 0) return await callback();
21
- return await serialized(key, () => serializedByKeys(remainingKeys, callback));
22
- }
23
18
  function getParentPath(path) {
24
19
  const lastSeparator = path.lastIndexOf("/");
25
20
  return lastSeparator < 0 ? "" : path.slice(0, lastSeparator);
@@ -1200,7 +1195,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
1200
1195
  let failed = false;
1201
1196
  let failure;
1202
1197
  try {
1203
- result = await serializedByKeys(lockKeys, async () => {
1198
+ result = await withLocks(lockKeys, async () => {
1204
1199
  for (const id of uniqueIds) this.deferredConflictChecks.set(id, pending);
1205
1200
  try {
1206
1201
  return await callback(documentIds.length === 2 && documentIds[0] === documentIds[1]);
@@ -1,11 +1,11 @@
1
1
  import { ConflictService } from "../../base/ConflictService.js";
2
2
  import type { ServiceContext } from "../../base/ServiceBase.js";
3
3
  export declare class InjectableConflictService<T extends ServiceContext> extends ConflictService<T> {
4
- queueCheckForIfOpen: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../common/types.js").FilePathWithPrefix) => Promise<void>, unknown>;
5
- queueCheckFor: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../common/types.js").FilePathWithPrefix) => Promise<void>, unknown>;
4
+ queueCheckForIfOpen: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../conflicts.js").FilePathWithPrefix) => Promise<void>, unknown>;
5
+ queueCheckFor: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../conflicts.js").FilePathWithPrefix) => Promise<void>, unknown>;
6
6
  ensureAllProcessed: import("../../lib/HandlerUtils.js").HandlerFunction<() => Promise<boolean>, unknown>;
7
- resolveByDeletingRevision: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../common/types.js").FilePathWithPrefix, deleteRevision: string, title: string) => Promise<typeof import("../../../common/types.js").MISSING_OR_ERROR | typeof import("../../../common/types.js").AUTO_MERGED>, unknown>;
8
- resolve: import("../../lib/HandlerUtils.js").HandlerFunction<(filename: import("../../../common/types.js").FilePathWithPrefix) => Promise<void>, unknown>;
9
- resolveByNewest: import("../../lib/HandlerUtils.js").HandlerFunction<(filename: import("../../../common/types.js").FilePathWithPrefix) => Promise<boolean>, unknown>;
7
+ resolveByDeletingRevision: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../conflicts.js").FilePathWithPrefix, deleteRevision: string, title: string) => Promise<typeof import("../../../common/types.js").MISSING_OR_ERROR | typeof import("../../../common/types.js").AUTO_MERGED>, unknown>;
8
+ resolve: import("../../lib/HandlerUtils.js").HandlerFunction<(filename: import("../../../conflicts.js").FilePathWithPrefix) => Promise<void>, unknown>;
9
+ resolveByNewest: import("../../lib/HandlerUtils.js").HandlerFunction<(filename: import("../../../conflicts.js").FilePathWithPrefix) => Promise<boolean>, unknown>;
10
10
  resolveAllConflictedFilesByNewerOnes: import("../../lib/HandlerUtils.js").HandlerFunction<() => Promise<void>, unknown>;
11
11
  }
@@ -94,11 +94,12 @@ export declare class Binder<T extends (...args: any[]) => any> implements Binder
94
94
  */
95
95
  constructor(name: string, initialCallback?: T);
96
96
  private _callback;
97
+ private _registrationToken;
97
98
  /**
98
99
  * Assigns a new handler function.
99
100
  * @param callback The new handler function to assign.
100
101
  */
101
- assign(callback: T, override?: boolean): () => void;
102
+ assign(callback: T, override?: boolean): UnregisterFunction;
102
103
  /**
103
104
  * Invokes the assigned handler function with the provided arguments.
104
105
  * @param args The arguments to pass to the handler function.
@@ -114,13 +115,14 @@ export declare class LazyBinder<T extends (...args: any[]) => any> implements Bi
114
115
  private _name;
115
116
  private _callbackPromise;
116
117
  private _callback;
118
+ private _registrationToken;
117
119
  /**
118
120
  * Creates a new LazyBinder instance.
119
121
  * @param name The name of the handler.
120
122
  * @param initialCallback An optional initial callback function to assign.
121
123
  */
122
124
  constructor(name: string, initialCallback?: T);
123
- assign(callback: T, override?: boolean): () => void;
125
+ assign(callback: T, override?: boolean): UnregisterFunction;
124
126
  /**
125
127
  * Invokes the assigned handler function with the provided arguments.
126
128
  * @param args The arguments to pass to the handler function.
@@ -300,9 +302,9 @@ export interface HandlerFunction<TFunc extends (...args: any[]) => U | Promise<U
300
302
  * Assigns a new handler function.
301
303
  * @param callback The new handler function to assign.
302
304
  * @param override Whether to override the existing handler if one is already assigned.
303
- * @returns A function to unregister the assigned handler.
305
+ * @returns A function that unregisters this registration if it is still current.
304
306
  */
305
- setHandler: (callback: TFunc, override?: boolean) => void;
307
+ setHandler: (callback: TFunc, override?: boolean) => UnregisterFunction;
306
308
  }
307
309
  /**
308
310
  * A function type that can be used as a handler with assignable functionality.
@@ -316,9 +318,9 @@ export interface LazyHandlerFunction<TFunc extends (...args: any[]) => U | Promi
316
318
  * Assigns a new handler function.
317
319
  * @param callback The new handler function to assign.
318
320
  * @param override Whether to override the existing handler if one is already assigned.
319
- * @returns A function to unregister the assigned handler.
321
+ * @returns A function that unregisters this registration if it is still current.
320
322
  */
321
- setHandler: (callback: TFunc, override?: boolean) => void;
323
+ setHandler: (callback: TFunc, override?: boolean) => UnregisterFunction;
322
324
  }
323
325
  /**
324
326
  * A function type that can be used as a multiple handler with add/remove functionality.