@vrtmrz/livesync-commonlib 0.3.2 → 0.4.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 +17 -0
- package/dist/managers/EntryManager/EntryManagerImpls.js +1 -1
- package/dist/p2p/P2PService.d.ts +10 -1
- package/dist/p2p/P2PService.js +25 -2
- package/dist/p2p/index.d.ts +4 -1
- package/dist/p2p/index.js +2 -0
- package/dist/p2p/p2pDiagnostics.d.ts +35 -0
- package/dist/p2p/p2pDiagnostics.js +76 -0
- package/dist/pouchdb/ReplicatorShim.batch.d.ts +30 -0
- package/dist/pouchdb/ReplicatorShim.batch.js +62 -0
- package/dist/pouchdb/ReplicatorShim.d.ts +14 -1
- package/dist/pouchdb/ReplicatorShim.js +64 -80
- package/dist/replication/trystero/P2PLogCollector.d.ts +12 -0
- package/dist/replication/trystero/P2PLogCollector.js +107 -72
- package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +4 -0
- package/dist/replication/trystero/P2PRoomSessionOwner.js +39 -2
- package/dist/replication/trystero/TrysteroReplicator.d.ts +21 -24
- package/dist/replication/trystero/TrysteroReplicator.js +191 -69
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +11 -0
- package/dist/replication/trystero/p2pReplicationDiagnostics.d.ts +9 -0
- package/dist/replication/trystero/p2pReplicationDiagnostics.js +51 -0
- package/dist/replication/trystero/p2pReplicationProgress.d.ts +15 -0
- package/dist/replication/trystero/p2pReplicationProgress.js +37 -0
- package/dist/replication/trystero/types.d.ts +3 -0
- package/dist/services/base/APIService.d.ts +10 -0
- package/dist/services/base/APIService.js +13 -0
- package/dist/services/base/IService.d.ts +2 -0
- package/docs/journal-receive-recovery.md +7 -4
- package/docs/p2p-transport-lifecycle.md +117 -10
- package/docs/releasing.md +6 -0
- package/package.json +2 -2
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import { reportP2PDiagnostic } from "../../p2p/p2pDiagnostics.js";
|
|
2
|
+
import {
|
|
3
|
+
authenticationResultDiagnostic,
|
|
4
|
+
authenticationFailureDiagnostic,
|
|
5
|
+
pullResultDiagnostic,
|
|
6
|
+
pullFailureDiagnostic,
|
|
7
|
+
notificationFailureDiagnostic
|
|
8
|
+
} from "./p2pReplicationDiagnostics.js";
|
|
9
|
+
import {
|
|
10
|
+
projectDirectionProgress,
|
|
11
|
+
projectAcknowledgedAttempt
|
|
12
|
+
} from "./p2pReplicationProgress.js";
|
|
1
13
|
import { parsePeerPatterns } from "./peerPatterns.js";
|
|
2
14
|
import {
|
|
3
15
|
TweakValuesShouldMatchedTemplate,
|
|
@@ -42,6 +54,7 @@ async function encrypt(data, passphrase) {
|
|
|
42
54
|
async function decrypt(encryptedData, passphrase) {
|
|
43
55
|
return await decryptWithEphemeralSalt(encryptedData, passphrase);
|
|
44
56
|
}
|
|
57
|
+
let nextProgressAttempt = 0;
|
|
45
58
|
const P2P_REPLICATION_COMPLETED = Object.freeze({ status: "completed", ok: true });
|
|
46
59
|
const P2P_REPLICATION_CANCELLED = Object.freeze({ status: "cancelled" });
|
|
47
60
|
function p2pReplicationFailed(error) {
|
|
@@ -59,11 +72,17 @@ class TrysteroReplicator {
|
|
|
59
72
|
_env;
|
|
60
73
|
finiteOperationOwner;
|
|
61
74
|
disposed = false;
|
|
75
|
+
acknowledgedAttempts = /* @__PURE__ */ new Map();
|
|
62
76
|
fallbackAutomationCoordinator = new P2PAutomationCoordinator();
|
|
63
77
|
server;
|
|
64
78
|
replicationStatus() {
|
|
65
79
|
return {};
|
|
66
80
|
}
|
|
81
|
+
reportDiagnostic(input) {
|
|
82
|
+
if (!this.disposed) {
|
|
83
|
+
reportP2PDiagnostic(this._env.reportDiagnostic, input);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
67
86
|
get settings() {
|
|
68
87
|
return this._env.settings;
|
|
69
88
|
}
|
|
@@ -137,6 +156,7 @@ class TrysteroReplicator {
|
|
|
137
156
|
async close() {
|
|
138
157
|
this.requestStatus();
|
|
139
158
|
await this.server?.shutdown();
|
|
159
|
+
this.acknowledgedAttempts.clear();
|
|
140
160
|
this._replicateFromPeers.clear();
|
|
141
161
|
this._replicateToPeers.clear();
|
|
142
162
|
this._watchingPeers.clear();
|
|
@@ -184,20 +204,46 @@ class TrysteroReplicator {
|
|
|
184
204
|
const peerName = peer.name;
|
|
185
205
|
const shouldAutoSync = this.autoSyncPeers.some((e) => e.test(peerName));
|
|
186
206
|
const shouldAutoWatch = this.autoWatchPeers.some((e) => e.test(peerName));
|
|
187
|
-
if (!shouldAutoSync && !shouldAutoWatch)
|
|
207
|
+
if (!shouldAutoSync && !shouldAutoWatch) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
188
210
|
const acceptance = await this.server?.evaluatePeerAcceptance(peer.peerId);
|
|
189
|
-
if (acceptance !== "accepted")
|
|
211
|
+
if (acceptance !== "accepted") {
|
|
212
|
+
if (shouldAutoWatch) {
|
|
213
|
+
this.reportDiagnostic({
|
|
214
|
+
category: "sync",
|
|
215
|
+
code: "watch-skipped",
|
|
216
|
+
peerId: peer.peerId,
|
|
217
|
+
reason: "not-accepted"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
190
222
|
if (shouldAutoSync) {
|
|
191
223
|
await this.automationCoordinator.runBaseline(
|
|
192
224
|
peerName,
|
|
193
225
|
() => this.runFiniteReplicationActivity(() => this.sync(peer.peerId))
|
|
194
226
|
);
|
|
195
227
|
}
|
|
196
|
-
if (shouldAutoWatch
|
|
197
|
-
this.
|
|
228
|
+
if (shouldAutoWatch) {
|
|
229
|
+
await this.watchBroadcastingPeer(peer.peerId);
|
|
198
230
|
}
|
|
199
231
|
}
|
|
232
|
+
async watchBroadcastingPeer(peerId) {
|
|
233
|
+
const broadcasting = await this.getRemoteIsBroadcasting(peerId);
|
|
234
|
+
if (broadcasting === true) {
|
|
235
|
+
this.watchPeer(peerId);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.reportDiagnostic({
|
|
239
|
+
category: "sync",
|
|
240
|
+
code: "watch-skipped",
|
|
241
|
+
peerId,
|
|
242
|
+
reason: broadcasting === false ? "peer-not-broadcasting" : "peer-broadcast-unobserved"
|
|
243
|
+
});
|
|
244
|
+
}
|
|
200
245
|
onPeerLeaved(peerId) {
|
|
246
|
+
this.acknowledgedAttempts.delete(peerId);
|
|
201
247
|
void this.unwatchPeer(peerId);
|
|
202
248
|
}
|
|
203
249
|
_onSetup = false;
|
|
@@ -257,6 +303,12 @@ class TrysteroReplicator {
|
|
|
257
303
|
},
|
|
258
304
|
onProgress: async (fromPeerId) => {
|
|
259
305
|
if (this._onSetup) {
|
|
306
|
+
this.reportDiagnostic({
|
|
307
|
+
category: "sync",
|
|
308
|
+
code: "change-notification-ignored",
|
|
309
|
+
peerId: fromPeerId,
|
|
310
|
+
reason: "setup-in-progress"
|
|
311
|
+
});
|
|
260
312
|
return { error: toP2PWireError(new Error("The setup is in progress")) };
|
|
261
313
|
}
|
|
262
314
|
await this.onUpdateDatabase(fromPeerId);
|
|
@@ -300,9 +352,9 @@ class TrysteroReplicator {
|
|
|
300
352
|
}
|
|
301
353
|
return encrypt(JSON.stringify(setting), passphrase.trim());
|
|
302
354
|
},
|
|
303
|
-
onProgressAcknowledged: async (fromPeerId, info) => {
|
|
355
|
+
onProgressAcknowledged: async (fromPeerId, info, acknowledgement) => {
|
|
304
356
|
try {
|
|
305
|
-
await Promise.resolve(this.onProgressAcknowledged(fromPeerId, info));
|
|
357
|
+
await Promise.resolve(this.onProgressAcknowledged(fromPeerId, info, acknowledgement));
|
|
306
358
|
} catch (e) {
|
|
307
359
|
Logger("Error while acknowledging the progress", LOG_LEVEL_VERBOSE);
|
|
308
360
|
Logger(e, LOG_LEVEL_VERBOSE);
|
|
@@ -341,8 +393,10 @@ class TrysteroReplicator {
|
|
|
341
393
|
2e4,
|
|
342
394
|
signal
|
|
343
395
|
);
|
|
396
|
+
this.reportDiagnostic(authenticationResultDiagnostic(peerId, r));
|
|
344
397
|
return r;
|
|
345
398
|
} catch (e) {
|
|
399
|
+
this.reportDiagnostic(authenticationFailureDiagnostic(peerId, e, signal));
|
|
346
400
|
Logger("Error while requesting authentication", LOG_LEVEL_VERBOSE);
|
|
347
401
|
Logger(e, LOG_LEVEL_VERBOSE);
|
|
348
402
|
return false;
|
|
@@ -402,9 +456,12 @@ class TrysteroReplicator {
|
|
|
402
456
|
changes;
|
|
403
457
|
_isBroadcasting = false;
|
|
404
458
|
disableBroadcastChanges() {
|
|
459
|
+
const wasBroadcasting = this._isBroadcasting;
|
|
405
460
|
this.changes?.cancel();
|
|
461
|
+
const needsDiagnostic = wasBroadcasting && this._isBroadcasting;
|
|
406
462
|
this._isBroadcasting = false;
|
|
407
463
|
this.dispatchStatus();
|
|
464
|
+
if (needsDiagnostic) this.reportDiagnostic({ category: "sync", code: "broadcast-disabled" });
|
|
408
465
|
}
|
|
409
466
|
enableBroadcastChanges() {
|
|
410
467
|
if (this._isBroadcasting) return;
|
|
@@ -429,6 +486,7 @@ class TrysteroReplicator {
|
|
|
429
486
|
await this.notifyProgress();
|
|
430
487
|
});
|
|
431
488
|
const closeChanges = (reason) => {
|
|
489
|
+
const wasBroadcasting = this._isBroadcasting;
|
|
432
490
|
if (reason) {
|
|
433
491
|
if (reason instanceof Error) {
|
|
434
492
|
Logger(`Error while broadcasting the changes`, LOG_LEVEL_INFO);
|
|
@@ -443,9 +501,16 @@ class TrysteroReplicator {
|
|
|
443
501
|
this.changes = void 0;
|
|
444
502
|
this._isBroadcasting = false;
|
|
445
503
|
this.dispatchStatus();
|
|
504
|
+
if (wasBroadcasting)
|
|
505
|
+
this.reportDiagnostic({
|
|
506
|
+
category: "sync",
|
|
507
|
+
code: "broadcast-disabled",
|
|
508
|
+
...reason instanceof Error ? { reason: "replication-error" } : {}
|
|
509
|
+
});
|
|
446
510
|
};
|
|
447
511
|
void this.changes.on("error", closeChanges);
|
|
448
512
|
void this.changes.on("complete", closeChanges);
|
|
513
|
+
this.reportDiagnostic({ category: "sync", code: "broadcast-enabled" });
|
|
449
514
|
fireAndForget(async () => await this.notifyProgress());
|
|
450
515
|
}
|
|
451
516
|
/** Apply automatic broadcast policy without restarting this room. */
|
|
@@ -494,55 +559,51 @@ class TrysteroReplicator {
|
|
|
494
559
|
this.onReplicationProgress(peerId, info);
|
|
495
560
|
}
|
|
496
561
|
onReplicationProgress(peerId, info) {
|
|
497
|
-
|
|
498
|
-
|
|
562
|
+
if (this.disposed) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
const name = this.server?._knownAdvertisements?.get(peerId)?.name || peerId;
|
|
566
|
+
this._env.events.emitEvent(EVENT_P2P_REPLICATOR_PROGRESS, {
|
|
499
567
|
peerId,
|
|
500
568
|
peerName: name,
|
|
501
|
-
fetching:
|
|
502
|
-
|
|
503
|
-
current: 0,
|
|
504
|
-
isActive: false
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
if (info) {
|
|
508
|
-
stat.fetching = {
|
|
509
|
-
max: info.maxSeqInBatch,
|
|
510
|
-
current: info.lastSeq,
|
|
511
|
-
isActive: true
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
this._env.events.emitEvent(EVENT_P2P_REPLICATOR_PROGRESS, stat);
|
|
569
|
+
fetching: projectDirectionProgress(info)
|
|
570
|
+
});
|
|
515
571
|
return true;
|
|
516
572
|
}
|
|
517
|
-
onProgressAcknowledged(peerId, info) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
isActive: false
|
|
573
|
+
onProgressAcknowledged(peerId, info, acknowledgement) {
|
|
574
|
+
if (this.disposed) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
if (acknowledgement) {
|
|
578
|
+
const next = projectAcknowledgedAttempt(this.acknowledgedAttempts.get(peerId), info, acknowledgement);
|
|
579
|
+
if (!next) {
|
|
580
|
+
return false;
|
|
526
581
|
}
|
|
527
|
-
|
|
528
|
-
if (info) {
|
|
529
|
-
ack.sending = {
|
|
530
|
-
max: info.maxSeqInBatch,
|
|
531
|
-
current: info.lastSeq,
|
|
532
|
-
isActive: true
|
|
533
|
-
};
|
|
582
|
+
this.acknowledgedAttempts.set(peerId, next);
|
|
534
583
|
}
|
|
535
|
-
this.
|
|
584
|
+
const name = this.server?._knownAdvertisements?.get(peerId)?.name || peerId;
|
|
585
|
+
const sending = projectDirectionProgress(info ?? acknowledgement?.finalProgress);
|
|
586
|
+
sending.isActive = info != null;
|
|
587
|
+
this._env.events.emitEvent(EVENT_P2P_REPLICATOR_PROGRESS, {
|
|
588
|
+
peerId,
|
|
589
|
+
peerName: name,
|
|
590
|
+
sending
|
|
591
|
+
});
|
|
536
592
|
return true;
|
|
537
593
|
}
|
|
538
|
-
// Sending the progress to the remote peer
|
|
539
|
-
acknowledgeProgress(remotePeerId, info) {
|
|
540
|
-
if (!this.server) return;
|
|
541
|
-
|
|
542
|
-
|
|
594
|
+
// Sending the progress to the remote peer.
|
|
595
|
+
acknowledgeProgress(remotePeerId, info, acknowledgement) {
|
|
596
|
+
if (!this.server || this.disposed) return;
|
|
597
|
+
try {
|
|
598
|
+
const connection = this.server.getConnection(remotePeerId);
|
|
599
|
+
void connection.invokeRemoteFunction("onProgressAcknowledged", [this.server.serverPeerId, info, acknowledgement], 500).catch((error) => {
|
|
600
|
+
Logger("Error while acknowledging the progress", LOG_LEVEL_VERBOSE);
|
|
601
|
+
Logger(error, LOG_LEVEL_VERBOSE);
|
|
602
|
+
});
|
|
603
|
+
} catch (error) {
|
|
543
604
|
Logger("Error while acknowledging the progress", LOG_LEVEL_VERBOSE);
|
|
544
|
-
Logger(
|
|
545
|
-
}
|
|
605
|
+
Logger(error, LOG_LEVEL_VERBOSE);
|
|
606
|
+
}
|
|
546
607
|
}
|
|
547
608
|
async replicateFrom(remotePeer, showNotice = false, fromStart = false, skipOrdinaryReplicationPolicy = false, callerSignal) {
|
|
548
609
|
return await this.runSessionFiniteOperation(
|
|
@@ -557,6 +618,23 @@ class TrysteroReplicator {
|
|
|
557
618
|
);
|
|
558
619
|
}
|
|
559
620
|
async replicateFromWithinSession(remotePeer, showNotice, fromStart, skipOrdinaryReplicationPolicy, signal) {
|
|
621
|
+
this.reportDiagnostic({ category: "sync", code: "pull-started", peerId: remotePeer });
|
|
622
|
+
try {
|
|
623
|
+
const result = await this.pullFromWithinSession(
|
|
624
|
+
remotePeer,
|
|
625
|
+
showNotice,
|
|
626
|
+
fromStart,
|
|
627
|
+
skipOrdinaryReplicationPolicy,
|
|
628
|
+
signal
|
|
629
|
+
);
|
|
630
|
+
this.reportDiagnostic(pullResultDiagnostic(remotePeer, result.status));
|
|
631
|
+
return result;
|
|
632
|
+
} catch (error) {
|
|
633
|
+
this.reportDiagnostic(pullFailureDiagnostic(remotePeer, signal?.aborted ?? false));
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async pullFromWithinSession(remotePeer, showNotice, fromStart, skipOrdinaryReplicationPolicy, signal) {
|
|
560
638
|
if (!skipOrdinaryReplicationPolicy && !await this.canStartOrdinaryReplication(showNotice)) {
|
|
561
639
|
return p2pReplicationFailed(new Error("Replication is not ready"));
|
|
562
640
|
}
|
|
@@ -571,14 +649,28 @@ class TrysteroReplicator {
|
|
|
571
649
|
}
|
|
572
650
|
this._replicateFromPeers.add(remotePeer);
|
|
573
651
|
this.dispatchStatus();
|
|
652
|
+
const acknowledgement = { attempt: ++nextProgressAttempt };
|
|
653
|
+
let latestProgress = {
|
|
654
|
+
lastSeq: 0,
|
|
655
|
+
maxSeqInBatch: 0,
|
|
656
|
+
statistics: { documents: 0, checkedChanges: 0 }
|
|
657
|
+
};
|
|
658
|
+
this.dispatchReplicationProgress(remotePeer, latestProgress);
|
|
659
|
+
this.acknowledgeProgress(remotePeer, latestProgress, acknowledgement);
|
|
574
660
|
try {
|
|
575
|
-
return await this.transferFromPeer(remotePeer, fromStart, logLevel, signal)
|
|
661
|
+
return await this.transferFromPeer(remotePeer, fromStart, logLevel, signal, (info) => {
|
|
662
|
+
latestProgress = info;
|
|
663
|
+
this.dispatchReplicationProgress(remotePeer, info);
|
|
664
|
+
this.acknowledgeProgress(remotePeer, info, acknowledgement);
|
|
665
|
+
});
|
|
576
666
|
} catch (e) {
|
|
577
667
|
if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
|
|
578
668
|
Logger("Error while P2P replicating", logLevel, "p2p-replicator");
|
|
579
669
|
Logger(e, LOG_LEVEL_VERBOSE);
|
|
580
670
|
return p2pReplicationFailed(e);
|
|
581
671
|
} finally {
|
|
672
|
+
this.dispatchReplicationProgress(remotePeer);
|
|
673
|
+
this.acknowledgeProgress(remotePeer, void 0, { ...acknowledgement, finalProgress: latestProgress });
|
|
582
674
|
this._replicateFromPeers.delete(remotePeer);
|
|
583
675
|
this.dispatchStatus();
|
|
584
676
|
}
|
|
@@ -605,7 +697,7 @@ class TrysteroReplicator {
|
|
|
605
697
|
}
|
|
606
698
|
return void 0;
|
|
607
699
|
}
|
|
608
|
-
async transferFromPeer(remotePeer, fromStart, logLevel, signal) {
|
|
700
|
+
async transferFromPeer(remotePeer, fromStart, logLevel, signal, onStatistics) {
|
|
609
701
|
if (!this.server) {
|
|
610
702
|
throw new Error("Server is not available");
|
|
611
703
|
}
|
|
@@ -640,8 +732,6 @@ class TrysteroReplicator {
|
|
|
640
732
|
remoteDB,
|
|
641
733
|
async (docs, info) => {
|
|
642
734
|
await this._env.processReplicatedDocs(docs);
|
|
643
|
-
void this.dispatchReplicationProgress(remotePeer, info);
|
|
644
|
-
void this.acknowledgeProgress(remotePeer, info);
|
|
645
735
|
void this.notifyProgress(remotePeer);
|
|
646
736
|
Logger(
|
|
647
737
|
`P2P Replication from ${remotePeer}
|
|
@@ -650,10 +740,14 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
|
|
|
650
740
|
"p2p-replicator"
|
|
651
741
|
);
|
|
652
742
|
},
|
|
653
|
-
{
|
|
743
|
+
{
|
|
744
|
+
live: false,
|
|
745
|
+
rewind: fromStart,
|
|
746
|
+
signal,
|
|
747
|
+
onStatistics
|
|
748
|
+
}
|
|
654
749
|
);
|
|
655
750
|
Logger(`P2P replicateFrom replicateShim returned for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
|
|
656
|
-
void this.acknowledgeProgress(remotePeer, void 0);
|
|
657
751
|
if (outcome.status === "cancelled") {
|
|
658
752
|
Logger(`P2P Replication from ${remotePeer} has been cancelled`, logLevel, "p2p-replicator");
|
|
659
753
|
return P2P_REPLICATION_CANCELLED;
|
|
@@ -662,28 +756,43 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
|
|
|
662
756
|
return P2P_REPLICATION_COMPLETED;
|
|
663
757
|
}
|
|
664
758
|
notifyProgress(excludePeerId) {
|
|
665
|
-
if (!this._isBroadcasting)
|
|
666
|
-
|
|
759
|
+
if (!this._isBroadcasting) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (!this.server) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
667
765
|
for (const peer of this.server.knownAdvertisements) {
|
|
668
766
|
const peerId = peer.peerId;
|
|
669
|
-
if (peerId === excludePeerId)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
try {
|
|
674
|
-
const ret = await this.server?.getConnection(peerId).invokeRemoteFunction("onProgress", [this.server?.serverPeerId], 0);
|
|
675
|
-
return ret;
|
|
676
|
-
} catch (e) {
|
|
677
|
-
Logger(`Error while notifying progress to ${peerId}`, LOG_LEVEL_VERBOSE);
|
|
678
|
-
Logger(e, LOG_LEVEL_VERBOSE);
|
|
679
|
-
}
|
|
680
|
-
} else {
|
|
681
|
-
Logger(`Peer ${peerId} is not acceptable to notify progress`, LOG_LEVEL_VERBOSE);
|
|
682
|
-
}
|
|
683
|
-
});
|
|
767
|
+
if (peerId === excludePeerId) {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
void serialized(`notifyProgress-${peerId}`, () => this.notifyPeerProgress(peerId));
|
|
684
771
|
}
|
|
685
772
|
return Promise.resolve();
|
|
686
773
|
}
|
|
774
|
+
async notifyPeerProgress(peerId) {
|
|
775
|
+
const isAcceptable = await this.server?.isAcceptablePeer(peerId);
|
|
776
|
+
if (!isAcceptable) {
|
|
777
|
+
this.reportDiagnostic({
|
|
778
|
+
category: "sync",
|
|
779
|
+
code: "change-notification-ignored",
|
|
780
|
+
peerId,
|
|
781
|
+
reason: "not-accepted"
|
|
782
|
+
});
|
|
783
|
+
Logger(`Peer ${peerId} is not acceptable to notify progress`, LOG_LEVEL_VERBOSE);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
const result = await this.server?.getConnection(peerId).invokeRemoteFunction("onProgress", [this.server?.serverPeerId], 0);
|
|
788
|
+
this.reportDiagnostic({ category: "sync", code: "change-notification-sent", peerId });
|
|
789
|
+
return result;
|
|
790
|
+
} catch (error) {
|
|
791
|
+
this.reportDiagnostic(notificationFailureDiagnostic(peerId, error));
|
|
792
|
+
Logger(`Error while notifying progress to ${peerId}`, LOG_LEVEL_VERBOSE);
|
|
793
|
+
Logger(error, LOG_LEVEL_VERBOSE);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
687
796
|
async requestBroadcastChanges(peerId) {
|
|
688
797
|
return await this.server?.getConnection(peerId).invokeRemoteFunction("requestBroadcasting", [this.server.serverPeerId], 0);
|
|
689
798
|
}
|
|
@@ -697,20 +806,33 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
|
|
|
697
806
|
}
|
|
698
807
|
_watchingPeers = /* @__PURE__ */ new Set();
|
|
699
808
|
watchPeer(peerId) {
|
|
809
|
+
if (!this._watchingPeers.has(peerId)) {
|
|
810
|
+
this.reportDiagnostic({ category: "sync", code: "watch-enabled", peerId });
|
|
811
|
+
}
|
|
700
812
|
this._watchingPeers.add(peerId);
|
|
701
813
|
this.dispatchStatus();
|
|
702
814
|
}
|
|
703
815
|
unwatchPeer(peerId) {
|
|
816
|
+
if (this._watchingPeers.has(peerId)) {
|
|
817
|
+
this.reportDiagnostic({ category: "sync", code: "watch-disabled", peerId });
|
|
818
|
+
}
|
|
704
819
|
this._watchingPeers.delete(peerId);
|
|
705
820
|
this.dispatchStatus();
|
|
706
821
|
}
|
|
707
822
|
async onUpdateDatabase(fromPeerId) {
|
|
708
823
|
if (this._watchingPeers.has(fromPeerId)) {
|
|
824
|
+
this.reportDiagnostic({ category: "sync", code: "change-notification-received", peerId: fromPeerId });
|
|
709
825
|
Logger(`Progress notification from ${fromPeerId}`, LOG_LEVEL_VERBOSE);
|
|
710
826
|
return await serialized(`onProgress-${fromPeerId}`, async () => {
|
|
711
827
|
return await this.runFiniteReplicationActivity(() => this.replicateFrom(fromPeerId));
|
|
712
828
|
});
|
|
713
829
|
}
|
|
830
|
+
this.reportDiagnostic({
|
|
831
|
+
category: "sync",
|
|
832
|
+
code: "change-notification-ignored",
|
|
833
|
+
peerId: fromPeerId,
|
|
834
|
+
reason: "not-watching"
|
|
835
|
+
});
|
|
714
836
|
return false;
|
|
715
837
|
}
|
|
716
838
|
async getRemoteConfig(peerId) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reportP2PDiagnostic } from "../../p2p/p2pDiagnostics.js";
|
|
1
2
|
import { parsePeerPatterns } from "./peerPatterns.js";
|
|
2
3
|
import { selfId, joinRoom } from "@trystero-p2p/nostr";
|
|
3
4
|
import { AutoAccepting, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE } from "../../common/types.js";
|
|
@@ -225,7 +226,10 @@ class TrysteroReplicatorP2PServer {
|
|
|
225
226
|
if (peerId === this.serverPeerId) return;
|
|
226
227
|
if (data.peerId === this.serverPeerId) return;
|
|
227
228
|
if (data.peerId !== peerId) return;
|
|
229
|
+
const isNewPeer = !this._knownAdvertisements.has(peerId);
|
|
228
230
|
this._knownAdvertisements.set(peerId, data);
|
|
231
|
+
if (isNewPeer)
|
|
232
|
+
reportP2PDiagnostic(this._env.reportDiagnostic, { category: "peer", code: "peer-advertised", peerId });
|
|
229
233
|
void this.dispatchConnectionStatus();
|
|
230
234
|
void Promise.resolve().then(() => this._sessionPeerHandlers?.onAdvertisement(data)).catch((error) => Logger(error, LOG_LEVEL_VERBOSE));
|
|
231
235
|
void this._env.events.emitEvent(EVENT_ADVERTISEMENT_RECEIVED, data);
|
|
@@ -368,6 +372,7 @@ You can chose as follows:
|
|
|
368
372
|
_onPeerLeave(peerId) {
|
|
369
373
|
if (this._hostLifetimeEnded) return;
|
|
370
374
|
Logger(`Peer left: ${peerId}`, LOG_LEVEL_VERBOSE);
|
|
375
|
+
reportP2PDiagnostic(this._env.reportDiagnostic, { category: "peer", code: "peer-left", peerId });
|
|
371
376
|
this._knownAdvertisements.delete(peerId);
|
|
372
377
|
void Promise.resolve().then(() => this._sessionPeerHandlers?.onPeerLeft(peerId)).catch((error) => Logger(error, LOG_LEVEL_VERBOSE));
|
|
373
378
|
void this._env.events.emitEvent(EVENT_DEVICE_LEAVED, peerId);
|
|
@@ -459,6 +464,12 @@ You can chose as follows:
|
|
|
459
464
|
const room = joinRoom(options, roomId, {
|
|
460
465
|
handshakeTimeoutMs: 3e4,
|
|
461
466
|
onJoinError: (error) => {
|
|
467
|
+
if (!this._hostLifetimeEnded)
|
|
468
|
+
reportP2PDiagnostic(this._env.reportDiagnostic, {
|
|
469
|
+
category: "relay",
|
|
470
|
+
code: "room-join-error",
|
|
471
|
+
reason: "transport-error"
|
|
472
|
+
});
|
|
462
473
|
Logger("Some peer Failed to join Trystero room");
|
|
463
474
|
this.logTransportError(error);
|
|
464
475
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type P2PDiagnosticInput } from "../../p2p/p2pDiagnostics.js";
|
|
2
|
+
import type { P2PReplicationResult } from "./TrysteroReplicator.js";
|
|
3
|
+
/** Describe an observed response without coercing an unavailable response to refusal. */
|
|
4
|
+
export declare function authenticationResultDiagnostic(peerId: string, response: unknown): P2PDiagnosticInput;
|
|
5
|
+
export declare function authenticationFailureDiagnostic(peerId: string, error: unknown, signal?: AbortSignal): P2PDiagnosticInput;
|
|
6
|
+
/** Classify the result without changing the operation's result object or error. */
|
|
7
|
+
export declare function pullResultDiagnostic(peerId: string, status: P2PReplicationResult["status"]): P2PDiagnosticInput;
|
|
8
|
+
export declare function pullFailureDiagnostic(peerId: string, cancelled: boolean): P2PDiagnosticInput;
|
|
9
|
+
export declare function notificationFailureDiagnostic(peerId: string, error: unknown): P2PDiagnosticInput;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { classifyP2PRpcFailure } from "../../p2p/p2pDiagnostics.js";
|
|
2
|
+
const authenticationFailureCodes = {
|
|
3
|
+
"rpc-timeout": "auth-timeout",
|
|
4
|
+
"rpc-cancelled": "auth-cancelled",
|
|
5
|
+
"rpc-error": "auth-error"
|
|
6
|
+
};
|
|
7
|
+
const notificationFailureCodes = {
|
|
8
|
+
"rpc-timeout": "change-notification-timeout",
|
|
9
|
+
"rpc-cancelled": "change-notification-cancelled",
|
|
10
|
+
"rpc-error": "change-notification-error"
|
|
11
|
+
};
|
|
12
|
+
function authenticationResultDiagnostic(peerId, response) {
|
|
13
|
+
if (response === true) {
|
|
14
|
+
return { category: "peer", code: "auth-accepted", peerId };
|
|
15
|
+
}
|
|
16
|
+
if (response === false) {
|
|
17
|
+
return { category: "peer", code: "auth-refused", reason: "authentication-refused", peerId };
|
|
18
|
+
}
|
|
19
|
+
return { category: "peer", code: "auth-error", reason: "authentication-unobserved", peerId };
|
|
20
|
+
}
|
|
21
|
+
function authenticationFailureDiagnostic(peerId, error, signal) {
|
|
22
|
+
const reason = classifyP2PRpcFailure(error, signal);
|
|
23
|
+
return { category: "peer", code: authenticationFailureCodes[reason], reason, peerId };
|
|
24
|
+
}
|
|
25
|
+
function pullResultDiagnostic(peerId, status) {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case "completed":
|
|
28
|
+
return { category: "sync", code: "pull-completed", peerId };
|
|
29
|
+
case "cancelled":
|
|
30
|
+
return { category: "sync", code: "pull-cancelled", reason: "operation-cancelled", peerId };
|
|
31
|
+
default:
|
|
32
|
+
return { category: "sync", code: "pull-failed", reason: "replication-error", peerId };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function pullFailureDiagnostic(peerId, cancelled) {
|
|
36
|
+
if (cancelled) {
|
|
37
|
+
return pullResultDiagnostic(peerId, "cancelled");
|
|
38
|
+
}
|
|
39
|
+
return pullResultDiagnostic(peerId, "failed");
|
|
40
|
+
}
|
|
41
|
+
function notificationFailureDiagnostic(peerId, error) {
|
|
42
|
+
const reason = classifyP2PRpcFailure(error);
|
|
43
|
+
return { category: "sync", code: notificationFailureCodes[reason], reason, peerId };
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
authenticationFailureDiagnostic,
|
|
47
|
+
authenticationResultDiagnostic,
|
|
48
|
+
notificationFailureDiagnostic,
|
|
49
|
+
pullFailureDiagnostic,
|
|
50
|
+
pullResultDiagnostic
|
|
51
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProgressInfo } from "../../pouchdb/ReplicatorShim.js";
|
|
2
|
+
import type { P2PReplicationDirectionProgress } from "./TrysteroReplicator.js";
|
|
3
|
+
/** Transient attempt correlation; no replication checkpoint is stored here. */
|
|
4
|
+
export type ProgressAcknowledgement = {
|
|
5
|
+
attempt: number;
|
|
6
|
+
finalProgress?: ProgressInfo;
|
|
7
|
+
};
|
|
8
|
+
export type AcknowledgedProgressAttempt = {
|
|
9
|
+
attempt: number;
|
|
10
|
+
isActive: boolean;
|
|
11
|
+
checkedChanges: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function projectDirectionProgress(info?: ProgressInfo | null): P2PReplicationDirectionProgress;
|
|
14
|
+
/** Return the next attempt snapshot, or reject stale, reordered, or already-ended progress. */
|
|
15
|
+
export declare function projectAcknowledgedAttempt(previous: AcknowledgedProgressAttempt | undefined, info: ProgressInfo | null | undefined, acknowledgement: ProgressAcknowledgement): AcknowledgedProgressAttempt | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function projectDirectionProgress(info) {
|
|
2
|
+
return {
|
|
3
|
+
max: info?.maxSeqInBatch ?? 0,
|
|
4
|
+
current: info?.lastSeq ?? 0,
|
|
5
|
+
isActive: info != null,
|
|
6
|
+
...info?.statistics ? { statistics: { ...info.statistics } } : {}
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function projectAcknowledgedAttempt(previous, info, acknowledgement) {
|
|
10
|
+
if (!previous) {
|
|
11
|
+
return {
|
|
12
|
+
attempt: acknowledgement.attempt,
|
|
13
|
+
isActive: info != null,
|
|
14
|
+
checkedChanges: info?.statistics?.checkedChanges ?? 0
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (acknowledgement.attempt < previous.attempt) {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
if (acknowledgement.attempt === previous.attempt) {
|
|
21
|
+
if (!previous.isActive) {
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
if ((info?.statistics?.checkedChanges ?? previous.checkedChanges) < previous.checkedChanges) {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
attempt: acknowledgement.attempt,
|
|
30
|
+
isActive: info != null,
|
|
31
|
+
checkedChanges: info?.statistics?.checkedChanges ?? previous.checkedChanges
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
projectAcknowledgedAttempt,
|
|
36
|
+
projectDirectionProgress
|
|
37
|
+
};
|
|
@@ -5,6 +5,7 @@ import type { Confirm } from "../../interfaces/Confirm.js";
|
|
|
5
5
|
import type { AsyncActivityRunner } from "../../interfaces/AsyncActivityRunner.js";
|
|
6
6
|
import type { LiveSyncEventHub } from "../../hub/hub.js";
|
|
7
7
|
import type { MessageTranslator } from "../../services/base/MessageTranslator.js";
|
|
8
|
+
import type { P2PDiagnosticReporter } from "../../p2p/p2pDiagnostics.js";
|
|
8
9
|
import type { P2PAutomationCoordinator } from "./P2PAutomationCoordinator.js";
|
|
9
10
|
export declare const DIRECTION_REQUEST = "request";
|
|
10
11
|
export type DIRECTION_REQUEST = typeof DIRECTION_REQUEST;
|
|
@@ -78,6 +79,8 @@ export interface ReplicatorHost {
|
|
|
78
79
|
confirm: Confirm;
|
|
79
80
|
}
|
|
80
81
|
export interface ReplicatorHostEnv extends ReplicatorHost {
|
|
82
|
+
/** Observation port fenced by the owning room session; it cannot control transport work. */
|
|
83
|
+
reportDiagnostic?: P2PDiagnosticReporter;
|
|
81
84
|
/** Event channel owned by the containing Commonlib service composition. */
|
|
82
85
|
events: LiveSyncEventHub;
|
|
83
86
|
/** Message translation owned by the containing Commonlib service composition. */
|
|
@@ -72,6 +72,16 @@ export declare abstract class APIService<T extends ServiceContext = ServiceConte
|
|
|
72
72
|
* @param handler The handler function for the protocol.
|
|
73
73
|
*/
|
|
74
74
|
abstract registerProtocolHandler(action: string, handler: (params: Record<string, string>) => unknown): void;
|
|
75
|
+
/**
|
|
76
|
+
* Register synchronous cleanup for this host's normal terminal unload.
|
|
77
|
+
* The default uses the context-owned platform-unloaded event once per registration,
|
|
78
|
+
* after lifecycle handlers and local database closure. An unload interrupted before
|
|
79
|
+
* that event does not execute these callbacks.
|
|
80
|
+
* Native hosts may override this to retain their own unload timing. Callbacks are
|
|
81
|
+
* not awaited; resource retirement which must finish before database closure
|
|
82
|
+
* belongs to AppLifecycleService.onUnload instead.
|
|
83
|
+
*/
|
|
84
|
+
registerCleanup(callback: () => void): void;
|
|
75
85
|
/**
|
|
76
86
|
* Get the basic UI component for showing a confirmation dialog to the user.
|
|
77
87
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EVENT_PLATFORM_UNLOADED } from "../../events/coreEvents.js";
|
|
1
2
|
import { ServiceBase } from "./ServiceBase.js";
|
|
2
3
|
import { reactiveSource } from "octagonal-wheels/dataobject/reactive";
|
|
3
4
|
import { _fetch, compatGlobal } from "../../common/coreEnvFunctions.js";
|
|
@@ -9,6 +10,18 @@ class APIService extends ServiceBase {
|
|
|
9
10
|
showWindowOnRight(type) {
|
|
10
11
|
return this.showWindow(type);
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Register synchronous cleanup for this host's normal terminal unload.
|
|
15
|
+
* The default uses the context-owned platform-unloaded event once per registration,
|
|
16
|
+
* after lifecycle handlers and local database closure. An unload interrupted before
|
|
17
|
+
* that event does not execute these callbacks.
|
|
18
|
+
* Native hosts may override this to retain their own unload timing. Callbacks are
|
|
19
|
+
* not awaited; resource retirement which must finish before database closure
|
|
20
|
+
* belongs to AppLifecycleService.onUnload instead.
|
|
21
|
+
*/
|
|
22
|
+
registerCleanup(callback) {
|
|
23
|
+
this.context.events.onceEvent(EVENT_PLATFORM_UNLOADED, callback);
|
|
24
|
+
}
|
|
12
25
|
requestCount = reactiveSource(0);
|
|
13
26
|
responseCount = reactiveSource(0);
|
|
14
27
|
get isOnline() {
|
|
@@ -49,6 +49,8 @@ export interface IAPIService {
|
|
|
49
49
|
registerWindow<T>(type: string, factory: (leaf: T) => unknown): void;
|
|
50
50
|
addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => unknown): HTMLElement;
|
|
51
51
|
registerProtocolHandler(action: string, handler: (params: Record<string, string>) => unknown): void;
|
|
52
|
+
/** Registers synchronous cleanup at host terminal unload; callbacks are not awaited. */
|
|
53
|
+
registerCleanup(callback: () => void): void;
|
|
52
54
|
confirm: Confirm;
|
|
53
55
|
responseCount: ReactiveSource<number>;
|
|
54
56
|
requestCount: ReactiveSource<number>;
|