@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 { withScope } from "octagonal-wheels/resource/scope";
1
2
  import {
2
3
  DOCID_SYNC_PARAMETERS,
3
4
  EntryTypes,
@@ -749,12 +750,11 @@ async function analyseDatabaseIntegrity(options) {
749
750
  let wasCancelled = cancelled(options);
750
751
  let configurationChanged = false;
751
752
  let pathEquivalenceGroups = [];
752
- let ownedRemote;
753
753
  if (!wasCancelled && !isConfigurationCurrent(options)) {
754
754
  configurationChanged = true;
755
755
  appendError(errors, "configuration", "configuration-changed");
756
756
  }
757
- try {
757
+ await withScope(void 0, async (scope) => {
758
758
  if (!wasCancelled && !configurationChanged) {
759
759
  const localRun = makeSourceRun("local", options.local);
760
760
  runs.push(localRun);
@@ -768,7 +768,14 @@ async function analyseDatabaseIntegrity(options) {
768
768
  runs.push(remoteRun);
769
769
  } else {
770
770
  try {
771
- ownedRemote = await options.remote();
771
+ const ownedRemote = await options.remote();
772
+ scope.defer(async () => {
773
+ try {
774
+ await ownedRemote.close();
775
+ } catch {
776
+ appendError(errors, "remote", "source-unavailable");
777
+ }
778
+ });
772
779
  if (cancelled(options)) {
773
780
  wasCancelled = true;
774
781
  } else {
@@ -851,15 +858,7 @@ async function analyseDatabaseIntegrity(options) {
851
858
  }
852
859
  }
853
860
  }
854
- } finally {
855
- if (ownedRemote) {
856
- try {
857
- await ownedRemote.close();
858
- } catch {
859
- appendError(errors, "remote", "source-unavailable");
860
- }
861
- }
862
- }
861
+ });
863
862
  if (cancelled(options)) wasCancelled = true;
864
863
  if (!wasCancelled && !configurationChanged && !isConfigurationCurrent(options)) {
865
864
  configurationChanged = true;
@@ -12,6 +12,8 @@ export interface IFileHandler {
12
12
  * operation never stores a document revision or writes to storage.
13
13
  */
14
14
  tryRecordUntrackedFileRevision(info: UXFileInfoStub | UXFileInfo, expectedRevision: string): Promise<boolean>;
15
+ /** Metadata-only scan hint; never certifies content equality or changes provenance. */
16
+ canSkipUnchangedFile?(info: UXFileInfoStub, expected: MetaEntry): Promise<boolean>;
15
17
  storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean, options?: StoreFileOptions): Promise<boolean>;
16
18
  /**
17
19
  * Store the current storage content as a child of an exact live database revision.
@@ -20,29 +20,49 @@ export interface ServiceModules {
20
20
  */
21
21
  rebuilder: Rebuilder;
22
22
  }
23
- export type RequiredServices<T extends keyof ServiceHub> = Pick<ServiceHub, T | "context">;
23
+ /**
24
+ * Selects service dependencies from the host's service type.
25
+ * A context is retained when the selected source exposes one. The default
26
+ * source preserves the existing ServiceHub dependency contract.
27
+ */
28
+ export type RequiredServices<T extends keyof THub, THub = ServiceHub> = Pick<THub, T | Extract<"context", keyof THub>>;
24
29
  export type RequiredServiceModules<T extends keyof ServiceModules> = Pick<ServiceModules, T>;
25
- export type RequiredServicesInterfaces<T extends keyof IServiceHub> = Pick<IServiceHub, T>;
30
+ export type RequiredServicesInterfaces<T extends keyof IServiceHub> = RequiredServices<T, IServiceHub>;
26
31
  export type RequiredServiceModulesInterfaces<T extends keyof ServiceModules> = Pick<ServiceModules, T>;
27
- export type NecessaryServices<T extends keyof ServiceHub, U extends keyof ServiceModules> = {
28
- services: RequiredServices<T>;
32
+ /**
33
+ * Declares the Services and ServiceModules consumed by a feature.
34
+ * Select a more specific source to retain injectable handlers or an extended
35
+ * host context, or use IServiceHub for operation-only interfaces. Additional
36
+ * collaborators, such as a borrowed LogFunction, belong in the feature's own
37
+ * dependency type. This type neither selects runtime values nor owns them.
38
+ */
39
+ export type NecessaryServices<T extends keyof THub, U extends keyof ServiceModules, THub = ServiceHub> = {
40
+ services: RequiredServices<T, THub>;
29
41
  serviceModules: RequiredServiceModules<U>;
30
42
  };
31
- export type NecessaryServicesInterfaces<T extends keyof IServiceHub, U extends keyof ServiceModules> = {
32
- services: RequiredServicesInterfaces<T>;
33
- serviceModules: RequiredServiceModulesInterfaces<U>;
34
- };
35
- export type ServiceFeatureFunction<T extends keyof ServiceHub, U extends keyof ServiceModules, TR> = (host: NecessaryServices<T, U>) => TR;
43
+ export type NecessaryServicesInterfaces<T extends keyof IServiceHub, U extends keyof ServiceModules> = NecessaryServices<T, U, IServiceHub>;
44
+ export type ServiceFeatureFunction<T extends keyof THub, U extends keyof ServiceModules, TR, THub = ServiceHub> = (host: NecessaryServices<T, U, THub>) => TR;
36
45
  type ServiceFeatureContext<T> = T & {
37
46
  _log: LogFunction;
38
47
  };
39
- export type ServiceFeatureFunctionWithContext<T extends keyof ServiceHub, U extends keyof ServiceModules, C, TR> = (host: NecessaryServices<T, U>, context: ServiceFeatureContext<C>) => TR;
48
+ export type ServiceFeatureFunctionWithContext<T extends keyof THub, U extends keyof ServiceModules, C, TR, THub = ServiceHub> = (host: NecessaryServices<T, U, THub>, context: ServiceFeatureContext<C>) => TR;
49
+ type AnyServiceFeature = (...args: never[]) => unknown;
50
+ type ServiceFeatureWithArguments<T extends keyof THub, U extends keyof ServiceModules, THub = ServiceHub> = (host: NecessaryServices<T, U, THub>, ...args: never[]) => unknown;
40
51
  /**
41
- * Helper function to create a service feature with proper typing.
42
- * @param featureFunction The feature function to be wrapped.
43
- * @returns The same feature function with proper typing.
52
+ * Retains a service feature's complete function type and identity.
53
+ * Declare the dependency type on the feature parameter so that required
54
+ * collaborators, concrete service types, and returned views remain explicit.
55
+ * Optional arguments, overloads, generic signatures, and asynchronous results
56
+ * are preserved. Older unannotated callbacks retain their ServiceHub context;
57
+ * their dependency keys cannot be inferred from the callback body.
58
+ *
59
+ * This helper does not invoke the feature, acquire or release its dependencies,
60
+ * or wrap its return value or errors. The host owns composition and disposal.
61
+ * @param featureFunction The feature function to retain.
62
+ * @returns The supplied function without a runtime wrapper.
44
63
  * @example
45
- * const useIndexFeature = createServiceFeature((host: NecessaryServices<"appLifecycle", "storageAccess">) => {
64
+ * type IndexHost = NecessaryServices<"appLifecycle", "storageAccess">;
65
+ * const useIndexFeature = createServiceFeature((host: IndexHost) => {
46
66
  * const { appLifecycle } = host.services;
47
67
  * const { storageAccess } = host.serviceModules;
48
68
  *
@@ -51,18 +71,16 @@ export type ServiceFeatureFunctionWithContext<T extends keyof ServiceHub, U exte
51
71
  * return true;
52
72
  * });
53
73
  * });
54
- *
55
- * useIndexFeature({
56
- * services: { context: serviceHub.context, appLifecycle: serviceHub.appLifecycle },
57
- * serviceModules: { storageAccess },
58
- * });
59
74
  */
75
+ export declare function createServiceFeature<TFeature extends ServiceFeatureWithArguments<keyof ServiceHub, keyof ServiceModules>>(featureFunction: TFeature): TFeature;
76
+ export declare function createServiceFeature<TFeature extends AnyServiceFeature>(featureFunction: TFeature): TFeature;
60
77
  export declare function createServiceFeature<T extends keyof ServiceHub, U extends keyof ServiceModules, TR>(featureFunction: ServiceFeatureFunction<T, U, TR>): ServiceFeatureFunction<T, U, TR>;
61
- type ContextFactory<T extends keyof ServiceHub, U extends keyof ServiceModules, C> = (host: NecessaryServices<T, U>) => ServiceFeatureContext<C>;
62
- export declare function serviceFeature<T extends keyof ServiceHub, U extends keyof ServiceModules>(): {
63
- create<TR>(featureFunction: ServiceFeatureFunction<T, U, TR>): ServiceFeatureFunction<T, U, TR>;
64
- withContext<C extends object = object>(ContextFactory: ContextFactory<T, U, C>): {
65
- create: <TR>(featureFunction: ServiceFeatureFunctionWithContext<T, U, C, TR>) => (host: NecessaryServices<T, U>, context: ServiceFeatureContext<C>) => TR;
78
+ type ContextFactory<T extends keyof THub, U extends keyof ServiceModules, C, THub = ServiceHub> = (host: NecessaryServices<T, U, THub>) => ServiceFeatureContext<C>;
79
+ /** Selects the host type for the existing service-feature builder. */
80
+ export declare function serviceFeature<T extends keyof THub, U extends keyof ServiceModules, THub = ServiceHub>(): {
81
+ create: <TR, TFeature extends (host: NecessaryServices<T, U, THub>, ...args: never[]) => TR = ServiceFeatureFunction<T, U, TR, THub>>(featureFunction: TFeature) => TFeature;
82
+ withContext<C extends object = object>(ContextFactory: ContextFactory<T, U, C, THub>): {
83
+ create: <TR>(featureFunction: ServiceFeatureFunctionWithContext<T, U, C, TR, THub>) => (host: NecessaryServices<T, U, THub>, context: ServiceFeatureContext<C>) => TR;
66
84
  };
67
85
  };
68
86
  export {};
@@ -2,10 +2,11 @@ function createServiceFeature(featureFunction) {
2
2
  return featureFunction;
3
3
  }
4
4
  function serviceFeature() {
5
+ function create(featureFunction) {
6
+ return featureFunction;
7
+ }
5
8
  return {
6
- create(featureFunction) {
7
- return featureFunction;
8
- },
9
+ create,
9
10
  withContext(ContextFactory) {
10
11
  return {
11
12
  create: (featureFunction) => (host, context) => featureFunction(host, ContextFactory(host))
@@ -12,6 +12,8 @@ export interface IStorageAccessManager {
12
12
  export interface StorageAccess {
13
13
  normalisePath(path: string): string;
14
14
  restoreState(): Promise<void>;
15
+ /** Includes queued, running, and failed work; absence requires a conservative content check. */
16
+ hasPendingFileEvent?(path: FilePath): boolean;
15
17
  deleteVaultItem(file: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise<void>;
16
18
  renameFile(file: UXFileInfoStub | FilePathWithPrefix, newPath: FilePathWithPrefix): Promise<UXFileInfoStub | null>;
17
19
  writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<boolean>;
@@ -11,6 +11,8 @@ export declare abstract class StorageEventManager {
11
11
  abstract beginWatch(): Promise<void>;
12
12
  abstract appendQueue(items: FileEvent[], ctx?: unknown): Promise<void>;
13
13
  abstract isWaiting(filename: FilePath): boolean;
14
+ /** Compatibility hosts without an ownership view must retain content checks. */
15
+ hasPendingFileEvent?(_filename: FilePath): boolean;
14
16
  abstract waitForIdle(): Promise<void>;
15
17
  abstract restoreState(): Promise<void>;
16
18
  /**
@@ -1,4 +1,8 @@
1
1
  class StorageEventManager {
2
+ /** Compatibility hosts without an ownership view must retain content checks. */
3
+ hasPendingFileEvent(_filename) {
4
+ return true;
5
+ }
2
6
  /**
3
7
  * Complete accepted work before its storage closes. Custom hosts must stop
4
8
  * their watcher admission before invoking this default implementation.
@@ -1,5 +1,6 @@
1
1
  import { type FileEventType, type FilePath, type UXFileInfoStub, type UXFolderInfo, type UXInternalFileInfoStub } from "../common/types.js";
2
2
  import { type FileEventItem } from "../common/types.js";
3
+ import { type DeferredAction } from "octagonal-wheels/concurrency/deferredAction";
3
4
  import type { IStorageAccessManager } from "../interfaces/StorageAccess.js";
4
5
  import { type PromiseWithResolvers } from "octagonal-wheels/promises";
5
6
  import { StorageEventManager, type FileEvent } from "../interfaces/StorageEventManager.js";
@@ -8,12 +9,11 @@ import type { SettingService } from "../services/base/SettingService.js";
8
9
  import type { FileProcessingService } from "../services/base/FileProcessingService.js";
9
10
  import { createInstanceLogFunction } from "../services/lib/logUtils.js";
10
11
  import type { IStorageEventManagerAdapter } from "./adapters/index.js";
11
- import { type CompatTimeoutHandle } from "../common/coreEnvFunctions.js";
12
12
  type WaitInfo = {
13
13
  since: number;
14
14
  type: FileEventType;
15
15
  canProceed: PromiseWithResolvers<boolean>;
16
- timerHandler: CompatTimeoutHandle;
16
+ deadline: DeferredAction;
17
17
  event: FileEventItem;
18
18
  };
19
19
  declare const TYPE_SENTINEL_FLUSH = "SENTINEL_FLUSH";
@@ -88,7 +88,7 @@ export declare abstract class StorageEventManagerBase<TAdapter extends IStorageE
88
88
  enqueue(newItem: FileEventItem): void;
89
89
  private _triggerTakeSnapshot;
90
90
  triggerTakeSnapshot: import("octagonal-wheels/function").ThrottledFunction<() => void>;
91
- protected concurrentProcessing: import("octagonal-wheels/concurrency/semaphore_v2").SemaphoreObject;
91
+ protected concurrentProcessing: import("octagonal-wheels/concurrency/semaphore").SemaphoreObject;
92
92
  protected _waitingMap: Map<string, WaitInfo>;
93
93
  private pendingOperations;
94
94
  private releaseOperations;
@@ -134,6 +134,7 @@ export declare abstract class StorageEventManagerBase<TAdapter extends IStorageE
134
134
  protected runQueuedEvents(options?: RunQueuedEventsOptions): Promise<void>;
135
135
  protected processingCount: number;
136
136
  protected requestProcessQueue(fei: FileEventItem): Promise<void>;
137
+ hasPendingFileEvent(filename: FilePath): boolean;
137
138
  isWaiting(filename: FilePath): boolean;
138
139
  protected handleFileEvent(queue: FileEventItem): Promise<void>;
139
140
  protected cancelRelativeEvent(item: FileEventItem): void;
@@ -10,10 +10,17 @@ import { delay, fireAndForget, isRemediationModeActive, throttle } from "../comm
10
10
  import {} from "../common/types.js";
11
11
  import { serialized } from "octagonal-wheels/concurrency/lock";
12
12
  import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
13
+ import {
14
+ createDeferredAction
15
+ } from "octagonal-wheels/concurrency/deferredAction";
13
16
  import { promiseWithResolvers } from "octagonal-wheels/promises";
14
17
  import { StorageEventManager } from "../interfaces/StorageEventManager.js";
15
18
  import { createInstanceLogFunction } from "../services/lib/logUtils.js";
16
19
  import { compatGlobal } from "../common/coreEnvFunctions.js";
20
+ const scheduleStorageTimeout = (callback, delayMs) => {
21
+ const handle = compatGlobal.setTimeout(callback, delayMs);
22
+ return () => compatGlobal.clearTimeout(handle);
23
+ };
17
24
  const TYPE_SENTINEL_FLUSH = "SENTINEL_FLUSH";
18
25
  class StorageEventManagerBase extends StorageEventManager {
19
26
  _log;
@@ -311,7 +318,7 @@ class StorageEventManagerBase extends StorageEventManager {
311
318
  const waitInfo = this._waitingMap.get(key);
312
319
  if (waitInfo) {
313
320
  waitInfo.canProceed.resolve(true);
314
- compatGlobal.clearTimeout(waitInfo.timerHandler);
321
+ waitInfo.deadline.dispose();
315
322
  this._waitingMap.delete(key);
316
323
  }
317
324
  this.triggerTakeSnapshot();
@@ -323,7 +330,7 @@ class StorageEventManagerBase extends StorageEventManager {
323
330
  const waitInfo = this._waitingMap.get(key);
324
331
  if (waitInfo) {
325
332
  waitInfo.canProceed.resolve(false);
326
- compatGlobal.clearTimeout(waitInfo.timerHandler);
333
+ waitInfo.deadline.dispose();
327
334
  this._waitingMap.delete(key);
328
335
  }
329
336
  this.triggerTakeSnapshot();
@@ -355,16 +362,18 @@ class StorageEventManagerBase extends StorageEventManager {
355
362
  LOG_LEVEL_DEBUG
356
363
  );
357
364
  }
365
+ const deadline = createDeferredAction(() => {
366
+ this._log(`Processing ${key}: Batch save timeout reached: ${event.type}`, LOG_LEVEL_DEBUG);
367
+ this._proceedWaiting(key);
368
+ }, scheduleStorageTimeout);
358
369
  const waitInfo = {
359
370
  since,
360
371
  type: event.type,
361
372
  event,
362
373
  canProceed: resolver,
363
- timerHandler: compatGlobal.setTimeout(() => {
364
- this._log(`Processing ${key}: Batch save timeout reached: ${event.type}`, LOG_LEVEL_DEBUG);
365
- this._proceedWaiting(key);
366
- }, nextDelay)
374
+ deadline
367
375
  };
376
+ deadline.schedule(nextDelay);
368
377
  this._waitingMap.set(key, waitInfo);
369
378
  this.triggerTakeSnapshot();
370
379
  return waitInfo;
@@ -530,6 +539,15 @@ class StorageEventManagerBase extends StorageEventManager {
530
539
  this.updateStatus();
531
540
  }
532
541
  }
542
+ hasPendingFileEvent(filename) {
543
+ for (const entry of this.pendingSnapshot.values()) {
544
+ if (entry.completed || entry.event.type === TYPE_SENTINEL_FLUSH) continue;
545
+ const event = entry.event;
546
+ const relatedPath = event.type === "RENAME" ? event.args.oldPath : event.args.renameTarget;
547
+ if ([event.args.file.path, relatedPath].some((path) => path && (filename === path || filename.startsWith(`${path}/`)))) return true;
548
+ }
549
+ return false;
550
+ }
533
551
  isWaiting(filename) {
534
552
  return this._waitingMap.has(filename);
535
553
  }
@@ -185,6 +185,7 @@ class LiveSyncLocalDB {
185
185
  this._log("Database initialisation was rejected by a required module.", LOG_LEVEL_NOTICE);
186
186
  return false;
187
187
  }
188
+ manager.changeManager.restartWatch();
188
189
  this._log("Opening Database...");
189
190
  this._log("Database info", LOG_LEVEL_VERBOSE);
190
191
  this._log(JSON.stringify(await openedDatabase.info(), null, 2), LOG_LEVEL_VERBOSE);
@@ -1,10 +1,26 @@
1
1
  import type { CouchDBConnection } from "../common/types.js";
2
2
  export declare function purgeUnreferencedChunks(db: PouchDB.Database, dryRun: boolean, connSetting?: CouchDBConnection, performCompact?: boolean): Promise<number>;
3
+ /**
4
+ * Copy chunks missing from the destination, preserving their source revisions.
5
+ *
6
+ * @deprecated Obsolete purge-based cleanup compatibility helper. Retained for
7
+ * existing package consumers; current Self-hosted LiveSync no longer calls it.
8
+ * Garbage Collection V3 propagates ordinary deletion revisions instead.
9
+ */
3
10
  export declare function transferChunks(key: string, label: string, dbFrom: PouchDB.Database, dbTo: PouchDB.Database, items: {
4
11
  id: string;
5
12
  rev: string;
6
13
  }[]): Promise<boolean>;
14
+ /**
15
+ * Complement chunks between databases which were separately purged.
16
+ * @deprecated Obsolete cleanup compatibility wrapper around transferChunks.
17
+ * Current Self-hosted LiveSync uses its standard locked-remote recovery flow.
18
+ */
7
19
  export declare function balanceChunkPurgedDBs(local: PouchDB.Database, remote: PouchDB.Database): Promise<void>;
20
+ /**
21
+ * @deprecated Obsolete transferChunks compatibility wrapper with no callers in
22
+ * the current Commonlib or Self-hosted LiveSync source trees.
23
+ */
8
24
  export declare function fetchAllUsedChunks(local: PouchDB.Database, remote: PouchDB.Database): Promise<void>;
9
25
  export declare function purgeChunksLocal(db: PouchDB.Database, docs: {
10
26
  id: string;
@@ -6,91 +6,9 @@ import mapreduce from "pouchdb-mapreduce";
6
6
  import replication from "pouchdb-replication";
7
7
  import find from "pouchdb-find";
8
8
  import transform from "transform-pouch";
9
- import { findPathToLeaf } from "pouchdb-merge";
10
- import { adapterFun } from "pouchdb-utils";
11
- import { createError, MISSING_DOC, UNKNOWN_ERROR } from "pouchdb-errors";
12
- import { mapAllTasksWithConcurrencyLimit, unwrapTaskResult } from "octagonal-wheels/concurrency/task";
9
+ import { installPurgeMulti } from "./purgeMulti.js";
13
10
  PouchDB.plugin(IDBPouch).plugin(INDEXEDDBPouch).plugin(HttpPouch).plugin(mapreduce).plugin(replication).plugin(find).plugin(transform);
14
- function appendPurgeSeqs(db, docs) {
15
- return db.get("_local/purges").then(function(doc) {
16
- for (const [docId, rev$$1] of docs) {
17
- const purgeSeq = doc.purgeSeq + 1;
18
- doc.purges.push({
19
- docId,
20
- rev: rev$$1,
21
- purgeSeq
22
- });
23
- if (doc.purges.length > db.purged_infos_limit) {
24
- doc.purges.splice(0, doc.purges.length - db.purged_infos_limit);
25
- }
26
- doc.purgeSeq = purgeSeq;
27
- }
28
- return doc;
29
- }).catch(function(err) {
30
- if (err.status !== 404) {
31
- throw err;
32
- }
33
- return {
34
- _id: "_local/purges",
35
- purges: docs.map(([docId, rev$$1], idx) => ({
36
- docId,
37
- rev: rev$$1,
38
- purgeSeq: idx
39
- })),
40
- purgeSeq: docs.length
41
- };
42
- }).then(function(doc) {
43
- return db.put(doc);
44
- });
45
- }
46
- PouchDB.prototype.purgeMulti = adapterFun(
47
- "_purgeMulti",
48
- function(docs, callback) {
49
- if (typeof this._purge === "undefined") {
50
- return callback(
51
- //@ts-ignore: this ts-ignore might be hiding a `this` bug where we don't have "this" conext.
52
- createError(UNKNOWN_ERROR, "Purge is not implemented in the " + this.adapter + " adapter.")
53
- );
54
- }
55
- const self = this;
56
- const tasks = docs.map(
57
- (param) => () => new Promise((res, rej) => {
58
- const [docId, rev$$1] = param;
59
- self._getRevisionTree(docId, (error, revs) => {
60
- if (error) {
61
- return res([param, error]);
62
- }
63
- if (!revs) {
64
- return res([param, createError(MISSING_DOC)]);
65
- }
66
- let path;
67
- try {
68
- path = findPathToLeaf(revs, rev$$1);
69
- } catch (error2) {
70
- return res([param, error2.message || error2]);
71
- }
72
- self._purge(docId, path, (error2, result) => {
73
- if (error2) {
74
- return res([param, error2]);
75
- } else {
76
- return res([param, result]);
77
- }
78
- });
79
- });
80
- })
81
- );
82
- (async () => {
83
- const ret = await mapAllTasksWithConcurrencyLimit(1, tasks);
84
- const retAll = ret.map((e) => unwrapTaskResult(e));
85
- await appendPurgeSeqs(
86
- self,
87
- retAll.filter((e) => "ok" in e[1]).map((e) => e[0])
88
- );
89
- const result = Object.fromEntries(retAll.map((e) => [e[0][0], e[1]]));
90
- return result;
91
- })().then((result) => callback(void 0, result)).catch((error) => callback(error));
92
- }
93
- );
11
+ installPurgeMulti(PouchDB);
94
12
  export {
95
13
  PouchDB
96
14
  };
@@ -4,91 +4,9 @@ import mapreduce from "pouchdb-mapreduce";
4
4
  import replication from "pouchdb-replication";
5
5
  import find from "pouchdb-find";
6
6
  import transform from "transform-pouch";
7
- import { findPathToLeaf } from "pouchdb-merge";
8
- import { adapterFun } from "pouchdb-utils";
9
- import { createError, MISSING_DOC, UNKNOWN_ERROR } from "pouchdb-errors";
10
- import { mapAllTasksWithConcurrencyLimit, unwrapTaskResult } from "octagonal-wheels/concurrency/task";
7
+ import { installPurgeMulti } from "./purgeMulti.js";
11
8
  PouchDB.plugin(HttpPouch).plugin(mapreduce).plugin(replication).plugin(find).plugin(transform);
12
- function appendPurgeSeqs(db, docs) {
13
- return db.get("_local/purges").then(function(doc) {
14
- for (const [docId, rev$$1] of docs) {
15
- const purgeSeq = doc.purgeSeq + 1;
16
- doc.purges.push({
17
- docId,
18
- rev: rev$$1,
19
- purgeSeq
20
- });
21
- if (doc.purges.length > db.purged_infos_limit) {
22
- doc.purges.splice(0, doc.purges.length - db.purged_infos_limit);
23
- }
24
- doc.purgeSeq = purgeSeq;
25
- }
26
- return doc;
27
- }).catch(function(err) {
28
- if (err.status !== 404) {
29
- throw err;
30
- }
31
- return {
32
- _id: "_local/purges",
33
- purges: docs.map(([docId, rev$$1], idx) => ({
34
- docId,
35
- rev: rev$$1,
36
- purgeSeq: idx
37
- })),
38
- purgeSeq: docs.length
39
- };
40
- }).then(function(doc) {
41
- return db.put(doc);
42
- });
43
- }
44
- PouchDB.prototype.purgeMulti = adapterFun(
45
- "_purgeMulti",
46
- function(docs, callback) {
47
- if (typeof this._purge === "undefined") {
48
- return callback(
49
- //@ts-ignore: this ts-ignore might be hiding a `this` bug where we don't have "this" conext.
50
- createError(UNKNOWN_ERROR, "Purge is not implemented in the " + this.adapter + " adapter.")
51
- );
52
- }
53
- const self = this;
54
- const tasks = docs.map(
55
- (param) => () => new Promise((res, rej) => {
56
- const [docId, rev$$1] = param;
57
- self._getRevisionTree(docId, (error, revs) => {
58
- if (error) {
59
- return res([param, error]);
60
- }
61
- if (!revs) {
62
- return res([param, createError(MISSING_DOC)]);
63
- }
64
- let path;
65
- try {
66
- path = findPathToLeaf(revs, rev$$1);
67
- } catch (error2) {
68
- return res([param, error2.message || error2]);
69
- }
70
- self._purge(docId, path, (error2, result) => {
71
- if (error2) {
72
- return res([param, error2]);
73
- } else {
74
- return res([param, result]);
75
- }
76
- });
77
- });
78
- })
79
- );
80
- (async () => {
81
- const ret = await mapAllTasksWithConcurrencyLimit(1, tasks);
82
- const retAll = ret.map((e) => unwrapTaskResult(e));
83
- await appendPurgeSeqs(
84
- self,
85
- retAll.filter((e) => "ok" in e[1]).map((e) => e[0])
86
- );
87
- const result = Object.fromEntries(retAll.map((e) => [e[0][0], e[1]]));
88
- return result;
89
- })().then((result) => callback(void 0, result)).catch((error) => callback(error));
90
- }
91
- );
9
+ installPurgeMulti(PouchDB);
92
10
  export {
93
11
  PouchDB
94
12
  };
@@ -0,0 +1,4 @@
1
+ import type PouchDB from "pouchdb-core";
2
+ type PouchDBConstructor = typeof PouchDB;
3
+ export declare function installPurgeMulti(PouchDB: PouchDBConstructor): void;
4
+ export {};
@@ -0,0 +1,89 @@
1
+ import { findPathToLeaf } from "pouchdb-merge";
2
+ import { adapterFun } from "pouchdb-utils";
3
+ import { createError, MISSING_DOC, UNKNOWN_ERROR } from "pouchdb-errors";
4
+ import { mapAllTasksWithConcurrencyLimit, unwrapTaskResult } from "octagonal-wheels/concurrency/task";
5
+ function appendPurgeSeqs(db, docs) {
6
+ return db.get("_local/purges").then(function(doc) {
7
+ for (const [docId, rev$$1] of docs) {
8
+ const purgeSeq = doc.purgeSeq + 1;
9
+ doc.purges.push({
10
+ docId,
11
+ rev: rev$$1,
12
+ purgeSeq
13
+ });
14
+ if (doc.purges.length > db.purged_infos_limit) {
15
+ doc.purges.splice(0, doc.purges.length - db.purged_infos_limit);
16
+ }
17
+ doc.purgeSeq = purgeSeq;
18
+ }
19
+ return doc;
20
+ }).catch(function(err) {
21
+ if (err.status !== 404) {
22
+ throw err;
23
+ }
24
+ return {
25
+ _id: "_local/purges",
26
+ purges: docs.map(([docId, rev$$1], idx) => ({
27
+ docId,
28
+ rev: rev$$1,
29
+ purgeSeq: idx
30
+ })),
31
+ purgeSeq: docs.length
32
+ };
33
+ }).then(function(doc) {
34
+ return db.put(doc);
35
+ });
36
+ }
37
+ function installPurgeMulti(PouchDB) {
38
+ PouchDB.prototype.purgeMulti = adapterFun(
39
+ "_purgeMulti",
40
+ function(docs, callback) {
41
+ if (typeof this._purge === "undefined") {
42
+ return callback(
43
+ //@ts-ignore: this ts-ignore might be hiding a `this` bug where we don't have "this" conext.
44
+ createError(UNKNOWN_ERROR, "Purge is not implemented in the " + this.adapter + " adapter.")
45
+ );
46
+ }
47
+ const self = this;
48
+ const tasks = docs.map(
49
+ (param) => () => new Promise((res, rej) => {
50
+ const [docId, rev$$1] = param;
51
+ self._getRevisionTree(docId, (error, revs) => {
52
+ if (error) {
53
+ return res([param, error]);
54
+ }
55
+ if (!revs) {
56
+ return res([param, createError(MISSING_DOC)]);
57
+ }
58
+ let path;
59
+ try {
60
+ path = findPathToLeaf(revs, rev$$1);
61
+ } catch (error2) {
62
+ return res([param, error2.message || error2]);
63
+ }
64
+ self._purge(docId, path, (error2, result) => {
65
+ if (error2) {
66
+ return res([param, error2]);
67
+ } else {
68
+ return res([param, result]);
69
+ }
70
+ });
71
+ });
72
+ })
73
+ );
74
+ (async () => {
75
+ const ret = await mapAllTasksWithConcurrencyLimit(1, tasks);
76
+ const retAll = ret.map((e) => unwrapTaskResult(e));
77
+ await appendPurgeSeqs(
78
+ self,
79
+ retAll.filter((e) => "ok" in e[1]).map((e) => e[0])
80
+ );
81
+ const result = Object.fromEntries(retAll.map((e) => [e[0][0], e[1]]));
82
+ return result;
83
+ })().then((result) => callback(void 0, result)).catch((error) => callback(error));
84
+ }
85
+ );
86
+ }
87
+ export {
88
+ installPurgeMulti
89
+ };
@@ -677,7 +677,12 @@ class JournalSyncCore {
677
677
  const piece = decompressed.slice(idxFrom, idxTo);
678
678
  const strPiece = d.decode(piece);
679
679
  if (strPiece.startsWith("~")) {
680
- const [idPart, dataPart] = strPiece.substring(1).split(UNIT_SPLIT);
680
+ const separatorIndex = strPiece.indexOf(UNIT_SPLIT);
681
+ if (separatorIndex === -1) {
682
+ throw new Error("Invalid journal chunk record: missing separator");
683
+ }
684
+ const idPart = strPiece.substring(1, separatorIndex);
685
+ const dataPart = strPiece.substring(separatorIndex + 1);
681
686
  result.push({
682
687
  _id: idPart,
683
688
  data: unescapeNewLineFromString(dataPart),
@@ -147,6 +147,7 @@ export declare class TrysteroReplicator {
147
147
  batchSaveInLiveSync?: boolean;
148
148
  syncOnSave?: boolean;
149
149
  syncOnStart?: boolean;
150
+ strictContentCheckOnStartup?: boolean;
150
151
  syncOnFileOpen?: boolean;
151
152
  syncOnEditorSave?: boolean;
152
153
  keepReplicationActiveInBackground?: boolean;
@@ -336,6 +337,7 @@ export declare class TrysteroReplicator {
336
337
  batchSaveInLiveSync?: boolean;
337
338
  syncOnSave?: boolean;
338
339
  syncOnStart?: boolean;
340
+ strictContentCheckOnStartup?: boolean;
339
341
  syncOnFileOpen?: boolean;
340
342
  syncOnEditorSave?: boolean;
341
343
  keepReplicationActiveInBackground?: boolean;