@vrtmrz/livesync-commonlib 0.1.32 → 0.1.34
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 +7 -0
- package/dist/common/idDerivation.d.ts +3 -1
- package/dist/common/idDerivation.js +27 -0
- package/dist/hashing.d.ts +3 -0
- package/dist/hashing.js +4 -0
- package/dist/managers/ChunkFetcher.d.ts +16 -0
- package/dist/managers/ChunkFetcher.js +231 -82
- package/dist/managers/HashManager/HashManager.d.ts +3 -0
- package/dist/managers/HashManager/HashManager.js +25 -5
- package/dist/managers/LiveSyncManagers.js +6 -0
- package/dist/managers/chunkFetcher.helper.d.ts +21 -0
- package/dist/managers/chunkFetcher.helper.js +43 -0
- package/dist/mock_and_interop/stores.d.ts +4 -0
- package/dist/mock_and_interop/stores.js +2 -0
- package/dist/replication/trystero/ProxiedDB.d.ts +1 -1
- package/dist/worker/bgWorker.js +1 -1
- package/docs/proven-in-use.md +8 -2
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ The package is ESM-only and declares Node 20 or later. Browser entry points are
|
|
|
26
26
|
| -------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
27
27
|
| `@vrtmrz/livesync-commonlib` | `DirectFileManipulator` for integrations which access CouchDB directly | Deliberately small, but its high-level lifecycle and file-operation semantics are not final |
|
|
28
28
|
| `@vrtmrz/livesync-commonlib/context` | Instance-owned events, translation injection, and host-neutral standard-I/O types | Focused, package-tested pre-1.0 contract |
|
|
29
|
+
| `@vrtmrz/livesync-commonlib/hashing` | Chunk ID calculation with the database's hash manager | Focused, package-tested pre-1.0 contract |
|
|
29
30
|
| `@vrtmrz/livesync-commonlib/browser` | Rooted File System Access API storage | Focused, package-tested pre-1.0 contract |
|
|
30
31
|
| `@vrtmrz/livesync-commonlib/node` | Rooted Node storage, Node standard I/O, and selected Node capabilities | Supported platform façade; keeps Node-only dependencies behind an explicit boundary |
|
|
31
32
|
| `@vrtmrz/livesync-commonlib/p2p` | Host-composed P2P service views and composition helpers | Focused, package-tested pre-1.0 contract with a temporary compatibility façade |
|
|
@@ -118,6 +119,12 @@ See [the settings lifecycle guide](docs/settings-lifecycle.md) before initialisi
|
|
|
118
119
|
|
|
119
120
|
The focused Setup URI entry offers an Ephemeral mode bound to the current fixed seven-day window and a Persistent mode compatible with existing readers. Both use the same URI structure. See [the Setup URI sharing guide](docs/setup-uri.md) for the availability boundary, import behaviour, and compatibility limits.
|
|
120
121
|
|
|
122
|
+
## Chunk ID calculation
|
|
123
|
+
|
|
124
|
+
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)`.
|
|
125
|
+
|
|
126
|
+
`computeHash` returns the Chunk hash, including the `+` prefix when encryption is enabled; the database adds its `h:` namespace separately. Reuse a manager for repeated calculations with the same configuration, create a new manager when changing the legacy algorithm or passphrase, and call `clearCaches()` when retiring it. This releases retained key preparation; it does not erase existing settings or in-flight operations. The entry performs ID calculation and does not write Chunks to a database.
|
|
127
|
+
|
|
121
128
|
## Remote connection profiles
|
|
122
129
|
|
|
123
130
|
New profile-management code can create or update a CouchDB, Object Storage, or P2P connection through the focused remote-configurations entry. Profile IDs provide identity, display names remain editable presentation, and the active selection is explicit. Older flat connection fields remain a runtime projection and an import-compatibility boundary rather than the preferred persistence model.
|
|
@@ -9,5 +9,7 @@ export declare function formatIdRecoveryCode(key: string): string;
|
|
|
9
9
|
export declare function deriveOrImportIdKey(input: string): Promise<string>;
|
|
10
10
|
/** Return the independent ID key, rejecting an incomplete new-mode setting. */
|
|
11
11
|
export declare function configuredIdKey(settings: Pick<EncryptionSettings, "idDerivationVersion" | "idDerivationKey">): string | false;
|
|
12
|
-
/**
|
|
12
|
+
/** Prepare a purpose-separated Chunk key for repeated ID calculations. */
|
|
13
|
+
export declare function createChunkIdGenerator(key: string): Promise<(value: string) => Promise<string>>;
|
|
14
|
+
/** Derive purpose-separated IDs; repeated Chunk calculations should reuse a prepared generator. */
|
|
13
15
|
export declare function computeKeyedId(key: string, purpose: "chunk" | "document" | "peer-agreement" | "remote-agreement", value: string): Promise<string>;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { getWebCrypto } from "../mods.js";
|
|
2
2
|
import { hexStringToUint8Array, uint8ArrayToHexString } from "../string_and_binary/convert.js";
|
|
3
|
+
import { xxhashNew } from "../string_and_binary/hash.js";
|
|
3
4
|
const ID_DERIVATION_VERSION = 1;
|
|
4
5
|
const ID_RECOVERY_CODE_PREFIX = "sls-id-v1:";
|
|
5
6
|
const SOURCE_SALT = new TextEncoder().encode("self-hosted-livesync:id-source:v1");
|
|
6
7
|
const SOURCE_ITERATIONS = 31e4;
|
|
7
8
|
const HEX_KEY = /^[0-9a-f]{64}$/u;
|
|
9
|
+
const CHUNK_KEY_CONTEXT = new TextEncoder().encode("self-hosted-livesync:id-v1:chunk-key:xxhash64");
|
|
10
|
+
const CHUNK_ID_PREFIX = "self-hosted-livesync:id-v1:chunk:xxhash64\0";
|
|
8
11
|
async function deriveIdKey(source) {
|
|
9
12
|
if (source.length === 0) {
|
|
10
13
|
throw new Error("An ID source is required.");
|
|
@@ -50,7 +53,30 @@ function configuredIdKey(settings) {
|
|
|
50
53
|
}
|
|
51
54
|
return key;
|
|
52
55
|
}
|
|
56
|
+
async function createChunkIdGenerator(key) {
|
|
57
|
+
if (!HEX_KEY.test(key)) throw new Error("The configured ID key is invalid.");
|
|
58
|
+
const crypto = await getWebCrypto();
|
|
59
|
+
const masterKey = await crypto.subtle.importKey(
|
|
60
|
+
"raw",
|
|
61
|
+
hexStringToUint8Array(key),
|
|
62
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
63
|
+
false,
|
|
64
|
+
["sign"]
|
|
65
|
+
);
|
|
66
|
+
const derivedKey = await crypto.subtle.sign("HMAC", masterKey, CHUNK_KEY_CONTEXT);
|
|
67
|
+
const chunkKey = await crypto.subtle.importKey("raw", derivedKey, { name: "HMAC", hash: "SHA-256" }, false, [
|
|
68
|
+
"sign"
|
|
69
|
+
]);
|
|
70
|
+
const xxhash = await xxhashNew();
|
|
71
|
+
const encoder = new TextEncoder();
|
|
72
|
+
return async (value) => {
|
|
73
|
+
const digest = xxhash.h64(value).toString(16).padStart(16, "0");
|
|
74
|
+
const id = await crypto.subtle.sign("HMAC", chunkKey, encoder.encode(CHUNK_ID_PREFIX + digest));
|
|
75
|
+
return uint8ArrayToHexString(new Uint8Array(id));
|
|
76
|
+
};
|
|
77
|
+
}
|
|
53
78
|
async function computeKeyedId(key, purpose, value) {
|
|
79
|
+
if (purpose === "chunk") return (await createChunkIdGenerator(key))(value);
|
|
54
80
|
if (!HEX_KEY.test(key)) throw new Error("The configured ID key is invalid.");
|
|
55
81
|
const crypto = await getWebCrypto();
|
|
56
82
|
const hmacKey = await crypto.subtle.importKey(
|
|
@@ -69,6 +95,7 @@ export {
|
|
|
69
95
|
ID_RECOVERY_CODE_PREFIX,
|
|
70
96
|
computeKeyedId,
|
|
71
97
|
configuredIdKey,
|
|
98
|
+
createChunkIdGenerator,
|
|
72
99
|
deriveIdKey,
|
|
73
100
|
deriveOrImportIdKey,
|
|
74
101
|
formatIdRecoveryCode
|
package/dist/hashing.js
ADDED
|
@@ -15,6 +15,13 @@ export declare class ChunkFetcher {
|
|
|
15
15
|
get chunkManager(): ChunkManager;
|
|
16
16
|
queue: DocumentID[];
|
|
17
17
|
private readonly pendingClaims;
|
|
18
|
+
private reportedPendingCount;
|
|
19
|
+
private reportedFetchCounts;
|
|
20
|
+
private requestTimer;
|
|
21
|
+
private requestTimerDueAt;
|
|
22
|
+
private finiteActivityWasActive;
|
|
23
|
+
private finiteCompletionVersion;
|
|
24
|
+
private readonly stopObservingDelivery;
|
|
18
25
|
private destroyed;
|
|
19
26
|
get interval(): number;
|
|
20
27
|
get concurrency(): number;
|
|
@@ -23,12 +30,20 @@ export declare class ChunkFetcher {
|
|
|
23
30
|
destroy(): void;
|
|
24
31
|
onEventHandler: (ids: DocumentID[]) => void;
|
|
25
32
|
onEvent(ids: DocumentID[]): void;
|
|
33
|
+
private clearRequestTimer;
|
|
34
|
+
private scheduleRequest;
|
|
26
35
|
private ensureClaims;
|
|
27
36
|
private removePendingDelivery;
|
|
37
|
+
private updatePendingCount;
|
|
28
38
|
private onClaimStalled;
|
|
29
39
|
private settleClaim;
|
|
40
|
+
private getActiveClaimIds;
|
|
30
41
|
private touchClaims;
|
|
31
42
|
private waitForActivityBoundary;
|
|
43
|
+
private waitBeforeRemoteRequest;
|
|
44
|
+
private resolveLocallyAvailableRetries;
|
|
45
|
+
private logFetchedChunks;
|
|
46
|
+
private applyMissingChunkResults;
|
|
32
47
|
/**
|
|
33
48
|
* Processing requests
|
|
34
49
|
*/
|
|
@@ -40,5 +55,6 @@ export declare class ChunkFetcher {
|
|
|
40
55
|
*/
|
|
41
56
|
previousRequestTime: number;
|
|
42
57
|
canRequestMore(): boolean;
|
|
58
|
+
private isReady;
|
|
43
59
|
requestMissingChunks(): Promise<void>;
|
|
44
60
|
}
|
|
@@ -5,6 +5,11 @@ import { DEFAULT_SETTINGS } from "../common/types.js";
|
|
|
5
5
|
import {} from "./ChunkManager.js";
|
|
6
6
|
import { compatGlobal } from "../common/coreEnvFunctions.js";
|
|
7
7
|
import { DEFAULT_CHUNK_DELIVERY_STALL_TIMEOUT_MS } from "./ChunkDeliveryCoordinator.js";
|
|
8
|
+
import { chunkFetchCounts, collectingChunks } from "../mock_and_interop/stores.js";
|
|
9
|
+
import {
|
|
10
|
+
classifyFetchedChunks,
|
|
11
|
+
decideMissingChunkAction
|
|
12
|
+
} from "./chunkFetcher.helper.js";
|
|
8
13
|
const EVENT_MISSING_CHUNKS = "missingChunks";
|
|
9
14
|
const EVENT_MISSING_CHUNK_REMOTE = "missingChunkRemote";
|
|
10
15
|
const EVENT_CHUNK_FETCHED = "chunkFetched";
|
|
@@ -19,6 +24,13 @@ class ChunkFetcher {
|
|
|
19
24
|
}
|
|
20
25
|
queue = [];
|
|
21
26
|
pendingClaims = /* @__PURE__ */ new Map();
|
|
27
|
+
reportedPendingCount = 0;
|
|
28
|
+
reportedFetchCounts = { initial: 0, retrying: 0 };
|
|
29
|
+
requestTimer;
|
|
30
|
+
requestTimerDueAt = 0;
|
|
31
|
+
finiteActivityWasActive;
|
|
32
|
+
finiteCompletionVersion = 0;
|
|
33
|
+
stopObservingDelivery;
|
|
22
34
|
destroyed = false;
|
|
23
35
|
get interval() {
|
|
24
36
|
const settings = this.options.settingService.currentSettings();
|
|
@@ -31,6 +43,18 @@ class ChunkFetcher {
|
|
|
31
43
|
abort = new AbortController();
|
|
32
44
|
constructor(options) {
|
|
33
45
|
this.options = options;
|
|
46
|
+
this.finiteActivityWasActive = this.chunkManager.deliveryCoordinator.isFiniteReplicationActive();
|
|
47
|
+
this.stopObservingDelivery = this.chunkManager.deliveryCoordinator.onChanged(() => {
|
|
48
|
+
const active = this.chunkManager.deliveryCoordinator.isFiniteReplicationActive();
|
|
49
|
+
const completed = this.finiteActivityWasActive && !active;
|
|
50
|
+
this.finiteActivityWasActive = active;
|
|
51
|
+
if (!completed) return;
|
|
52
|
+
this.finiteCompletionVersion++;
|
|
53
|
+
for (const pending of this.pendingClaims.values()) {
|
|
54
|
+
if (pending.missingResponses > 0 && !pending.inFlight) pending.nextRequestAt = Date.now();
|
|
55
|
+
}
|
|
56
|
+
this.scheduleRequest();
|
|
57
|
+
});
|
|
34
58
|
this.chunkManager.addListener(EVENT_MISSING_CHUNKS, this.onEventHandler, {
|
|
35
59
|
signal: this.abort.signal
|
|
36
60
|
});
|
|
@@ -39,9 +63,12 @@ class ChunkFetcher {
|
|
|
39
63
|
if (this.destroyed) return;
|
|
40
64
|
this.destroyed = true;
|
|
41
65
|
this.abort.abort();
|
|
66
|
+
this.stopObservingDelivery();
|
|
67
|
+
this.clearRequestTimer();
|
|
42
68
|
this.queue = [];
|
|
43
|
-
const pendingDeliveries = new Set(this.pendingClaims.values());
|
|
69
|
+
const pendingDeliveries = new Set([...this.pendingClaims.values()].map((pending) => pending.delivery));
|
|
44
70
|
this.pendingClaims.clear();
|
|
71
|
+
this.updatePendingCount();
|
|
45
72
|
for (const pending of pendingDeliveries) {
|
|
46
73
|
pending.resolveActivityBoundary(false);
|
|
47
74
|
pending.claim.release();
|
|
@@ -52,9 +79,35 @@ class ChunkFetcher {
|
|
|
52
79
|
if (this.destroyed) return;
|
|
53
80
|
const claimedIds = this.ensureClaims(ids);
|
|
54
81
|
this.queue = unique([...this.queue, ...claimedIds]);
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
this.scheduleRequest(1);
|
|
83
|
+
}
|
|
84
|
+
clearRequestTimer() {
|
|
85
|
+
if (this.requestTimer !== void 0) compatGlobal.clearTimeout(this.requestTimer);
|
|
86
|
+
this.requestTimer = void 0;
|
|
87
|
+
}
|
|
88
|
+
scheduleRequest(minimumDelayMs = 0) {
|
|
89
|
+
if (this.destroyed || this.currentProcessing >= this.concurrency || this.queue.length === 0) {
|
|
90
|
+
this.clearRequestTimer();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let nextRequestAt = Infinity;
|
|
94
|
+
for (const id of this.queue) {
|
|
95
|
+
const pending = this.pendingClaims.get(id);
|
|
96
|
+
if (!pending?.inFlight) nextRequestAt = Math.min(nextRequestAt, pending?.nextRequestAt ?? 0);
|
|
57
97
|
}
|
|
98
|
+
if (nextRequestAt === Infinity) {
|
|
99
|
+
this.clearRequestTimer();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const dueAt = Math.max(nextRequestAt, now + minimumDelayMs);
|
|
104
|
+
if (this.requestTimer !== void 0 && this.requestTimerDueAt <= dueAt) return;
|
|
105
|
+
this.clearRequestTimer();
|
|
106
|
+
this.requestTimerDueAt = dueAt;
|
|
107
|
+
this.requestTimer = compatGlobal.setTimeout(() => {
|
|
108
|
+
this.requestTimer = void 0;
|
|
109
|
+
void this.requestMissingChunks();
|
|
110
|
+
}, dueAt - now);
|
|
58
111
|
}
|
|
59
112
|
ensureClaims(ids) {
|
|
60
113
|
const unclaimedIds = unique(ids.filter((id) => !this.pendingClaims.has(id)));
|
|
@@ -71,8 +124,9 @@ class ChunkFetcher {
|
|
|
71
124
|
resolveActivityBoundary: activityBoundary.resolve
|
|
72
125
|
};
|
|
73
126
|
for (const id of unclaimedIds) {
|
|
74
|
-
this.pendingClaims.set(id, pending);
|
|
127
|
+
this.pendingClaims.set(id, { delivery: pending, missingResponses: 0, nextRequestAt: 0, inFlight: false });
|
|
75
128
|
}
|
|
129
|
+
this.updatePendingCount();
|
|
76
130
|
void this.options.replicatorService.runBoundedRemoteActivity(
|
|
77
131
|
() => {
|
|
78
132
|
pending.claim.touch();
|
|
@@ -92,45 +146,142 @@ class ChunkFetcher {
|
|
|
92
146
|
}
|
|
93
147
|
removePendingDelivery(pending, ids) {
|
|
94
148
|
for (const id of ids) {
|
|
95
|
-
if (this.pendingClaims.get(id) === pending) {
|
|
149
|
+
if (this.pendingClaims.get(id)?.delivery === pending) {
|
|
96
150
|
this.pendingClaims.delete(id);
|
|
97
151
|
}
|
|
98
152
|
}
|
|
99
153
|
const removed = new Set(ids);
|
|
100
154
|
this.queue = this.queue.filter((id) => !(removed.has(id) && !this.pendingClaims.has(id)));
|
|
155
|
+
this.updatePendingCount();
|
|
156
|
+
this.scheduleRequest();
|
|
157
|
+
}
|
|
158
|
+
updatePendingCount() {
|
|
159
|
+
const nextCount = this.pendingClaims.size;
|
|
160
|
+
const difference = nextCount - this.reportedPendingCount;
|
|
161
|
+
this.reportedPendingCount = nextCount;
|
|
162
|
+
if (difference !== 0) collectingChunks.value += difference;
|
|
163
|
+
const nextFetchCounts = { initial: 0, retrying: 0 };
|
|
164
|
+
for (const pending of this.pendingClaims.values()) {
|
|
165
|
+
if (pending.missingResponses > 0) nextFetchCounts.retrying++;
|
|
166
|
+
else nextFetchCounts.initial++;
|
|
167
|
+
}
|
|
168
|
+
const initialDifference = nextFetchCounts.initial - this.reportedFetchCounts.initial;
|
|
169
|
+
const retryingDifference = nextFetchCounts.retrying - this.reportedFetchCounts.retrying;
|
|
170
|
+
this.reportedFetchCounts = nextFetchCounts;
|
|
171
|
+
if (initialDifference !== 0 || retryingDifference !== 0) {
|
|
172
|
+
chunkFetchCounts.value = {
|
|
173
|
+
initial: chunkFetchCounts.value.initial + initialDifference,
|
|
174
|
+
retrying: chunkFetchCounts.value.retrying + retryingDifference
|
|
175
|
+
};
|
|
176
|
+
}
|
|
101
177
|
}
|
|
102
178
|
onClaimStalled(pending, stalledIds) {
|
|
103
179
|
pending.resolveActivityBoundary(false);
|
|
104
180
|
this.removePendingDelivery(pending, stalledIds);
|
|
105
181
|
Logger(`Chunk delivery stalled for the following IDs: ${stalledIds.join(", ")}`, LOG_LEVEL_VERBOSE);
|
|
106
182
|
}
|
|
107
|
-
settleClaim(id) {
|
|
108
|
-
|
|
109
|
-
if (!pending) return;
|
|
183
|
+
settleClaim(id, pending) {
|
|
184
|
+
if (!pending || this.pendingClaims.get(id) !== pending) return;
|
|
110
185
|
this.pendingClaims.delete(id);
|
|
111
|
-
pending.claim.settle(id);
|
|
186
|
+
pending.delivery.claim.settle(id);
|
|
187
|
+
this.updatePendingCount();
|
|
112
188
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
189
|
+
getActiveClaimIds(ids, claims) {
|
|
190
|
+
return ids.filter((id) => claims.has(id) && this.pendingClaims.get(id) === claims.get(id));
|
|
191
|
+
}
|
|
192
|
+
touchClaims(ids, claims) {
|
|
193
|
+
const pendingDeliveries = new Set(this.getActiveClaimIds(ids, claims).map((id) => claims.get(id).delivery));
|
|
117
194
|
for (const pending of pendingDeliveries) {
|
|
118
195
|
pending.claim.touch();
|
|
119
196
|
}
|
|
120
197
|
}
|
|
121
|
-
async waitForActivityBoundary(
|
|
122
|
-
const pendingDeliveries = new Set(
|
|
123
|
-
ids.map((id) => this.pendingClaims.get(id)).filter((pending) => pending !== void 0)
|
|
124
|
-
);
|
|
198
|
+
async waitForActivityBoundary(claims) {
|
|
199
|
+
const pendingDeliveries = new Set([...claims.values()].map((pending) => pending.delivery));
|
|
125
200
|
const entered = new Map(
|
|
126
201
|
await Promise.all(
|
|
127
202
|
[...pendingDeliveries].map(async (pending) => [pending, await pending.activityBoundaryEntered])
|
|
128
203
|
)
|
|
129
204
|
);
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
|
|
205
|
+
return [...claims].flatMap(
|
|
206
|
+
([id, pending]) => this.pendingClaims.get(id) === pending && entered.get(pending.delivery) === true ? [id] : []
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
async waitBeforeRemoteRequest(ids, claims, localCheckCompletionVersion) {
|
|
210
|
+
this.touchClaims(ids, claims);
|
|
211
|
+
while (!this.destroyed) {
|
|
212
|
+
const activeIds = this.getActiveClaimIds(ids, claims);
|
|
213
|
+
if (activeIds.length === 0) return [];
|
|
214
|
+
if (localCheckCompletionVersion !== this.finiteCompletionVersion) {
|
|
215
|
+
localCheckCompletionVersion = this.finiteCompletionVersion;
|
|
216
|
+
await this.resolveLocallyAvailableRetries(claims);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
const timeToWait = this.previousRequestTime + this.interval - now;
|
|
221
|
+
if (timeToWait > 0) {
|
|
222
|
+
await delay(timeToWait);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
this.previousRequestTime = now;
|
|
226
|
+
this.touchClaims(activeIds, claims);
|
|
227
|
+
return activeIds;
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
async resolveLocallyAvailableRetries(claims) {
|
|
232
|
+
const retryIds = this.getActiveClaimIds([...claims.keys()], claims).filter(
|
|
233
|
+
(id) => claims.get(id).missingResponses > 0
|
|
234
|
+
);
|
|
235
|
+
if (retryIds.length === 0) return;
|
|
236
|
+
const localChunks = await this.chunkManager.read(retryIds, {
|
|
237
|
+
preventRemoteRequest: true,
|
|
238
|
+
skipCache: true,
|
|
239
|
+
waitForDelivery: false
|
|
133
240
|
});
|
|
241
|
+
for (const chunk of localChunks) {
|
|
242
|
+
if (!chunk) continue;
|
|
243
|
+
this.chunkManager.emitEvent(EVENT_CHUNK_FETCHED, chunk);
|
|
244
|
+
this.settleClaim(chunk._id, claims.get(chunk._id));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
logFetchedChunks(requestedIds, { chunks, invalidChunks }) {
|
|
248
|
+
if (invalidChunks.length > 0) {
|
|
249
|
+
Logger(
|
|
250
|
+
`Some fetched chunks are invalid and will be ignored: (${invalidChunks.length} / ${chunks.length + invalidChunks.length}).`,
|
|
251
|
+
LOG_LEVEL_VERBOSE
|
|
252
|
+
);
|
|
253
|
+
for (const chunk of invalidChunks) {
|
|
254
|
+
Logger(`Invalid chunk: ${JSON.stringify(chunk)}`, LOG_LEVEL_VERBOSE);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (chunks.length === 0) {
|
|
258
|
+
Logger(`No valid chunks were found for the following IDs: ${requestedIds.join(", ")}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
applyMissingChunkResults(missingIds, requestClaims, requestCompletionVersion) {
|
|
262
|
+
for (const chunkID of this.getActiveClaimIds(missingIds, requestClaims)) {
|
|
263
|
+
const pending = requestClaims.get(chunkID);
|
|
264
|
+
const completedDuringRequest = requestCompletionVersion !== this.finiteCompletionVersion;
|
|
265
|
+
const action = decideMissingChunkAction({
|
|
266
|
+
missingResponses: pending.missingResponses,
|
|
267
|
+
finiteReplicationActive: this.chunkManager.deliveryCoordinator.isFiniteReplicationActive(),
|
|
268
|
+
completedDuringRequest
|
|
269
|
+
});
|
|
270
|
+
if (action.kind === "missing") {
|
|
271
|
+
this.chunkManager.emitEvent(EVENT_MISSING_CHUNK_REMOTE, chunkID);
|
|
272
|
+
this.settleClaim(chunkID, pending);
|
|
273
|
+
} else {
|
|
274
|
+
pending.missingResponses = action.missingResponses;
|
|
275
|
+
pending.inFlight = false;
|
|
276
|
+
pending.nextRequestAt = Date.now() + action.delayMs;
|
|
277
|
+
this.queue.push(chunkID);
|
|
278
|
+
Logger(
|
|
279
|
+
`Remote chunk is not available yet; retrying ${chunkID} in ${action.delayMs} ms.`,
|
|
280
|
+
LOG_LEVEL_VERBOSE
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
this.updatePendingCount();
|
|
134
285
|
}
|
|
135
286
|
/**
|
|
136
287
|
* Processing requests
|
|
@@ -143,30 +294,39 @@ class ChunkFetcher {
|
|
|
143
294
|
*/
|
|
144
295
|
previousRequestTime = 0;
|
|
145
296
|
canRequestMore() {
|
|
146
|
-
return this.currentProcessing < this.concurrency && this.queue.
|
|
297
|
+
return this.currentProcessing < this.concurrency && this.queue.some((id) => this.isReady(id));
|
|
298
|
+
}
|
|
299
|
+
isReady(id) {
|
|
300
|
+
const pending = this.pendingClaims.get(id);
|
|
301
|
+
return !pending || !pending.inFlight && pending.nextRequestAt <= Date.now();
|
|
147
302
|
}
|
|
148
303
|
async requestMissingChunks() {
|
|
149
304
|
if (this.destroyed || !this.canRequestMore()) {
|
|
305
|
+
this.scheduleRequest();
|
|
150
306
|
return;
|
|
151
307
|
}
|
|
152
308
|
let requestIDs = [];
|
|
309
|
+
const requestClaims = /* @__PURE__ */ new Map();
|
|
153
310
|
try {
|
|
154
|
-
let isValidChunk2 = function(chunk) {
|
|
155
|
-
return chunk && typeof chunk?._id === "string" && typeof chunk?.data === "string";
|
|
156
|
-
};
|
|
157
|
-
var isValidChunk = isValidChunk2;
|
|
158
311
|
this.currentProcessing++;
|
|
159
|
-
requestIDs = this.queue.
|
|
312
|
+
requestIDs = unique(this.queue.filter((id) => this.isReady(id))).slice(0, BATCH_SIZE);
|
|
313
|
+
const selectedIds = new Set(requestIDs);
|
|
314
|
+
this.queue = this.queue.filter((id) => !selectedIds.has(id));
|
|
160
315
|
this.ensureClaims(requestIDs);
|
|
161
|
-
|
|
316
|
+
for (const id of requestIDs) {
|
|
317
|
+
const pending = this.pendingClaims.get(id);
|
|
318
|
+
if (pending) {
|
|
319
|
+
pending.inFlight = true;
|
|
320
|
+
requestClaims.set(id, pending);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
this.scheduleRequest();
|
|
324
|
+
requestIDs = await this.waitForActivityBoundary(requestClaims);
|
|
162
325
|
if (requestIDs.length === 0) return;
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const timeToWait = Math.max(this.interval - timeSinceLastRequest, 0);
|
|
168
|
-
if (timeToWait > 0) await delay(timeToWait);
|
|
169
|
-
this.touchClaims(requestIDs);
|
|
326
|
+
const localCheckCompletionVersion = this.finiteCompletionVersion;
|
|
327
|
+
await this.resolveLocallyAvailableRetries(requestClaims);
|
|
328
|
+
let pendingIDs = await this.waitBeforeRemoteRequest(requestIDs, requestClaims, localCheckCompletionVersion);
|
|
329
|
+
if (pendingIDs.length === 0) return;
|
|
170
330
|
const replicator = this.options.replicatorService.getActiveReplicator();
|
|
171
331
|
if (!replicator) {
|
|
172
332
|
Logger("No active replicator was found to request missing chunks.");
|
|
@@ -174,58 +334,46 @@ class ChunkFetcher {
|
|
|
174
334
|
}
|
|
175
335
|
if (!canFetchRemoteChunks(replicator)) {
|
|
176
336
|
Logger("The active replicator does not support fetching individual remote chunks.");
|
|
177
|
-
for (const chunkID of
|
|
337
|
+
for (const chunkID of pendingIDs) {
|
|
178
338
|
this.chunkManager.emitEvent(EVENT_MISSING_CHUNK_REMOTE, chunkID);
|
|
179
339
|
}
|
|
180
340
|
return;
|
|
181
341
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
342
|
+
const requestCompletionVersion = this.finiteCompletionVersion;
|
|
343
|
+
const fetched = await replicator.fetchRemoteChunks(pendingIDs, false);
|
|
344
|
+
pendingIDs = this.getActiveClaimIds(pendingIDs, requestClaims);
|
|
345
|
+
this.touchClaims(pendingIDs, requestClaims);
|
|
184
346
|
if (!fetched) {
|
|
185
|
-
Logger(`No chunks were found for the following IDs: ${
|
|
186
|
-
for (const chunkID of
|
|
347
|
+
Logger(`No chunks were found for the following IDs: ${pendingIDs.join(", ")}`);
|
|
348
|
+
for (const chunkID of pendingIDs) {
|
|
187
349
|
this.chunkManager.emitEvent(EVENT_MISSING_CHUNK_REMOTE, chunkID);
|
|
188
350
|
}
|
|
189
351
|
return;
|
|
190
352
|
}
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
`Some fetched chunks are invalid and will be ignored: (${fetched.length - chunks.length} / ${fetched.length}).`,
|
|
195
|
-
LOG_LEVEL_VERBOSE
|
|
196
|
-
);
|
|
197
|
-
for (const chunk of fetched) {
|
|
198
|
-
if (!isValidChunk2(chunk)) {
|
|
199
|
-
Logger(`Invalid chunk: ${JSON.stringify(chunk)}`, LOG_LEVEL_VERBOSE);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (chunks.length === 0) {
|
|
204
|
-
Logger(`No valid chunks were found for the following IDs: ${requestIDs.join(", ")}`);
|
|
205
|
-
}
|
|
206
|
-
const missingIDs = requestIDs.filter((id) => !chunks.some((chunk) => chunk._id === id));
|
|
353
|
+
const classification = classifyFetchedChunks(pendingIDs, fetched);
|
|
354
|
+
this.logFetchedChunks(pendingIDs, classification);
|
|
355
|
+
const { chunks, missingIds, invalidIds } = classification;
|
|
207
356
|
try {
|
|
208
|
-
if (chunks.length
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
},
|
|
219
|
-
"ChunkFetcher"
|
|
220
|
-
);
|
|
221
|
-
this.touchClaims(requestIDs);
|
|
222
|
-
if (result.result === true) {
|
|
223
|
-
Logger(`Fetched chunks were stored successfully: ${chunks.length}`, LOG_LEVEL_VERBOSE);
|
|
224
|
-
} else {
|
|
225
|
-
Logger(
|
|
226
|
-
`Fetched chunks could not be stored: ${chunks.map((chunk) => chunk._id).join(", ")}`,
|
|
227
|
-
LOG_LEVEL_VERBOSE
|
|
357
|
+
if (chunks.length > 0) {
|
|
358
|
+
Logger(`Writing fetched chunks (${chunks.length}) to the database...`);
|
|
359
|
+
const result = await this.chunkManager.write(
|
|
360
|
+
chunks,
|
|
361
|
+
{
|
|
362
|
+
skipCache: true,
|
|
363
|
+
force: true
|
|
364
|
+
// Force writing to ensure the chunks with existing _rev.
|
|
365
|
+
},
|
|
366
|
+
"ChunkFetcher"
|
|
228
367
|
);
|
|
368
|
+
this.touchClaims(pendingIDs, requestClaims);
|
|
369
|
+
if (result.result === true) {
|
|
370
|
+
Logger(`Fetched chunks were stored successfully: ${chunks.length}`, LOG_LEVEL_VERBOSE);
|
|
371
|
+
} else {
|
|
372
|
+
Logger(
|
|
373
|
+
`Fetched chunks could not be stored: ${chunks.map((chunk) => chunk._id).join(", ")}`,
|
|
374
|
+
LOG_LEVEL_VERBOSE
|
|
375
|
+
);
|
|
376
|
+
}
|
|
229
377
|
}
|
|
230
378
|
} catch (error) {
|
|
231
379
|
Logger(`An error occurred while storing fetched chunks!`, LOG_LEVEL_VERBOSE);
|
|
@@ -233,23 +381,24 @@ class ChunkFetcher {
|
|
|
233
381
|
} finally {
|
|
234
382
|
for (const chunk of chunks) {
|
|
235
383
|
this.chunkManager.emitEvent(EVENT_CHUNK_FETCHED, chunk);
|
|
384
|
+
this.settleClaim(chunk._id, requestClaims.get(chunk._id));
|
|
236
385
|
}
|
|
237
|
-
for (const
|
|
238
|
-
this.chunkManager.emitEvent(EVENT_MISSING_CHUNK_REMOTE,
|
|
386
|
+
for (const invalidID of this.getActiveClaimIds(invalidIds, requestClaims)) {
|
|
387
|
+
this.chunkManager.emitEvent(EVENT_MISSING_CHUNK_REMOTE, invalidID);
|
|
388
|
+
this.settleClaim(invalidID, requestClaims.get(invalidID));
|
|
239
389
|
}
|
|
240
390
|
}
|
|
391
|
+
this.applyMissingChunkResults(missingIds, requestClaims, requestCompletionVersion);
|
|
241
392
|
} catch (error) {
|
|
242
393
|
Logger("An error occurred while fetching remote chunks.", LOG_LEVEL_VERBOSE);
|
|
243
394
|
Logger(error, LOG_LEVEL_VERBOSE);
|
|
244
395
|
} finally {
|
|
245
|
-
for (const id of
|
|
246
|
-
this.settleClaim(id);
|
|
396
|
+
for (const [id, pending] of requestClaims) {
|
|
397
|
+
if (pending.inFlight) this.settleClaim(id, pending);
|
|
247
398
|
}
|
|
248
399
|
this.currentProcessing--;
|
|
249
400
|
this.previousRequestTime = Date.now();
|
|
250
|
-
|
|
251
|
-
compatGlobal.setTimeout(() => void this.requestMissingChunks(), 0);
|
|
252
|
-
}
|
|
401
|
+
this.scheduleRequest();
|
|
253
402
|
}
|
|
254
403
|
}
|
|
255
404
|
}
|
|
@@ -9,6 +9,9 @@ export declare class HashManager extends HashManagerCore {
|
|
|
9
9
|
* Instance of the hash manager currently in use.
|
|
10
10
|
*/
|
|
11
11
|
manager: HashManagerCore;
|
|
12
|
+
private chunkIdGenerator?;
|
|
13
|
+
clearCaches(): void;
|
|
14
|
+
private computeIndependentChunkId;
|
|
12
15
|
/**
|
|
13
16
|
* Checks whether the specified hash algorithm is available.
|
|
14
17
|
*
|
|
@@ -2,7 +2,7 @@ import { FallbackWasmHashManager, XXHash32RawHashManager, XXHash64HashManager }
|
|
|
2
2
|
import { FallbackPureJSHashManager, PureJSHashManager, SHA1HashManager } from "./PureJSHashManager.js";
|
|
3
3
|
import { HashEncryptedPrefix, HashManagerCore } from "./HashManagerCore.js";
|
|
4
4
|
import { LOG_LEVEL_VERBOSE, Logger } from "../../common/logger.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createChunkIdGenerator, configuredIdKey } from "../../common/idDerivation.js";
|
|
6
6
|
const HashManagers = [
|
|
7
7
|
XXHash64HashManager,
|
|
8
8
|
XXHash32RawHashManager,
|
|
@@ -17,6 +17,22 @@ class HashManager extends HashManagerCore {
|
|
|
17
17
|
* Instance of the hash manager currently in use.
|
|
18
18
|
*/
|
|
19
19
|
manager = void 0;
|
|
20
|
+
chunkIdGenerator;
|
|
21
|
+
clearCaches() {
|
|
22
|
+
this.chunkIdGenerator = void 0;
|
|
23
|
+
}
|
|
24
|
+
async computeIndependentChunkId(key, piece) {
|
|
25
|
+
let cached = this.chunkIdGenerator;
|
|
26
|
+
if (cached?.key !== key) {
|
|
27
|
+
const entry = { key, task: createChunkIdGenerator(key) };
|
|
28
|
+
this.chunkIdGenerator = cached = entry;
|
|
29
|
+
void entry.task.catch(() => {
|
|
30
|
+
if (this.chunkIdGenerator === entry) this.clearCaches();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const generate = await cached.task;
|
|
34
|
+
return await generate(piece);
|
|
35
|
+
}
|
|
20
36
|
/**
|
|
21
37
|
* Checks whether the specified hash algorithm is available.
|
|
22
38
|
*
|
|
@@ -75,13 +91,16 @@ class HashManager extends HashManagerCore {
|
|
|
75
91
|
const settings = this.options.settingService.currentSettings();
|
|
76
92
|
const key = configuredIdKey(settings);
|
|
77
93
|
if (settings.encrypt && key) {
|
|
78
|
-
return HashEncryptedPrefix + await
|
|
94
|
+
return HashEncryptedPrefix + await this.computeIndependentChunkId(key, piece);
|
|
79
95
|
}
|
|
80
|
-
|
|
96
|
+
this.clearCaches();
|
|
97
|
+
return settings.encrypt ? HashEncryptedPrefix + await this.manager.computeHashWithEncryption(piece) : await this.manager.computeHashWithoutEncryption(piece);
|
|
81
98
|
}
|
|
82
99
|
usesIndependentIdKey() {
|
|
83
100
|
const settings = this.options.settingService.currentSettings();
|
|
84
|
-
|
|
101
|
+
const active = settings.encrypt && configuredIdKey(settings) !== false;
|
|
102
|
+
if (!active) this.clearCaches();
|
|
103
|
+
return active;
|
|
85
104
|
}
|
|
86
105
|
/**
|
|
87
106
|
* Computes the hash value without encryption.
|
|
@@ -100,7 +119,8 @@ class HashManager extends HashManagerCore {
|
|
|
100
119
|
*/
|
|
101
120
|
computeHashWithEncryption(piece) {
|
|
102
121
|
const key = configuredIdKey(this.options.settingService.currentSettings());
|
|
103
|
-
if (key) return
|
|
122
|
+
if (key) return this.computeIndependentChunkId(key, piece);
|
|
123
|
+
this.clearCaches();
|
|
104
124
|
return this.manager.computeHashWithEncryption(piece);
|
|
105
125
|
}
|
|
106
126
|
}
|
|
@@ -39,6 +39,8 @@ class LiveSyncManagers {
|
|
|
39
39
|
}
|
|
40
40
|
async teardownManagers() {
|
|
41
41
|
this.log("Teardown LiveSync Managers...", LOG_LEVEL_VERBOSE);
|
|
42
|
+
this.hashManager?.clearCaches();
|
|
43
|
+
this.entryManager?.hashManager?.clearCaches();
|
|
42
44
|
if (this.changeManager) {
|
|
43
45
|
this.changeManager.teardown();
|
|
44
46
|
this.changeManager = void 0;
|
|
@@ -123,8 +125,12 @@ class LiveSyncManagers {
|
|
|
123
125
|
}
|
|
124
126
|
clearCaches() {
|
|
125
127
|
this.chunkManager?.clearCaches();
|
|
128
|
+
this.hashManager?.clearCaches();
|
|
129
|
+
this.entryManager?.hashManager?.clearCaches();
|
|
126
130
|
}
|
|
127
131
|
async prepareHashFunction() {
|
|
132
|
+
this.hashManager?.clearCaches();
|
|
133
|
+
this.entryManager?.hashManager?.clearCaches();
|
|
128
134
|
this.hashManager = new HashManager({
|
|
129
135
|
settingService: this.options.settingService
|
|
130
136
|
});
|