@vrtmrz/livesync-commonlib 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/dist/API/processSetting.d.ts +1 -0
- package/dist/ContentSplitter/ContentSplitterRabinKarp.js +2 -17
- package/dist/ContentSplitter/ContentSplitterV1.js +9 -19
- package/dist/ContentSplitter/ContentSplitterV2.js +9 -19
- package/dist/common/models/setting.const.defaults.js +1 -0
- package/dist/common/models/setting.const.qr.js +2 -0
- package/dist/common/models/setting.policy.d.ts +4 -0
- package/dist/common/models/setting.policy.js +1 -0
- package/dist/common/models/setting.type.d.ts +4 -0
- package/dist/common/models/tweak.definition.d.ts +1 -0
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/common/settingConstants.js +4 -0
- package/dist/common/utils.d.ts +33 -3
- package/dist/common/utils.js +12 -44
- package/dist/common/utils.object.js +2 -1
- package/dist/concurrency/keyedPendingWork.d.ts +13 -0
- package/dist/concurrency/keyedPendingWork.js +27 -0
- package/dist/conflicts/conflictCheckQueue.d.ts +45 -0
- package/dist/conflicts/conflictCheckQueue.js +108 -0
- package/dist/conflicts.d.ts +3 -0
- package/dist/conflicts.js +4 -0
- package/dist/diagnostics/integrity.js +11 -12
- package/dist/interfaces/FileHandler.d.ts +2 -0
- package/dist/interfaces/ServiceModule.d.ts +42 -24
- package/dist/interfaces/ServiceModule.js +4 -3
- package/dist/interfaces/StorageAccess.d.ts +2 -0
- package/dist/interfaces/StorageEventManager.d.ts +2 -0
- package/dist/interfaces/StorageEventManager.js +4 -0
- package/dist/managers/StorageEventManager.d.ts +4 -3
- package/dist/managers/StorageEventManager.js +24 -6
- package/dist/pouchdb/LiveSyncLocalDB.js +1 -0
- package/dist/pouchdb/chunks.d.ts +16 -0
- package/dist/pouchdb/pouchdb-browser.js +2 -84
- package/dist/pouchdb/pouchdb-http.js +2 -84
- package/dist/pouchdb/purgeMulti.d.ts +4 -0
- package/dist/pouchdb/purgeMulti.js +89 -0
- package/dist/replication/journal/JournalSyncCore.js +6 -1
- package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
- package/dist/replication/trystero/TrysteroReplicator.js +3 -4
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +3 -2
- package/dist/replication/trystero/peerPatterns.d.ts +1 -0
- package/dist/replication/trystero/peerPatterns.js +6 -0
- package/dist/rpc/RpcRoom.d.ts +5 -1
- package/dist/rpc/RpcRoom.js +80 -105
- package/dist/serviceFeatures/offlineScanner.d.ts +8 -8
- package/dist/serviceFeatures/offlineScanner.js +25 -21
- package/dist/serviceFeatures/prepareDatabaseForUse.d.ts +4 -3
- package/dist/serviceFeatures/prepareDatabaseForUse.js +5 -4
- package/dist/serviceFeatures/remoteConfig.d.ts +10 -5
- package/dist/serviceModules/ServiceFileAccessBase.d.ts +1 -0
- package/dist/serviceModules/ServiceFileAccessBase.js +3 -0
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +1 -0
- package/dist/serviceModules/ServiceFileHandlerBase.js +23 -7
- package/dist/services/base/DatabaseEventService.d.ts +1 -1
- package/dist/services/base/IService.d.ts +3 -3
- package/dist/services/base/VaultScanResult.d.ts +5 -0
- package/dist/services/base/VaultService.d.ts +1 -1
- package/dist/services/implements/injectable/InjectableConflictService.d.ts +5 -5
- package/dist/services/lib/HandlerUtils.d.ts +8 -6
- package/dist/services/lib/HandlerUtils.js +14 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/conflict-resolution.md +30 -0
- package/docs/development.md +46 -0
- package/docs/offline-scanner.md +12 -4
- package/docs/proven-in-use.md +63 -0
- package/docs/releasing.md +2 -0
- package/docs/service-feature-composition.md +56 -9
- package/docs/storage-events-and-reflection.md +6 -2
- package/package.json +10 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parsePeerPatterns } from "./peerPatterns.js";
|
|
1
2
|
import {
|
|
2
3
|
TweakValuesShouldMatchedTemplate,
|
|
3
4
|
omitP2PRuntimeSettings
|
|
@@ -171,12 +172,10 @@ class TrysteroReplicator {
|
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
get autoSyncPeers() {
|
|
174
|
-
|
|
175
|
-
return peers;
|
|
175
|
+
return parsePeerPatterns(this.currentSettings.P2P_AutoSyncPeers);
|
|
176
176
|
}
|
|
177
177
|
get autoWatchPeers() {
|
|
178
|
-
|
|
179
|
-
return peers;
|
|
178
|
+
return parsePeerPatterns(this.currentSettings.P2P_AutoWatchPeers);
|
|
180
179
|
}
|
|
181
180
|
async onNewPeer(peer) {
|
|
182
181
|
const peerName = peer.name;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parsePeerPatterns } from "./peerPatterns.js";
|
|
1
2
|
import { selfId, joinRoom } from "@trystero-p2p/nostr";
|
|
2
3
|
import { AutoAccepting, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE } from "../../common/types.js";
|
|
3
4
|
import { LOG_LEVEL_VERBOSE, Logger } from "../../common/logger.js";
|
|
@@ -281,12 +282,12 @@ You can chose as follows:
|
|
|
281
282
|
}
|
|
282
283
|
_acceptablePeers = new Computed({
|
|
283
284
|
evaluation: (settings) => {
|
|
284
|
-
return `${settings?.P2P_AutoAcceptingPeers ?? ""}
|
|
285
|
+
return parsePeerPatterns(`${settings?.P2P_AutoAcceptingPeers ?? ""}`);
|
|
285
286
|
}
|
|
286
287
|
});
|
|
287
288
|
_shouldDenyPeers = new Computed({
|
|
288
289
|
evaluation: (settings) => {
|
|
289
|
-
return `${settings?.P2P_AutoDenyingPeers ?? ""}
|
|
290
|
+
return parsePeerPatterns(`${settings?.P2P_AutoDenyingPeers ?? ""}`);
|
|
290
291
|
}
|
|
291
292
|
});
|
|
292
293
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parsePeerPatterns(value: string): RegExp[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
function parsePeerPatterns(value) {
|
|
2
|
+
return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => entry.startsWith("~") ? new RegExp(entry.substring(1), "i") : new RegExp(`^${entry}$`, "i"));
|
|
3
|
+
}
|
|
4
|
+
export {
|
|
5
|
+
parsePeerPatterns
|
|
6
|
+
};
|
package/dist/rpc/RpcRoom.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare class RpcRoom {
|
|
|
8
8
|
private sessions;
|
|
9
9
|
private outgoingChunkMap;
|
|
10
10
|
private incomingChunkMap;
|
|
11
|
-
private
|
|
11
|
+
private incomingChunkWaits;
|
|
12
12
|
private peerVersion;
|
|
13
13
|
private disposers;
|
|
14
14
|
private closed;
|
|
@@ -32,7 +32,11 @@ export declare class RpcRoom {
|
|
|
32
32
|
private notifyRemoteCancellation;
|
|
33
33
|
private abortInboundCall;
|
|
34
34
|
private sendEnvelope;
|
|
35
|
+
private sendChunk;
|
|
36
|
+
private sendErrorResponse;
|
|
35
37
|
private scheduleMissingAck;
|
|
36
38
|
private onWireMessage;
|
|
37
39
|
private onEnvelopePayload;
|
|
40
|
+
private onRequest;
|
|
41
|
+
private runMethod;
|
|
38
42
|
}
|
package/dist/rpc/RpcRoom.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { compatGlobal } from "../common/coreEnvFunctions.js";
|
|
2
|
+
import {
|
|
3
|
+
createDeferredAction
|
|
4
|
+
} from "octagonal-wheels/concurrency/deferredAction";
|
|
2
5
|
import { IncomingChunkBuffer, estimateBytes, splitIntoChunks } from "./chunking.js";
|
|
3
6
|
import { asRpcErrorShape, RpcError } from "./errors.js";
|
|
4
7
|
import { RpcSession } from "./RpcSession.js";
|
|
@@ -12,6 +15,10 @@ function newId(prefix) {
|
|
|
12
15
|
function validNamespacedMethod(method) {
|
|
13
16
|
return method.includes(".") || method.includes("/");
|
|
14
17
|
}
|
|
18
|
+
const scheduleRpcTimeout = (callback, delayMs) => {
|
|
19
|
+
const handle = compatGlobal.setTimeout(callback, delayMs);
|
|
20
|
+
return () => compatGlobal.clearTimeout(handle);
|
|
21
|
+
};
|
|
15
22
|
class RpcRoom {
|
|
16
23
|
options;
|
|
17
24
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -21,7 +28,7 @@ class RpcRoom {
|
|
|
21
28
|
sessions = /* @__PURE__ */ new Map();
|
|
22
29
|
outgoingChunkMap = /* @__PURE__ */ new Map();
|
|
23
30
|
incomingChunkMap = /* @__PURE__ */ new Map();
|
|
24
|
-
|
|
31
|
+
incomingChunkWaits = /* @__PURE__ */ new Map();
|
|
25
32
|
peerVersion = /* @__PURE__ */ new Map();
|
|
26
33
|
disposers = [];
|
|
27
34
|
closed = false;
|
|
@@ -83,8 +90,8 @@ class RpcRoom {
|
|
|
83
90
|
this.sessions.clear();
|
|
84
91
|
this.outgoingChunkMap.clear();
|
|
85
92
|
this.incomingChunkMap.clear();
|
|
86
|
-
this.
|
|
87
|
-
this.
|
|
93
|
+
this.incomingChunkWaits.forEach((wait) => wait.dispose());
|
|
94
|
+
this.incomingChunkWaits.clear();
|
|
88
95
|
}
|
|
89
96
|
session(peerId) {
|
|
90
97
|
const existing = this.sessions.get(peerId);
|
|
@@ -125,30 +132,32 @@ class RpcRoom {
|
|
|
125
132
|
const requestId = newId("req");
|
|
126
133
|
let pending;
|
|
127
134
|
const p = new Promise((resolve, reject) => {
|
|
135
|
+
const signal = options.signal;
|
|
136
|
+
const onAbort = () => {
|
|
137
|
+
const current = this.pending.get(requestId);
|
|
138
|
+
if (!current) return;
|
|
139
|
+
this.rejectPending(requestId, new RpcError("CANCELLED", `RPC cancelled: ${method}`), true);
|
|
140
|
+
};
|
|
141
|
+
const deadline = timeoutMs > 0 ? createDeferredAction(() => {
|
|
142
|
+
this.rejectPending(requestId, new RpcError("TIMEOUT", `RPC timed out: ${method}`), true);
|
|
143
|
+
}, scheduleRpcTimeout) : void 0;
|
|
128
144
|
pending = {
|
|
129
145
|
peerId,
|
|
130
146
|
method,
|
|
131
147
|
resolve,
|
|
132
148
|
reject,
|
|
133
|
-
|
|
149
|
+
releaseWait: () => {
|
|
150
|
+
deadline?.dispose();
|
|
151
|
+
signal?.removeEventListener("abort", onAbort);
|
|
152
|
+
},
|
|
134
153
|
requestSent: false,
|
|
135
154
|
cancelRequested: false
|
|
136
155
|
};
|
|
137
|
-
|
|
138
|
-
pending.timeoutHandle = compatGlobal.setTimeout(() => {
|
|
139
|
-
this.rejectPending(requestId, new RpcError("TIMEOUT", `RPC timed out: ${method}`), true);
|
|
140
|
-
}, timeoutMs);
|
|
141
|
-
}
|
|
156
|
+
deadline?.schedule(timeoutMs);
|
|
142
157
|
this.pending.set(requestId, pending);
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!current) return;
|
|
147
|
-
this.rejectPending(requestId, new RpcError("CANCELLED", `RPC cancelled: ${method}`), true);
|
|
148
|
-
};
|
|
149
|
-
pending.abortListener = onAbort;
|
|
150
|
-
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
151
|
-
if (options.signal.aborted) onAbort();
|
|
158
|
+
if (signal) {
|
|
159
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
160
|
+
if (signal.aborted) onAbort();
|
|
152
161
|
}
|
|
153
162
|
});
|
|
154
163
|
const sendTask = this.sendEnvelope(peerId, {
|
|
@@ -191,12 +200,7 @@ class RpcRoom {
|
|
|
191
200
|
const pending = this.pending.get(requestId);
|
|
192
201
|
if (!pending) return void 0;
|
|
193
202
|
this.pending.delete(requestId);
|
|
194
|
-
|
|
195
|
-
compatGlobal.clearTimeout(pending.timeoutHandle);
|
|
196
|
-
}
|
|
197
|
-
if (pending.signal && pending.abortListener) {
|
|
198
|
-
pending.signal.removeEventListener("abort", pending.abortListener);
|
|
199
|
-
}
|
|
203
|
+
pending.releaseWait();
|
|
200
204
|
return pending;
|
|
201
205
|
}
|
|
202
206
|
rejectPending(requestId, error, notifyRemote = false) {
|
|
@@ -229,28 +233,25 @@ class RpcRoom {
|
|
|
229
233
|
const chunks = splitIntoChunks(serialized, this.options.maxWirePayloadBytes);
|
|
230
234
|
this.outgoingChunkMap.set(streamId, { peerId, chunks });
|
|
231
235
|
for (let i = 0; i < chunks.length; i++) {
|
|
232
|
-
await this.
|
|
233
|
-
{
|
|
234
|
-
wire: "chunk",
|
|
235
|
-
streamId,
|
|
236
|
-
index: i,
|
|
237
|
-
total: chunks.length,
|
|
238
|
-
payload: chunks[i]
|
|
239
|
-
},
|
|
240
|
-
peerId
|
|
241
|
-
);
|
|
236
|
+
await this.sendChunk(peerId, streamId, chunks.length, i, chunks[i]);
|
|
242
237
|
}
|
|
243
238
|
}
|
|
239
|
+
sendChunk(peerId, streamId, total, index, payload) {
|
|
240
|
+
return this.options.transport.send({ wire: "chunk", streamId, index, total, payload }, peerId);
|
|
241
|
+
}
|
|
242
|
+
sendErrorResponse(peerId, requestId, error) {
|
|
243
|
+
return this.sendEnvelope(peerId, { kind: "response", requestId, ok: false, error });
|
|
244
|
+
}
|
|
244
245
|
scheduleMissingAck(streamId, peerId) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const handle = compatGlobal.setTimeout(() => {
|
|
246
|
+
this.incomingChunkWaits.get(streamId)?.dispose();
|
|
247
|
+
const wait = createDeferredAction(() => {
|
|
248
248
|
const state = this.incomingChunkMap.get(streamId);
|
|
249
249
|
if (!state || state.isComplete()) return;
|
|
250
250
|
const missing = state.missingIndices();
|
|
251
251
|
void this.options.transport.send({ wire: "chunk-ack", streamId, missing }, peerId);
|
|
252
|
-
},
|
|
253
|
-
this.
|
|
252
|
+
}, scheduleRpcTimeout);
|
|
253
|
+
wait.schedule(this.options.chunkMissingRetryMs);
|
|
254
|
+
this.incomingChunkWaits.set(streamId, wait);
|
|
254
255
|
}
|
|
255
256
|
async onWireMessage(message, peerId) {
|
|
256
257
|
if (message.wire === "raw") {
|
|
@@ -266,9 +267,8 @@ class RpcRoom {
|
|
|
266
267
|
state2.add(message.index, message.payload);
|
|
267
268
|
this.scheduleMissingAck(message.streamId, peerId);
|
|
268
269
|
if (state2.isComplete()) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
this.incomingChunkTimers.delete(message.streamId);
|
|
270
|
+
this.incomingChunkWaits.get(message.streamId)?.dispose();
|
|
271
|
+
this.incomingChunkWaits.delete(message.streamId);
|
|
272
272
|
this.incomingChunkMap.delete(message.streamId);
|
|
273
273
|
await this.options.transport.send(
|
|
274
274
|
{ wire: "chunk-ack", streamId: message.streamId, missing: [] },
|
|
@@ -287,16 +287,7 @@ class RpcRoom {
|
|
|
287
287
|
for (const index of message.missing) {
|
|
288
288
|
const payload = state.chunks[index];
|
|
289
289
|
if (payload === void 0) continue;
|
|
290
|
-
await this.
|
|
291
|
-
{
|
|
292
|
-
wire: "chunk",
|
|
293
|
-
streamId: message.streamId,
|
|
294
|
-
index,
|
|
295
|
-
total: state.chunks.length,
|
|
296
|
-
payload
|
|
297
|
-
},
|
|
298
|
-
state.peerId
|
|
299
|
-
);
|
|
290
|
+
await this.sendChunk(state.peerId, message.streamId, state.chunks.length, index, payload);
|
|
300
291
|
}
|
|
301
292
|
}
|
|
302
293
|
async onEnvelopePayload(payload, peerId) {
|
|
@@ -319,8 +310,8 @@ class RpcRoom {
|
|
|
319
310
|
return;
|
|
320
311
|
}
|
|
321
312
|
if (envelope.kind === "cancel") {
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
313
|
+
const ctx = this.inboundCalls.get(envelope.requestId);
|
|
314
|
+
if (ctx && ctx.peerId === peerId) this.abortInboundCall(envelope.requestId, ctx);
|
|
324
315
|
return;
|
|
325
316
|
}
|
|
326
317
|
if (envelope.kind === "response") {
|
|
@@ -335,6 +326,9 @@ class RpcRoom {
|
|
|
335
326
|
}
|
|
336
327
|
return;
|
|
337
328
|
}
|
|
329
|
+
return this.onRequest(envelope, peerId);
|
|
330
|
+
}
|
|
331
|
+
async onRequest(envelope, peerId) {
|
|
338
332
|
const controller = new AbortController();
|
|
339
333
|
const ctx = { controller, signal: controller.signal, peerId };
|
|
340
334
|
this.inboundCalls.set(envelope.requestId, ctx);
|
|
@@ -344,12 +338,7 @@ class RpcRoom {
|
|
|
344
338
|
} catch (ex) {
|
|
345
339
|
this.inboundCalls.delete(envelope.requestId);
|
|
346
340
|
if (!ctx.signal.aborted && !this.closed) {
|
|
347
|
-
await this.
|
|
348
|
-
kind: "response",
|
|
349
|
-
requestId: envelope.requestId,
|
|
350
|
-
ok: false,
|
|
351
|
-
error: asRpcErrorShape(ex)
|
|
352
|
-
});
|
|
341
|
+
await this.sendErrorResponse(peerId, envelope.requestId, asRpcErrorShape(ex));
|
|
353
342
|
}
|
|
354
343
|
return;
|
|
355
344
|
}
|
|
@@ -364,14 +353,9 @@ class RpcRoom {
|
|
|
364
353
|
const version = this.peerVersion.get(peerId);
|
|
365
354
|
if (version && version.major !== RPC_VERSION_MAJOR) {
|
|
366
355
|
if (!ctx.signal.aborted && !this.closed) {
|
|
367
|
-
await this.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
ok: false,
|
|
371
|
-
error: {
|
|
372
|
-
code: "REMOTE_ERROR",
|
|
373
|
-
message: `RPC major mismatch: local=${RPC_VERSION_MAJOR}, remote=${version.major}`
|
|
374
|
-
}
|
|
356
|
+
await this.sendErrorResponse(peerId, envelope.requestId, {
|
|
357
|
+
code: "REMOTE_ERROR",
|
|
358
|
+
message: `RPC major mismatch: local=${RPC_VERSION_MAJOR}, remote=${version.major}`
|
|
375
359
|
});
|
|
376
360
|
}
|
|
377
361
|
this.inboundCalls.delete(envelope.requestId);
|
|
@@ -386,48 +370,15 @@ class RpcRoom {
|
|
|
386
370
|
const method = this.methods.get(envelope.method);
|
|
387
371
|
if (!method) {
|
|
388
372
|
if (!ctx.signal.aborted && !this.closed) {
|
|
389
|
-
await this.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
ok: false,
|
|
393
|
-
error: {
|
|
394
|
-
code: "REMOTE_ERROR",
|
|
395
|
-
message: `Method not found: ${envelope.method}`
|
|
396
|
-
}
|
|
373
|
+
await this.sendErrorResponse(peerId, envelope.requestId, {
|
|
374
|
+
code: "REMOTE_ERROR",
|
|
375
|
+
message: `Method not found: ${envelope.method}`
|
|
397
376
|
});
|
|
398
377
|
}
|
|
399
378
|
this.inboundCalls.delete(envelope.requestId);
|
|
400
379
|
return;
|
|
401
380
|
}
|
|
402
|
-
const runner =
|
|
403
|
-
try {
|
|
404
|
-
if (ctx.signal.aborted || this.closed) return;
|
|
405
|
-
const data = method.cancellationAware ? await method.handler(
|
|
406
|
-
ctx,
|
|
407
|
-
peerId,
|
|
408
|
-
...envelope.args
|
|
409
|
-
) : await method.handler(peerId, ...envelope.args);
|
|
410
|
-
if (ctx.signal.aborted || this.closed) return;
|
|
411
|
-
await this.sendEnvelope(peerId, {
|
|
412
|
-
kind: "response",
|
|
413
|
-
requestId: envelope.requestId,
|
|
414
|
-
ok: true,
|
|
415
|
-
data
|
|
416
|
-
});
|
|
417
|
-
} catch (ex) {
|
|
418
|
-
if (ctx.signal.aborted || this.closed) return;
|
|
419
|
-
await this.sendEnvelope(peerId, {
|
|
420
|
-
kind: "response",
|
|
421
|
-
requestId: envelope.requestId,
|
|
422
|
-
ok: false,
|
|
423
|
-
error: asRpcErrorShape(ex)
|
|
424
|
-
});
|
|
425
|
-
} finally {
|
|
426
|
-
if (this.inboundCalls.get(envelope.requestId) === ctx) {
|
|
427
|
-
this.inboundCalls.delete(envelope.requestId);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
};
|
|
381
|
+
const runner = () => this.runMethod(method, envelope, peerId, ctx);
|
|
431
382
|
if (method.serial) {
|
|
432
383
|
method.queue = method.queue.then(runner, runner);
|
|
433
384
|
await method.queue;
|
|
@@ -435,6 +386,30 @@ class RpcRoom {
|
|
|
435
386
|
await runner();
|
|
436
387
|
}
|
|
437
388
|
}
|
|
389
|
+
async runMethod(method, envelope, peerId, ctx) {
|
|
390
|
+
try {
|
|
391
|
+
if (ctx.signal.aborted || this.closed) return;
|
|
392
|
+
const data = method.cancellationAware ? await method.handler(
|
|
393
|
+
ctx,
|
|
394
|
+
peerId,
|
|
395
|
+
...envelope.args
|
|
396
|
+
) : await method.handler(peerId, ...envelope.args);
|
|
397
|
+
if (ctx.signal.aborted || this.closed) return;
|
|
398
|
+
await this.sendEnvelope(peerId, {
|
|
399
|
+
kind: "response",
|
|
400
|
+
requestId: envelope.requestId,
|
|
401
|
+
ok: true,
|
|
402
|
+
data
|
|
403
|
+
});
|
|
404
|
+
} catch (ex) {
|
|
405
|
+
if (ctx.signal.aborted || this.closed) return;
|
|
406
|
+
await this.sendErrorResponse(peerId, envelope.requestId, asRpcErrorShape(ex));
|
|
407
|
+
} finally {
|
|
408
|
+
if (this.inboundCalls.get(envelope.requestId) === ctx) {
|
|
409
|
+
this.inboundCalls.delete(envelope.requestId);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
438
413
|
}
|
|
439
414
|
export {
|
|
440
415
|
RpcRoom
|
|
@@ -3,7 +3,7 @@ import { type LogFunction } from "../services/lib/logUtils.js";
|
|
|
3
3
|
import type { NecessaryServices } from "../interfaces/ServiceModule.js";
|
|
4
4
|
import type { IPathService } from "../services/base/IService.js";
|
|
5
5
|
import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
|
|
6
|
-
import { VaultScanResults, type VaultScanResult } from "../services/base/VaultScanResult.js";
|
|
6
|
+
import { VaultScanResults, type VaultScanResult, type VaultScanOptions } from "../services/base/VaultScanResult.js";
|
|
7
7
|
export { VaultScanResults };
|
|
8
8
|
export type { VaultScanResult };
|
|
9
9
|
/**
|
|
@@ -156,7 +156,7 @@ export declare function getPathFromEntry(host: {
|
|
|
156
156
|
* @param file Storage file information
|
|
157
157
|
* @param doc Database entry
|
|
158
158
|
*/
|
|
159
|
-
export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry): Promise<FilePairProcessResult>;
|
|
159
|
+
export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry, scanOptions?: VaultScanOptions): Promise<FilePairProcessResult>;
|
|
160
160
|
export declare function canProceedScan(host: NecessaryServices<"keyValueDB" | "setting", never>, errorManager: UnresolvedErrorManager, log: LogFunction, showingNotice?: boolean, ignoreSuspending?: boolean): boolean;
|
|
161
161
|
/**
|
|
162
162
|
* Normalise the logical comparison key and lower case it when filename case should be handled insensitively.
|
|
@@ -180,7 +180,7 @@ export declare function collectDatabaseFiles(host: NecessaryServices<"database"
|
|
|
180
180
|
}>;
|
|
181
181
|
export declare function updateToDatabase(host: NecessaryServices<"vault", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, file: UXFileInfoStub): Promise<FilePairProcessResult>;
|
|
182
182
|
export declare function updateToStorage(host: NecessaryServices<"vault" | "path", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, w: MetaEntry): Promise<FilePairProcessResult>;
|
|
183
|
-
export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry): Promise<FilePairProcessResult>;
|
|
183
|
+
export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry, scanOptions?: VaultScanOptions): Promise<FilePairProcessResult>;
|
|
184
184
|
export declare const FullScanModes: {
|
|
185
185
|
readonly DB_APPLY: "db-apply";
|
|
186
186
|
readonly NEWER_WINS: "newer-wins";
|
|
@@ -205,7 +205,7 @@ export declare const ExtraOnLocal: {
|
|
|
205
205
|
*/
|
|
206
206
|
readonly APPEND_STORAGE_ONLY: "append-storage-only";
|
|
207
207
|
};
|
|
208
|
-
export interface FullScanOptions {
|
|
208
|
+
export interface FullScanOptions extends VaultScanOptions {
|
|
209
209
|
mode: FullScanMode;
|
|
210
210
|
extraOnLocal?: (typeof ExtraOnLocal)[keyof typeof ExtraOnLocal];
|
|
211
211
|
extraOnRemote?: (typeof ExtraOnRemote)[keyof typeof ExtraOnRemote];
|
|
@@ -245,7 +245,7 @@ export declare function resolveFilePairAction(state: FilePairState, options: Ful
|
|
|
245
245
|
* @param options Full scan options
|
|
246
246
|
* @returns The scan result for all file pairs
|
|
247
247
|
*/
|
|
248
|
-
export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "
|
|
248
|
+
export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options: FullScanOptions): Promise<VaultScanResult>;
|
|
249
249
|
export declare function normaliseFullScanOptions(showingNoticeOrOptions: Partial<FullScanOptions> | boolean | undefined, ignoreSuspending?: boolean): FullScanOptions;
|
|
250
250
|
/**
|
|
251
251
|
* Remove one path from the Offline Scanner's durable last-seen state.
|
|
@@ -264,11 +264,11 @@ export declare function clearOfflineScannerLastSeen(host: NecessaryServices<"set
|
|
|
264
264
|
* @param ignoreSuspending Legacy suspension flag used with the notice flag
|
|
265
265
|
* @returns The scan outcome, including accepted individual file failures
|
|
266
266
|
*/
|
|
267
|
-
export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "
|
|
268
|
-
export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "
|
|
267
|
+
export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options?: Partial<FullScanOptions>): Promise<VaultScanResult>;
|
|
268
|
+
export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, ignoreSuspending?: boolean): Promise<VaultScanResult>;
|
|
269
269
|
/**
|
|
270
270
|
* Associate the initialiser file feature with the app lifecycle events.
|
|
271
271
|
* This function binds initialization handlers to the appropriate lifecycle events.
|
|
272
272
|
* @param host Services container with required dependencies
|
|
273
273
|
*/
|
|
274
|
-
export declare function useOfflineScanner(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "
|
|
274
|
+
export declare function useOfflineScanner(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">): void;
|
|
@@ -15,7 +15,7 @@ import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "../common/models/shared.const.
|
|
|
15
15
|
import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
|
|
16
16
|
import { compatGlobal } from "../common/coreEnvFunctions.js";
|
|
17
17
|
import { ICHeader, ICXHeader, PSCHeader } from "../common/models/fileaccess.const.js";
|
|
18
|
-
import { serialized } from "octagonal-wheels/concurrency/lock";
|
|
18
|
+
import { serialized, withLocks } from "octagonal-wheels/concurrency/lock";
|
|
19
19
|
import { VaultScanResults } from "../services/base/VaultScanResult.js";
|
|
20
20
|
const FilePairProcessResults = {
|
|
21
21
|
COMPLETED: "completed",
|
|
@@ -218,17 +218,10 @@ function createReaddressedMetadata(source, expectedDocumentId) {
|
|
|
218
218
|
}
|
|
219
219
|
async function serializedByMetadataDocumentIds(documentIds, callback) {
|
|
220
220
|
const lockKeys = [...new Set(documentIds)].sort().map((documentId) => `processFileEvent-${documentId}`);
|
|
221
|
-
|
|
222
|
-
const key = lockKeys[index];
|
|
223
|
-
if (key === void 0) return await callback();
|
|
224
|
-
return await serialized(key, () => acquire(index + 1));
|
|
225
|
-
};
|
|
226
|
-
return await acquire(0);
|
|
221
|
+
return await withLocks(lockKeys, callback);
|
|
227
222
|
}
|
|
228
223
|
function findRepairEntry(entries, actualDocumentId) {
|
|
229
|
-
return entries.find(
|
|
230
|
-
({ inspection }) => inspection.diagnostic.actualDocumentId === actualDocumentId
|
|
231
|
-
);
|
|
224
|
+
return entries.find(({ inspection }) => inspection.diagnostic.actualDocumentId === actualDocumentId);
|
|
232
225
|
}
|
|
233
226
|
async function repairMetadataDocumentIdentity(host, request) {
|
|
234
227
|
const baseResult = {
|
|
@@ -350,7 +343,7 @@ function getPathFromEntry(host, doc) {
|
|
|
350
343
|
const path = host.services.path.getPath(doc);
|
|
351
344
|
return path;
|
|
352
345
|
}
|
|
353
|
-
async function syncFileBetweenDBandStorage(host, log, file, doc) {
|
|
346
|
+
async function syncFileBetweenDBandStorage(host, log, file, doc, scanOptions) {
|
|
354
347
|
const docPath = getPathFromEntry(host, doc);
|
|
355
348
|
if (!doc) {
|
|
356
349
|
throw new Error(`Missing doc:${docPath}`);
|
|
@@ -393,6 +386,10 @@ async function syncFileBetweenDBandStorage(host, log, file, doc) {
|
|
|
393
386
|
log(`STORAGE ? DB : ${file.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE);
|
|
394
387
|
return FilePairProcessResults.SKIPPED;
|
|
395
388
|
}
|
|
389
|
+
if (scanOptions?.strictContentCheck === false && await host.serviceModules.fileHandler.canSkipUnchangedFile?.(file, doc)) {
|
|
390
|
+
log(`STORAGE ? DB : ${file.path} skipped using unchanged storage and revision metadata`, LOG_LEVEL_DEBUG);
|
|
391
|
+
return FilePairProcessResults.SKIPPED;
|
|
392
|
+
}
|
|
396
393
|
if (doc._rev && await host.serviceModules.fileHandler.tryRecordUntrackedFileRevision(file, doc._rev)) {
|
|
397
394
|
log("STORAGE == DB :" + file.path, LOG_LEVEL_DEBUG);
|
|
398
395
|
return FilePairProcessResults.COMPLETED;
|
|
@@ -439,6 +436,9 @@ function convertCase(settings, path) {
|
|
|
439
436
|
}
|
|
440
437
|
return normalisedPath.toLowerCase();
|
|
441
438
|
}
|
|
439
|
+
function createComparisonPathLookup(settings, paths) {
|
|
440
|
+
return Object.fromEntries(paths.map((path) => [convertCase(settings, path), path]));
|
|
441
|
+
}
|
|
442
442
|
async function collectFilesOnStorage(host, settings, log) {
|
|
443
443
|
log("Collecting local files on the storage", LOG_LEVEL_VERBOSE);
|
|
444
444
|
const filesStorageSrc = await host.serviceModules.storageAccess.getFiles();
|
|
@@ -451,8 +451,7 @@ async function collectFilesOnStorage(host, settings, log) {
|
|
|
451
451
|
}
|
|
452
452
|
const storageFileNameMap = Object.fromEntries(_filesStorage.map((e) => [e.path, e]));
|
|
453
453
|
const storageFileNames = Object.keys(storageFileNameMap);
|
|
454
|
-
const
|
|
455
|
-
const storageFileNameCI2CS = Object.fromEntries(storageFileNameCapsPair.map((e) => [e[1], e[0]]));
|
|
454
|
+
const storageFileNameCI2CS = createComparisonPathLookup(settings, storageFileNames);
|
|
456
455
|
return { storageFileNameMap, storageFileNames, storageFileNameCI2CS };
|
|
457
456
|
}
|
|
458
457
|
async function collectDatabaseFiles(host, settings, log, showingNotice) {
|
|
@@ -496,8 +495,7 @@ async function collectDatabaseFiles(host, settings, log, showingNotice) {
|
|
|
496
495
|
}
|
|
497
496
|
const databaseFileNameMap = Object.fromEntries(_DBEntries.map((e) => [getPathFromEntry(host, e), e]));
|
|
498
497
|
const databaseFileNames = Object.keys(databaseFileNameMap);
|
|
499
|
-
const
|
|
500
|
-
const databaseFileNameCI2CS = Object.fromEntries(databaseFileNameCapsPair.map((e) => [e[1], e[0]]));
|
|
498
|
+
const databaseFileNameCI2CS = createComparisonPathLookup(settings, databaseFileNames);
|
|
501
499
|
const quarantinedFileNamesLC = new Set(
|
|
502
500
|
[...unresolvedFileNamesLC].filter((path) => !resolvableFileNamesLC.has(path))
|
|
503
501
|
);
|
|
@@ -549,13 +547,13 @@ async function updateToStorage(host, log, logLevel, w) {
|
|
|
549
547
|
return FilePairProcessResults.FAILED;
|
|
550
548
|
}
|
|
551
549
|
}
|
|
552
|
-
async function syncStorageAndDatabase(host, log, file, logLevel, doc) {
|
|
550
|
+
async function syncStorageAndDatabase(host, log, file, logLevel, doc, scanOptions) {
|
|
553
551
|
if ((doc._conflicts?.length ?? 0) > 0) {
|
|
554
552
|
log(`SYNC DATABASE AND STORAGE: ${file.path} has conflicts. skipped`, LOG_LEVEL_INFO);
|
|
555
553
|
return FilePairProcessResults.SKIPPED;
|
|
556
554
|
}
|
|
557
555
|
if (!host.services.vault.isFileSizeTooLarge(file.stat.size) && !host.services.vault.isFileSizeTooLarge(doc.size)) {
|
|
558
|
-
return await syncFileBetweenDBandStorage(host, log, file, doc);
|
|
556
|
+
return await syncFileBetweenDBandStorage(host, log, file, doc, scanOptions);
|
|
559
557
|
} else {
|
|
560
558
|
log(
|
|
561
559
|
`SYNC DATABASE AND STORAGE: ${getPathFromEntry(host, doc)} has been skipped due to file size exceeding the limit`,
|
|
@@ -698,7 +696,7 @@ async function processFilePair(host, log, pair, options) {
|
|
|
698
696
|
if (!file || !doc) {
|
|
699
697
|
throw new Error(`Cannot compare freshness for ${path}`);
|
|
700
698
|
}
|
|
701
|
-
const syncResult = await syncStorageAndDatabase(host, log, file, LOG_LEVEL_INFO, doc);
|
|
699
|
+
const syncResult = await syncStorageAndDatabase(host, log, file, LOG_LEVEL_INFO, doc, options);
|
|
702
700
|
if (syncResult === FilePairProcessResults.COMPLETED) {
|
|
703
701
|
updateFileMTimeInMap(host, fileMapKey, Math.max(file.stat.mtime, doc.mtime));
|
|
704
702
|
}
|
|
@@ -746,7 +744,12 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
|
|
|
746
744
|
const showingNotice = options.showingNotice ?? false;
|
|
747
745
|
await loadFileStatus(host);
|
|
748
746
|
const { storageFileNameMap, storageFileNameCI2CS } = await collectFilesOnStorage(host, settings, log);
|
|
749
|
-
const { databaseFileNameMap, databaseFileNameCI2CS, quarantinedFileNamesLC } = await collectDatabaseFiles(
|
|
747
|
+
const { databaseFileNameMap, databaseFileNameCI2CS, quarantinedFileNamesLC } = await collectDatabaseFiles(
|
|
748
|
+
host,
|
|
749
|
+
settings,
|
|
750
|
+
log,
|
|
751
|
+
showingNotice
|
|
752
|
+
);
|
|
750
753
|
const pairs = [];
|
|
751
754
|
for (const fileNameLC of unique([
|
|
752
755
|
...Object.keys(storageFileNameCI2CS),
|
|
@@ -922,11 +925,12 @@ async function performFullScan(host, log, errorManager, showingNoticeOrOptions =
|
|
|
922
925
|
function useOfflineScanner(host) {
|
|
923
926
|
const log = createInstanceLogFunction("SF:OfflineScanner", host.services.API);
|
|
924
927
|
const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events);
|
|
925
|
-
const handleScanVault = async (showingNotice, ignoreSuspending = false, continueOnFileFailure = false) => {
|
|
928
|
+
const handleScanVault = async (showingNotice, ignoreSuspending = false, continueOnFileFailure = false, scanOptions) => {
|
|
926
929
|
return await performFullScan(host, log, errorManager, {
|
|
927
930
|
showingNotice,
|
|
928
931
|
ignoreSuspending,
|
|
929
|
-
continueOnFileFailure
|
|
932
|
+
continueOnFileFailure,
|
|
933
|
+
strictContentCheck: scanOptions?.strictContentCheck
|
|
930
934
|
});
|
|
931
935
|
};
|
|
932
936
|
host.services.vault.scanVault.addHandler(handleScanVault);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NecessaryServices } from "../interfaces/ServiceModule.js";
|
|
2
2
|
import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
|
|
3
3
|
import { type LogFunction } from "../services/lib/logUtils.js";
|
|
4
|
-
import { type VaultScanResult } from "../services/base/VaultScanResult.js";
|
|
4
|
+
import { type VaultScanResult, type VaultScanOptions } from "../services/base/VaultScanResult.js";
|
|
5
5
|
/**
|
|
6
6
|
* Initialise the database and trigger a full vault scan.
|
|
7
7
|
* @param host Services container
|
|
@@ -11,12 +11,13 @@ import { type VaultScanResult } from "../services/base/VaultScanResult.js";
|
|
|
11
11
|
* @param reopenDatabase Whether to reopen the database connection
|
|
12
12
|
* @param ignoreSuspending Whether to ignore suspension settings
|
|
13
13
|
* @param continueOnFileFailure Whether individual file failures may satisfy application readiness
|
|
14
|
+
* @param scanOptions Optional content-check policy; omitted options keep strict checks
|
|
14
15
|
* @returns The initialisation outcome, including accepted individual file failures
|
|
15
16
|
*/
|
|
16
|
-
export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "
|
|
17
|
+
export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "vault" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean, scanOptions?: VaultScanOptions): Promise<VaultScanResult>;
|
|
17
18
|
/**
|
|
18
19
|
* Associate the initialiser file feature with the app lifecycle events.
|
|
19
20
|
* This function binds initialization handlers to the appropriate lifecycle events.
|
|
20
21
|
* @param host Services container with required dependencies
|
|
21
22
|
*/
|
|
22
|
-
export declare function usePrepareDatabaseForUse(host: NecessaryServices<"API" | "appLifecycle" | "
|
|
23
|
+
export declare function usePrepareDatabaseForUse(host: NecessaryServices<"API" | "appLifecycle" | "vault" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>): void;
|
|
@@ -6,7 +6,7 @@ function reportPreparationFailure(log, message) {
|
|
|
6
6
|
log(message, LOG_LEVEL_VERBOSE);
|
|
7
7
|
return false;
|
|
8
8
|
}
|
|
9
|
-
async function prepareDatabaseForUse(host, log, errorManager, showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false) {
|
|
9
|
+
async function prepareDatabaseForUse(host, log, errorManager, showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false, scanOptions) {
|
|
10
10
|
const appLifecycle = host.services.appLifecycle;
|
|
11
11
|
appLifecycle.resetIsReady();
|
|
12
12
|
try {
|
|
@@ -25,7 +25,7 @@ async function prepareDatabaseForUse(host, log, errorManager, showingNotice = fa
|
|
|
25
25
|
"Database preparation stopped because the local database is not ready."
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
|
-
const scanResult = await host.services.vault.scanVault(showingNotice, ignoreSuspending, continueOnFileFailure);
|
|
28
|
+
const scanResult = await host.services.vault.scanVault(showingNotice, ignoreSuspending, continueOnFileFailure, scanOptions);
|
|
29
29
|
if (scanResult === VaultScanResults.FAILED) {
|
|
30
30
|
return reportPreparationFailure(log, "Database preparation stopped because the Vault scan failed.");
|
|
31
31
|
}
|
|
@@ -57,7 +57,7 @@ async function prepareDatabaseForUse(host, log, errorManager, showingNotice = fa
|
|
|
57
57
|
function usePrepareDatabaseForUse(host) {
|
|
58
58
|
const log = createInstanceLogFunction("SF:prepareDatabaseForUse", host.services.API);
|
|
59
59
|
const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events);
|
|
60
|
-
const initialiseDatabaseHandler = async (showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false) => {
|
|
60
|
+
const initialiseDatabaseHandler = async (showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false, scanOptions) => {
|
|
61
61
|
return await prepareDatabaseForUse(
|
|
62
62
|
host,
|
|
63
63
|
log,
|
|
@@ -65,7 +65,8 @@ function usePrepareDatabaseForUse(host) {
|
|
|
65
65
|
showingNotice,
|
|
66
66
|
reopenDatabase,
|
|
67
67
|
ignoreSuspending,
|
|
68
|
-
continueOnFileFailure
|
|
68
|
+
continueOnFileFailure,
|
|
69
|
+
scanOptions
|
|
69
70
|
);
|
|
70
71
|
};
|
|
71
72
|
host.services.databaseEvents.initialiseDatabase.addHandler(initialiseDatabaseHandler);
|