@vrtmrz/livesync-commonlib 0.1.19 → 0.1.20

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 (114) hide show
  1. package/README.md +5 -1
  2. package/dist/common/models/setting.p2p.d.ts +2 -0
  3. package/dist/common/models/setting.p2p.js +5 -0
  4. package/dist/interfaces/ServiceModule.d.ts +13 -4
  5. package/dist/managers/ChunkFetcher.js +10 -0
  6. package/dist/p2p/P2PService.d.ts +123 -0
  7. package/dist/p2p/P2PService.js +271 -0
  8. package/dist/p2p/index.d.ts +8 -0
  9. package/dist/p2p/index.js +8 -0
  10. package/dist/pouchdb/LiveSyncLocalDB.d.ts +3 -0
  11. package/dist/pouchdb/LiveSyncLocalDB.js +23 -3
  12. package/dist/pouchdb/ReplicatorShim.d.ts +43 -4
  13. package/dist/pouchdb/ReplicatorShim.js +78 -5
  14. package/dist/replication/CentralCompatibility.d.ts +38 -0
  15. package/dist/replication/CentralCompatibility.js +39 -0
  16. package/dist/replication/CentralRemoteAdministration.d.ts +84 -0
  17. package/dist/replication/CentralRemoteAdministration.js +71 -0
  18. package/dist/replication/LiveSyncAbstractReplicator.d.ts +19 -6
  19. package/dist/replication/ProviderCapability.d.ts +32 -0
  20. package/dist/replication/ProviderCapability.js +27 -0
  21. package/dist/replication/RemoteResource.d.ts +85 -0
  22. package/dist/replication/RemoteResource.js +17 -0
  23. package/dist/replication/ReplicatorInstance.d.ts +23 -0
  24. package/dist/replication/ReplicatorProvider.d.ts +249 -0
  25. package/dist/replication/ReplicatorProvider.js +145 -0
  26. package/dist/replication/couchdb/LiveSyncReplicator.d.ts +54 -8
  27. package/dist/replication/couchdb/LiveSyncReplicator.js +411 -158
  28. package/dist/replication/index.d.ts +10 -0
  29. package/dist/replication/index.js +70 -0
  30. package/dist/replication/journal/JournalSyncCore.d.ts +2 -0
  31. package/dist/replication/journal/JournalSyncCore.js +18 -5
  32. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +38 -10
  33. package/dist/replication/journal/LiveSyncJournalReplicator.js +271 -63
  34. package/dist/replication/journal/LiveSyncJournalReplicatorEnv.d.ts +8 -0
  35. package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +2 -0
  36. package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -0
  37. package/dist/replication/journal/objectstore/MinioStorageAdapter.js +10 -1
  38. package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +12 -40
  39. package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +52 -152
  40. package/dist/replication/trystero/P2PAutomationCoordinator.d.ts +20 -0
  41. package/dist/replication/trystero/P2PAutomationCoordinator.js +43 -0
  42. package/dist/replication/trystero/P2PConnectionProbeAdmission.d.ts +32 -0
  43. package/dist/replication/trystero/P2PConnectionProbeAdmission.js +10 -0
  44. package/dist/replication/trystero/P2PReplicationWire.d.ts +31 -0
  45. package/dist/replication/trystero/P2PReplicationWire.js +50 -0
  46. package/dist/replication/trystero/P2PRoomSession.d.ts +32 -0
  47. package/dist/replication/trystero/P2PRoomSession.js +101 -0
  48. package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +61 -0
  49. package/dist/replication/trystero/P2PRoomSessionOwner.js +249 -0
  50. package/dist/replication/trystero/TrysteroReplicator.d.ts +88 -36
  51. package/dist/replication/trystero/TrysteroReplicator.js +273 -113
  52. package/dist/replication/trystero/TrysteroReplicatorP2PClient.d.ts +7 -4
  53. package/dist/replication/trystero/TrysteroReplicatorP2PClient.js +21 -14
  54. package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +26 -3
  55. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +84 -39
  56. package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -6
  57. package/dist/replication/trystero/addP2PEventHandlers.d.ts +8 -12
  58. package/dist/replication/trystero/addP2PEventHandlers.js +19 -19
  59. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.d.ts +9 -0
  60. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +30 -0
  61. package/dist/replication/trystero/types.d.ts +18 -0
  62. package/dist/replication/trystero/types.js +5 -1
  63. package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +3 -3
  64. package/dist/replication/trystero/useP2PReplicatorCommands.js +6 -8
  65. package/dist/replication/trystero/useP2PReplicatorFeature.d.ts +16 -7
  66. package/dist/replication/trystero/useP2PReplicatorFeature.js +72 -55
  67. package/dist/rpc/RpcRoom.d.ts +15 -1
  68. package/dist/rpc/RpcRoom.js +176 -41
  69. package/dist/rpc/RpcSession.d.ts +3 -1
  70. package/dist/rpc/RpcSession.js +2 -2
  71. package/dist/rpc/index.d.ts +1 -1
  72. package/dist/rpc/transports/trysteroUtils.js +2 -1
  73. package/dist/rpc/types.d.ts +26 -1
  74. package/dist/serviceFeatures/checkRemoteSize.js +5 -2
  75. package/dist/serviceModules/Rebuilder.js +66 -56
  76. package/dist/services/base/AppLifecycleService.d.ts +4 -0
  77. package/dist/services/base/AppLifecycleService.js +4 -0
  78. package/dist/services/base/ControlService.js +0 -4
  79. package/dist/services/base/DatabaseEventService.js +1 -1
  80. package/dist/services/base/IService.d.ts +105 -8
  81. package/dist/services/base/ReadinessEvaluator.d.ts +40 -0
  82. package/dist/services/base/ReadinessEvaluator.js +16 -0
  83. package/dist/services/base/RemoteConnection.d.ts +3 -0
  84. package/dist/services/base/RemoteService.js +2 -2
  85. package/dist/services/base/ReplicationService.d.ts +45 -10
  86. package/dist/services/base/ReplicationService.js +248 -91
  87. package/dist/services/base/ReplicationService.readiness.d.ts +52 -0
  88. package/dist/services/base/ReplicationService.readiness.js +87 -0
  89. package/dist/services/base/ReplicationService.typedReplication.d.ts +83 -0
  90. package/dist/services/base/ReplicationService.typedReplication.js +191 -0
  91. package/dist/services/base/ReplicatorService.activeReplicatorState.d.ts +47 -0
  92. package/dist/services/base/ReplicatorService.activeReplicatorState.js +107 -0
  93. package/dist/services/base/ReplicatorService.centralRemoteAdministration.d.ts +5 -0
  94. package/dist/services/base/ReplicatorService.centralRemoteAdministration.js +44 -0
  95. package/dist/services/base/ReplicatorService.d.ts +75 -14
  96. package/dist/services/base/ReplicatorService.js +232 -53
  97. package/dist/services/base/ReplicatorService.remoteResourceResolver.d.ts +4 -0
  98. package/dist/services/base/ReplicatorService.remoteResourceResolver.js +11 -0
  99. package/dist/services/base/TweakValueService.d.ts +3 -2
  100. package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +1 -1
  101. package/dist/services/lib/HandlerUtils.d.ts +24 -0
  102. package/dist/services/lib/HandlerUtils.js +40 -0
  103. package/docs/development.md +40 -7
  104. package/docs/p2p-transport-lifecycle.md +135 -11
  105. package/docs/proven-in-use.md +5 -1
  106. package/docs/remote-configurations.md +4 -0
  107. package/docs/service-feature-composition.md +250 -0
  108. package/package.json +17 -1
  109. package/dist/replication/trystero/P2PReplicatorBase.d.ts +0 -19
  110. package/dist/replication/trystero/P2PReplicatorCore.d.ts +0 -8
  111. package/dist/replication/trystero/P2PReplicatorCore.js +0 -56
  112. package/dist/replication/trystero/TrysteroReplicatorP2PConnection.d.ts +0 -2
  113. package/dist/replication/trystero/TrysteroReplicatorP2PConnection.js +0 -4
  114. /package/dist/replication/{trystero/P2PReplicatorBase.js → ReplicatorInstance.js} +0 -0
package/README.md CHANGED
@@ -28,6 +28,8 @@ The package is ESM-only and declares Node 20 or later. Browser entry points are
28
28
  | `@vrtmrz/livesync-commonlib/context` | Instance-owned events, translation injection, and host-neutral standard-I/O types | Focused, package-tested pre-1.0 contract |
29
29
  | `@vrtmrz/livesync-commonlib/browser` | Rooted File System Access API storage | Focused, package-tested pre-1.0 contract |
30
30
  | `@vrtmrz/livesync-commonlib/node` | Rooted Node storage, Node standard I/O, and selected Node capabilities | Supported platform façade; keeps Node-only dependencies behind an explicit boundary |
31
+ | `@vrtmrz/livesync-commonlib/p2p` | Host-composed P2P service views and composition helpers | Focused, package-tested pre-1.0 contract with a temporary compatibility façade |
32
+ | `@vrtmrz/livesync-commonlib/replication` | Provider capabilities, interaction authority, and typed replication outcomes | Focused, package-tested pre-1.0 host-composition contract |
31
33
  | `@vrtmrz/livesync-commonlib/rpc` | Existing LiveSync RPC and PouchDB bridge migration | Transitional only; not part of the stable Commonlib 1.0 contract |
32
34
  | `@vrtmrz/livesync-commonlib/remote-configurations` | Multiple-remote profile creation, naming, and selection | Focused, package-tested pre-1.0 contract |
33
35
  | `@vrtmrz/livesync-commonlib/settings` | New-Vault defaults, stored-setting fallbacks, and settings migration results | Focused, package-tested pre-1.0 contract |
@@ -40,6 +42,8 @@ The Node and browser entries are platform façades. They keep platform-specific
40
42
 
41
43
  The RPC entry exists only while the maintained LiveSync P2P composition depends on the current implementation. Its draft wire protocol and PouchDB bridge are not stable Commonlib 1.0 contracts. RPC is planned to move to Fancy Kit as an independently owned rewrite; new Commonlib consumers should not adopt the transitional entry.
42
44
 
45
+ The P2P entry exposes eight focused views over one service owner, together with the maintained host-composition helpers. New consumers select only the lifecycle, peer-directory, peer-admission, targeted-transfer, change-relay, configuration-exchange, diagnostic, or connection-probe-admission view which they need. The composition result still carries a deprecated concrete Replicator façade while existing panes migrate; new consumers must not depend on that property or reach through it to a raw room or host.
46
+
43
47
  ## Host context and initialisation
44
48
 
45
49
  One service context belongs to one independently composed Commonlib client. It owns that client's event hub and translator. Creating a context without options creates a fresh event hub and renders Commonlib's typed canonical English messages. A host may inject a translator to provide another language:
@@ -123,7 +127,7 @@ The package is currently an infrastructure and compatibility boundary. The conte
123
127
 
124
128
  The accepted replacement direction is an asynchronously created file client with stable `list`, `get`, `put`, `delete`, `watch`, and `close` operations. The operation result, conflict, concurrency, watch checkpoint, retry, and error contracts still require focused decisions and tests before that client can be published.
125
129
 
126
- Package developers should read [the developer guide](docs/development.md). The focused contracts are described in [the storage guide](docs/platform-storage.md), [the standard-I/O guide](docs/platform-standard-io.md), [the settings lifecycle guide](docs/settings-lifecycle.md), [the remote configuration profile guide](docs/remote-configurations.md), and [the conflict-resolution guide](docs/conflict-resolution.md).
130
+ Package developers should read [the developer guide](docs/development.md). The focused contracts are described in [the storage guide](docs/platform-storage.md), [the standard-I/O guide](docs/platform-standard-io.md), [the settings lifecycle guide](docs/settings-lifecycle.md), [the remote configuration profile guide](docs/remote-configurations.md), [the conflict-resolution guide](docs/conflict-resolution.md), and [the P2P transport lifecycle guide](docs/p2p-transport-lifecycle.md).
127
131
 
128
132
  ## Proven in maintained hosts
129
133
 
@@ -8,6 +8,8 @@ import { type P2PConnectionPath } from "./setting.const.js";
8
8
  export declare function normaliseP2PMaxWirePayloadBytes(value: unknown): number;
9
9
  /** Return a recognised P2P connection path, defaulting legacy values to automatic selection. */
10
10
  export declare function normaliseP2PConnectionPath(value: unknown): P2PConnectionPath;
11
+ /** Split the stored comma-separated signalling relay list without changing its entries. */
12
+ export declare function splitP2PRelayUrls(value: string | undefined): string[];
11
13
  /** Split the stored comma-separated TURN server list without changing its entries. */
12
14
  export declare function splitP2PTurnServerUrls(value: string): string[];
13
15
  /**
@@ -8,6 +8,10 @@ function normaliseP2PMaxWirePayloadBytes(value) {
8
8
  function normaliseP2PConnectionPath(value) {
9
9
  return value === P2PConnectionPaths.Relay ? P2PConnectionPaths.Relay : P2PConnectionPaths.Automatic;
10
10
  }
11
+ function splitP2PRelayUrls(value) {
12
+ if (!value) return [];
13
+ return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
14
+ }
11
15
  function splitP2PTurnServerUrls(value) {
12
16
  return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
13
17
  }
@@ -31,5 +35,6 @@ export {
31
35
  isValidP2PTurnServerUrl,
32
36
  normaliseP2PConnectionPath,
33
37
  normaliseP2PMaxWirePayloadBytes,
38
+ splitP2PRelayUrls,
34
39
  splitP2PTurnServerUrls
35
40
  };
@@ -42,11 +42,20 @@ export type ServiceFeatureFunctionWithContext<T extends keyof ServiceHub, U exte
42
42
  * @param featureFunction The feature function to be wrapped.
43
43
  * @returns The same feature function with proper typing.
44
44
  * @example
45
- * const myFeatureDef = createServiceFeature(({ services: { API }, serviceModules: { storageAccess } }) => {
46
- * // ...
45
+ * const useIndexFeature = createServiceFeature((host: NecessaryServices<"appLifecycle", "storageAccess">) => {
46
+ * const { appLifecycle } = host.services;
47
+ * const { storageAccess } = host.serviceModules;
48
+ *
49
+ * appLifecycle.onLoaded.addHandler(async () => {
50
+ * await storageAccess.getFileNames();
51
+ * return true;
52
+ * });
53
+ * });
54
+ *
55
+ * useIndexFeature({
56
+ * services: { context: serviceHub.context, appLifecycle: serviceHub.appLifecycle },
57
+ * serviceModules: { storageAccess },
47
58
  * });
48
- * const myFeature = myFeatureDef.bind(null, this); // <- `this` may `ObsidianLiveSyncPlugin` or a custom context object
49
- * appLifecycle.onLayoutReady(myFeature);
50
59
  */
51
60
  export declare function createServiceFeature<T extends keyof ServiceHub, U extends keyof ServiceModules, TR>(featureFunction: ServiceFeatureFunction<T, U, TR>): ServiceFeatureFunction<T, U, TR>;
52
61
  type ContextFactory<T extends keyof ServiceHub, U extends keyof ServiceModules, C> = (host: NecessaryServices<T, U>) => ServiceFeatureContext<C>;
@@ -9,6 +9,9 @@ const EVENT_MISSING_CHUNKS = "missingChunks";
9
9
  const EVENT_MISSING_CHUNK_REMOTE = "missingChunkRemote";
10
10
  const EVENT_CHUNK_FETCHED = "chunkFetched";
11
11
  const BATCH_SIZE = 100;
12
+ function canFetchRemoteChunks(replicator) {
13
+ return "fetchRemoteChunks" in replicator && typeof replicator.fetchRemoteChunks === "function";
14
+ }
12
15
  class ChunkFetcher {
13
16
  options;
14
17
  get chunkManager() {
@@ -169,6 +172,13 @@ class ChunkFetcher {
169
172
  Logger("No active replicator was found to request missing chunks.");
170
173
  return;
171
174
  }
175
+ if (!canFetchRemoteChunks(replicator)) {
176
+ Logger("The active replicator does not support fetching individual remote chunks.");
177
+ for (const chunkID of requestIDs) {
178
+ this.chunkManager.emitEvent(EVENT_MISSING_CHUNK_REMOTE, chunkID);
179
+ }
180
+ return;
181
+ }
172
182
  const fetched = await replicator.fetchRemoteChunks(requestIDs, false);
173
183
  this.touchClaims(requestIDs);
174
184
  if (!fetched) {
@@ -0,0 +1,123 @@
1
+ import type { ObsidianLiveSyncSettings } from "../common/types.js";
2
+ import type { ReplicatorInstance } from "../replication/ReplicatorInstance.js";
3
+ import type { DiagRTCPeerConnectionMetrics } from "../rpc/transports/DiagRTCPeerConnections.types.js";
4
+ import { LiveSyncTrysteroReplicator, type LiveSyncTrysteroReplicatorEnv } from "../replication/trystero/LiveSyncTrysteroReplicator.js";
5
+ import type { P2PReplicationResult } from "../replication/trystero/TrysteroReplicator.js";
6
+ import type { AcceptanceDecision, RevokeAcceptanceDecision } from "../replication/trystero/TrysteroReplicatorP2PServer.js";
7
+ import type { Advertisement } from "../replication/trystero/types.js";
8
+ import { type ReplicationOutcome } from "../replication/ReplicatorProvider.js";
9
+ import type { P2PConnectionProbeAdmission } from "../replication/trystero/P2PConnectionProbeAdmission.js";
10
+ export { ACTIVE_P2P_RELAY_BINDING_CONFLICT } from "../replication/trystero/P2PConnectionProbeAdmission.js";
11
+ export type { P2PConnectionProbeAdmission, P2PConnectionProbeAdmissionResult, P2PConnectionProbeSettings, } from "../replication/trystero/P2PConnectionProbeAdmission.js";
12
+ /** Candidate details projected from browser RTC statistics. */
13
+ export interface P2PCandidateSummary {
14
+ readonly id: string;
15
+ readonly candidateType: string;
16
+ readonly protocol: string;
17
+ readonly relayProtocol: string;
18
+ }
19
+ /**
20
+ * Current peer-connection metrics without a raw RTC report or peer handle.
21
+ *
22
+ * Missing numeric values use the existing `unknown` sentinel so maintained
23
+ * diagnostic output can distinguish absence from zero.
24
+ */
25
+ export interface P2PPeerConnectionMetrics {
26
+ readonly selectedPairPresent: boolean;
27
+ readonly selectedPairId: string;
28
+ readonly state: string;
29
+ readonly currentRoundTripTime: number | "unknown";
30
+ readonly totalRoundTripTime: number | "unknown";
31
+ readonly requestsSent: number | "unknown";
32
+ readonly responsesReceived: number | "unknown";
33
+ readonly packetsDiscardedOnSend: number | "unknown";
34
+ readonly bytesSent: number | "unknown";
35
+ readonly bytesReceived: number | "unknown";
36
+ readonly localCandidate?: P2PCandidateSummary;
37
+ readonly remoteCandidate?: P2PCandidateSummary;
38
+ }
39
+ /** Project low-level RTC statistics into the focused P2P diagnostic contract. */
40
+ export declare function projectP2PPeerConnectionMetrics(metrics: DiagRTCPeerConnectionMetrics): P2PPeerConnectionMetrics;
41
+ /** Observes and explicitly controls the room transport owned by the P2P service. */
42
+ export interface P2PTransportLifecycle {
43
+ readonly isConnected: boolean;
44
+ connect(): Promise<void>;
45
+ disconnect(): Promise<void>;
46
+ }
47
+ /** Supplies a current peer snapshot without exposing the underlying room. */
48
+ export interface P2PPeerDirectory {
49
+ getPeers(): readonly Advertisement[];
50
+ }
51
+ /** Administers temporary and persisted peer acceptance decisions. */
52
+ export interface P2PPeerAdmission {
53
+ makeDecision(decision: AcceptanceDecision): Promise<void>;
54
+ revokeDecision(decision: RevokeAcceptanceDecision): Promise<void>;
55
+ }
56
+ /** Performs finite transfer operations against an explicitly selected peer. */
57
+ export interface P2PTargetedTransfer {
58
+ pullFromPeer(peerId: string, options?: {
59
+ readonly showNotice?: boolean;
60
+ readonly skipOrdinaryReplicationPolicy?: boolean;
61
+ }): Promise<P2PReplicationResult>;
62
+ requestPushToPeer(peerId: string): Promise<P2PReplicationResult>;
63
+ synchroniseWithPeer(peerId: string, showNotice?: boolean): Promise<P2PReplicationResult>;
64
+ /** Synchronise the persisted target set without interactive peer selection. */
65
+ synchroniseConfiguredTargets(): Promise<ReplicationOutcome>;
66
+ }
67
+ /** Controls watch and broadcast behaviour attached to the current room session. */
68
+ export interface P2PChangeRelay {
69
+ watchPeer(peerId: string): void;
70
+ unwatchPeer(peerId: string): void;
71
+ enableBroadcastChanges(): void;
72
+ disableBroadcastChanges(): void;
73
+ }
74
+ /** Exchanges configuration with one explicitly selected peer. */
75
+ export interface P2PConfigurationExchange {
76
+ getRemoteConfiguration(peerId: string): Promise<ObsidianLiveSyncSettings | false>;
77
+ }
78
+ /** Supplies transport diagnostics without exposing a room or peer connection. */
79
+ export interface P2PDiagnostics {
80
+ requestStatus(): void;
81
+ getPeerConnectionMetrics(peerId: string): Promise<P2PPeerConnectionMetrics | undefined>;
82
+ }
83
+ /** Focused contract views backed by one private P2P service context. */
84
+ export interface P2PServiceViews {
85
+ readonly transportLifecycle: P2PTransportLifecycle;
86
+ readonly connectionProbe: P2PConnectionProbeAdmission;
87
+ readonly peerDirectory: P2PPeerDirectory;
88
+ readonly peerAdmission: P2PPeerAdmission;
89
+ readonly targetedTransfer: P2PTargetedTransfer;
90
+ readonly changeRelay: P2PChangeRelay;
91
+ readonly configurationExchange: P2PConfigurationExchange;
92
+ readonly diagnostics: P2PDiagnostics;
93
+ }
94
+ /**
95
+ * Host-only lifecycle operations which are deliberately absent from ordinary
96
+ * P2P capability views.
97
+ */
98
+ export interface P2PServiceLifecycle {
99
+ requestStatus(): void;
100
+ openAfterDatabaseRebuild(): Promise<void>;
101
+ closeForLifecycle(): Promise<void>;
102
+ /** Reconcile the automatic room demand; the room owner also reapplies current room-local policy. */
103
+ reconcileAutoStart(settings: Pick<ObsidianLiveSyncSettings, "P2P_Enabled" | "P2P_AutoStart">): Promise<void>;
104
+ scheduleAutoStart(delayMs?: number): void;
105
+ }
106
+ /** Host composition result. Ordinary consumers receive only `views`. */
107
+ export interface P2PServiceComposition {
108
+ /** Deprecated compatibility surface for consumers which still require it. */
109
+ readonly compatibilityReplicator: LiveSyncTrysteroReplicator;
110
+ /** Stable capability projections over the private service context. */
111
+ readonly views: P2PServiceViews;
112
+ /** Host lifecycle operations which are not ordinary P2P capabilities. */
113
+ readonly lifecycle: P2PServiceLifecycle;
114
+ /** Create a non-owning adapter for the active Replicator selection. */
115
+ createActiveReplicator(): ReplicatorInstance;
116
+ }
117
+ /**
118
+ * Compose stable capability views over one private P2P service context.
119
+ *
120
+ * `P2PRoomSessionOwner` remains the resource owner. The returned composition
121
+ * object is host wiring, not a capability façade for ordinary consumers.
122
+ */
123
+ export declare function createP2PService(env: LiveSyncTrysteroReplicatorEnv): P2PServiceComposition;
@@ -0,0 +1,271 @@
1
+ import { getPeerConnectionStats } from "../rpc/transports/DiagRTCPeerConnections.utils.js";
2
+ import {
3
+ LiveSyncTrysteroReplicator
4
+ } from "../replication/trystero/LiveSyncTrysteroReplicator.js";
5
+ import { P2PRoomSessionOwner } from "../replication/trystero/P2PRoomSessionOwner.js";
6
+ import { compatGlobal } from "../common/coreEnvFunctions.js";
7
+ import {
8
+ REPLICATION_CANCELLED,
9
+ REPLICATION_COMPLETED,
10
+ replicationBlocked,
11
+ replicationFailed
12
+ } from "../replication/ReplicatorProvider.js";
13
+ import { ACTIVE_P2P_RELAY_BINDING_CONFLICT } from "../replication/trystero/P2PConnectionProbeAdmission.js";
14
+ function getReportValue(report, key) {
15
+ const value = report?.[key];
16
+ return typeof value === "string" || typeof value === "number" ? String(value) : "unknown";
17
+ }
18
+ function isUnknownRecord(value) {
19
+ return typeof value === "object" && value !== null;
20
+ }
21
+ function projectCandidate(reports, candidateId) {
22
+ if (candidateId === "unknown") return void 0;
23
+ const report = reports.filter(isUnknownRecord).find((value) => value.id === candidateId);
24
+ if (!report) return void 0;
25
+ return {
26
+ id: candidateId,
27
+ candidateType: getReportValue(report, "candidateType"),
28
+ protocol: getReportValue(report, "protocol"),
29
+ relayProtocol: getReportValue(report, "relayProtocol")
30
+ };
31
+ }
32
+ function projectP2PPeerConnectionMetrics(metrics) {
33
+ return {
34
+ selectedPairPresent: metrics.selectedPair !== void 0,
35
+ selectedPairId: metrics.selectedPairId,
36
+ state: metrics.state,
37
+ currentRoundTripTime: metrics.currentRoundTripTime,
38
+ totalRoundTripTime: metrics.totalRoundTripTime,
39
+ requestsSent: metrics.requestsSent,
40
+ responsesReceived: metrics.responsesReceived,
41
+ packetsDiscardedOnSend: metrics.packetsDiscardedOnSend,
42
+ bytesSent: metrics.bytesSent,
43
+ bytesReceived: metrics.bytesReceived,
44
+ localCandidate: projectCandidate(metrics.reports, metrics.localCandidateId),
45
+ remoteCandidate: projectCandidate(metrics.reports, metrics.remoteCandidateId)
46
+ };
47
+ }
48
+ function connect(owner, state) {
49
+ state.explicitDisconnectVeto = false;
50
+ state.lifecycleClosed = false;
51
+ return owner.setPersistentDemand("explicit", true);
52
+ }
53
+ function clearDelayedAutoStart(state) {
54
+ if (state.delayedAutoStart !== void 0) {
55
+ compatGlobal.clearTimeout(state.delayedAutoStart);
56
+ state.delayedAutoStart = void 0;
57
+ }
58
+ }
59
+ function cancelDelayedAutomation(owner, state) {
60
+ state.lifecycleGeneration += 1;
61
+ owner.beginAutomationLifecycle();
62
+ clearDelayedAutoStart(state);
63
+ }
64
+ function disconnect(owner, state) {
65
+ state.explicitDisconnectVeto = true;
66
+ cancelDelayedAutomation(owner, state);
67
+ return owner.close();
68
+ }
69
+ function openAfterDatabaseRebuild(owner, state) {
70
+ state.lifecycleClosed = false;
71
+ return owner.setPersistentDemand("rebuild-continuation", true);
72
+ }
73
+ function closeForLifecycle(owner, state) {
74
+ state.lifecycleClosed = true;
75
+ cancelDelayedAutomation(owner, state);
76
+ return owner.close();
77
+ }
78
+ function reconcileAutoStart(context, settings) {
79
+ if (!settings.P2P_Enabled || !settings.P2P_AutoStart) {
80
+ return context.roomSessionOwner.setPersistentDemand("automatic", false);
81
+ }
82
+ if (context.state.explicitDisconnectVeto || context.state.lifecycleClosed) return Promise.resolve();
83
+ return context.roomSessionOwner.setPersistentDemand("automatic", true);
84
+ }
85
+ function scheduleAutoStart(context, delayMs = 100) {
86
+ clearDelayedAutoStart(context.state);
87
+ context.state.lifecycleClosed = false;
88
+ const generation = context.state.lifecycleGeneration;
89
+ context.state.delayedAutoStart = compatGlobal.setTimeout(() => {
90
+ context.state.delayedAutoStart = void 0;
91
+ if (generation !== context.state.lifecycleGeneration) return;
92
+ void reconcileAutoStart(context, context.env.services.setting.currentSettings());
93
+ }, delayMs);
94
+ }
95
+ function runWithFiniteRoomDemand(context, task) {
96
+ if (context.state.explicitDisconnectVeto) {
97
+ return Promise.reject(new Error("The P2P room was explicitly disconnected."));
98
+ }
99
+ if (context.state.lifecycleClosed) return Promise.reject(new Error("The P2P room is not ready."));
100
+ return context.roomSessionOwner.runWithFiniteDemand(task);
101
+ }
102
+ async function synchroniseConfiguredTargets(context) {
103
+ if (context.state.explicitDisconnectVeto || context.state.lifecycleClosed) return replicationBlocked("not-ready");
104
+ try {
105
+ const result = await context.roomSessionOwner.runWithFiniteDemand(
106
+ (session) => session.runFiniteOperation((signal) => session.replicator.replicateFromCommand(false, void 0, signal))
107
+ );
108
+ if (result.status === "completed") return REPLICATION_COMPLETED;
109
+ if (result.status === "cancelled") return REPLICATION_CANCELLED;
110
+ if (result.status === "blocked") return replicationBlocked(result.reason);
111
+ return {
112
+ status: "partial",
113
+ detail: { kind: "p2p-configured-targets", targets: result.targets }
114
+ };
115
+ } catch (error) {
116
+ return replicationFailed(error);
117
+ }
118
+ }
119
+ function pullFromPeer(context, peerId, options = {}) {
120
+ return runWithFiniteRoomDemand(
121
+ context,
122
+ (session) => context.env.services.replicator.runFiniteReplicationActivity(
123
+ () => session.replicator.replicateFrom(
124
+ peerId,
125
+ options.showNotice ?? false,
126
+ false,
127
+ options.skipOrdinaryReplicationPolicy ?? false
128
+ ),
129
+ { label: "replication" }
130
+ )
131
+ );
132
+ }
133
+ function requestPushToPeer(context, peerId) {
134
+ return runWithFiniteRoomDemand(
135
+ context,
136
+ (session) => context.env.services.replicator.runBoundedRemoteActivity(
137
+ () => session.replicator.requestSynchroniseToPeer(peerId),
138
+ { label: "replication" }
139
+ )
140
+ );
141
+ }
142
+ function synchroniseWithPeer(context, peerId, showNotice = false) {
143
+ return runWithFiniteRoomDemand(
144
+ context,
145
+ (session) => context.env.services.replicator.runFiniteReplicationActivity(
146
+ () => session.replicator.sync(peerId, showNotice),
147
+ { label: "replication" }
148
+ )
149
+ );
150
+ }
151
+ async function getPeerConnectionMetrics(context, peerId) {
152
+ const peerConnection = context.compatibilityReplicator.rawHost?.room?.getPeers()[peerId];
153
+ if (!peerConnection) return void 0;
154
+ const metrics = await getPeerConnectionStats(`p2p-service-${peerId}`, peerConnection);
155
+ return metrics ? projectP2PPeerConnectionMetrics(metrics) : void 0;
156
+ }
157
+ function createCompatibilitySessionAccess(owner, state) {
158
+ return {
159
+ get currentSession() {
160
+ return owner.currentSession;
161
+ },
162
+ get isConnected() {
163
+ return owner.isConnected;
164
+ },
165
+ cancelActiveTransfers: () => owner.cancelActiveTransfers(),
166
+ open: () => connect(owner, state),
167
+ close: () => disconnect(owner, state)
168
+ };
169
+ }
170
+ function createServiceViews(context) {
171
+ const transportLifecycle = {
172
+ get isConnected() {
173
+ return context.roomSessionOwner.isConnected;
174
+ },
175
+ connect: () => connect(context.roomSessionOwner, context.state),
176
+ disconnect: () => disconnect(context.roomSessionOwner, context.state)
177
+ };
178
+ const connectionProbe = {
179
+ run: (trialSettings, runOwnedTrial) => context.roomSessionOwner.runConnectionProbe(trialSettings, runOwnedTrial)
180
+ };
181
+ const peerDirectory = {
182
+ getPeers: () => context.compatibilityReplicator.knownAdvertisements
183
+ };
184
+ const peerAdmission = {
185
+ makeDecision: (decision) => context.compatibilityReplicator.makeDecision(decision),
186
+ revokeDecision: (decision) => context.compatibilityReplicator.revokeDecision(decision)
187
+ };
188
+ const targetedTransfer = {
189
+ pullFromPeer: (peerId, options) => pullFromPeer(context, peerId, options),
190
+ requestPushToPeer: (peerId) => requestPushToPeer(context, peerId),
191
+ synchroniseWithPeer: (peerId, showNotice) => synchroniseWithPeer(context, peerId, showNotice),
192
+ synchroniseConfiguredTargets: () => synchroniseConfiguredTargets(context)
193
+ };
194
+ const changeRelay = {
195
+ watchPeer: (peerId) => context.compatibilityReplicator.watchPeer(peerId),
196
+ unwatchPeer: (peerId) => context.compatibilityReplicator.unwatchPeer(peerId),
197
+ enableBroadcastChanges: () => context.compatibilityReplicator.enableBroadcastChanges(),
198
+ disableBroadcastChanges: () => context.compatibilityReplicator.disableBroadcastChanges()
199
+ };
200
+ const configurationExchange = {
201
+ getRemoteConfiguration: (peerId) => context.compatibilityReplicator.getRemoteConfig(peerId)
202
+ };
203
+ const diagnostics = {
204
+ requestStatus: () => context.compatibilityReplicator.requestStatus(),
205
+ getPeerConnectionMetrics: (peerId) => getPeerConnectionMetrics(context, peerId)
206
+ };
207
+ return {
208
+ transportLifecycle,
209
+ connectionProbe,
210
+ peerDirectory,
211
+ peerAdmission,
212
+ targetedTransfer,
213
+ changeRelay,
214
+ configurationExchange,
215
+ diagnostics
216
+ };
217
+ }
218
+ function createServiceLifecycle(context) {
219
+ return {
220
+ requestStatus: () => context.compatibilityReplicator.requestStatus(),
221
+ openAfterDatabaseRebuild: () => openAfterDatabaseRebuild(context.roomSessionOwner, context.state),
222
+ closeForLifecycle: () => closeForLifecycle(context.roomSessionOwner, context.state),
223
+ reconcileAutoStart: (settings) => reconcileAutoStart(context, settings),
224
+ scheduleAutoStart: (delayMs) => scheduleAutoStart(context, delayMs)
225
+ };
226
+ }
227
+ function createP2PService(env) {
228
+ const roomSessionOwner = new P2PRoomSessionOwner(env);
229
+ const state = {
230
+ explicitDisconnectVeto: false,
231
+ lifecycleClosed: false,
232
+ lifecycleGeneration: 0,
233
+ delayedAutoStart: void 0
234
+ };
235
+ const compatibilityReplicator = new LiveSyncTrysteroReplicator(
236
+ env,
237
+ createCompatibilitySessionAccess(roomSessionOwner, state)
238
+ );
239
+ const context = { env, roomSessionOwner, compatibilityReplicator, state };
240
+ return {
241
+ compatibilityReplicator,
242
+ views: createServiceViews(context),
243
+ lifecycle: createServiceLifecycle(context),
244
+ createActiveReplicator: () => new P2PActiveReplicatorAdapter(compatibilityReplicator)
245
+ };
246
+ }
247
+ class P2PActiveReplicatorAdapter {
248
+ constructor(delegate) {
249
+ this.delegate = delegate;
250
+ }
251
+ delegate;
252
+ initializeDatabaseForReplication() {
253
+ return this.delegate.initializeDatabaseForReplication();
254
+ }
255
+ terminateSync() {
256
+ this.delegate.terminateSync();
257
+ }
258
+ openReplication(setting, keepAlive, showResult, ignoreCleanLock) {
259
+ return this.delegate.openReplication(setting, keepAlive, showResult, ignoreCleanLock);
260
+ }
261
+ replicateAllFromServer(setting, showingNotice) {
262
+ return this.delegate.replicateAllFromServer(setting, showingNotice);
263
+ }
264
+ closeReplication() {
265
+ }
266
+ }
267
+ export {
268
+ ACTIVE_P2P_RELAY_BINDING_CONFLICT,
269
+ createP2PService,
270
+ projectP2PPeerConnectionMetrics
271
+ };
@@ -0,0 +1,8 @@
1
+ export type { P2PCandidateSummary, P2PChangeRelay, P2PConfigurationExchange, P2PDiagnostics, P2PPeerConnectionMetrics, P2PPeerAdmission, P2PPeerDirectory, P2PServiceViews, P2PTargetedTransfer, P2PTransportLifecycle, P2PConnectionProbeAdmission, P2PConnectionProbeAdmissionResult, P2PConnectionProbeSettings, } from "./P2PService.js";
2
+ export { ACTIVE_P2P_RELAY_BINDING_CONFLICT } from "./P2PService.js";
3
+ export { useP2PReplicatorCommands } from "../replication/trystero/useP2PReplicatorCommands.js";
4
+ export { useP2PReplicatorFeature } from "../replication/trystero/useP2PReplicatorFeature.js";
5
+ export type { OpenRebuildUIFactory, OpenReplicationUIFactory, } from "../replication/trystero/useP2PReplicatorFeature.js";
6
+ export type { UseP2PReplicatorResult } from "../replication/trystero/UseP2PReplicatorResult.js";
7
+ export type { P2PReplicationResult } from "../replication/trystero/TrysteroReplicator.js";
8
+ export type { Advertisement } from "../replication/trystero/types.js";
@@ -0,0 +1,8 @@
1
+ import { ACTIVE_P2P_RELAY_BINDING_CONFLICT } from "./P2PService.js";
2
+ import { useP2PReplicatorCommands } from "../replication/trystero/useP2PReplicatorCommands.js";
3
+ import { useP2PReplicatorFeature } from "../replication/trystero/useP2PReplicatorFeature.js";
4
+ export {
5
+ ACTIVE_P2P_RELAY_BINDING_CONFLICT,
6
+ useP2PReplicatorCommands,
7
+ useP2PReplicatorFeature
8
+ };
@@ -49,12 +49,15 @@ export declare class LiveSyncLocalDB {
49
49
  refreshSettings(): void;
50
50
  offRemoteChunkFetchedHandler?: ReturnType<LiveSyncEventHub["onEvent"]>;
51
51
  private databaseCloseCleanup?;
52
+ /** Share owner retirement across every close path for one physical database lifetime. */
53
+ private activeReplicationClose?;
52
54
  private databaseUnloadNotification?;
53
55
  constructor(dbname: string, env: LiveSyncLocalDBEnv);
54
56
  /** Notify dependent services at most once for each physical database lifecycle. */
55
57
  private notifyDatabaseUnload;
56
58
  onunload(): void;
57
59
  close(): Promise<void>;
60
+ private closeActiveReplication;
58
61
  private teardownDatabaseDependencies;
59
62
  private rollbackFailedInitialisation;
60
63
  onNewLeaf(chunk: EntryLeaf): void;
@@ -44,6 +44,8 @@ class LiveSyncLocalDB {
44
44
  }
45
45
  offRemoteChunkFetchedHandler;
46
46
  databaseCloseCleanup;
47
+ /** Share owner retirement across every close path for one physical database lifetime. */
48
+ activeReplicationClose;
47
49
  databaseUnloadNotification;
48
50
  constructor(dbname, env) {
49
51
  this.auth = {
@@ -72,8 +74,10 @@ class LiveSyncLocalDB {
72
74
  this.isReady = false;
73
75
  this.offRemoteChunkFetchedHandler?.();
74
76
  this.offRemoteChunkFetchedHandler = void 0;
77
+ await this.closeActiveReplication();
75
78
  if (this.localDatabase != null) {
76
79
  const database = this.localDatabase;
80
+ await this.env.services.databaseEvents.onCloseDatabase(this);
77
81
  await database.close();
78
82
  if (this.databaseCloseCleanup?.database === database) {
79
83
  await this.databaseCloseCleanup.promise;
@@ -85,9 +89,24 @@ class LiveSyncLocalDB {
85
89
  this._managers = void 0;
86
90
  await this.notifyDatabaseUnload();
87
91
  }
92
+ async closeActiveReplication() {
93
+ if (this.activeReplicationClose) {
94
+ return await this.activeReplicationClose;
95
+ }
96
+ const closeOperation = this.env.services.replicator.onCloseActiveReplication();
97
+ this.activeReplicationClose = closeOperation;
98
+ try {
99
+ return await closeOperation;
100
+ } catch (error) {
101
+ if (this.activeReplicationClose === closeOperation) {
102
+ this.activeReplicationClose = void 0;
103
+ }
104
+ throw error;
105
+ }
106
+ }
88
107
  async teardownDatabaseDependencies(managers) {
89
108
  try {
90
- await Promise.resolve(this.env.services.replicator.getActiveReplicator()?.closeReplication());
109
+ await this.closeActiveReplication();
91
110
  } catch (error) {
92
111
  this._log("Failed to close replication while tearing down the local database.", LOG_LEVEL_VERBOSE);
93
112
  this._log(error, LOG_LEVEL_VERBOSE);
@@ -147,6 +166,7 @@ class LiveSyncLocalDB {
147
166
  deterministic_revs: true
148
167
  }
149
168
  );
169
+ this.activeReplicationClose = void 0;
150
170
  this.databaseUnloadNotification = void 0;
151
171
  const manager = new LiveSyncManagers({
152
172
  database: this.localDatabase,
@@ -292,12 +312,12 @@ class LiveSyncLocalDB {
292
312
  }
293
313
  async resetDatabase() {
294
314
  this.isReady = false;
295
- await this.managers.teardownManagers();
296
- this.env.services.replicator.getActiveReplicator()?.closeReplication();
315
+ await this.closeActiveReplication();
297
316
  if (!await this.env.services.databaseEvents.onResetDatabase(this)) {
298
317
  Logger("Database reset has been prevented or failed on some modules.", LOG_LEVEL_NOTICE);
299
318
  return false;
300
319
  }
320
+ await this.managers.teardownManagers();
301
321
  Logger("Database closed for reset Database.");
302
322
  await this.localDatabase.destroy();
303
323
  this.localDatabase = null;