@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.
- package/README.md +60 -0
- package/dist/API/processSetting.d.ts +1 -0
- package/dist/ContentSplitter/ContentSplitterRabinKarp.js +2 -17
- package/dist/ContentSplitter/ContentSplitterV1.js +9 -19
- package/dist/ContentSplitter/ContentSplitterV2.js +9 -19
- package/dist/common/models/setting.const.defaults.js +1 -0
- package/dist/common/models/setting.const.qr.js +2 -0
- package/dist/common/models/setting.policy.d.ts +4 -0
- package/dist/common/models/setting.policy.js +1 -0
- package/dist/common/models/setting.type.d.ts +4 -0
- package/dist/common/models/tweak.definition.d.ts +1 -0
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/common/settingConstants.js +4 -0
- package/dist/common/utils.d.ts +33 -3
- package/dist/common/utils.js +12 -44
- package/dist/common/utils.object.js +2 -1
- package/dist/concurrency/keyedPendingWork.d.ts +13 -0
- package/dist/concurrency/keyedPendingWork.js +27 -0
- package/dist/conflicts/conflictCheckQueue.d.ts +45 -0
- package/dist/conflicts/conflictCheckQueue.js +108 -0
- package/dist/conflicts.d.ts +3 -0
- package/dist/conflicts.js +4 -0
- package/dist/diagnostics/integrity.js +11 -12
- package/dist/interfaces/FileHandler.d.ts +2 -0
- package/dist/interfaces/ServiceModule.d.ts +42 -24
- package/dist/interfaces/ServiceModule.js +4 -3
- package/dist/interfaces/StorageAccess.d.ts +2 -0
- package/dist/interfaces/StorageEventManager.d.ts +2 -0
- package/dist/interfaces/StorageEventManager.js +4 -0
- package/dist/managers/StorageEventManager.d.ts +4 -3
- package/dist/managers/StorageEventManager.js +24 -6
- package/dist/pouchdb/LiveSyncLocalDB.js +1 -0
- package/dist/pouchdb/chunks.d.ts +16 -0
- package/dist/pouchdb/pouchdb-browser.js +2 -84
- package/dist/pouchdb/pouchdb-http.js +2 -84
- package/dist/pouchdb/purgeMulti.d.ts +4 -0
- package/dist/pouchdb/purgeMulti.js +89 -0
- package/dist/replication/journal/JournalSyncCore.js +6 -1
- package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
- package/dist/replication/trystero/TrysteroReplicator.js +3 -4
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +3 -2
- package/dist/replication/trystero/peerPatterns.d.ts +1 -0
- package/dist/replication/trystero/peerPatterns.js +6 -0
- package/dist/rpc/RpcRoom.d.ts +5 -1
- package/dist/rpc/RpcRoom.js +80 -105
- package/dist/serviceFeatures/offlineScanner.d.ts +8 -8
- package/dist/serviceFeatures/offlineScanner.js +25 -21
- package/dist/serviceFeatures/prepareDatabaseForUse.d.ts +4 -3
- package/dist/serviceFeatures/prepareDatabaseForUse.js +5 -4
- package/dist/serviceFeatures/remoteConfig.d.ts +10 -5
- package/dist/serviceModules/ServiceFileAccessBase.d.ts +1 -0
- package/dist/serviceModules/ServiceFileAccessBase.js +3 -0
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +1 -0
- package/dist/serviceModules/ServiceFileHandlerBase.js +23 -7
- package/dist/services/base/DatabaseEventService.d.ts +1 -1
- package/dist/services/base/IService.d.ts +3 -3
- package/dist/services/base/VaultScanResult.d.ts +5 -0
- package/dist/services/base/VaultService.d.ts +1 -1
- package/dist/services/implements/injectable/InjectableConflictService.d.ts +5 -5
- package/dist/services/lib/HandlerUtils.d.ts +8 -6
- package/dist/services/lib/HandlerUtils.js +14 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/conflict-resolution.md +30 -0
- package/docs/development.md +46 -0
- package/docs/offline-scanner.md +12 -4
- package/docs/proven-in-use.md +63 -0
- package/docs/releasing.md +2 -0
- package/docs/service-feature-composition.md +56 -9
- package/docs/storage-events-and-reflection.md +6 -2
- 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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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> =
|
|
30
|
+
export type RequiredServicesInterfaces<T extends keyof IServiceHub> = RequiredServices<T, IServiceHub>;
|
|
26
31
|
export type RequiredServiceModulesInterfaces<T extends keyof ServiceModules> = Pick<ServiceModules, T>;
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
-
*
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/pouchdb/chunks.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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,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
|
|
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;
|