@vrtmrz/livesync-commonlib 0.1.19-rc.0 → 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.
- package/README.md +5 -1
- package/dist/common/models/setting.p2p.d.ts +2 -0
- package/dist/common/models/setting.p2p.js +5 -0
- package/dist/interfaces/ServiceModule.d.ts +13 -4
- package/dist/managers/ChunkFetcher.js +10 -0
- package/dist/p2p/P2PService.d.ts +123 -0
- package/dist/p2p/P2PService.js +271 -0
- package/dist/p2p/index.d.ts +8 -0
- package/dist/p2p/index.js +8 -0
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +3 -0
- package/dist/pouchdb/LiveSyncLocalDB.js +23 -3
- package/dist/pouchdb/ReplicatorShim.d.ts +43 -4
- package/dist/pouchdb/ReplicatorShim.js +78 -5
- package/dist/replication/CentralCompatibility.d.ts +38 -0
- package/dist/replication/CentralCompatibility.js +39 -0
- package/dist/replication/CentralRemoteAdministration.d.ts +84 -0
- package/dist/replication/CentralRemoteAdministration.js +71 -0
- package/dist/replication/LiveSyncAbstractReplicator.d.ts +19 -6
- package/dist/replication/ProviderCapability.d.ts +32 -0
- package/dist/replication/ProviderCapability.js +27 -0
- package/dist/replication/RemoteResource.d.ts +85 -0
- package/dist/replication/RemoteResource.js +17 -0
- package/dist/replication/ReplicatorInstance.d.ts +23 -0
- package/dist/replication/ReplicatorProvider.d.ts +249 -0
- package/dist/replication/ReplicatorProvider.js +145 -0
- package/dist/replication/couchdb/LiveSyncReplicator.d.ts +54 -8
- package/dist/replication/couchdb/LiveSyncReplicator.js +411 -158
- package/dist/replication/index.d.ts +10 -0
- package/dist/replication/index.js +70 -0
- package/dist/replication/journal/JournalSyncCore.d.ts +2 -0
- package/dist/replication/journal/JournalSyncCore.js +18 -5
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +38 -10
- package/dist/replication/journal/LiveSyncJournalReplicator.js +271 -63
- package/dist/replication/journal/LiveSyncJournalReplicatorEnv.d.ts +8 -0
- package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +2 -0
- package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -0
- package/dist/replication/journal/objectstore/MinioStorageAdapter.js +10 -1
- package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +12 -40
- package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +52 -152
- package/dist/replication/trystero/P2PAutomationCoordinator.d.ts +20 -0
- package/dist/replication/trystero/P2PAutomationCoordinator.js +43 -0
- package/dist/replication/trystero/P2PConnectionProbeAdmission.d.ts +32 -0
- package/dist/replication/trystero/P2PConnectionProbeAdmission.js +10 -0
- package/dist/replication/trystero/P2PReplicationWire.d.ts +31 -0
- package/dist/replication/trystero/P2PReplicationWire.js +50 -0
- package/dist/replication/trystero/P2PRoomSession.d.ts +32 -0
- package/dist/replication/trystero/P2PRoomSession.js +101 -0
- package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +61 -0
- package/dist/replication/trystero/P2PRoomSessionOwner.js +249 -0
- package/dist/replication/trystero/TrysteroReplicator.d.ts +88 -36
- package/dist/replication/trystero/TrysteroReplicator.js +273 -113
- package/dist/replication/trystero/TrysteroReplicatorP2PClient.d.ts +7 -4
- package/dist/replication/trystero/TrysteroReplicatorP2PClient.js +21 -14
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +26 -3
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +84 -39
- package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -6
- package/dist/replication/trystero/addP2PEventHandlers.d.ts +8 -12
- package/dist/replication/trystero/addP2PEventHandlers.js +19 -19
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.d.ts +9 -0
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +30 -0
- package/dist/replication/trystero/types.d.ts +18 -0
- package/dist/replication/trystero/types.js +5 -1
- package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +3 -3
- package/dist/replication/trystero/useP2PReplicatorCommands.js +6 -8
- package/dist/replication/trystero/useP2PReplicatorFeature.d.ts +16 -7
- package/dist/replication/trystero/useP2PReplicatorFeature.js +72 -55
- package/dist/rpc/RpcRoom.d.ts +15 -1
- package/dist/rpc/RpcRoom.js +176 -41
- package/dist/rpc/RpcSession.d.ts +3 -1
- package/dist/rpc/RpcSession.js +2 -2
- package/dist/rpc/index.d.ts +1 -1
- package/dist/rpc/transports/trysteroUtils.js +2 -1
- package/dist/rpc/types.d.ts +26 -1
- package/dist/serviceFeatures/checkRemoteSize.js +5 -2
- package/dist/serviceModules/Rebuilder.js +66 -56
- package/dist/services/base/AppLifecycleService.d.ts +4 -0
- package/dist/services/base/AppLifecycleService.js +4 -0
- package/dist/services/base/ControlService.js +0 -4
- package/dist/services/base/DatabaseEventService.js +1 -1
- package/dist/services/base/IService.d.ts +105 -8
- package/dist/services/base/ReadinessEvaluator.d.ts +40 -0
- package/dist/services/base/ReadinessEvaluator.js +16 -0
- package/dist/services/base/RemoteConnection.d.ts +3 -0
- package/dist/services/base/RemoteService.js +2 -2
- package/dist/services/base/ReplicationService.d.ts +45 -10
- package/dist/services/base/ReplicationService.js +248 -91
- package/dist/services/base/ReplicationService.readiness.d.ts +52 -0
- package/dist/services/base/ReplicationService.readiness.js +87 -0
- package/dist/services/base/ReplicationService.typedReplication.d.ts +83 -0
- package/dist/services/base/ReplicationService.typedReplication.js +191 -0
- package/dist/services/base/ReplicatorService.activeReplicatorState.d.ts +47 -0
- package/dist/services/base/ReplicatorService.activeReplicatorState.js +107 -0
- package/dist/services/base/ReplicatorService.centralRemoteAdministration.d.ts +5 -0
- package/dist/services/base/ReplicatorService.centralRemoteAdministration.js +44 -0
- package/dist/services/base/ReplicatorService.d.ts +75 -14
- package/dist/services/base/ReplicatorService.js +232 -53
- package/dist/services/base/ReplicatorService.remoteResourceResolver.d.ts +4 -0
- package/dist/services/base/ReplicatorService.remoteResourceResolver.js +11 -0
- package/dist/services/base/TweakValueService.d.ts +3 -2
- package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +1 -1
- package/dist/services/lib/HandlerUtils.d.ts +24 -0
- package/dist/services/lib/HandlerUtils.js +40 -0
- package/docs/development.md +40 -7
- package/docs/p2p-transport-lifecycle.md +135 -11
- package/docs/proven-in-use.md +5 -1
- package/docs/remote-configurations.md +4 -0
- package/docs/service-feature-composition.md +250 -0
- package/package.json +17 -1
- package/dist/replication/trystero/P2PReplicatorBase.d.ts +0 -19
- package/dist/replication/trystero/P2PReplicatorCore.d.ts +0 -8
- package/dist/replication/trystero/P2PReplicatorCore.js +0 -56
- package/dist/replication/trystero/TrysteroReplicatorP2PConnection.d.ts +0 -2
- package/dist/replication/trystero/TrysteroReplicatorP2PConnection.js +0 -4
- /package/dist/replication/{trystero/P2PReplicatorBase.js → ReplicatorInstance.js} +0 -0
|
@@ -14,6 +14,9 @@ var DeviceDecisions = /* @__PURE__ */ ((DeviceDecisions2) => {
|
|
|
14
14
|
return DeviceDecisions2;
|
|
15
15
|
})(DeviceDecisions || {});
|
|
16
16
|
const ID_P2PKnownDevices = "_local/P2PKnownDevices";
|
|
17
|
+
function resolveCurrentP2PSettings(env) {
|
|
18
|
+
return env.currentSettings?.() ?? env.settings;
|
|
19
|
+
}
|
|
17
20
|
const KEY_DEVICE_DECISIONS = "p2p-device-decisions";
|
|
18
21
|
export {
|
|
19
22
|
BULK_GET_RPC_TIMEOUT,
|
|
@@ -23,5 +26,6 @@ export {
|
|
|
23
26
|
DeviceDecisions,
|
|
24
27
|
ID_P2PKnownDevices,
|
|
25
28
|
KEY_DEVICE_DECISIONS,
|
|
26
|
-
ResponsePreventedError
|
|
29
|
+
ResponsePreventedError,
|
|
30
|
+
resolveCurrentP2PSettings
|
|
27
31
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NecessaryServices } from "../../interfaces/ServiceModule.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { P2PServiceViews } from "../../p2p/P2PService.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Register manual transport commands against the focused lifecycle view.
|
|
5
5
|
* @param host
|
|
6
6
|
*/
|
|
7
|
-
export declare function useP2PReplicatorCommands(host: NecessaryServices<"API" | "setting", never>, result:
|
|
7
|
+
export declare function useP2PReplicatorCommands(host: NecessaryServices<"API" | "setting", never>, result: Pick<P2PServiceViews, "transportLifecycle">): void;
|
|
@@ -4,21 +4,19 @@ function useP2PReplicatorCommands(host, result) {
|
|
|
4
4
|
id: "p2p-establish-connection",
|
|
5
5
|
name: "P2P Sync : Connect to the Signalling Server",
|
|
6
6
|
checkCallback: (isChecking) => {
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
void replicator.open();
|
|
7
|
+
const lifecycle = result.transportLifecycle;
|
|
8
|
+
if (isChecking) return !lifecycle.isConnected;
|
|
9
|
+
void lifecycle.connect();
|
|
11
10
|
}
|
|
12
11
|
});
|
|
13
12
|
host.services.API.addCommand({
|
|
14
13
|
id: "p2p-close-connection",
|
|
15
14
|
name: "P2P Sync : Disconnect from the Signalling Server",
|
|
16
15
|
checkCallback: (isChecking) => {
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
if (isChecking) return replicator.server?.isServing ?? false;
|
|
16
|
+
const lifecycle = result.transportLifecycle;
|
|
17
|
+
if (isChecking) return lifecycle.isConnected;
|
|
20
18
|
Logger("Closing P2P Connection", LOG_LEVEL_NOTICE);
|
|
21
|
-
void
|
|
19
|
+
void lifecycle.disconnect();
|
|
22
20
|
}
|
|
23
21
|
});
|
|
24
22
|
}
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import type { NecessaryServices } from "../../interfaces/ServiceModule.js";
|
|
2
|
-
import { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator.js";
|
|
2
|
+
import type { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator.js";
|
|
3
3
|
import { type UseP2PReplicatorResult } from "./UseP2PReplicatorResult.js";
|
|
4
|
+
import { type P2PServiceViews } from "../../p2p/P2PService.js";
|
|
4
5
|
/**
|
|
5
|
-
* Factory type: given
|
|
6
|
-
*
|
|
6
|
+
* Factory type: given the compatibility Replicator and the stable service
|
|
7
|
+
* views, returns the openReplicationUI callback for that instance.
|
|
8
|
+
*
|
|
9
|
+
* The Replicator remains available for rebuild-specific compatibility work;
|
|
10
|
+
* modal lifecycle, status, and ordinary actions should use the narrow views.
|
|
11
|
+
* Injected by the host platform (for example, Obsidian). CLI and headless
|
|
12
|
+
* environments omit this.
|
|
7
13
|
*/
|
|
8
|
-
export type OpenReplicationUIFactory = (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise<boolean | void>;
|
|
14
|
+
export type OpenReplicationUIFactory = (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) => (showResult: boolean) => Promise<boolean | void>;
|
|
9
15
|
/** Same shape as OpenReplicationUIFactory, used for the rebuild/replicateAllFromServer flow. */
|
|
10
16
|
export type OpenRebuildUIFactory = OpenReplicationUIFactory;
|
|
11
17
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
18
|
+
* Compose one private P2P service context and register non-owning active
|
|
19
|
+
* provider adapters over it.
|
|
20
|
+
*
|
|
21
|
+
* The returned views remain valid for the host composition lifetime. The
|
|
22
|
+
* compatibility facade is retained only for consumers which have not yet
|
|
23
|
+
* migrated to those views.
|
|
15
24
|
* @param host
|
|
16
25
|
*/
|
|
17
26
|
export declare function useP2PReplicatorFeature(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator" | "remote", never>, openReplicationUIFactory?: OpenReplicationUIFactory, openRebuildUIFactory?: OpenRebuildUIFactory): UseP2PReplicatorResult;
|
|
@@ -1,76 +1,93 @@
|
|
|
1
|
-
import { Logger, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
|
2
1
|
import { AutoAccepting, REMOTE_P2P } from "../../common/types.js";
|
|
3
|
-
import { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator.js";
|
|
4
2
|
import {} from "./UseP2PReplicatorResult.js";
|
|
5
3
|
import { addP2PEventHandlers } from "./addP2PEventHandlers.js";
|
|
6
|
-
import {
|
|
4
|
+
import { createP2PService } from "../../p2p/P2PService.js";
|
|
5
|
+
import {
|
|
6
|
+
CAPABILITY_NOT_APPLICABLE,
|
|
7
|
+
NO_REMOTE_RESOURCE_CAPABILITIES,
|
|
8
|
+
PEER_REPLICATION_READINESS,
|
|
9
|
+
defineReplicatorProviderDefinitions,
|
|
10
|
+
outcomeFromFiniteOpenReplication,
|
|
11
|
+
replicationBlocked,
|
|
12
|
+
replicationFailed,
|
|
13
|
+
supportedCapability,
|
|
14
|
+
supportedStopActiveTransfer
|
|
15
|
+
} from "../index.js";
|
|
16
|
+
import { getP2PReplicatorConfigurationIdentity } from "./p2pReplicatorConfigurationIdentity.js";
|
|
7
17
|
function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFactory) {
|
|
8
|
-
|
|
18
|
+
const service = createP2PService({
|
|
9
19
|
services: host.services
|
|
10
20
|
});
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
get replicator() {
|
|
20
|
-
return replicator;
|
|
21
|
+
const replicator = service.compatibilityReplicator;
|
|
22
|
+
const { views, lifecycle } = service;
|
|
23
|
+
const configureReplicator = (instance) => {
|
|
24
|
+
if (openReplicationUIFactory) {
|
|
25
|
+
instance.env.openReplicationUI = openReplicationUIFactory(instance, views);
|
|
26
|
+
}
|
|
27
|
+
if (openRebuildUIFactory) {
|
|
28
|
+
instance.env.openRebuildUI = openRebuildUIFactory(instance, views);
|
|
21
29
|
}
|
|
22
30
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
replicator = newReplicator;
|
|
45
|
-
return replicator;
|
|
46
|
-
})();
|
|
47
|
-
replacementPromise = operation;
|
|
48
|
-
try {
|
|
49
|
-
return await operation;
|
|
50
|
-
} finally {
|
|
51
|
-
if (replacementPromise === operation) replacementPromise = void 0;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return void 0;
|
|
31
|
+
configureReplicator(replicator);
|
|
32
|
+
const createP2PReplicator = async () => service.createActiveReplicator();
|
|
33
|
+
const userInitiatedOneShot = async (instance, setting, request) => {
|
|
34
|
+
if (request.interaction.kind !== "permitted" || !request.interaction.permissions.peerSelection) {
|
|
35
|
+
return replicationBlocked("interaction-required");
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const result = await instance.openReplication(
|
|
39
|
+
setting,
|
|
40
|
+
false,
|
|
41
|
+
request.interaction.permissions.failureRecovery,
|
|
42
|
+
false
|
|
43
|
+
);
|
|
44
|
+
return outcomeFromFiniteOpenReplication(result);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return replicationFailed(error);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const unattendedOneShot = async (_instance, _setting, request) => {
|
|
50
|
+
if (request.interaction.kind !== "forbidden") {
|
|
51
|
+
return replicationBlocked("interaction-required");
|
|
55
52
|
}
|
|
53
|
+
return await views.targetedTransfer.synchroniseConfiguredTargets();
|
|
54
|
+
};
|
|
55
|
+
host.services.replicator.registerReplicatorProviderDefinitions(
|
|
56
|
+
defineReplicatorProviderDefinitions([REMOTE_P2P], {
|
|
57
|
+
[REMOTE_P2P]: {
|
|
58
|
+
kind: REMOTE_P2P,
|
|
59
|
+
diagnosticName: "P2P",
|
|
60
|
+
readiness: PEER_REPLICATION_READINESS,
|
|
61
|
+
isConfigured: (settings) => settings.remoteType === REMOTE_P2P && settings.P2P_Enabled,
|
|
62
|
+
configurationIdentity: getP2PReplicatorConfigurationIdentity,
|
|
63
|
+
create: createP2PReplicator,
|
|
64
|
+
remoteResources: NO_REMOTE_RESOURCE_CAPABILITIES,
|
|
65
|
+
userInitiatedOneShot: supportedCapability(userInitiatedOneShot),
|
|
66
|
+
unattendedOneShot: supportedCapability(unattendedOneShot),
|
|
67
|
+
continuous: CAPABILITY_NOT_APPLICABLE,
|
|
68
|
+
stopActiveTransfer: supportedStopActiveTransfer()
|
|
69
|
+
}
|
|
70
|
+
})
|
|
56
71
|
);
|
|
72
|
+
const activeReplicator = { replicator, ...views };
|
|
73
|
+
addP2PEventHandlers(lifecycle, host.services.context.events);
|
|
57
74
|
host.services.appLifecycle.onUnload.addHandler(async () => {
|
|
58
|
-
await
|
|
75
|
+
await lifecycle.closeForLifecycle();
|
|
59
76
|
return true;
|
|
60
77
|
});
|
|
61
78
|
host.services.appLifecycle.onSuspending.addHandler(async () => {
|
|
62
|
-
await
|
|
79
|
+
await lifecycle.closeForLifecycle();
|
|
63
80
|
return true;
|
|
64
81
|
});
|
|
65
|
-
|
|
66
|
-
await
|
|
82
|
+
const closeForDatabaseLifecycle = async () => {
|
|
83
|
+
await lifecycle.closeForLifecycle();
|
|
67
84
|
return true;
|
|
68
|
-
}
|
|
85
|
+
};
|
|
86
|
+
host.services.databaseEvents.onResetDatabase.addHandler(closeForDatabaseLifecycle);
|
|
87
|
+
host.services.databaseEvents.onCloseDatabase.addHandler(closeForDatabaseLifecycle);
|
|
88
|
+
host.services.databaseEvents.onDatabaseInitialisation.addHandler(closeForDatabaseLifecycle);
|
|
69
89
|
host.services.appLifecycle.onResumed.addHandler(() => {
|
|
70
|
-
|
|
71
|
-
if (settings.P2P_Enabled && settings.P2P_AutoStart) {
|
|
72
|
-
compatGlobal.setTimeout(() => void replicator?.open(), 100);
|
|
73
|
-
}
|
|
90
|
+
lifecycle.scheduleAutoStart();
|
|
74
91
|
return Promise.resolve(true);
|
|
75
92
|
});
|
|
76
93
|
host.services.setting.suspendExtraSync.addHandler(() => {
|
package/dist/rpc/RpcRoom.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RpcSession } from "./RpcSession.js";
|
|
2
|
-
import { type JsonLike, type RpcMethodHandler, type RpcRegisterOptions, type RpcRoomOptions } from "./types.js";
|
|
2
|
+
import { type JsonLike, type RpcCallOptions, type RpcCancellationAwareMethodHandler, type RpcMethodHandler, type RpcRegisterOptions, type RpcRoomOptions } from "./types.js";
|
|
3
3
|
export declare class RpcRoom {
|
|
4
4
|
private options;
|
|
5
5
|
private pending;
|
|
@@ -11,12 +11,26 @@ export declare class RpcRoom {
|
|
|
11
11
|
private incomingChunkTimers;
|
|
12
12
|
private peerVersion;
|
|
13
13
|
private disposers;
|
|
14
|
+
private closed;
|
|
14
15
|
constructor(options: RpcRoomOptions);
|
|
15
16
|
close(): void;
|
|
16
17
|
session(peerId: string): RpcSession;
|
|
17
18
|
register<T extends JsonLike[], U>(method: string, handler: RpcMethodHandler<T, U>, options?: RpcRegisterOptions): void;
|
|
19
|
+
/**
|
|
20
|
+
* Register a handler which receives an AbortSignal for cooperative
|
|
21
|
+
* cancellation. The original `register` API deliberately remains
|
|
22
|
+
* unchanged for handlers which do not need request context.
|
|
23
|
+
*/
|
|
24
|
+
registerCancellable<T extends JsonLike[], U>(method: string, handler: RpcCancellationAwareMethodHandler<T, U>, options?: RpcRegisterOptions): void;
|
|
25
|
+
private registerMethod;
|
|
18
26
|
invoke(peerId: string, method: string, args: JsonLike[], timeoutMs?: number): Promise<JsonLike>;
|
|
27
|
+
invoke(peerId: string, method: string, args: JsonLike[], options?: RpcCallOptions): Promise<JsonLike>;
|
|
28
|
+
invoke(peerId: string, method: string, args: JsonLike[], timeoutOrOptions?: number | RpcCallOptions): Promise<JsonLike>;
|
|
19
29
|
cancel(peerId: string, requestId: string): Promise<void>;
|
|
30
|
+
private removePending;
|
|
31
|
+
private rejectPending;
|
|
32
|
+
private notifyRemoteCancellation;
|
|
33
|
+
private abortInboundCall;
|
|
20
34
|
private sendEnvelope;
|
|
21
35
|
private scheduleMissingAck;
|
|
22
36
|
private onWireMessage;
|
package/dist/rpc/RpcRoom.js
CHANGED
|
@@ -24,6 +24,7 @@ class RpcRoom {
|
|
|
24
24
|
incomingChunkTimers = /* @__PURE__ */ new Map();
|
|
25
25
|
peerVersion = /* @__PURE__ */ new Map();
|
|
26
26
|
disposers = [];
|
|
27
|
+
closed = false;
|
|
27
28
|
constructor(options) {
|
|
28
29
|
this.options = {
|
|
29
30
|
maxWirePayloadBytes: options.maxWirePayloadBytes ?? 32 * 1024,
|
|
@@ -49,6 +50,18 @@ class RpcRoom {
|
|
|
49
50
|
if (this.options.transport.onPeerLeave) {
|
|
50
51
|
this.disposers.push(
|
|
51
52
|
this.options.transport.onPeerLeave((peerId) => {
|
|
53
|
+
this.pending.forEach((pending, requestId) => {
|
|
54
|
+
if (pending.peerId !== peerId) return;
|
|
55
|
+
this.rejectPending(requestId, new RpcError("NOT_CONNECTED", "Peer disconnected"));
|
|
56
|
+
});
|
|
57
|
+
this.inboundCalls.forEach((context, requestId) => {
|
|
58
|
+
if (context.peerId !== peerId) return;
|
|
59
|
+
this.abortInboundCall(requestId, context);
|
|
60
|
+
this.inboundCalls.delete(requestId);
|
|
61
|
+
});
|
|
62
|
+
this.outgoingChunkMap.forEach((state, streamId) => {
|
|
63
|
+
if (state.peerId === peerId) this.outgoingChunkMap.delete(streamId);
|
|
64
|
+
});
|
|
52
65
|
this.sessions.delete(peerId);
|
|
53
66
|
this.peerVersion.delete(peerId);
|
|
54
67
|
})
|
|
@@ -56,9 +69,15 @@ class RpcRoom {
|
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
close() {
|
|
72
|
+
if (this.closed) return;
|
|
73
|
+
this.closed = true;
|
|
59
74
|
this.disposers.splice(0).forEach((dispose) => dispose());
|
|
60
|
-
this.pending.forEach((
|
|
61
|
-
|
|
75
|
+
this.pending.forEach((_pending, requestId) => {
|
|
76
|
+
this.rejectPending(requestId, new RpcError("NOT_CONNECTED", "Room closed"), true);
|
|
77
|
+
});
|
|
78
|
+
this.inboundCalls.forEach((context, requestId) => {
|
|
79
|
+
this.abortInboundCall(requestId, context);
|
|
80
|
+
});
|
|
62
81
|
this.inboundCalls.clear();
|
|
63
82
|
this.methods.clear();
|
|
64
83
|
this.sessions.clear();
|
|
@@ -75,44 +94,131 @@ class RpcRoom {
|
|
|
75
94
|
return s;
|
|
76
95
|
}
|
|
77
96
|
register(method, handler, options = {}) {
|
|
97
|
+
this.registerMethod(method, handler, false, options);
|
|
98
|
+
}
|
|
99
|
+
registerCancellable(method, handler, options = {}) {
|
|
100
|
+
this.registerMethod(method, handler, true, options);
|
|
101
|
+
}
|
|
102
|
+
registerMethod(method, handler, cancellationAware, options) {
|
|
78
103
|
if (!validNamespacedMethod(method)) {
|
|
79
104
|
throw new RpcError("PROTOCOL_ERROR", `Method must be namespaced: ${method}`);
|
|
80
105
|
}
|
|
81
106
|
this.methods.set(method, {
|
|
82
107
|
handler,
|
|
108
|
+
cancellationAware,
|
|
83
109
|
serial: options.serial ?? false,
|
|
84
110
|
queue: Promise.resolve()
|
|
85
111
|
});
|
|
86
112
|
}
|
|
87
|
-
async invoke(peerId, method, args,
|
|
113
|
+
async invoke(peerId, method, args, timeoutOrOptions = 3e4) {
|
|
88
114
|
if (!validNamespacedMethod(method)) {
|
|
89
115
|
throw new RpcError("PROTOCOL_ERROR", `Method must be namespaced: ${method}`);
|
|
90
116
|
}
|
|
117
|
+
if (this.closed) {
|
|
118
|
+
throw new RpcError("NOT_CONNECTED", "Room is closed");
|
|
119
|
+
}
|
|
120
|
+
const options = typeof timeoutOrOptions === "number" ? { timeoutMs: timeoutOrOptions } : timeoutOrOptions ?? {};
|
|
121
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
122
|
+
if (options.signal?.aborted) {
|
|
123
|
+
throw new RpcError("CANCELLED", `RPC cancelled: ${method}`);
|
|
124
|
+
}
|
|
91
125
|
const requestId = newId("req");
|
|
126
|
+
let pending;
|
|
92
127
|
const p = new Promise((resolve, reject) => {
|
|
93
|
-
|
|
128
|
+
pending = {
|
|
129
|
+
peerId,
|
|
130
|
+
method,
|
|
131
|
+
resolve,
|
|
132
|
+
reject,
|
|
133
|
+
signal: options.signal,
|
|
134
|
+
requestSent: false,
|
|
135
|
+
cancelRequested: false
|
|
136
|
+
};
|
|
94
137
|
if (timeoutMs > 0) {
|
|
95
138
|
pending.timeoutHandle = compatGlobal.setTimeout(() => {
|
|
96
|
-
this.
|
|
97
|
-
reject(new RpcError("TIMEOUT", `RPC timed out: ${method}`));
|
|
139
|
+
this.rejectPending(requestId, new RpcError("TIMEOUT", `RPC timed out: ${method}`), true);
|
|
98
140
|
}, timeoutMs);
|
|
99
141
|
}
|
|
100
142
|
this.pending.set(requestId, pending);
|
|
143
|
+
if (options.signal) {
|
|
144
|
+
const onAbort = () => {
|
|
145
|
+
const current = this.pending.get(requestId);
|
|
146
|
+
if (!current) return;
|
|
147
|
+
this.rejectPending(requestId, new RpcError("CANCELLED", `RPC cancelled: ${method}`), true);
|
|
148
|
+
};
|
|
149
|
+
pending.abortListener = onAbort;
|
|
150
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
151
|
+
if (options.signal.aborted) onAbort();
|
|
152
|
+
}
|
|
101
153
|
});
|
|
102
|
-
|
|
154
|
+
const sendTask = this.sendEnvelope(peerId, {
|
|
103
155
|
kind: "request",
|
|
104
156
|
requestId,
|
|
105
157
|
method,
|
|
106
158
|
args
|
|
107
159
|
});
|
|
160
|
+
void sendTask.then(
|
|
161
|
+
() => {
|
|
162
|
+
const current = this.pending.get(requestId);
|
|
163
|
+
pending.requestSent = true;
|
|
164
|
+
if (!current && pending.cancelRequested) {
|
|
165
|
+
this.notifyRemoteCancellation(peerId, requestId);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
(ex) => {
|
|
169
|
+
const current = this.pending.get(requestId);
|
|
170
|
+
if (!current) return;
|
|
171
|
+
const failed = this.removePending(requestId);
|
|
172
|
+
if (failed) failed.reject(ex);
|
|
173
|
+
}
|
|
174
|
+
);
|
|
108
175
|
return await p;
|
|
109
176
|
}
|
|
110
177
|
async cancel(peerId, requestId) {
|
|
178
|
+
const pending = this.pending.get(requestId);
|
|
179
|
+
if (pending?.peerId === peerId) {
|
|
180
|
+
const requestSent = pending.requestSent;
|
|
181
|
+
pending.cancelRequested = true;
|
|
182
|
+
this.rejectPending(requestId, new RpcError("CANCELLED", "Invocation cancelled"));
|
|
183
|
+
if (!requestSent) return;
|
|
184
|
+
}
|
|
111
185
|
await this.sendEnvelope(peerId, {
|
|
112
186
|
kind: "cancel",
|
|
113
187
|
requestId
|
|
114
188
|
});
|
|
115
189
|
}
|
|
190
|
+
removePending(requestId) {
|
|
191
|
+
const pending = this.pending.get(requestId);
|
|
192
|
+
if (!pending) return void 0;
|
|
193
|
+
this.pending.delete(requestId);
|
|
194
|
+
if (pending.timeoutHandle !== void 0) {
|
|
195
|
+
compatGlobal.clearTimeout(pending.timeoutHandle);
|
|
196
|
+
}
|
|
197
|
+
if (pending.signal && pending.abortListener) {
|
|
198
|
+
pending.signal.removeEventListener("abort", pending.abortListener);
|
|
199
|
+
}
|
|
200
|
+
return pending;
|
|
201
|
+
}
|
|
202
|
+
rejectPending(requestId, error, notifyRemote = false) {
|
|
203
|
+
const pending = this.pending.get(requestId);
|
|
204
|
+
if (!pending) return;
|
|
205
|
+
if (notifyRemote) pending.cancelRequested = true;
|
|
206
|
+
const removed = this.removePending(requestId);
|
|
207
|
+
if (!removed) return;
|
|
208
|
+
removed.reject(error);
|
|
209
|
+
if (notifyRemote && removed.requestSent) this.notifyRemoteCancellation(removed.peerId, requestId);
|
|
210
|
+
}
|
|
211
|
+
notifyRemoteCancellation(peerId, requestId) {
|
|
212
|
+
void this.sendEnvelope(peerId, { kind: "cancel", requestId }).catch((error) => {
|
|
213
|
+
this.options.onProtocolWarning?.(`Failed to send RPC cancellation: ${String(error)}`, peerId);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
abortInboundCall(requestId, context) {
|
|
217
|
+
if (this.inboundCalls.get(requestId) !== context) return;
|
|
218
|
+
if (!context.signal.aborted) {
|
|
219
|
+
context.controller.abort(new RpcError("CANCELLED", "Invocation cancelled"));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
116
222
|
async sendEnvelope(peerId, envelope) {
|
|
117
223
|
const serialized = JSON.stringify(envelope);
|
|
118
224
|
if (estimateBytes(serialized) <= this.options.maxWirePayloadBytes) {
|
|
@@ -214,36 +320,61 @@ class RpcRoom {
|
|
|
214
320
|
}
|
|
215
321
|
if (envelope.kind === "cancel") {
|
|
216
322
|
const ctx2 = this.inboundCalls.get(envelope.requestId);
|
|
217
|
-
if (ctx2
|
|
323
|
+
if (ctx2 && ctx2.peerId === peerId) this.abortInboundCall(envelope.requestId, ctx2);
|
|
218
324
|
return;
|
|
219
325
|
}
|
|
220
326
|
if (envelope.kind === "response") {
|
|
221
327
|
const pending = this.pending.get(envelope.requestId);
|
|
222
328
|
if (!pending) return;
|
|
223
|
-
this.
|
|
224
|
-
if (
|
|
329
|
+
const settled = this.removePending(envelope.requestId);
|
|
330
|
+
if (!settled) return;
|
|
225
331
|
if (envelope.ok === true) {
|
|
226
|
-
|
|
332
|
+
settled.resolve(envelope.data);
|
|
227
333
|
} else {
|
|
228
|
-
|
|
334
|
+
settled.reject(new RpcError(envelope.error.code, envelope.error.message, envelope.error.details));
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const controller = new AbortController();
|
|
339
|
+
const ctx = { controller, signal: controller.signal, peerId };
|
|
340
|
+
this.inboundCalls.set(envelope.requestId, ctx);
|
|
341
|
+
let accepted;
|
|
342
|
+
try {
|
|
343
|
+
accepted = await this.options.canAcceptRequest?.(peerId, envelope.method, ctx);
|
|
344
|
+
} catch (ex) {
|
|
345
|
+
this.inboundCalls.delete(envelope.requestId);
|
|
346
|
+
if (!ctx.signal.aborted && !this.closed) {
|
|
347
|
+
await this.sendEnvelope(peerId, {
|
|
348
|
+
kind: "response",
|
|
349
|
+
requestId: envelope.requestId,
|
|
350
|
+
ok: false,
|
|
351
|
+
error: asRpcErrorShape(ex)
|
|
352
|
+
});
|
|
229
353
|
}
|
|
230
354
|
return;
|
|
231
355
|
}
|
|
232
|
-
const accepted = await this.options.canAcceptRequest?.(peerId, envelope.method);
|
|
233
356
|
if (accepted === false) {
|
|
357
|
+
this.inboundCalls.delete(envelope.requestId);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (ctx.signal.aborted || this.closed) {
|
|
361
|
+
this.inboundCalls.delete(envelope.requestId);
|
|
234
362
|
return;
|
|
235
363
|
}
|
|
236
364
|
const version = this.peerVersion.get(peerId);
|
|
237
365
|
if (version && version.major !== RPC_VERSION_MAJOR) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
366
|
+
if (!ctx.signal.aborted && !this.closed) {
|
|
367
|
+
await this.sendEnvelope(peerId, {
|
|
368
|
+
kind: "response",
|
|
369
|
+
requestId: envelope.requestId,
|
|
370
|
+
ok: false,
|
|
371
|
+
error: {
|
|
372
|
+
code: "REMOTE_ERROR",
|
|
373
|
+
message: `RPC major mismatch: local=${RPC_VERSION_MAJOR}, remote=${version.major}`
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
this.inboundCalls.delete(envelope.requestId);
|
|
247
378
|
return;
|
|
248
379
|
}
|
|
249
380
|
if (version && version.minor !== RPC_VERSION_MINOR) {
|
|
@@ -254,28 +385,29 @@ class RpcRoom {
|
|
|
254
385
|
}
|
|
255
386
|
const method = this.methods.get(envelope.method);
|
|
256
387
|
if (!method) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
388
|
+
if (!ctx.signal.aborted && !this.closed) {
|
|
389
|
+
await this.sendEnvelope(peerId, {
|
|
390
|
+
kind: "response",
|
|
391
|
+
requestId: envelope.requestId,
|
|
392
|
+
ok: false,
|
|
393
|
+
error: {
|
|
394
|
+
code: "REMOTE_ERROR",
|
|
395
|
+
message: `Method not found: ${envelope.method}`
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
this.inboundCalls.delete(envelope.requestId);
|
|
266
400
|
return;
|
|
267
401
|
}
|
|
268
|
-
const ctx = { cancelled: false };
|
|
269
|
-
this.inboundCalls.set(envelope.requestId, ctx);
|
|
270
402
|
const runner = async () => {
|
|
271
403
|
try {
|
|
272
|
-
if (ctx.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
404
|
+
if (ctx.signal.aborted || this.closed) return;
|
|
405
|
+
const data = method.cancellationAware ? await method.handler(
|
|
406
|
+
ctx,
|
|
407
|
+
peerId,
|
|
408
|
+
...envelope.args
|
|
409
|
+
) : await method.handler(peerId, ...envelope.args);
|
|
410
|
+
if (ctx.signal.aborted || this.closed) return;
|
|
279
411
|
await this.sendEnvelope(peerId, {
|
|
280
412
|
kind: "response",
|
|
281
413
|
requestId: envelope.requestId,
|
|
@@ -283,6 +415,7 @@ class RpcRoom {
|
|
|
283
415
|
data
|
|
284
416
|
});
|
|
285
417
|
} catch (ex) {
|
|
418
|
+
if (ctx.signal.aborted || this.closed) return;
|
|
286
419
|
await this.sendEnvelope(peerId, {
|
|
287
420
|
kind: "response",
|
|
288
421
|
requestId: envelope.requestId,
|
|
@@ -290,7 +423,9 @@ class RpcRoom {
|
|
|
290
423
|
error: asRpcErrorShape(ex)
|
|
291
424
|
});
|
|
292
425
|
} finally {
|
|
293
|
-
this.inboundCalls.
|
|
426
|
+
if (this.inboundCalls.get(envelope.requestId) === ctx) {
|
|
427
|
+
this.inboundCalls.delete(envelope.requestId);
|
|
428
|
+
}
|
|
294
429
|
}
|
|
295
430
|
};
|
|
296
431
|
if (method.serial) {
|
package/dist/rpc/RpcSession.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { JsonLike } from "./types.js";
|
|
1
|
+
import type { JsonLike, RpcCallOptions } from "./types.js";
|
|
2
2
|
import type { RpcRoom } from "./RpcRoom.js";
|
|
3
3
|
export declare class RpcSession {
|
|
4
4
|
readonly peerId: string;
|
|
5
5
|
private room;
|
|
6
6
|
constructor(room: RpcRoom, peerId: string);
|
|
7
7
|
call<T = JsonLike>(method: string, args?: JsonLike[], timeoutMs?: number): Promise<T>;
|
|
8
|
+
call<T = JsonLike>(method: string, args?: JsonLike[], options?: RpcCallOptions): Promise<T>;
|
|
9
|
+
call<T = JsonLike>(method: string, args: JsonLike[] | undefined, timeoutOrOptions?: number | RpcCallOptions): Promise<T>;
|
|
8
10
|
createProxy<T extends object>(namespace: string): T;
|
|
9
11
|
}
|
package/dist/rpc/RpcSession.js
CHANGED
|
@@ -6,11 +6,11 @@ class RpcSession {
|
|
|
6
6
|
this.room = room;
|
|
7
7
|
this.peerId = peerId;
|
|
8
8
|
}
|
|
9
|
-
async call(method, args = [],
|
|
9
|
+
async call(method, args = [], timeoutOrOptions) {
|
|
10
10
|
if (!this.peerId) {
|
|
11
11
|
throw new RpcError("NOT_CONNECTED", "Peer is not connected");
|
|
12
12
|
}
|
|
13
|
-
return await this.room.invoke(this.peerId, method, args,
|
|
13
|
+
return await this.room.invoke(this.peerId, method, args, timeoutOrOptions);
|
|
14
14
|
}
|
|
15
15
|
createProxy(namespace) {
|
|
16
16
|
const session = this;
|
package/dist/rpc/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export { RpcSession } from "./RpcSession.js";
|
|
|
3
3
|
export { RpcError } from "./errors.js";
|
|
4
4
|
export { exposeDB } from "./pouchdb/RpcPouchDBServer.js";
|
|
5
5
|
export { RpcPouchDBProxy } from "./pouchdb/RpcPouchDBProxy.js";
|
|
6
|
-
export type { JsonLike, RpcEnvelope, RpcErrorCode, RpcErrorShape, RpcMethodHandler, RpcRegisterOptions, RpcRoomOptions, RpcWireMessage, TransportAdapter, } from "./types.js";
|
|
6
|
+
export type { JsonLike, RpcEnvelope, RpcErrorCode, RpcErrorShape, RpcCallOptions, RpcCancellationAwareMethodHandler, RpcMethodHandler, RpcRegisterOptions, RpcRequestContext, RpcRoomOptions, RpcWireMessage, TransportAdapter, } from "./types.js";
|
|
@@ -2,6 +2,7 @@ import { P2PConnectionPaths } from "../../common/models/setting.const.js";
|
|
|
2
2
|
import {
|
|
3
3
|
hasValidP2PTurnServerUrl,
|
|
4
4
|
normaliseP2PConnectionPath,
|
|
5
|
+
splitP2PRelayUrls,
|
|
5
6
|
splitP2PTurnServerUrls
|
|
6
7
|
} from "../../common/models/setting.p2p.js";
|
|
7
8
|
import { mixedHash } from "octagonal-wheels/hash/purejs";
|
|
@@ -10,7 +11,7 @@ import { createDiagRTCPeerConnectionConstructor } from "./DiagRTCPeerConnections
|
|
|
10
11
|
function generateJoinRoomOptions(settings) {
|
|
11
12
|
const passphraseNumbers = mixedHash(settings.P2P_passphrase, 0);
|
|
12
13
|
const passphrase = passphraseNumbers[0].toString(36) + passphraseNumbers[1].toString(36);
|
|
13
|
-
const relays = settings.P2P_relays
|
|
14
|
+
const relays = splitP2PRelayUrls(settings.P2P_relays);
|
|
14
15
|
const turnServers = splitP2PTurnServerUrls(settings.P2P_turnServers);
|
|
15
16
|
const relayConfig = {
|
|
16
17
|
manualReconnection: true,
|