@vrtmrz/livesync-commonlib 0.1.43 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -6
- package/dist/API/DirectFileManipulatorV2.d.ts +2 -0
- package/dist/API/DirectFileManipulatorV2.js +3 -0
- package/dist/common/configForDoc.d.ts +1 -0
- package/dist/common/configForDoc.js +11 -1
- package/dist/common/models/db.definition.d.ts +1 -0
- package/dist/common/models/setting.const.preferred.js +1 -3
- package/dist/diagnostics/couchdb.js +86 -13
- package/dist/diagnostics/index.d.ts +2 -2
- package/dist/diagnostics/index.js +2 -0
- package/dist/diagnostics/integrity.js +385 -158
- package/dist/diagnostics/names.d.ts +16 -0
- package/dist/diagnostics/names.js +50 -0
- package/dist/diagnostics/reports.d.ts +13 -10
- package/dist/diagnostics/reports.js +191 -50
- package/dist/diagnostics/types.d.ts +64 -4
- package/dist/diagnostics/usage.d.ts +4 -0
- package/dist/diagnostics/usage.js +158 -54
- package/dist/managers/EntryManager/EntryManagerImpls.js +3 -2
- package/dist/pouchdb/LiveSyncDBFunctions.js +4 -0
- package/dist/pouchdb/StreamingFetch.d.ts +5 -1
- package/dist/pouchdb/StreamingFetch.js +529 -65
- package/dist/replication/RemoteResource.d.ts +23 -2
- package/dist/replication/RemoteResource.js +6 -2
- package/dist/replication/couchdb/LiveSyncReplicator.d.ts +8 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +132 -1
- package/dist/replication/couchdb/fastForward.d.ts +19 -0
- package/dist/replication/couchdb/fastForward.js +119 -0
- package/dist/replication/index.d.ts +3 -1
- package/dist/replication/joinedDevices.d.ts +25 -0
- package/dist/replication/joinedDevices.js +123 -0
- package/dist/replication/journal/JournalSyncCore.d.ts +2 -1
- package/dist/replication/journal/JournalSyncCore.js +11 -1
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +4 -0
- package/dist/replication/journal/LiveSyncJournalReplicator.js +56 -1
- package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +3 -0
- package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -1
- package/dist/replication/journal/objectstore/MinioStorageAdapter.js +27 -1
- package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +1 -1
- package/dist/replication/trystero/P2PRoomSessionOwner.js +27 -14
- package/dist/serviceModules/Rebuilder.js +41 -15
- package/dist/services/BrowserServices.d.ts +4 -2
- package/dist/services/BrowserServices.js +2 -5
- package/dist/services/HeadlessServices.d.ts +3 -0
- package/dist/services/HeadlessServices.js +2 -5
- package/dist/services/ServiceHub.d.ts +4 -4
- package/dist/services/base/IService.d.ts +10 -18
- package/dist/services/base/ReplicatorService.remoteResourceResolver.js +1 -1
- package/dist/services/base/SettingService.d.ts +8 -18
- package/dist/services/base/SettingService.js +28 -46
- package/dist/services/implements/base/SvelteDialog.d.ts +2 -2
- package/dist/services/implements/base/UIService.d.ts +2 -0
- package/dist/services/implements/base/UIService.js +5 -0
- package/dist/services/implements/injectable/InjectableServiceHub.d.ts +4 -4
- package/dist/services/implements/injectable/InjectableServices.d.ts +2 -2
- package/dist/services/implements/injectable/InjectableSettingService.d.ts +0 -3
- package/dist/services/implements/injectable/InjectableSettingService.js +0 -11
- package/dist/string_and_binary/chunks.js +7 -5
- package/dist/uiCommands.d.ts +26 -0
- package/dist/uiCommands.js +57 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/database-diagnostics.md +45 -0
- package/docs/development.md +15 -3
- package/docs/proven-in-use.md +45 -5
- package/docs/releasing.md +20 -13
- package/docs/settings-lifecycle.md +37 -5
- package/package.json +13 -17
- package/dist/services/base/ConfigService.d.ts +0 -7
- package/dist/services/base/ConfigService.js +0 -6
- package/dist/services/implements/browser/ConfigServiceBrowserCompat.d.ts +0 -16
- package/dist/services/implements/browser/ConfigServiceBrowserCompat.js +0 -23
package/README.md
CHANGED
|
@@ -14,12 +14,19 @@ This package is primarily for maintainers of those clients and for integrations
|
|
|
14
14
|
|
|
15
15
|
## Status and versioning
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Commonlib is pre-1.0. The compatibility policy described here begins with 0.2.0 and does not retroactively apply to published 0.1.x versions. Within 0.2.x, Commonlib preserves published APIs, types, behaviour, supported persisted data, and exported `compat/*` paths; intentional breaking changes use 0.3.0. Consumers may declare ranges such as `^0.2.0`; retain a lockfile and use `npm ci` for reproducible installs. An entry in the package export map means that the import is deliberate and package-tested; it does not make every semantic detail of that entry a final 1.0 guarantee.
|
|
18
18
|
|
|
19
19
|
Treat every `compat/*` subpath as a migration-only interface. A source-directory import, Git submodule, or consumer-defined TypeScript alias is not a supported package boundary.
|
|
20
20
|
|
|
21
21
|
The package is ESM-only and declares Node 20 or later. Browser entry points are built with browser conditions, but the host remains responsible for checking the availability of browser capabilities such as the File System Access API.
|
|
22
22
|
|
|
23
|
+
Fast Fetch imports all current leaf revisions listed by CouchDB, including
|
|
24
|
+
deleted conflict leaves. A host may supply the optional
|
|
25
|
+
`initial-fetch-fast-forward` resource to hand a fresh, complete import to
|
|
26
|
+
ordinary replication before resuming local writers. Existing hosts can omit
|
|
27
|
+
this optimisation. Resume cursors retain their existing meaning; this resource
|
|
28
|
+
does not provide an arbitrary-time checkpoint jump or On Demand repair.
|
|
29
|
+
|
|
23
30
|
## Package entry points
|
|
24
31
|
|
|
25
32
|
| Entry point | Intended use | Current contract status |
|
|
@@ -36,7 +43,8 @@ The package is ESM-only and declares Node 20 or later. Browser entry points are
|
|
|
36
43
|
| `@vrtmrz/livesync-commonlib/remote-configurations` | Multiple-remote profile creation, naming, and selection | Focused, package-tested pre-1.0 contract |
|
|
37
44
|
| `@vrtmrz/livesync-commonlib/settings` | New-Vault defaults, stored-setting fallbacks, and settings migration results | Focused, package-tested pre-1.0 contract |
|
|
38
45
|
| `@vrtmrz/livesync-commonlib/setup-uri` | Encrypted Setup URI generation, availability, and import | Focused, package-tested pre-1.0 contract |
|
|
39
|
-
| `@vrtmrz/livesync-commonlib/
|
|
46
|
+
| `@vrtmrz/livesync-commonlib/ui-commands` | Instance-owned typed UI command declarations and handlers | Focused, package-tested contract |
|
|
47
|
+
| `@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 |
|
|
40
48
|
| `@vrtmrz/livesync-commonlib/package.json` | Package metadata for tooling | Metadata export, not a runtime API |
|
|
41
49
|
|
|
42
50
|
Import only the entry point required by the current runtime. Browser code must not import the Node entry.
|
|
@@ -75,6 +83,31 @@ After the Self-hosted LiveSync 1.0 work, the planned high-level composition is a
|
|
|
75
83
|
|
|
76
84
|
The factory is not implemented yet. Until it is available, service implementations exposed under `compat/*` retain their owning client's lifecycle rather than becoming a general public composition API.
|
|
77
85
|
|
|
86
|
+
## UI commands
|
|
87
|
+
|
|
88
|
+
Import `declareUICommand` and `UICommandRegistry` from `@vrtmrz/livesync-commonlib/ui-commands`. A declaration binds a stable command ID to its request and result types. Each `UIService` owns its own `commands` registry; declarations do not register global handlers.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { declareUICommand } from "@vrtmrz/livesync-commonlib/ui-commands";
|
|
92
|
+
|
|
93
|
+
const showDevices = declareUICommand<void, 'closed'>('example:show-devices');
|
|
94
|
+
const unregister = services.UI.commands.register(showDevices, async () => {
|
|
95
|
+
await openHostDeviceDialogue();
|
|
96
|
+
return 'closed';
|
|
97
|
+
});
|
|
98
|
+
const result = await services.UI.commands.invoke(showDevices, undefined);
|
|
99
|
+
// result is { status: 'handled', value: 'closed' } or { status: 'unavailable' }.
|
|
100
|
+
unregister();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A feature releases its handler when it unloads. The host dialogue owner remains responsible for active UI work. Duplicate handlers and conflicting declarations with the same ID are rejected; handler failures propagate. Calling an unavailable or disposed registry settles immediately. `UIService` disposes its registry on the context's plug-in-unloaded event.
|
|
104
|
+
|
|
105
|
+
## Joined device records
|
|
106
|
+
|
|
107
|
+
The `/replication` entry includes the `joined-devices` finite resource contract. A provider binds each resource to one settings snapshot; its owner calls `dispose()` in `finally`. `read()` returns recorded device IDs, acceptance, versions, platform, and connection times where available. An absent milestone is explicit. Joined-device operations never initialise a missing CouchDB database. These records do not establish online presence.
|
|
108
|
+
|
|
109
|
+
`forget(id)` removes one other device from all participation maps and the accepted list, while retaining the lock state, preferred tweaks, and unknown fields. The CouchDB implementation rereads the existing milestone before editing and verifies removal after saving. CouchDB local documents do not reject stale revisions, so concurrent changes to other device records can be overwritten. A failed verification is reported to the caller for manual refresh and retry. Object Storage checks ETags with conditional writes and retries conflicts a bounded number of times. Missing records, unsupported conditional writes, and exhausted conflicts have distinct results. Forgetting is not credential revocation: an unlocked remote permits a returning device to register again.
|
|
110
|
+
|
|
78
111
|
## Rooted storage
|
|
79
112
|
|
|
80
113
|
Both platform storage factories receive an existing root from the host. They never select a directory, request browser permission, infer a process directory, or persist a handle.
|
|
@@ -122,7 +155,9 @@ The focused Setup URI entry offers an Ephemeral mode bound to the current fixed
|
|
|
122
155
|
|
|
123
156
|
## Database diagnostics
|
|
124
157
|
|
|
125
|
-
The focused `/diagnostics` entry provides read-only Usage and integrity reports, with original names or temporary pseudonyms selected by the host for each run. Integrity inspection covers normal-file Metadata, current conflict branches, and content comparisons for suspect groups in the local database and CouchDB. See [Database diagnostics](docs/database-diagnostics.md) for coverage, cancellation, and privacy boundaries.
|
|
158
|
+
The focused `/diagnostics` entry provides read-only Usage and integrity reports, with original names or temporary pseudonyms selected by the host for each run. Integrity inspection covers normal-file Metadata, current conflict branches, and content comparisons for suspect groups in the local database and, by default, CouchDB; the host can exclude the remote source for one run. See [Database diagnostics](docs/database-diagnostics.md) for coverage, progress, cancellation, pseudonym lookup, and privacy boundaries.
|
|
159
|
+
|
|
160
|
+
Integrity Markdown places a compact state summary and review index before all findings, while retaining each observation and its follow-up references. This formatting does not change the projected JSON data or schema; see [the Markdown field guide](docs/database-diagnostics.md#compact-integrity-markdown-report) for the meaning and limits of its review index.
|
|
126
161
|
|
|
127
162
|
```typescript
|
|
128
163
|
import {
|
|
@@ -130,17 +165,33 @@ import {
|
|
|
130
165
|
createDiagnosticNameMapper,
|
|
131
166
|
createLocalDiagnosticSource,
|
|
132
167
|
formatUsageTSV,
|
|
168
|
+
parseDiagnosticPathToken,
|
|
133
169
|
projectUsageReport,
|
|
134
170
|
} from '@vrtmrz/livesync-commonlib/diagnostics';
|
|
135
171
|
|
|
136
172
|
const result = await analyseDatabaseUsage({ source: createLocalDiagnosticSource(localPouchDB) });
|
|
137
|
-
const
|
|
173
|
+
const mapper = createDiagnosticNameMapper('pseudonym');
|
|
174
|
+
const report = projectUsageReport(result, mapper);
|
|
138
175
|
if (report.status === 'complete') {
|
|
139
176
|
const tsv = formatUsageTSV(report);
|
|
140
177
|
// Present the report through the host's own UI or output stream.
|
|
141
178
|
}
|
|
142
179
|
```
|
|
143
180
|
|
|
181
|
+
Path pseudonyms include the report salt. A consumer can parse that token and supply its salt to a new mapper for a targeted follow-up against exact local Metadata paths:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
const pathToken = report.rows[0]?.path;
|
|
185
|
+
const parsed = pathToken === undefined ? undefined : parseDiagnosticPathToken(pathToken);
|
|
186
|
+
if (parsed) {
|
|
187
|
+
const followUpMapper = createDiagnosticNameMapper('pseudonym', parsed.salt);
|
|
188
|
+
const candidateToken = followUpMapper.path(storedVaultRelativePath);
|
|
189
|
+
// Compare this token with the path pseudonym on this device.
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The parser accepts only complete path tokens; it does not reverse them to recover a path. The salt is public, so a guessed path can be checked. Older ordinal pseudonyms have no corresponding lookup.
|
|
194
|
+
|
|
144
195
|
## Chunk ID calculation
|
|
145
196
|
|
|
146
197
|
The `/hashing` entry exports `HashManager` and its `HashManagerCoreOptions` type. It uses the same implementation as database writes, including legacy algorithms and independent ID keys. Hosts supply their settings service and await `initialise()` before calling `computeHash(piece)`.
|
|
@@ -173,11 +224,11 @@ The accepted replacement direction is an asynchronously created file client with
|
|
|
173
224
|
|
|
174
225
|
Package developers should read [the developer guide](docs/development.md). The focused contracts are described in [the storage guide](docs/platform-storage.md), [the standard-I/O guide](docs/platform-standard-io.md), [the settings lifecycle guide](docs/settings-lifecycle.md), [the remote configuration profile guide](docs/remote-configurations.md), [the remote feature compatibility contract](docs/remote-feature-compatibility.md), [the conflict-resolution guide](docs/conflict-resolution.md), and [the P2P transport lifecycle guide](docs/p2p-transport-lifecycle.md).
|
|
175
226
|
|
|
176
|
-
##
|
|
227
|
+
## Maintained-host examples and evidence
|
|
177
228
|
|
|
178
229
|
The package boundary is exercised by Self-hosted LiveSync's Obsidian plug-in, CLI, WebApp, and WebPeer compositions. Their validation is not uniform: the Obsidian plug-in and CLI have the strongest device and long-running synchronisation evidence, while WebApp and WebPeer have focused adapter and runtime tests, browser smoke tests, a cross-application P2P scenario ending at the production CLI, and public Pages smoke validation.
|
|
179
230
|
|
|
180
|
-
See [
|
|
231
|
+
See [Maintained-host examples and evidence](docs/proven-in-use.md) for composition points, downstream tests, and remaining limits. This optional page records examples and evidence; it does not define package support or create a release or adoption requirement. The examples deliberately separate focused public entries which a new consumer may copy from `compat/*` imports which exist to migrate the current application.
|
|
181
232
|
|
|
182
233
|
## Licence
|
|
183
234
|
|
|
@@ -24,6 +24,8 @@ export type DirectFileManipulatorOptions = {
|
|
|
24
24
|
maxChunksInEden?: number;
|
|
25
25
|
maxTotalLengthInEden?: number;
|
|
26
26
|
maxAgeInEden?: number;
|
|
27
|
+
/** Enables synchronisation of dot-prefixed internal files. */
|
|
28
|
+
syncInternalFiles?: boolean;
|
|
27
29
|
/**
|
|
28
30
|
* @deprecated use chunkSplitterVersion instead.
|
|
29
31
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MemoryValueStore } from "@vrtmrz/value-store";
|
|
1
2
|
import {
|
|
2
3
|
DEFAULT_SETTINGS,
|
|
3
4
|
DEFAULT_SYNC_PARAMETERS,
|
|
@@ -119,6 +120,7 @@ class DirectFileManipulator {
|
|
|
119
120
|
const getSettings = () => this.settings;
|
|
120
121
|
const context = new ServiceContext();
|
|
121
122
|
this.services = new HeadlessServiceHub(context, {
|
|
123
|
+
deviceValues: new MemoryValueStore(),
|
|
122
124
|
pouchDB: PouchDB,
|
|
123
125
|
database: this.getBoundDatabaseService(() => this.options, this.runtimeOptions),
|
|
124
126
|
getPathObfuscationPassphrase: () => this.options.obfuscatePassphrase ?? false,
|
|
@@ -253,6 +255,7 @@ class DirectFileManipulator {
|
|
|
253
255
|
handleFilenameCaseSensitive: this.options.handleFilenameCaseSensitive ?? DEFAULT_SETTINGS.handleFilenameCaseSensitive,
|
|
254
256
|
usePathObfuscation: !!this.options.obfuscatePassphrase,
|
|
255
257
|
encryptInternalMetadata: this.options.encryptInternalMetadata ?? false,
|
|
258
|
+
syncInternalFiles: this.options.syncInternalFiles ?? DEFAULT_SETTINGS.syncInternalFiles,
|
|
256
259
|
E2EEAlgorithm: this.options.E2EEAlgorithm ?? E2EEAlgorithms.V2
|
|
257
260
|
}
|
|
258
261
|
};
|
|
@@ -53,6 +53,7 @@ export declare const DoctorRegulationV0_25_0: DoctorRegulation;
|
|
|
53
53
|
export declare const DoctorRegulationV0_25_27: DoctorRegulation;
|
|
54
54
|
export declare const DoctorRegulationV1_0_0: DoctorRegulation;
|
|
55
55
|
export declare const DoctorRegulationV1_0_33: DoctorRegulation;
|
|
56
|
+
export declare const DoctorRegulationV1_0_40: DoctorRegulation;
|
|
56
57
|
export declare const DoctorRegulation: DoctorRegulation;
|
|
57
58
|
export declare function checkUnsuitableValues(setting: Partial<ObsidianLiveSyncSettings>, regulation?: DoctorRegulation): DoctorRegulation;
|
|
58
59
|
export declare const RebuildOptions: {
|
|
@@ -139,7 +139,16 @@ const DoctorRegulationV1_0_33 = {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
|
-
const
|
|
142
|
+
const DoctorRegulationV1_0_40 = {
|
|
143
|
+
// Only withdraw a recommendation. Retain the consultation version so users
|
|
144
|
+
// who dismissed 1.0.33 are not prompted again for unchanged recommendations.
|
|
145
|
+
version: DoctorRegulationV1_0_33.version,
|
|
146
|
+
rules: {
|
|
147
|
+
...DoctorRegulationV1_0_33.rules,
|
|
148
|
+
customChunkSize: void 0
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const DoctorRegulation = DoctorRegulationV1_0_40;
|
|
143
152
|
function checkUnsuitableValues(setting, regulation = DoctorRegulation) {
|
|
144
153
|
const result = {
|
|
145
154
|
version: regulation.version,
|
|
@@ -339,6 +348,7 @@ export {
|
|
|
339
348
|
DoctorRegulationV0_25_27,
|
|
340
349
|
DoctorRegulationV1_0_0,
|
|
341
350
|
DoctorRegulationV1_0_33,
|
|
351
|
+
DoctorRegulationV1_0_40,
|
|
342
352
|
RebuildOptions,
|
|
343
353
|
RuleLevel,
|
|
344
354
|
checkUnsuitableValues,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { E2EEAlgorithms } from "./setting.const.js";
|
|
2
2
|
const PREFERRED_BASE = {
|
|
3
3
|
syncMaxSizeInMB: 50,
|
|
4
|
+
customChunkSize: 0,
|
|
4
5
|
chunkSplitterVersion: "v3-rabin-karp",
|
|
5
6
|
usePluginSyncV2: true,
|
|
6
7
|
handleFilenameCaseSensitive: false,
|
|
@@ -8,21 +9,18 @@ const PREFERRED_BASE = {
|
|
|
8
9
|
};
|
|
9
10
|
const PREFERRED_SETTING_CLOUDANT = {
|
|
10
11
|
...PREFERRED_BASE,
|
|
11
|
-
customChunkSize: 0,
|
|
12
12
|
sendChunksBulkMaxSize: 1,
|
|
13
13
|
concurrencyOfReadChunksOnline: 100,
|
|
14
14
|
minimumIntervalOfReadChunksOnline: 333
|
|
15
15
|
};
|
|
16
16
|
const PREFERRED_SETTING_SELF_HOSTED = {
|
|
17
17
|
...PREFERRED_BASE,
|
|
18
|
-
customChunkSize: 60,
|
|
19
18
|
sendChunksBulkMaxSize: 1,
|
|
20
19
|
concurrencyOfReadChunksOnline: 30,
|
|
21
20
|
minimumIntervalOfReadChunksOnline: 25
|
|
22
21
|
};
|
|
23
22
|
const PREFERRED_JOURNAL_SYNC = {
|
|
24
23
|
...PREFERRED_BASE,
|
|
25
|
-
customChunkSize: 10,
|
|
26
24
|
concurrencyOfReadChunksOnline: 30,
|
|
27
25
|
minimumIntervalOfReadChunksOnline: 25
|
|
28
26
|
};
|
|
@@ -6,6 +6,8 @@ import { decompressDoc } from "../pouchdb/compress.js";
|
|
|
6
6
|
import { AuthorizationHeaderGenerator, generateCredentialObject } from "../replication/httplib.js";
|
|
7
7
|
import { base64ToArrayBuffer } from "../string_and_binary/convert.js";
|
|
8
8
|
import { DiagnosticReadError } from "./types.js";
|
|
9
|
+
const MAX_BULK_DOCUMENTS = 64;
|
|
10
|
+
const MAX_BULK_URL_LENGTH = 6e3;
|
|
9
11
|
function createCouchDBDiagnosticSource({
|
|
10
12
|
settings: originalSettings,
|
|
11
13
|
fetch,
|
|
@@ -30,13 +32,16 @@ function createCouchDBDiagnosticSource({
|
|
|
30
32
|
const checkCancelled = () => {
|
|
31
33
|
if (controller.signal.aborted) throw new DOMException("Diagnostic cancelled", "AbortError");
|
|
32
34
|
};
|
|
35
|
+
const requestURL = (path, parameters = {}) => {
|
|
36
|
+
const requestURL2 = new URL(`${root}${path}`);
|
|
37
|
+
for (const [key, value] of Object.entries(parameters)) requestURL2.searchParams.set(key, value);
|
|
38
|
+
return requestURL2.toString();
|
|
39
|
+
};
|
|
33
40
|
const readJSON = async (path, parameters = {}) => {
|
|
34
41
|
checkCancelled();
|
|
35
|
-
const requestURL = new URL(`${root}${path}`);
|
|
36
|
-
for (const [key, value] of Object.entries(parameters)) requestURL.searchParams.set(key, value);
|
|
37
42
|
headers.set("authorization", await auth.getAuthorizationHeader(credentials));
|
|
38
43
|
checkCancelled();
|
|
39
|
-
const response = await fetch(requestURL
|
|
44
|
+
const response = await fetch(requestURL(path, parameters), {
|
|
40
45
|
method: "GET",
|
|
41
46
|
headers,
|
|
42
47
|
cache: "no-store",
|
|
@@ -73,10 +78,20 @@ function createCouchDBDiagnosticSource({
|
|
|
73
78
|
() => {
|
|
74
79
|
}
|
|
75
80
|
).outgoing : async (doc) => doc;
|
|
76
|
-
|
|
81
|
+
const decode = async (id, value) => {
|
|
82
|
+
if (!value || typeof value !== "object" || !("_id" in value) || value._id !== id || !("_rev" in value) || typeof value._rev !== "string") {
|
|
83
|
+
throw new DiagnosticReadError("document-unreadable");
|
|
84
|
+
}
|
|
85
|
+
const decoded = await decompressDoc(await decrypt(value));
|
|
86
|
+
if ("e_" in decoded && decoded.e_ === true || "path" in decoded && typeof decoded.path === "string" && (isEncryptedMeta(decoded) || isObfuscatedEntry(decoded) && isPathProbablyObfuscated(decoded.path)))
|
|
87
|
+
throw new DiagnosticReadError("document-unreadable");
|
|
88
|
+
checkCancelled();
|
|
89
|
+
return decoded;
|
|
90
|
+
};
|
|
91
|
+
const source = {
|
|
77
92
|
async info() {
|
|
78
93
|
const info = await readJSON("");
|
|
79
|
-
return { update_seq: info.update_seq };
|
|
94
|
+
return { update_seq: info.update_seq, doc_count: info.doc_count };
|
|
80
95
|
},
|
|
81
96
|
async *enumerateIds() {
|
|
82
97
|
let previous;
|
|
@@ -106,16 +121,73 @@ function createCouchDBDiagnosticSource({
|
|
|
106
121
|
if (options.conflicts) parameters.conflicts = "true";
|
|
107
122
|
if (options.revs) parameters.revs = "true";
|
|
108
123
|
if (options.revs_info) parameters.revs_info = "true";
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
return await decode(id, await readJSON(`/${encodeURIComponent(id)}`, parameters));
|
|
125
|
+
},
|
|
126
|
+
async getMany(ids, options = {}) {
|
|
127
|
+
const result = [];
|
|
128
|
+
const parameters = (keys) => ({
|
|
129
|
+
keys: JSON.stringify(keys),
|
|
130
|
+
include_docs: "true",
|
|
131
|
+
...options.conflicts ? { conflicts: "true" } : {}
|
|
132
|
+
});
|
|
133
|
+
const readBatch = async (keys) => {
|
|
134
|
+
if (!keys.length) return;
|
|
135
|
+
let rows;
|
|
136
|
+
try {
|
|
137
|
+
const page = await readJSON("/_all_docs", parameters(keys));
|
|
138
|
+
if (!Array.isArray(page.rows)) throw new DiagnosticReadError("document-unreadable");
|
|
139
|
+
rows = page.rows;
|
|
140
|
+
} catch {
|
|
141
|
+
checkCancelled();
|
|
142
|
+
result.push(...keys.map((id) => ({ id, error: new DiagnosticReadError("document-unreadable") })));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (let offset = 0; offset < keys.length; offset += 4) {
|
|
146
|
+
checkCancelled();
|
|
147
|
+
const decoded = await Promise.allSettled(
|
|
148
|
+
keys.slice(offset, offset + 4).map(async (id, index) => {
|
|
149
|
+
try {
|
|
150
|
+
const row = rows[offset + index];
|
|
151
|
+
if (!row || row.key !== id) throw new DiagnosticReadError("document-unreadable");
|
|
152
|
+
if (row.error === "not_found" || row.value?.deleted)
|
|
153
|
+
throw new DiagnosticReadError("document-unreadable", true);
|
|
154
|
+
if (row.error || row.id !== id) throw new DiagnosticReadError("document-unreadable");
|
|
155
|
+
return { id, document: await decode(id, row.doc) };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
checkCancelled();
|
|
158
|
+
return {
|
|
159
|
+
id,
|
|
160
|
+
error: error instanceof DiagnosticReadError ? error : new DiagnosticReadError("document-unreadable")
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
checkCancelled();
|
|
166
|
+
for (const outcome of decoded) {
|
|
167
|
+
if (outcome.status === "rejected") throw outcome.reason;
|
|
168
|
+
result.push(outcome.value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
let pending = [];
|
|
173
|
+
for (const id of ids) {
|
|
174
|
+
checkCancelled();
|
|
175
|
+
if (pending.length >= MAX_BULK_DOCUMENTS || requestURL("/_all_docs", parameters([...pending, id])).length > MAX_BULK_URL_LENGTH) {
|
|
176
|
+
await readBatch(pending);
|
|
177
|
+
pending = [];
|
|
178
|
+
}
|
|
179
|
+
if (requestURL("/_all_docs", parameters([id])).length > MAX_BULK_URL_LENGTH) {
|
|
180
|
+
try {
|
|
181
|
+
result.push({ id, document: await source.get(id, options) });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
checkCancelled();
|
|
184
|
+
result.push({ id, error });
|
|
185
|
+
}
|
|
186
|
+
} else pending.push(id);
|
|
116
187
|
}
|
|
188
|
+
await readBatch(pending);
|
|
117
189
|
checkCancelled();
|
|
118
|
-
return
|
|
190
|
+
return result;
|
|
119
191
|
},
|
|
120
192
|
close() {
|
|
121
193
|
signal?.removeEventListener("abort", abort);
|
|
@@ -123,6 +195,7 @@ function createCouchDBDiagnosticSource({
|
|
|
123
195
|
return Promise.resolve();
|
|
124
196
|
}
|
|
125
197
|
};
|
|
198
|
+
return source;
|
|
126
199
|
}
|
|
127
200
|
export {
|
|
128
201
|
createCouchDBDiagnosticSource
|
|
@@ -4,7 +4,7 @@ export { analyseDatabaseIntegrity } from "./integrity.js";
|
|
|
4
4
|
export { createLocalDiagnosticSource } from "./local.js";
|
|
5
5
|
export { createCouchDBDiagnosticSource } from "./couchdb.js";
|
|
6
6
|
export type { CouchDBDiagnosticSourceOptions } from "./couchdb.js";
|
|
7
|
-
export { createDiagnosticNameMapper, projectIntegrityReport, projectUsageReport, formatDiagnosticJSON, formatIntegrityMarkdown, formatUsageTSV, } from "./reports.js";
|
|
7
|
+
export { createDiagnosticNameMapper, parseDiagnosticPathToken, projectIntegrityReport, projectUsageReport, formatDiagnosticJSON, formatIntegrityMarkdown, formatUsageTSV, } from "./reports.js";
|
|
8
8
|
export type { DiagnosticNameMapper, DiagnosticReportVersions, ProjectedDiagnosticReport, ProjectedDatabaseIntegrityReport, ProjectedDatabaseUsageReport, } from "./reports.js";
|
|
9
9
|
export { DiagnosticReadError } from "./types.js";
|
|
10
|
-
export type { DiagnosticStatus, DiagnosticNameMode, DiagnosticSourceName, DiagnosticReason, DiagnosticReadOptions, DiagnosticDatabaseSource, DiagnosticError, DiagnosticContent, DiagnosticCoverage, DiagnosticObservation, DiagnosticIdentitySettings, DatabaseIntegrityResult, DatabaseIntegrityOptions, } from "./types.js";
|
|
10
|
+
export type { DiagnosticStatus, DiagnosticNameMode, DiagnosticSourceName, DiagnosticReason, DiagnosticReadOptions, DiagnosticDocumentReadResult, DiagnosticDatabaseSource, DiagnosticError, DiagnosticContent, DiagnosticCoverage, DiagnosticObservation, DiagnosticPathEquivalenceGroup, DiagnosticIdentitySettings, DatabaseIntegrityResult, DatabaseIntegrityOptions, DatabaseIntegrityProgress, DatabaseIntegrityLog, } from "./types.js";
|
|
@@ -4,6 +4,7 @@ import { createLocalDiagnosticSource } from "./local.js";
|
|
|
4
4
|
import { createCouchDBDiagnosticSource } from "./couchdb.js";
|
|
5
5
|
import {
|
|
6
6
|
createDiagnosticNameMapper,
|
|
7
|
+
parseDiagnosticPathToken,
|
|
7
8
|
projectIntegrityReport,
|
|
8
9
|
projectUsageReport,
|
|
9
10
|
formatDiagnosticJSON,
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
formatDiagnosticJSON,
|
|
22
23
|
formatIntegrityMarkdown,
|
|
23
24
|
formatUsageTSV,
|
|
25
|
+
parseDiagnosticPathToken,
|
|
24
26
|
projectIntegrityReport,
|
|
25
27
|
projectUsageReport
|
|
26
28
|
};
|