@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
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ does not provide an arbitrary-time checkpoint jump or On Demand repair.
|
|
|
32
32
|
| Entry point | Intended use | Current contract status |
|
|
33
33
|
| -------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
34
34
|
| `@vrtmrz/livesync-commonlib` | `DirectFileManipulator` for integrations which access CouchDB directly | Deliberately small, but its high-level lifecycle and file-operation semantics are not final |
|
|
35
|
+
| `@vrtmrz/livesync-commonlib/conflicts` | Keyed conflict-check scheduling with bounded concurrency and explicit completion | Focused, package-tested pre-1.0 contract |
|
|
35
36
|
| `@vrtmrz/livesync-commonlib/context` | Instance-owned events, translation injection, and host-neutral standard-I/O types | Focused, package-tested pre-1.0 contract |
|
|
36
37
|
| `@vrtmrz/livesync-commonlib/diagnostics` | Read-only Usage and integrity inspection, name projection, and reports | Focused pre-1.0 diagnostic contract |
|
|
37
38
|
| `@vrtmrz/livesync-commonlib/hashing` | Chunk ID calculation and exact file-content hashing | Focused, package-tested pre-1.0 contract |
|
|
@@ -47,6 +48,16 @@ does not provide an arbitrary-time checkpoint jump or On Demand repair.
|
|
|
47
48
|
| `@vrtmrz/livesync-commonlib/compat/*` | Exact legacy imports still required by existing clients | Migration-only; retained within 0.2.x, with removals treated as 0.3.0 breaking changes |
|
|
48
49
|
| `@vrtmrz/livesync-commonlib/package.json` | Package metadata for tooling | Metadata export, not a runtime API |
|
|
49
50
|
|
|
51
|
+
The existing `compat/services/lib` boundary retains `bindableFunction` and `lazyBindableFunction` for current clients. Their `setHandler()` methods return an `UnregisterFunction` which removes only its own current assignment; stale removers, including after an override with the same callback, and repeated removals are no-ops. Existing callers may ignore the return value. These helpers remain a migration boundary, not a general-purpose SDK integration surface.
|
|
52
|
+
|
|
53
|
+
The `compat/pouchdb/chunks` entry retains the obsolete `transferChunks`,
|
|
54
|
+
`balanceChunkPurgedDBs`, and `fetchAllUsedChunks` helpers for existing consumers.
|
|
55
|
+
The unreleased Self-hosted LiveSync integration removes its purge-based cleanup
|
|
56
|
+
recovery and uses the standard locked-remote recovery dialogue. Garbage
|
|
57
|
+
Collection V3 propagates ordinary deletion revisions and does not use these
|
|
58
|
+
helpers. Their compatibility exports and existing transfer behaviour remain
|
|
59
|
+
available; new integrations should not adopt this obsolete cleanup path.
|
|
60
|
+
|
|
50
61
|
Import only the entry point required by the current runtime. Browser code must not import the Node entry.
|
|
51
62
|
|
|
52
63
|
The Node and browser entries are platform façades. They keep platform-specific adapters and capabilities behind package-owned boundaries, so a consumer does not have to reproduce those imports or their build and review exceptions. The Node entry's selected built-in-module exports are deliberate parts of that façade. Package checks ensure that the root, context, and browser entries do not load the Node implementation.
|
|
@@ -55,6 +66,32 @@ The RPC entry exists only while the maintained LiveSync P2P composition depends
|
|
|
55
66
|
|
|
56
67
|
The P2P entry exposes eight focused views over one service owner, together with the maintained host-composition helpers. New consumers select only the lifecycle, peer-directory, peer-admission, targeted-transfer, change-relay, configuration-exchange, diagnostic, or connection-probe-admission view which they need. The composition result still carries a deprecated concrete Replicator façade while existing panes migrate; new consumers must not depend on that property or reach through it to a raw room or host.
|
|
57
68
|
|
|
69
|
+
## Blob helper migration
|
|
70
|
+
|
|
71
|
+
The unreleased Blob extraction retains `createBlob`, `createTextBlob`, and
|
|
72
|
+
`createBinaryBlob` through the existing `compat/common/utils` entry. Existing
|
|
73
|
+
callers can keep those imports. `createBlob` preserves an existing Blob's
|
|
74
|
+
identity and MIME type; text and binary inputs produce new Blobs with
|
|
75
|
+
`text/plain` and `application/octet-stream` respectively.
|
|
76
|
+
|
|
77
|
+
New code which only needs Blob conversion or byte comparison should use the focused
|
|
78
|
+
`octagonal-wheels/binary/blob` entry in Octagonal Wheels 0.1.56 or later.
|
|
79
|
+
Its normalisation function is named `toBlob`; its creation functions retain
|
|
80
|
+
`createTextBlob` and `createBinaryBlob`. Its `isBlobContentEqual` function
|
|
81
|
+
compares two Blobs in bounded parts and accepts an optional synchronous or
|
|
82
|
+
asynchronous ArrayBuffer equality predicate. See the
|
|
83
|
+
[Octagonal Wheels Blob guide](https://github.com/vrtmrz/fancy-kit/blob/main/packages/octagonal-wheels/guides/imports-and-runtime.md#blob-creation-and-normalisation)
|
|
84
|
+
for input and runtime contracts. This unreleased Commonlib integration now pins
|
|
85
|
+
the published Octagonal Wheels 0.1.56 dependency.
|
|
86
|
+
|
|
87
|
+
Commonlib continues to own document classification, decoding in `readAsBlob`,
|
|
88
|
+
and content-comparison policy. The legacy `isDocContentSame` retains its
|
|
89
|
+
IndexedDB selection and native receiver. Its fallback delegates bounded reads
|
|
90
|
+
to `isBlobContentEqual`, using the existing Commonlib Base64 encoder and host
|
|
91
|
+
Buffer fallback. The compatibility import retains its existing
|
|
92
|
+
environment initialisation; the focused Octagonal Wheels entry does not
|
|
93
|
+
initialise fetch or DOM services.
|
|
94
|
+
|
|
58
95
|
## Host context and initialisation
|
|
59
96
|
|
|
60
97
|
One service context belongs to one independently composed Commonlib client. It owns that client's event hub and translator. Creating a context without options creates a fresh event hub and renders Commonlib's typed canonical English messages. A host may inject a translator to provide another language:
|
|
@@ -276,3 +313,26 @@ See [Maintained-host examples and evidence](docs/proven-in-use.md) for compositi
|
|
|
276
313
|
## Licence
|
|
277
314
|
|
|
278
315
|
MIT
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
## Shared value helper migration
|
|
319
|
+
|
|
320
|
+
The shared formatter displays positive infinity as `∞y`, including through the existing `timeDeltaToHumanReadable` entry. Other duration formatting cases retain their previous behaviour.
|
|
321
|
+
|
|
322
|
+
The unreleased extraction keeps `timeDeltaToHumanReadable`,
|
|
323
|
+
`splitCustomRegExpList`, and `constructCustomRegExpList` at the existing
|
|
324
|
+
`compat/common/utils` entry. They delegate to `formatDuration` in
|
|
325
|
+
`octagonal-wheels/notation/duration` and the delimiter helpers in
|
|
326
|
+
`octagonal-wheels/string`. Commonlib's internal `asCopy` helper delegates to
|
|
327
|
+
`cloneViaJSON` in `octagonal-wheels/encoding/json`; it does not add a Commonlib
|
|
328
|
+
public entry. New general-purpose callers can use the focused Octagonal Wheels
|
|
329
|
+
entries once a release containing them is available.
|
|
330
|
+
|
|
331
|
+
Duration units and rounding remain unchanged. The delimiter adapters retain
|
|
332
|
+
their tagged types and string coercion; splitting removes empty parts and
|
|
333
|
+
does not interpret escapes. `asCopy<T>` retains its existing type signature
|
|
334
|
+
for compatibility. The typed `cloneViaJSON` entry instead constrains its input
|
|
335
|
+
to JSON-compatible types and defaults its output type to that input shape.
|
|
336
|
+
Explicit output types are caller assertions, not runtime validation. The legacy
|
|
337
|
+
adapter still follows native JSON behaviour for Dates, undefined fields, and
|
|
338
|
+
toJSON hooks; neither entry preserves prototypes or shared object identity.
|
|
@@ -62,6 +62,7 @@ export declare function decodeSettingsFromSetupURI(uri: string, passphrase: stri
|
|
|
62
62
|
batchSaveInLiveSync?: boolean;
|
|
63
63
|
syncOnSave: boolean;
|
|
64
64
|
syncOnStart: boolean;
|
|
65
|
+
strictContentCheckOnStartup?: boolean;
|
|
65
66
|
syncOnFileOpen: boolean;
|
|
66
67
|
syncOnEditorSave: boolean;
|
|
67
68
|
keepReplicationActiveInBackground: boolean;
|
|
@@ -8,23 +8,8 @@ class ContentSplitterRabinKarp extends ContentSplitterBase {
|
|
|
8
8
|
return settings.chunkSplitterVersion === ChunkAlgorithms.RabinKarp;
|
|
9
9
|
}
|
|
10
10
|
async processSplit(options) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
options.blob,
|
|
14
|
-
options.pieceSize,
|
|
15
|
-
options.plainSplit,
|
|
16
|
-
options.minimumChunkSize,
|
|
17
|
-
options.path
|
|
18
|
-
))();
|
|
19
|
-
} else {
|
|
20
|
-
return (await splitPiecesRabinKarp(
|
|
21
|
-
options.blob,
|
|
22
|
-
options.pieceSize,
|
|
23
|
-
options.plainSplit,
|
|
24
|
-
options.minimumChunkSize,
|
|
25
|
-
options.path
|
|
26
|
-
))();
|
|
27
|
-
}
|
|
11
|
+
const split = options.useWorker ? splitPieces2WorkerRabinKarp : splitPiecesRabinKarp;
|
|
12
|
+
return (await split(options.blob, options.pieceSize, options.plainSplit, options.minimumChunkSize, options.path))();
|
|
28
13
|
}
|
|
29
14
|
}
|
|
30
15
|
export {
|
|
@@ -8,25 +8,15 @@ class ContentSplitterV1 extends ContentSplitterBase {
|
|
|
8
8
|
return settings.chunkSplitterVersion === ChunkAlgorithms.V1 || settings.chunkSplitterVersion === "" || settings.chunkSplitterVersion === void 0;
|
|
9
9
|
}
|
|
10
10
|
async processSplit(options) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} else {
|
|
21
|
-
return (await splitPieces2(
|
|
22
|
-
options.blob,
|
|
23
|
-
options.pieceSize,
|
|
24
|
-
options.plainSplit,
|
|
25
|
-
options.minimumChunkSize,
|
|
26
|
-
options.path,
|
|
27
|
-
options.useSegmenter
|
|
28
|
-
))();
|
|
29
|
-
}
|
|
11
|
+
const split = options.useWorker ? splitPieces2Worker : splitPieces2;
|
|
12
|
+
return (await split(
|
|
13
|
+
options.blob,
|
|
14
|
+
options.pieceSize,
|
|
15
|
+
options.plainSplit,
|
|
16
|
+
options.minimumChunkSize,
|
|
17
|
+
options.path,
|
|
18
|
+
options.useSegmenter
|
|
19
|
+
))();
|
|
30
20
|
}
|
|
31
21
|
}
|
|
32
22
|
export {
|
|
@@ -8,25 +8,15 @@ class ContentSplitterV2 extends ContentSplitterBase {
|
|
|
8
8
|
return settings.chunkSplitterVersion === ChunkAlgorithms.V2 || settings.chunkSplitterVersion === ChunkAlgorithms.V2Segmenter;
|
|
9
9
|
}
|
|
10
10
|
async processSplit(options) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} else {
|
|
21
|
-
return (await splitPieces2V2(
|
|
22
|
-
options.blob,
|
|
23
|
-
options.pieceSize,
|
|
24
|
-
options.plainSplit,
|
|
25
|
-
options.minimumChunkSize,
|
|
26
|
-
options.path,
|
|
27
|
-
options.useSegmenter
|
|
28
|
-
))();
|
|
29
|
-
}
|
|
11
|
+
const split = options.useWorker ? splitPieces2WorkerV2 : splitPieces2V2;
|
|
12
|
+
return (await split(
|
|
13
|
+
options.blob,
|
|
14
|
+
options.pieceSize,
|
|
15
|
+
options.plainSplit,
|
|
16
|
+
options.minimumChunkSize,
|
|
17
|
+
options.path,
|
|
18
|
+
options.useSegmenter
|
|
19
|
+
))();
|
|
30
20
|
}
|
|
31
21
|
}
|
|
32
22
|
export {
|
|
@@ -51,6 +51,8 @@ const KeyIndexOfSettings = {
|
|
|
51
51
|
liveSync: 49,
|
|
52
52
|
syncOnSave: 50,
|
|
53
53
|
syncOnStart: 51,
|
|
54
|
+
strictContentCheckOnStartup: -1,
|
|
55
|
+
// Installation-local preference; do not encode into the QR Code.
|
|
54
56
|
syncOnFileOpen: 52,
|
|
55
57
|
syncOnEditorSave: 53,
|
|
56
58
|
keepReplicationActiveInBackground: -1,
|
|
@@ -65,6 +65,10 @@ export declare const SETTINGS_PROPERTY_POLICY: {
|
|
|
65
65
|
readonly markdown: "include";
|
|
66
66
|
readonly persistence: "include";
|
|
67
67
|
};
|
|
68
|
+
readonly strictContentCheckOnStartup: {
|
|
69
|
+
readonly markdown: "omit";
|
|
70
|
+
readonly persistence: "include";
|
|
71
|
+
};
|
|
68
72
|
readonly syncOnFileOpen: {
|
|
69
73
|
readonly markdown: "include";
|
|
70
74
|
readonly persistence: "include";
|
|
@@ -17,6 +17,7 @@ const SETTINGS_PROPERTY_POLICY = {
|
|
|
17
17
|
batchSaveInLiveSync: SettingPolicies.Default,
|
|
18
18
|
syncOnSave: SettingPolicies.Default,
|
|
19
19
|
syncOnStart: SettingPolicies.Default,
|
|
20
|
+
strictContentCheckOnStartup: SettingPolicies.LocalOnly,
|
|
20
21
|
syncOnFileOpen: SettingPolicies.Default,
|
|
21
22
|
syncOnEditorSave: SettingPolicies.Default,
|
|
22
23
|
keepReplicationActiveInBackground: SettingPolicies.Default,
|
|
@@ -91,6 +91,10 @@ interface SyncMethodSettings {
|
|
|
91
91
|
* automatically run sync on starting the plug-in.
|
|
92
92
|
*/
|
|
93
93
|
syncOnStart: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Compare file contents during startup checks when modification time and size match.
|
|
96
|
+
*/
|
|
97
|
+
strictContentCheckOnStartup?: boolean;
|
|
94
98
|
/**
|
|
95
99
|
* automatically run sync on opening a file.
|
|
96
100
|
*/
|
|
@@ -145,6 +145,7 @@ export declare const TweakValuesTemplate: {
|
|
|
145
145
|
batchSaveInLiveSync?: boolean;
|
|
146
146
|
syncOnSave?: boolean;
|
|
147
147
|
syncOnStart?: boolean;
|
|
148
|
+
strictContentCheckOnStartup?: boolean;
|
|
148
149
|
syncOnFileOpen?: boolean;
|
|
149
150
|
syncOnEditorSave?: boolean;
|
|
150
151
|
keepReplicationActiveInBackground?: boolean;
|
|
@@ -33,6 +33,7 @@ export declare const AllSettingDefault: {
|
|
|
33
33
|
batchSaveInLiveSync?: boolean;
|
|
34
34
|
syncOnSave: boolean;
|
|
35
35
|
syncOnStart: boolean;
|
|
36
|
+
strictContentCheckOnStartup?: boolean;
|
|
36
37
|
syncOnFileOpen: boolean;
|
|
37
38
|
syncOnEditorSave: boolean;
|
|
38
39
|
keepReplicationActiveInBackground: boolean;
|
|
@@ -104,6 +104,10 @@ const SettingInformation = {
|
|
|
104
104
|
name: "Sync on Startup",
|
|
105
105
|
desc: "Automatically Sync all files when opening Obsidian."
|
|
106
106
|
},
|
|
107
|
+
strictContentCheckOnStartup: {
|
|
108
|
+
name: "Strict content check on startup",
|
|
109
|
+
desc: "Compare file contents even when modification time and size match. This can slow startup in large vaults."
|
|
110
|
+
},
|
|
107
111
|
syncAfterMerge: {
|
|
108
112
|
name: "Sync after merging file",
|
|
109
113
|
desc: "Sync automatically after merging files"
|
package/dist/common/utils.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { replaceAll, replaceAllPairs } from "octagonal-wheels/string";
|
|
|
3
3
|
export { replaceAll, replaceAllPairs };
|
|
4
4
|
import { concatUInt8Array } from "octagonal-wheels/binary";
|
|
5
5
|
export { concatUInt8Array };
|
|
6
|
+
import { createBinaryBlob, createTextBlob } from "octagonal-wheels/binary/blob";
|
|
7
|
+
export { createBinaryBlob, createTextBlob };
|
|
6
8
|
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
|
7
9
|
export { delay, fireAndForget };
|
|
8
10
|
import { arrayToChunkedArray, unique } from "octagonal-wheels/collection";
|
|
@@ -28,12 +30,40 @@ export declare function getDocData(doc: string | string[]): string;
|
|
|
28
30
|
export declare function getDocDataAsArray(doc: string | string[]): string[];
|
|
29
31
|
export declare function getDocDataAsArrayBuffer(doc: string | string[] | ArrayBuffer): Uint8Array<ArrayBuffer>;
|
|
30
32
|
export declare function isTextBlob(blob: Blob): boolean;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Normalises content to a Blob through the legacy Commonlib entry point.
|
|
35
|
+
*
|
|
36
|
+
* @param data - Text, ordered text parts, binary bytes, or an existing Blob.
|
|
37
|
+
* @returns The original Blob, or a new text or binary Blob for other inputs.
|
|
38
|
+
* @remarks
|
|
39
|
+
* This compatibility wrapper retains the `createBlob` name and delegates to
|
|
40
|
+
* `toBlob` from `octagonal-wheels/binary/blob`. Existing Blob instances retain
|
|
41
|
+
* their identity and MIME type. Text retains transparent line endings; binary
|
|
42
|
+
* views contribute only their visible bytes. Calls use the current realm's
|
|
43
|
+
* Blob constructor. Document classification and decoding remain in
|
|
44
|
+
* {@link readAsBlob}; importing this legacy module retains its existing
|
|
45
|
+
* environment initialisation.
|
|
46
|
+
*/
|
|
33
47
|
export declare function createBlob(data: string | string[] | Uint8Array<ArrayBuffer> | ArrayBuffer | Blob): Blob;
|
|
34
48
|
export declare function isTextDocument(doc: LoadedEntry): boolean;
|
|
35
49
|
export declare function readAsBlob(doc: LoadedEntry): Blob;
|
|
36
50
|
export declare function readContent(doc: LoadedEntry): string | ArrayBuffer;
|
|
51
|
+
/**
|
|
52
|
+
* Tests whether legacy document-content inputs represent equal bytes.
|
|
53
|
+
*
|
|
54
|
+
* @param docA - Text, ordered text parts, a Blob, or binary bytes.
|
|
55
|
+
* @param docB - The second content value in any supported representation.
|
|
56
|
+
* @returns Whether the normalised inputs contain equal bytes, ignoring MIME types.
|
|
57
|
+
* @remarks
|
|
58
|
+
* Inputs are normalised with {@link createBlob}. Different sizes return false
|
|
59
|
+
* without reading content. IndexedDB comparison availability is selected at
|
|
60
|
+
* module import; that path resolves the current method and receiver before
|
|
61
|
+
* reading both complete Blobs sequentially, including empty inputs.
|
|
62
|
+
* Otherwise, `isBlobContentEqual` from `octagonal-wheels/binary/blob` reads
|
|
63
|
+
* pairs of parts of at most 10,000 bytes, using Commonlib's Base64 encoder and
|
|
64
|
+
* its existing host Buffer fallback. Empty inputs on this path return true
|
|
65
|
+
* without reads. Read and comparison failures propagate unchanged.
|
|
66
|
+
*/
|
|
37
67
|
export declare function isDocContentSame(docA: string | string[] | Blob | ArrayBuffer, docB: string | string[] | Blob | ArrayBuffer): Promise<boolean>;
|
|
38
68
|
export declare function isObfuscatedEntry(doc: DatabaseEntry): doc is AnyEntry;
|
|
39
69
|
export declare function isEncryptedChunkEntry(doc: DatabaseEntry): doc is EntryLeaf;
|
|
@@ -66,7 +96,7 @@ export declare function runWithInterval<T>(key: string, interval: number, task:
|
|
|
66
96
|
* @remarks This function is not designed to be concurrent.
|
|
67
97
|
*/
|
|
68
98
|
export declare function runWithStartInterval<T>(key: string, interval: number, task: () => Promise<T>): Promise<T>;
|
|
69
|
-
export declare const globalConcurrencyController: import("octagonal-wheels/concurrency/
|
|
99
|
+
export declare const globalConcurrencyController: import("octagonal-wheels/concurrency/semaphore").SemaphoreObject;
|
|
70
100
|
export declare function determineTypeFromBlob(data: Blob): "newnote" | "plain";
|
|
71
101
|
/**
|
|
72
102
|
* Whether remediation mode is active.
|
package/dist/common/utils.js
CHANGED
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
} from "./types.js";
|
|
10
10
|
import { normaliseP2PConnectionPath, normaliseP2PMaxWirePayloadBytes } from "./models/setting.p2p.js";
|
|
11
11
|
import { isErrorOfMissingDoc } from "../pouchdb/utils_couchdb.js";
|
|
12
|
-
import { replaceAll, replaceAllPairs } from "octagonal-wheels/string";
|
|
12
|
+
import { joinDelimitedList, replaceAll, replaceAllPairs, splitDelimitedList } from "octagonal-wheels/string";
|
|
13
|
+
import { formatDuration } from "octagonal-wheels/notation/duration";
|
|
13
14
|
import { concatUInt8Array } from "octagonal-wheels/binary";
|
|
15
|
+
import { createBinaryBlob, createTextBlob, isBlobContentEqual, toBlob } from "octagonal-wheels/binary/blob";
|
|
14
16
|
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
|
15
17
|
import { arrayToChunkedArray, unique } from "octagonal-wheels/collection";
|
|
16
18
|
import { extractObject, isObjectDifferent } from "octagonal-wheels/object";
|
|
@@ -55,17 +57,8 @@ function getDocDataAsArrayBuffer(doc) {
|
|
|
55
57
|
function isTextBlob(blob) {
|
|
56
58
|
return blob.type === "text/plain";
|
|
57
59
|
}
|
|
58
|
-
function createTextBlob(data) {
|
|
59
|
-
const d = Array.isArray(data) ? data : [data];
|
|
60
|
-
return new Blob(d, { endings: "transparent", type: "text/plain" });
|
|
61
|
-
}
|
|
62
|
-
function createBinaryBlob(data) {
|
|
63
|
-
return new Blob([data], { endings: "transparent", type: "application/octet-stream" });
|
|
64
|
-
}
|
|
65
60
|
function createBlob(data) {
|
|
66
|
-
|
|
67
|
-
if (data instanceof Uint8Array || data instanceof ArrayBuffer) return createBinaryBlob(data);
|
|
68
|
-
return createTextBlob(data);
|
|
61
|
+
return toBlob(data);
|
|
69
62
|
}
|
|
70
63
|
function isTextDocument(doc) {
|
|
71
64
|
if (doc.type == "plain") return true;
|
|
@@ -88,23 +81,17 @@ function readContent(doc) {
|
|
|
88
81
|
}
|
|
89
82
|
}
|
|
90
83
|
const isIndexDBCmpExist = typeof compatGlobal?.indexedDB?.cmp !== "undefined";
|
|
84
|
+
async function isArrayBufferContentEqual(left, right) {
|
|
85
|
+
return await arrayBufferToBase64Single(left) === await arrayBufferToBase64Single(right);
|
|
86
|
+
}
|
|
91
87
|
async function isDocContentSame(docA, docB) {
|
|
92
88
|
const blob1 = createBlob(docA);
|
|
93
89
|
const blob2 = createBlob(docB);
|
|
94
|
-
if (blob1.size != blob2.size) return false;
|
|
95
90
|
if (isIndexDBCmpExist) {
|
|
91
|
+
if (blob1.size != blob2.size) return false;
|
|
96
92
|
return compatGlobal.indexedDB.cmp(await blob1.arrayBuffer(), await blob2.arrayBuffer()) === 0;
|
|
97
93
|
}
|
|
98
|
-
|
|
99
|
-
const length = blob1.size;
|
|
100
|
-
let i = 0;
|
|
101
|
-
while (i < length) {
|
|
102
|
-
const ab1 = await blob1.slice(i, i + checkQuantum).arrayBuffer();
|
|
103
|
-
const ab2 = await blob2.slice(i, i + checkQuantum).arrayBuffer();
|
|
104
|
-
i += checkQuantum;
|
|
105
|
-
if (await arrayBufferToBase64Single(ab1) != await arrayBufferToBase64Single(ab2)) return false;
|
|
106
|
-
}
|
|
107
|
-
return true;
|
|
94
|
+
return isBlobContentEqual(blob1, blob2, isArrayBufferContentEqual);
|
|
108
95
|
}
|
|
109
96
|
function isObfuscatedEntry(doc) {
|
|
110
97
|
if (doc._id.startsWith(PREFIX_OBFUSCATED)) {
|
|
@@ -267,24 +254,7 @@ function escapeMarkdownValue(value) {
|
|
|
267
254
|
}
|
|
268
255
|
}
|
|
269
256
|
function timeDeltaToHumanReadable(delta) {
|
|
270
|
-
|
|
271
|
-
if (sec < 60) {
|
|
272
|
-
return `${sec.toFixed(2)}s`;
|
|
273
|
-
}
|
|
274
|
-
const min = sec / 60;
|
|
275
|
-
if (min < 60) {
|
|
276
|
-
return `${min.toFixed(2)}m`;
|
|
277
|
-
}
|
|
278
|
-
const hour = min / 60;
|
|
279
|
-
if (hour < 24) {
|
|
280
|
-
return `${hour.toFixed(2)}h`;
|
|
281
|
-
}
|
|
282
|
-
const day = hour / 24;
|
|
283
|
-
if (day < 365) {
|
|
284
|
-
return `${day.toFixed(2)}d`;
|
|
285
|
-
}
|
|
286
|
-
const year = day / 365;
|
|
287
|
-
return `${year.toFixed(2)}y`;
|
|
257
|
+
return formatDuration(delta);
|
|
288
258
|
}
|
|
289
259
|
async function wrapException(func) {
|
|
290
260
|
try {
|
|
@@ -378,12 +348,10 @@ function parseCustomRegExpList(list, flags, delimiter) {
|
|
|
378
348
|
return items.map((e) => new CustomRegExp(e, flags));
|
|
379
349
|
}
|
|
380
350
|
function constructCustomRegExpList(items, delimiter) {
|
|
381
|
-
return items.map((e) => `${e}`)
|
|
351
|
+
return joinDelimitedList(items.map((e) => `${e}`), `${delimiter}`);
|
|
382
352
|
}
|
|
383
353
|
function splitCustomRegExpList(list, delimiter) {
|
|
384
|
-
|
|
385
|
-
const source = `${list ?? ""}`;
|
|
386
|
-
return source.split(d).filter((e) => e);
|
|
354
|
+
return splitDelimitedList(`${list ?? ""}`, delimiter);
|
|
387
355
|
}
|
|
388
356
|
class CustomRegExp {
|
|
389
357
|
regexp;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LiveSyncError } from "./LSError.js";
|
|
2
|
+
import { cloneViaJSON } from "octagonal-wheels/encoding/json";
|
|
2
3
|
function asCopy(obj) {
|
|
3
|
-
return
|
|
4
|
+
return cloneViaJSON(obj);
|
|
4
5
|
}
|
|
5
6
|
function ensureError(error) {
|
|
6
7
|
if (error instanceof Error) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** An unclaimed value; its identity distinguishes replacements for the same key. */
|
|
2
|
+
export interface PendingWork<K, T> {
|
|
3
|
+
readonly key: K;
|
|
4
|
+
readonly value: T;
|
|
5
|
+
}
|
|
6
|
+
/** Keep only the latest unclaimed value for each key, in replacement order. */
|
|
7
|
+
export declare function createKeyedPendingWork<K, T>(keyOf: (value: T) => K): {
|
|
8
|
+
add(value: T): void;
|
|
9
|
+
peek(): PendingWork<K, T> | undefined;
|
|
10
|
+
claim(entry: PendingWork<K, T>): boolean;
|
|
11
|
+
values(): readonly T[];
|
|
12
|
+
readonly size: number;
|
|
13
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function createKeyedPendingWork(keyOf) {
|
|
2
|
+
const pending = /* @__PURE__ */ new Map();
|
|
3
|
+
return {
|
|
4
|
+
add(value) {
|
|
5
|
+
const key = keyOf(value);
|
|
6
|
+
pending.delete(key);
|
|
7
|
+
pending.set(key, { key, value });
|
|
8
|
+
},
|
|
9
|
+
peek() {
|
|
10
|
+
return pending.values().next().value;
|
|
11
|
+
},
|
|
12
|
+
claim(entry) {
|
|
13
|
+
if (pending.get(entry.key) !== entry) return false;
|
|
14
|
+
pending.delete(entry.key);
|
|
15
|
+
return true;
|
|
16
|
+
},
|
|
17
|
+
values() {
|
|
18
|
+
return Array.from(pending.values(), (entry) => entry.value);
|
|
19
|
+
},
|
|
20
|
+
get size() {
|
|
21
|
+
return pending.size;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
createKeyedPendingWork
|
|
27
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { FilePathWithPrefix } from "../common/types.js";
|
|
2
|
+
import { type Awaitable, type Scope } from "octagonal-wheels/resource/scope";
|
|
3
|
+
/** Host-owned conflict processing and observation for one queue. */
|
|
4
|
+
export interface ConflictCheckQueueOptions {
|
|
5
|
+
/** Process one admitted path. Deferred cleanup occupies the same execution slot. */
|
|
6
|
+
readonly process: (path: FilePathWithPrefix, scope: Scope<undefined>) => Awaitable<unknown>;
|
|
7
|
+
/** Positive safe integer; defaults to ten. Pending paths have no fixed capacity. */
|
|
8
|
+
readonly concurrency?: number;
|
|
9
|
+
/** Observe pending plus active checks. A thrown error stops the queue after active work settles. */
|
|
10
|
+
readonly onProgress?: (remaining: number) => void;
|
|
11
|
+
}
|
|
12
|
+
/** One queue owns pending checks; the host owns conflict decisions and recovery policy. */
|
|
13
|
+
export interface ConflictCheckQueue {
|
|
14
|
+
/** Accept a path without waiting for its check; a pending duplicate moves to the tail. */
|
|
15
|
+
enqueue(path: FilePathWithPrefix): void;
|
|
16
|
+
/** Wait for quiescence, including requeues and cleanup; false means the queue was aborted. Do not await from its processor. */
|
|
17
|
+
waitForIdle(): Promise<boolean>;
|
|
18
|
+
/** Stop admission and drain accepted work. A processor must not await its own queue's closure. */
|
|
19
|
+
close(): Promise<boolean>;
|
|
20
|
+
/** Stop admission and dispatch, join active cleanup, and return the retained, unstarted paths. */
|
|
21
|
+
abort(): Promise<readonly FilePathWithPrefix[]>;
|
|
22
|
+
/** Snapshot of unstarted paths; retained after abort or failure for the host to inspect. */
|
|
23
|
+
readonly pending: readonly FilePathWithPrefix[];
|
|
24
|
+
/** Pending and active checks, including item cleanup. */
|
|
25
|
+
readonly remaining: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compose keyed pending checks with bounded scoped execution.
|
|
29
|
+
*
|
|
30
|
+
* Input is accepted synchronously without a fixed pending-count limit. Each
|
|
31
|
+
* execution consumes a synchronous iterable which ends when no pending path
|
|
32
|
+
* remains; later arrivals join it or start the next execution. An empty queue has no input read,
|
|
33
|
+
* timer, or worker waiting in the background.
|
|
34
|
+
*
|
|
35
|
+
* Replacement affects unclaimed entries only. A running check may enqueue its
|
|
36
|
+
* own path without waiting for an execution slot. It is not cancelled or replaced
|
|
37
|
+
* by that later input. The host remains responsible for serialising any shared
|
|
38
|
+
* resource or interaction inside the processor.
|
|
39
|
+
*
|
|
40
|
+
* Unhandled processing, cleanup, or observation errors stop further dispatch.
|
|
41
|
+
* Started work and cleanup settle before completion waiters reject; unstarted
|
|
42
|
+
* paths remain inspectable. Catch an item error inside process when the host's
|
|
43
|
+
* explicit policy is to log and continue. There is no implicit retry or logging.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createConflictCheckQueue(options: ConflictCheckQueueOptions): ConflictCheckQueue;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createKeyedPendingWork } from "../concurrency/keyedPendingWork.js";
|
|
2
|
+
import { createNotifier } from "octagonal-wheels/concurrency/notifier";
|
|
3
|
+
import { forEachInScope } from "octagonal-wheels/concurrency/scoped.iteration";
|
|
4
|
+
import { withScope } from "octagonal-wheels/resource/scope";
|
|
5
|
+
function createConflictCheckQueue(options) {
|
|
6
|
+
const concurrency = options.concurrency ?? 10;
|
|
7
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
|
8
|
+
throw new RangeError("Conflict check concurrency must be a positive safe integer.");
|
|
9
|
+
}
|
|
10
|
+
const pending = createKeyedPendingWork((path) => path);
|
|
11
|
+
const changed = createNotifier();
|
|
12
|
+
const failures = [];
|
|
13
|
+
let accepting = true;
|
|
14
|
+
let aborted = false;
|
|
15
|
+
let stoppingAfterFailure = false;
|
|
16
|
+
let active = 0;
|
|
17
|
+
let running;
|
|
18
|
+
const remaining = () => pending.size + active;
|
|
19
|
+
const canDispatch = () => !aborted && !stoppingAfterFailure && failures.length === 0;
|
|
20
|
+
const recordFailure = (error) => {
|
|
21
|
+
accepting = false;
|
|
22
|
+
stoppingAfterFailure = true;
|
|
23
|
+
failures.push(error);
|
|
24
|
+
changed.notify();
|
|
25
|
+
};
|
|
26
|
+
const reportProgress = () => {
|
|
27
|
+
if (failures.length === 0) {
|
|
28
|
+
try {
|
|
29
|
+
options.onProgress?.(remaining());
|
|
30
|
+
} catch (error) {
|
|
31
|
+
recordFailure(error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
changed.notify();
|
|
35
|
+
};
|
|
36
|
+
function* availableWork() {
|
|
37
|
+
while (canDispatch()) {
|
|
38
|
+
const entry = pending.peek();
|
|
39
|
+
if (!entry) return;
|
|
40
|
+
yield entry;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function schedule() {
|
|
44
|
+
if (running || !canDispatch() || pending.size === 0) return;
|
|
45
|
+
running = Promise.resolve().then(
|
|
46
|
+
() => withScope(
|
|
47
|
+
void 0,
|
|
48
|
+
(parent) => forEachInScope(parent, availableWork(), { concurrency }, async (entry, child) => {
|
|
49
|
+
if (!canDispatch() || !pending.claim(entry)) return;
|
|
50
|
+
active++;
|
|
51
|
+
child.defer(() => {
|
|
52
|
+
active--;
|
|
53
|
+
reportProgress();
|
|
54
|
+
});
|
|
55
|
+
reportProgress();
|
|
56
|
+
try {
|
|
57
|
+
await options.process(entry.value, child);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
accepting = false;
|
|
60
|
+
stoppingAfterFailure = true;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
)
|
|
65
|
+
).then(() => void 0, recordFailure).finally(() => {
|
|
66
|
+
running = void 0;
|
|
67
|
+
schedule();
|
|
68
|
+
changed.notify();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function waitForIdle() {
|
|
72
|
+
schedule();
|
|
73
|
+
while (running) await changed.wait();
|
|
74
|
+
if (failures.length === 1) throw failures[0];
|
|
75
|
+
if (failures.length > 1) throw new AggregateError(failures, "Conflict checks failed.");
|
|
76
|
+
return !aborted;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
enqueue(path) {
|
|
80
|
+
if (!accepting) throw new Error("The conflict check queue is closed.");
|
|
81
|
+
pending.add(path);
|
|
82
|
+
reportProgress();
|
|
83
|
+
schedule();
|
|
84
|
+
},
|
|
85
|
+
waitForIdle,
|
|
86
|
+
close() {
|
|
87
|
+
accepting = false;
|
|
88
|
+
schedule();
|
|
89
|
+
return waitForIdle();
|
|
90
|
+
},
|
|
91
|
+
async abort() {
|
|
92
|
+
accepting = false;
|
|
93
|
+
aborted = true;
|
|
94
|
+
changed.notify();
|
|
95
|
+
await waitForIdle();
|
|
96
|
+
return pending.values();
|
|
97
|
+
},
|
|
98
|
+
get pending() {
|
|
99
|
+
return pending.values();
|
|
100
|
+
},
|
|
101
|
+
get remaining() {
|
|
102
|
+
return remaining();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
createConflictCheckQueue
|
|
108
|
+
};
|