@vrtmrz/livesync-commonlib 0.3.1 → 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.
Files changed (51) hide show
  1. package/README.md +29 -2
  2. package/dist/managers/EntryManager/EntryManagerImpls.js +1 -1
  3. package/dist/p2p/P2PService.d.ts +10 -1
  4. package/dist/p2p/P2PService.js +25 -2
  5. package/dist/p2p/index.d.ts +4 -1
  6. package/dist/p2p/index.js +2 -0
  7. package/dist/p2p/p2pDiagnostics.d.ts +35 -0
  8. package/dist/p2p/p2pDiagnostics.js +76 -0
  9. package/dist/pouchdb/ReplicatorShim.batch.d.ts +30 -0
  10. package/dist/pouchdb/ReplicatorShim.batch.js +62 -0
  11. package/dist/pouchdb/ReplicatorShim.d.ts +14 -1
  12. package/dist/pouchdb/ReplicatorShim.js +64 -80
  13. package/dist/replication/SyncParamsHandler.d.ts +1 -0
  14. package/dist/replication/SyncParamsHandler.js +5 -1
  15. package/dist/replication/journal/JournalReceiveFailure.d.ts +7 -0
  16. package/dist/replication/journal/JournalReceiveFailure.js +16 -0
  17. package/dist/replication/journal/JournalSyncCore.d.ts +22 -3
  18. package/dist/replication/journal/JournalSyncCore.js +288 -166
  19. package/dist/replication/journal/JournalSyncTypes.d.ts +13 -0
  20. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +6 -1
  21. package/dist/replication/journal/LiveSyncJournalReplicator.js +31 -44
  22. package/dist/replication/journal/journalCompatibility.d.ts +9 -0
  23. package/dist/replication/journal/journalCompatibility.js +58 -0
  24. package/dist/replication/journal/journalReceive.d.ts +23 -0
  25. package/dist/replication/journal/journalReceive.js +44 -0
  26. package/dist/replication/journal/journalRecords.d.ts +10 -0
  27. package/dist/replication/journal/journalRecords.js +96 -0
  28. package/dist/replication/trystero/P2PLogCollector.d.ts +12 -0
  29. package/dist/replication/trystero/P2PLogCollector.js +107 -72
  30. package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +4 -0
  31. package/dist/replication/trystero/P2PRoomSessionOwner.js +39 -2
  32. package/dist/replication/trystero/TrysteroReplicator.d.ts +21 -24
  33. package/dist/replication/trystero/TrysteroReplicator.js +191 -69
  34. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +11 -0
  35. package/dist/replication/trystero/p2pReplicationDiagnostics.d.ts +9 -0
  36. package/dist/replication/trystero/p2pReplicationDiagnostics.js +51 -0
  37. package/dist/replication/trystero/p2pReplicationProgress.d.ts +15 -0
  38. package/dist/replication/trystero/p2pReplicationProgress.js +37 -0
  39. package/dist/replication/trystero/types.d.ts +3 -0
  40. package/dist/services/base/APIService.d.ts +10 -0
  41. package/dist/services/base/APIService.js +13 -0
  42. package/dist/services/base/IService.d.ts +2 -0
  43. package/docs/adr/2026_11_shared_storage_path_resolution.md +4 -2
  44. package/docs/design_docs/storage_path_resolution.md +8 -7
  45. package/docs/development.md +2 -2
  46. package/docs/journal-receive-recovery.md +69 -0
  47. package/docs/p2p-transport-lifecycle.md +117 -10
  48. package/docs/platform-storage.md +2 -2
  49. package/docs/releasing.md +6 -0
  50. package/docs/unicode-path-identity.md +9 -8
  51. package/package.json +2 -2
@@ -1,3 +1,4 @@
1
+ import { reportP2PDiagnostic } from "../../p2p/p2pDiagnostics.js";
1
2
  import { Logger } from "../../common/logger.js";
2
3
  import {
3
4
  LOG_LEVEL_NOTICE,
@@ -39,6 +40,7 @@ class P2PRoomSessionOwner {
39
40
  activeBinding;
40
41
  pendingPreparation;
41
42
  runtimeGeneration = 0;
43
+ diagnosticGeneration = 0;
42
44
  get currentSession() {
43
45
  return this.current;
44
46
  }
@@ -105,7 +107,9 @@ class P2PRoomSessionOwner {
105
107
  /** Arbitrate a complete signalling probe on the room lifecycle queue. */
106
108
  runConnectionProbe(trialSettings, runOwnedTrial) {
107
109
  try {
108
- assertSupportedPathNormalizationVersion(this.env.services.setting.currentSettings().pathNormalizationVersion);
110
+ assertSupportedPathNormalizationVersion(
111
+ this.env.services.setting.currentSettings().pathNormalizationVersion
112
+ );
109
113
  } catch (error) {
110
114
  return Promise.reject(error);
111
115
  }
@@ -154,6 +158,7 @@ class P2PRoomSessionOwner {
154
158
  }
155
159
  let candidate;
156
160
  const operationGeneration = this.runtimeGeneration;
161
+ this.reportDiagnostic({ category: "relay", code: "room-opening", reason: "relay-details-unobserved" });
157
162
  try {
158
163
  const connectionSettings = await this.prepareConnectionSettings(binding, operationGeneration);
159
164
  await this.assertReconciliationCurrent(binding, operationGeneration, connectionSettings);
@@ -169,13 +174,21 @@ class P2PRoomSessionOwner {
169
174
  }
170
175
  const currentBinding = await this.getEffectiveBinding();
171
176
  if (!currentBinding.enabled || !this.hasRoomDemand() || !this.bindingsMatch(binding, currentBinding) || operationGeneration !== this.runtimeGeneration || !this.connectionSettingsRemainUsable(connectionSettings)) {
177
+ this.diagnosticGeneration += 1;
172
178
  await candidate.retire();
179
+ this.reportDiagnostic({
180
+ category: "relay",
181
+ code: "room-open-cancelled",
182
+ reason: "operation-cancelled"
183
+ });
173
184
  return;
174
185
  }
175
186
  this.current = candidate;
176
187
  this.activeBinding = connectionBinding;
177
188
  this.reconcileCurrentSessionPolicy(candidate);
189
+ this.reportDiagnostic({ category: "relay", code: "room-opened", reason: "relay-details-unobserved" });
178
190
  } catch (error) {
191
+ this.diagnosticGeneration += 1;
179
192
  await candidate?.retire(error).catch((retirementError) => {
180
193
  if (hasManagedP2PTurnConfiguration(binding.settings)) {
181
194
  Logger("The managed P2P room could not be retired cleanly.", LOG_LEVEL_VERBOSE);
@@ -186,8 +199,14 @@ class P2PRoomSessionOwner {
186
199
  this.current = void 0;
187
200
  this.activeBinding = void 0;
188
201
  if (error instanceof StaleRoomReconciliationError || operationGeneration !== this.runtimeGeneration) {
202
+ this.reportDiagnostic({
203
+ category: "relay",
204
+ code: "room-open-cancelled",
205
+ reason: "operation-cancelled"
206
+ });
189
207
  return;
190
208
  }
209
+ this.reportDiagnostic({ category: "relay", code: "room-open-failed", reason: "transport-error" });
191
210
  if (hasManagedP2PTurnConfiguration(binding.settings)) {
192
211
  Logger("The managed P2P room could not be prepared or opened.", LOG_LEVEL_NOTICE);
193
212
  Logger("Managed P2P room failure details were omitted.", LOG_LEVEL_VERBOSE);
@@ -222,9 +241,19 @@ class P2PRoomSessionOwner {
222
241
  this.current = void 0;
223
242
  this.activeBinding = void 0;
224
243
  if (session) {
225
- await session.retire();
244
+ this.diagnosticGeneration += 1;
245
+ try {
246
+ await session.retire();
247
+ this.reportDiagnostic({ category: "relay", code: "room-closed", reason: "relay-details-unobserved" });
248
+ } catch (error) {
249
+ this.reportDiagnostic({ category: "relay", code: "room-close-failed", reason: "transport-error" });
250
+ throw error;
251
+ }
226
252
  }
227
253
  }
254
+ reportDiagnostic(input) {
255
+ reportP2PDiagnostic(this.options.onDiagnosticEvent, input);
256
+ }
228
257
  bindingsMatch(current, candidate) {
229
258
  return current?.database === candidate.database && current.signature === candidate.signature;
230
259
  }
@@ -413,7 +442,15 @@ class P2PRoomSessionOwner {
413
442
  }
414
443
  buildSessionEnv(binding) {
415
444
  const services = this.env.services;
445
+ const diagnosticGeneration = ++this.diagnosticGeneration;
416
446
  return {
447
+ reportDiagnostic: (event) => {
448
+ if (diagnosticGeneration !== this.diagnosticGeneration) return;
449
+ try {
450
+ this.options.onDiagnosticEvent?.(event);
451
+ } catch {
452
+ }
453
+ },
417
454
  events: services.context.events,
418
455
  translate: services.context.translate,
419
456
  settings: binding.settings,
@@ -1,6 +1,7 @@
1
+ import { type ProgressAcknowledgement } from "./p2pReplicationProgress.js";
1
2
  import { type EntryDoc, type ObsidianLiveSyncSettings, type P2PSyncSetting } from "../../common/types.js";
2
3
  import { type LOG_LEVEL } from "octagonal-wheels/common/logger";
3
- import { type ProgressInfo } from "../../pouchdb/ReplicatorShim.js";
4
+ import { type ProgressInfo, type ReplicationStatistics } from "../../pouchdb/ReplicatorShim.js";
4
5
  import type { Confirm } from "../../interfaces/Confirm.js";
5
6
  import { type Advertisement, type ReplicatorHostEnv } from "./types.js";
6
7
  import { EVENT_P2P_REPLICATOR_PROGRESS, EVENT_P2P_REPLICATOR_STATUS, P2PHost } from "./TrysteroReplicatorP2PServer.js";
@@ -12,35 +13,26 @@ export type P2PReplicatorStatus = {
12
13
  replicatingFrom: string[];
13
14
  watchingPeers: string[];
14
15
  };
16
+ /** Sequence progress retained for compatibility, with independently observed work counts. */
17
+ export type P2PReplicationDirectionProgress = {
18
+ max: number;
19
+ current: number;
20
+ isActive: boolean;
21
+ statistics?: ReplicationStatistics;
22
+ };
15
23
  export type P2PReplicationProgress = {
16
24
  peerId: string;
17
25
  peerName: string;
18
- fetching: {
19
- max: number;
20
- current: number;
21
- isActive: boolean;
22
- };
23
- sending: {
24
- max: number;
25
- current: number;
26
- isActive: boolean;
27
- };
26
+ fetching: P2PReplicationDirectionProgress;
27
+ sending: P2PReplicationDirectionProgress;
28
28
  };
29
29
  export type P2PReplicationReport = {
30
30
  peerId: string;
31
31
  peerName: string;
32
32
  } & ({
33
- fetching: {
34
- max: number;
35
- current: number;
36
- isActive: boolean;
37
- };
33
+ fetching: P2PReplicationDirectionProgress;
38
34
  } | {
39
- sending: {
40
- max: number;
41
- current: number;
42
- isActive: boolean;
43
- };
35
+ sending: P2PReplicationDirectionProgress;
44
36
  });
45
37
  /**
46
38
  * In-process outcome of one P2P replication request.
@@ -112,9 +104,11 @@ export declare class TrysteroReplicator {
112
104
  _env: ReplicatorHostEnv;
113
105
  private readonly finiteOperationOwner?;
114
106
  private disposed;
107
+ private readonly acknowledgedAttempts;
115
108
  private readonly fallbackAutomationCoordinator;
116
109
  server?: P2PHost;
117
110
  replicationStatus(): {};
111
+ private reportDiagnostic;
118
112
  get settings(): P2PSyncSetting;
119
113
  /** Latest room-local policy settings supplied by the owning host. */
120
114
  get currentSettings(): P2PSyncSetting;
@@ -136,6 +130,7 @@ export declare class TrysteroReplicator {
136
130
  get autoSyncPeers(): RegExp[];
137
131
  get autoWatchPeers(): RegExp[];
138
132
  onNewPeer(peer: Advertisement): Promise<void>;
133
+ private watchBroadcastingPeer;
139
134
  onPeerLeaved(peerId: string): void;
140
135
  _onSetup: boolean;
141
136
  setOnSetup(): void;
@@ -524,7 +519,7 @@ export declare class TrysteroReplicator {
524
519
  getAllConfig: (fromPeerId: string) => Promise<string | {
525
520
  error: import("../../rpc/types.js").RpcErrorShape;
526
521
  }>;
527
- onProgressAcknowledged: (fromPeerId: string, info: ProgressInfo) => Promise<void>;
522
+ onProgressAcknowledged: (fromPeerId: string, info?: ProgressInfo | null, acknowledgement?: ProgressAcknowledgement) => Promise<void>;
528
523
  getIsBroadcasting: () => Promise<boolean>;
529
524
  requestBroadcasting: (peerId: string) => Promise<true | {
530
525
  error: import("../../rpc/types.js").RpcErrorShape;
@@ -551,13 +546,15 @@ export declare class TrysteroReplicator {
551
546
  _replicateFromPeers: Set<string>;
552
547
  dispatchReplicationProgress(peerId: string, info?: ProgressInfo): void;
553
548
  onReplicationProgress(peerId: string, info?: ProgressInfo): boolean;
554
- onProgressAcknowledged(peerId: string, info?: ProgressInfo): boolean;
555
- acknowledgeProgress(remotePeerId: string, info?: ProgressInfo): void;
549
+ onProgressAcknowledged(peerId: string, info?: ProgressInfo | null, acknowledgement?: ProgressAcknowledgement): boolean;
550
+ acknowledgeProgress(remotePeerId: string, info?: ProgressInfo, acknowledgement?: ProgressAcknowledgement): void;
556
551
  replicateFrom(remotePeer: string, showNotice?: boolean, fromStart?: boolean, skipOrdinaryReplicationPolicy?: boolean, callerSignal?: AbortSignal): Promise<P2PReplicationResult>;
557
552
  private replicateFromWithinSession;
553
+ private pullFromWithinSession;
558
554
  private checkPeerForReplication;
559
555
  private transferFromPeer;
560
556
  notifyProgress(excludePeerId?: string): Promise<void>;
557
+ private notifyPeerProgress;
561
558
  requestBroadcastChanges(peerId: string): Promise<unknown>;
562
559
  getRemoteIsBroadcasting(peerId: string): Promise<unknown>;
563
560
  _watchingPeers: Set<string>;
@@ -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) return;
207
+ if (!shouldAutoSync && !shouldAutoWatch) {
208
+ return;
209
+ }
188
210
  const acceptance = await this.server?.evaluatePeerAcceptance(peer.peerId);
189
- if (acceptance !== "accepted") return;
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 && await this.getRemoteIsBroadcasting(peer.peerId) === true) {
197
- this.watchPeer(peer.peerId);
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
- const name = this.server?._knownAdvertisements.get(peerId)?.name || peerId;
498
- const stat = {
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
- max: 0,
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
- const name = this.server?._knownAdvertisements.get(peerId)?.name || peerId;
519
- const ack = {
520
- peerId,
521
- peerName: name,
522
- sending: {
523
- max: 0,
524
- current: 0,
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._env.events.emitEvent(EVENT_P2P_REPLICATOR_PROGRESS, ack);
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
- const connection = this.server.getConnection(remotePeerId);
542
- void connection.invokeRemoteFunction("onProgressAcknowledged", [this.server.serverPeerId, info], 500).catch((ex) => {
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(ex, LOG_LEVEL_VERBOSE);
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
- { live: false, rewind: fromStart, signal }
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) return;
666
- if (!this.server) return;
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) continue;
670
- void serialized(`notifyProgress-${peerId}`, async () => {
671
- const isAcceptable = await this.server?.isAcceptablePeer(peerId);
672
- if (isAcceptable) {
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
  }