@vrtmrz/livesync-commonlib 0.2.1 → 0.2.3

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 (70) hide show
  1. package/README.md +60 -0
  2. package/dist/API/processSetting.d.ts +1 -0
  3. package/dist/ContentSplitter/ContentSplitterRabinKarp.js +2 -17
  4. package/dist/ContentSplitter/ContentSplitterV1.js +9 -19
  5. package/dist/ContentSplitter/ContentSplitterV2.js +9 -19
  6. package/dist/common/models/setting.const.defaults.js +1 -0
  7. package/dist/common/models/setting.const.qr.js +2 -0
  8. package/dist/common/models/setting.policy.d.ts +4 -0
  9. package/dist/common/models/setting.policy.js +1 -0
  10. package/dist/common/models/setting.type.d.ts +4 -0
  11. package/dist/common/models/tweak.definition.d.ts +1 -0
  12. package/dist/common/settingConstants.d.ts +1 -0
  13. package/dist/common/settingConstants.js +4 -0
  14. package/dist/common/utils.d.ts +33 -3
  15. package/dist/common/utils.js +12 -44
  16. package/dist/common/utils.object.js +2 -1
  17. package/dist/concurrency/keyedPendingWork.d.ts +13 -0
  18. package/dist/concurrency/keyedPendingWork.js +27 -0
  19. package/dist/conflicts/conflictCheckQueue.d.ts +45 -0
  20. package/dist/conflicts/conflictCheckQueue.js +108 -0
  21. package/dist/conflicts.d.ts +3 -0
  22. package/dist/conflicts.js +4 -0
  23. package/dist/diagnostics/integrity.js +11 -12
  24. package/dist/interfaces/FileHandler.d.ts +2 -0
  25. package/dist/interfaces/ServiceModule.d.ts +42 -24
  26. package/dist/interfaces/ServiceModule.js +4 -3
  27. package/dist/interfaces/StorageAccess.d.ts +2 -0
  28. package/dist/interfaces/StorageEventManager.d.ts +2 -0
  29. package/dist/interfaces/StorageEventManager.js +4 -0
  30. package/dist/managers/StorageEventManager.d.ts +4 -3
  31. package/dist/managers/StorageEventManager.js +24 -6
  32. package/dist/pouchdb/LiveSyncLocalDB.js +1 -0
  33. package/dist/pouchdb/chunks.d.ts +16 -0
  34. package/dist/pouchdb/pouchdb-browser.js +2 -84
  35. package/dist/pouchdb/pouchdb-http.js +2 -84
  36. package/dist/pouchdb/purgeMulti.d.ts +4 -0
  37. package/dist/pouchdb/purgeMulti.js +89 -0
  38. package/dist/replication/journal/JournalSyncCore.js +6 -1
  39. package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
  40. package/dist/replication/trystero/TrysteroReplicator.js +3 -4
  41. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +3 -2
  42. package/dist/replication/trystero/peerPatterns.d.ts +1 -0
  43. package/dist/replication/trystero/peerPatterns.js +6 -0
  44. package/dist/rpc/RpcRoom.d.ts +5 -1
  45. package/dist/rpc/RpcRoom.js +80 -105
  46. package/dist/serviceFeatures/offlineScanner.d.ts +8 -8
  47. package/dist/serviceFeatures/offlineScanner.js +25 -21
  48. package/dist/serviceFeatures/prepareDatabaseForUse.d.ts +4 -3
  49. package/dist/serviceFeatures/prepareDatabaseForUse.js +5 -4
  50. package/dist/serviceFeatures/remoteConfig.d.ts +10 -5
  51. package/dist/serviceModules/ServiceFileAccessBase.d.ts +1 -0
  52. package/dist/serviceModules/ServiceFileAccessBase.js +3 -0
  53. package/dist/serviceModules/ServiceFileHandlerBase.d.ts +1 -0
  54. package/dist/serviceModules/ServiceFileHandlerBase.js +23 -7
  55. package/dist/services/base/DatabaseEventService.d.ts +1 -1
  56. package/dist/services/base/IService.d.ts +3 -3
  57. package/dist/services/base/VaultScanResult.d.ts +5 -0
  58. package/dist/services/base/VaultService.d.ts +1 -1
  59. package/dist/services/implements/injectable/InjectableConflictService.d.ts +5 -5
  60. package/dist/services/lib/HandlerUtils.d.ts +8 -6
  61. package/dist/services/lib/HandlerUtils.js +14 -0
  62. package/dist/worker/bgWorker.js +1 -1
  63. package/docs/conflict-resolution.md +30 -0
  64. package/docs/development.md +46 -0
  65. package/docs/offline-scanner.md +12 -4
  66. package/docs/proven-in-use.md +63 -0
  67. package/docs/releasing.md +2 -0
  68. package/docs/service-feature-composition.md +56 -9
  69. package/docs/storage-events-and-reflection.md +6 -2
  70. package/package.json +10 -2
@@ -1,3 +1,4 @@
1
+ import { parsePeerPatterns } from "./peerPatterns.js";
1
2
  import {
2
3
  TweakValuesShouldMatchedTemplate,
3
4
  omitP2PRuntimeSettings
@@ -171,12 +172,10 @@ class TrysteroReplicator {
171
172
  }
172
173
  }
173
174
  get autoSyncPeers() {
174
- const peers = this.currentSettings.P2P_AutoSyncPeers.split(",").map((e) => e.trim()).filter((e) => e.length > 0).map((e) => e.startsWith("~") ? new RegExp(e.substring(1), "i") : new RegExp(`^${e}$`, "i"));
175
- return peers;
175
+ return parsePeerPatterns(this.currentSettings.P2P_AutoSyncPeers);
176
176
  }
177
177
  get autoWatchPeers() {
178
- const peers = this.currentSettings.P2P_AutoWatchPeers.split(",").map((e) => e.trim()).filter((e) => e.length > 0).map((e) => e.startsWith("~") ? new RegExp(e.substring(1), "i") : new RegExp(`^${e}$`, "i"));
179
- return peers;
178
+ return parsePeerPatterns(this.currentSettings.P2P_AutoWatchPeers);
180
179
  }
181
180
  async onNewPeer(peer) {
182
181
  const peerName = peer.name;
@@ -1,3 +1,4 @@
1
+ import { parsePeerPatterns } from "./peerPatterns.js";
1
2
  import { selfId, joinRoom } from "@trystero-p2p/nostr";
2
3
  import { AutoAccepting, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE } from "../../common/types.js";
3
4
  import { LOG_LEVEL_VERBOSE, Logger } from "../../common/logger.js";
@@ -281,12 +282,12 @@ You can chose as follows:
281
282
  }
282
283
  _acceptablePeers = new Computed({
283
284
  evaluation: (settings) => {
284
- return `${settings?.P2P_AutoAcceptingPeers ?? ""}`.split(",").map((e) => e.trim()).filter((e) => !!e).map((e) => e.startsWith("~") ? new RegExp(e.substring(1), "i") : new RegExp(`^${e}$`, "i"));
285
+ return parsePeerPatterns(`${settings?.P2P_AutoAcceptingPeers ?? ""}`);
285
286
  }
286
287
  });
287
288
  _shouldDenyPeers = new Computed({
288
289
  evaluation: (settings) => {
289
- return `${settings?.P2P_AutoDenyingPeers ?? ""}`.split(",").map((e) => e.trim()).filter((e) => !!e).map((e) => e.startsWith("~") ? new RegExp(e.substring(1), "i") : new RegExp(`^${e}$`, "i"));
290
+ return parsePeerPatterns(`${settings?.P2P_AutoDenyingPeers ?? ""}`);
290
291
  }
291
292
  });
292
293
  /**
@@ -0,0 +1 @@
1
+ export declare function parsePeerPatterns(value: string): RegExp[];
@@ -0,0 +1,6 @@
1
+ function parsePeerPatterns(value) {
2
+ return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => entry.startsWith("~") ? new RegExp(entry.substring(1), "i") : new RegExp(`^${entry}$`, "i"));
3
+ }
4
+ export {
5
+ parsePeerPatterns
6
+ };
@@ -8,7 +8,7 @@ export declare class RpcRoom {
8
8
  private sessions;
9
9
  private outgoingChunkMap;
10
10
  private incomingChunkMap;
11
- private incomingChunkTimers;
11
+ private incomingChunkWaits;
12
12
  private peerVersion;
13
13
  private disposers;
14
14
  private closed;
@@ -32,7 +32,11 @@ export declare class RpcRoom {
32
32
  private notifyRemoteCancellation;
33
33
  private abortInboundCall;
34
34
  private sendEnvelope;
35
+ private sendChunk;
36
+ private sendErrorResponse;
35
37
  private scheduleMissingAck;
36
38
  private onWireMessage;
37
39
  private onEnvelopePayload;
40
+ private onRequest;
41
+ private runMethod;
38
42
  }
@@ -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
@@ -3,7 +3,7 @@ import { type LogFunction } from "../services/lib/logUtils.js";
3
3
  import type { NecessaryServices } from "../interfaces/ServiceModule.js";
4
4
  import type { IPathService } from "../services/base/IService.js";
5
5
  import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
6
- import { VaultScanResults, type VaultScanResult } from "../services/base/VaultScanResult.js";
6
+ import { VaultScanResults, type VaultScanResult, type VaultScanOptions } from "../services/base/VaultScanResult.js";
7
7
  export { VaultScanResults };
8
8
  export type { VaultScanResult };
9
9
  /**
@@ -156,7 +156,7 @@ export declare function getPathFromEntry(host: {
156
156
  * @param file Storage file information
157
157
  * @param doc Database entry
158
158
  */
159
- export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry): Promise<FilePairProcessResult>;
159
+ export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry, scanOptions?: VaultScanOptions): Promise<FilePairProcessResult>;
160
160
  export declare function canProceedScan(host: NecessaryServices<"keyValueDB" | "setting", never>, errorManager: UnresolvedErrorManager, log: LogFunction, showingNotice?: boolean, ignoreSuspending?: boolean): boolean;
161
161
  /**
162
162
  * Normalise the logical comparison key and lower case it when filename case should be handled insensitively.
@@ -180,7 +180,7 @@ export declare function collectDatabaseFiles(host: NecessaryServices<"database"
180
180
  }>;
181
181
  export declare function updateToDatabase(host: NecessaryServices<"vault", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, file: UXFileInfoStub): Promise<FilePairProcessResult>;
182
182
  export declare function updateToStorage(host: NecessaryServices<"vault" | "path", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, w: MetaEntry): Promise<FilePairProcessResult>;
183
- export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry): Promise<FilePairProcessResult>;
183
+ export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry, scanOptions?: VaultScanOptions): Promise<FilePairProcessResult>;
184
184
  export declare const FullScanModes: {
185
185
  readonly DB_APPLY: "db-apply";
186
186
  readonly NEWER_WINS: "newer-wins";
@@ -205,7 +205,7 @@ export declare const ExtraOnLocal: {
205
205
  */
206
206
  readonly APPEND_STORAGE_ONLY: "append-storage-only";
207
207
  };
208
- export interface FullScanOptions {
208
+ export interface FullScanOptions extends VaultScanOptions {
209
209
  mode: FullScanMode;
210
210
  extraOnLocal?: (typeof ExtraOnLocal)[keyof typeof ExtraOnLocal];
211
211
  extraOnRemote?: (typeof ExtraOnRemote)[keyof typeof ExtraOnRemote];
@@ -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 = {
@@ -350,7 +343,7 @@ function getPathFromEntry(host, doc) {
350
343
  const path = host.services.path.getPath(doc);
351
344
  return path;
352
345
  }
353
- async function syncFileBetweenDBandStorage(host, log, file, doc) {
346
+ async function syncFileBetweenDBandStorage(host, log, file, doc, scanOptions) {
354
347
  const docPath = getPathFromEntry(host, doc);
355
348
  if (!doc) {
356
349
  throw new Error(`Missing doc:${docPath}`);
@@ -393,6 +386,10 @@ async function syncFileBetweenDBandStorage(host, log, file, doc) {
393
386
  log(`STORAGE ? DB : ${file.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE);
394
387
  return FilePairProcessResults.SKIPPED;
395
388
  }
389
+ if (scanOptions?.strictContentCheck === false && await host.serviceModules.fileHandler.canSkipUnchangedFile?.(file, doc)) {
390
+ log(`STORAGE ? DB : ${file.path} skipped using unchanged storage and revision metadata`, LOG_LEVEL_DEBUG);
391
+ return FilePairProcessResults.SKIPPED;
392
+ }
396
393
  if (doc._rev && await host.serviceModules.fileHandler.tryRecordUntrackedFileRevision(file, doc._rev)) {
397
394
  log("STORAGE == DB :" + file.path, LOG_LEVEL_DEBUG);
398
395
  return FilePairProcessResults.COMPLETED;
@@ -439,6 +436,9 @@ function convertCase(settings, path) {
439
436
  }
440
437
  return normalisedPath.toLowerCase();
441
438
  }
439
+ function createComparisonPathLookup(settings, paths) {
440
+ return Object.fromEntries(paths.map((path) => [convertCase(settings, path), path]));
441
+ }
442
442
  async function collectFilesOnStorage(host, settings, log) {
443
443
  log("Collecting local files on the storage", LOG_LEVEL_VERBOSE);
444
444
  const filesStorageSrc = await host.serviceModules.storageAccess.getFiles();
@@ -451,8 +451,7 @@ async function collectFilesOnStorage(host, settings, log) {
451
451
  }
452
452
  const storageFileNameMap = Object.fromEntries(_filesStorage.map((e) => [e.path, e]));
453
453
  const storageFileNames = Object.keys(storageFileNameMap);
454
- const storageFileNameCapsPair = storageFileNames.map((e) => [e, convertCase(settings, e)]);
455
- const storageFileNameCI2CS = Object.fromEntries(storageFileNameCapsPair.map((e) => [e[1], e[0]]));
454
+ const storageFileNameCI2CS = createComparisonPathLookup(settings, storageFileNames);
456
455
  return { storageFileNameMap, storageFileNames, storageFileNameCI2CS };
457
456
  }
458
457
  async function collectDatabaseFiles(host, settings, log, showingNotice) {
@@ -496,8 +495,7 @@ async function collectDatabaseFiles(host, settings, log, showingNotice) {
496
495
  }
497
496
  const databaseFileNameMap = Object.fromEntries(_DBEntries.map((e) => [getPathFromEntry(host, e), e]));
498
497
  const databaseFileNames = Object.keys(databaseFileNameMap);
499
- const databaseFileNameCapsPair = databaseFileNames.map((e) => [e, convertCase(settings, e)]);
500
- const databaseFileNameCI2CS = Object.fromEntries(databaseFileNameCapsPair.map((e) => [e[1], e[0]]));
498
+ const databaseFileNameCI2CS = createComparisonPathLookup(settings, databaseFileNames);
501
499
  const quarantinedFileNamesLC = new Set(
502
500
  [...unresolvedFileNamesLC].filter((path) => !resolvableFileNamesLC.has(path))
503
501
  );
@@ -549,13 +547,13 @@ async function updateToStorage(host, log, logLevel, w) {
549
547
  return FilePairProcessResults.FAILED;
550
548
  }
551
549
  }
552
- async function syncStorageAndDatabase(host, log, file, logLevel, doc) {
550
+ async function syncStorageAndDatabase(host, log, file, logLevel, doc, scanOptions) {
553
551
  if ((doc._conflicts?.length ?? 0) > 0) {
554
552
  log(`SYNC DATABASE AND STORAGE: ${file.path} has conflicts. skipped`, LOG_LEVEL_INFO);
555
553
  return FilePairProcessResults.SKIPPED;
556
554
  }
557
555
  if (!host.services.vault.isFileSizeTooLarge(file.stat.size) && !host.services.vault.isFileSizeTooLarge(doc.size)) {
558
- return await syncFileBetweenDBandStorage(host, log, file, doc);
556
+ return await syncFileBetweenDBandStorage(host, log, file, doc, scanOptions);
559
557
  } else {
560
558
  log(
561
559
  `SYNC DATABASE AND STORAGE: ${getPathFromEntry(host, doc)} has been skipped due to file size exceeding the limit`,
@@ -698,7 +696,7 @@ async function processFilePair(host, log, pair, options) {
698
696
  if (!file || !doc) {
699
697
  throw new Error(`Cannot compare freshness for ${path}`);
700
698
  }
701
- const syncResult = await syncStorageAndDatabase(host, log, file, LOG_LEVEL_INFO, doc);
699
+ const syncResult = await syncStorageAndDatabase(host, log, file, LOG_LEVEL_INFO, doc, options);
702
700
  if (syncResult === FilePairProcessResults.COMPLETED) {
703
701
  updateFileMTimeInMap(host, fileMapKey, Math.max(file.stat.mtime, doc.mtime));
704
702
  }
@@ -746,7 +744,12 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
746
744
  const showingNotice = options.showingNotice ?? false;
747
745
  await loadFileStatus(host);
748
746
  const { storageFileNameMap, storageFileNameCI2CS } = await collectFilesOnStorage(host, settings, log);
749
- 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
+ );
750
753
  const pairs = [];
751
754
  for (const fileNameLC of unique([
752
755
  ...Object.keys(storageFileNameCI2CS),
@@ -922,11 +925,12 @@ async function performFullScan(host, log, errorManager, showingNoticeOrOptions =
922
925
  function useOfflineScanner(host) {
923
926
  const log = createInstanceLogFunction("SF:OfflineScanner", host.services.API);
924
927
  const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events);
925
- const handleScanVault = async (showingNotice, ignoreSuspending = false, continueOnFileFailure = false) => {
928
+ const handleScanVault = async (showingNotice, ignoreSuspending = false, continueOnFileFailure = false, scanOptions) => {
926
929
  return await performFullScan(host, log, errorManager, {
927
930
  showingNotice,
928
931
  ignoreSuspending,
929
- continueOnFileFailure
932
+ continueOnFileFailure,
933
+ strictContentCheck: scanOptions?.strictContentCheck
930
934
  });
931
935
  };
932
936
  host.services.vault.scanVault.addHandler(handleScanVault);
@@ -1,7 +1,7 @@
1
1
  import type { NecessaryServices } from "../interfaces/ServiceModule.js";
2
2
  import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
3
3
  import { type LogFunction } from "../services/lib/logUtils.js";
4
- import { type VaultScanResult } from "../services/base/VaultScanResult.js";
4
+ import { type VaultScanResult, type VaultScanOptions } from "../services/base/VaultScanResult.js";
5
5
  /**
6
6
  * Initialise the database and trigger a full vault scan.
7
7
  * @param host Services container
@@ -11,12 +11,13 @@ import { type VaultScanResult } from "../services/base/VaultScanResult.js";
11
11
  * @param reopenDatabase Whether to reopen the database connection
12
12
  * @param ignoreSuspending Whether to ignore suspension settings
13
13
  * @param continueOnFileFailure Whether individual file failures may satisfy application readiness
14
+ * @param scanOptions Optional content-check policy; omitted options keep strict checks
14
15
  * @returns The initialisation outcome, including accepted individual file failures
15
16
  */
16
- 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): 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>;
17
18
  /**
18
19
  * Associate the initialiser file feature with the app lifecycle events.
19
20
  * This function binds initialization handlers to the appropriate lifecycle events.
20
21
  * @param host Services container with required dependencies
21
22
  */
22
- 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;
@@ -6,7 +6,7 @@ function reportPreparationFailure(log, message) {
6
6
  log(message, LOG_LEVEL_VERBOSE);
7
7
  return false;
8
8
  }
9
- async function prepareDatabaseForUse(host, log, errorManager, showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false) {
9
+ async function prepareDatabaseForUse(host, log, errorManager, showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false, scanOptions) {
10
10
  const appLifecycle = host.services.appLifecycle;
11
11
  appLifecycle.resetIsReady();
12
12
  try {
@@ -25,7 +25,7 @@ async function prepareDatabaseForUse(host, log, errorManager, showingNotice = fa
25
25
  "Database preparation stopped because the local database is not ready."
26
26
  );
27
27
  }
28
- const scanResult = await host.services.vault.scanVault(showingNotice, ignoreSuspending, continueOnFileFailure);
28
+ const scanResult = await host.services.vault.scanVault(showingNotice, ignoreSuspending, continueOnFileFailure, scanOptions);
29
29
  if (scanResult === VaultScanResults.FAILED) {
30
30
  return reportPreparationFailure(log, "Database preparation stopped because the Vault scan failed.");
31
31
  }
@@ -57,7 +57,7 @@ async function prepareDatabaseForUse(host, log, errorManager, showingNotice = fa
57
57
  function usePrepareDatabaseForUse(host) {
58
58
  const log = createInstanceLogFunction("SF:prepareDatabaseForUse", host.services.API);
59
59
  const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events);
60
- const initialiseDatabaseHandler = async (showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false) => {
60
+ const initialiseDatabaseHandler = async (showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false, scanOptions) => {
61
61
  return await prepareDatabaseForUse(
62
62
  host,
63
63
  log,
@@ -65,7 +65,8 @@ function usePrepareDatabaseForUse(host) {
65
65
  showingNotice,
66
66
  reopenDatabase,
67
67
  ignoreSuspending,
68
- continueOnFileFailure
68
+ continueOnFileFailure,
69
+ scanOptions
69
70
  );
70
71
  };
71
72
  host.services.databaseEvents.initialiseDatabase.addHandler(initialiseDatabaseHandler);