@peers-app/peers-device 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connection-manager/connection-manager.d.ts +5 -19
- package/dist/connection-manager/connection-manager.js +25 -466
- package/dist/connection-manager/connection-manager.js.map +1 -1
- package/dist/connection-manager/connection-manager.test.js +195 -0
- package/dist/connection-manager/connection-manager.test.js.map +1 -1
- package/dist/connection-manager/device-messages.d.ts +51 -0
- package/dist/connection-manager/device-messages.js +516 -0
- package/dist/connection-manager/device-messages.js.map +1 -0
- package/dist/connection-manager/hops-map.d.ts +38 -0
- package/dist/connection-manager/hops-map.js +149 -0
- package/dist/connection-manager/hops-map.js.map +1 -0
- package/dist/connection-manager/hops-map.test.d.ts +1 -0
- package/dist/connection-manager/hops-map.test.js +225 -0
- package/dist/connection-manager/hops-map.test.js.map +1 -0
- package/dist/connection-manager/network-manager.js +3 -3
- package/dist/connection-manager/network-manager.js.map +1 -1
- package/dist/local.data-source.d.ts +12 -0
- package/dist/local.data-source.js +37 -3
- package/dist/local.data-source.js.map +1 -1
- package/dist/sync-group.js +5 -9
- package/dist/sync-group.js.map +1 -1
- package/dist/tracked-data-source.d.ts +5 -0
- package/dist/tracked-data-source.js +141 -33
- package/dist/tracked-data-source.js.map +1 -1
- package/dist/tracked-data-source.test.js +160 -1
- package/dist/tracked-data-source.test.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,41 +1,35 @@
|
|
|
1
1
|
import { ChangeTrackingTable, Connection, DataContext, Device, IDeviceMessage, IFileChunkInfo, INetworkInfo, Observable, UserContext } from "@peers-app/peers-sdk";
|
|
2
2
|
import { ChunkDownloadManager } from "../chunk-download-manager";
|
|
3
3
|
import { SyncGroup } from "../sync-group";
|
|
4
|
+
import { DeviceMessages } from "./device-messages";
|
|
4
5
|
interface IDownloadDevice {
|
|
5
6
|
userId: string;
|
|
6
7
|
deviceId: string;
|
|
7
8
|
getFileChunkInfo(chunkHash: string): Promise<IFileChunkInfo>;
|
|
8
9
|
downloadFileChunk(chunkHash: string): Promise<Uint8Array | null>;
|
|
9
10
|
}
|
|
10
|
-
export
|
|
11
|
+
export { TTL0 } from "./device-messages";
|
|
11
12
|
export declare class ConnectionManager {
|
|
12
13
|
readonly userContext: UserContext;
|
|
13
14
|
readonly changeTrackingTableFactory: (groupId?: string) => ChangeTrackingTable;
|
|
14
15
|
private readonly localDevice;
|
|
15
16
|
private readonly getGroupSecretKey;
|
|
16
17
|
MAX_CONNECTIONS: number;
|
|
17
|
-
MAX_CONNECTIONS_TO_FORWARD_MESSAGES_TO: number;
|
|
18
|
-
|
|
18
|
+
get MAX_CONNECTIONS_TO_FORWARD_MESSAGES_TO(): number;
|
|
19
|
+
set MAX_CONNECTIONS_TO_FORWARD_MESSAGES_TO(value: number);
|
|
19
20
|
readonly chunkDownloadManager: ChunkDownloadManager;
|
|
21
|
+
readonly deviceMessages: DeviceMessages;
|
|
20
22
|
private syncGroups;
|
|
21
23
|
private connectionStates;
|
|
22
24
|
private deviceSyncConnections;
|
|
23
25
|
private groupMemberSubscriptionCleanups;
|
|
24
26
|
private allConnections;
|
|
25
|
-
private cleanOldDeviceMessagesSeenInterval;
|
|
26
27
|
private pingInterval;
|
|
27
|
-
private cleanupExpiredAliasesInterval;
|
|
28
|
-
private userConnectCodeOfferSubscription;
|
|
29
|
-
private userConnectCodeAnswerSubscription;
|
|
30
|
-
private deviceMessageHandlers;
|
|
31
28
|
private connectionMetrics;
|
|
32
29
|
readonly onConnectionAdded: Observable<Connection | undefined>;
|
|
33
30
|
readonly onConnectionRemoved: Observable<Connection | undefined>;
|
|
34
31
|
readonly onUserConnectExchangeFinished: Observable<string | undefined>;
|
|
35
32
|
constructor(userContext: UserContext, changeTrackingTableFactory: (groupId?: string) => ChangeTrackingTable, localDevice: Device, getGroupSecretKey: (groupId: string) => Promise<string>);
|
|
36
|
-
userConnectOffer(userConnectCode: string): Promise<void>;
|
|
37
|
-
private userConnectAnswer;
|
|
38
|
-
cleanOldDeviceMessagesSeen(): void;
|
|
39
33
|
private pingAllConnections;
|
|
40
34
|
/**
|
|
41
35
|
* Register a handler for device messages with a specific payload type
|
|
@@ -59,13 +53,7 @@ export declare class ConnectionManager {
|
|
|
59
53
|
getFileChunkInfo(chunkHash: string): Promise<IFileChunkInfo>;
|
|
60
54
|
getDownloadDevices(): IDownloadDevice[];
|
|
61
55
|
removeLeastPreferredConnection(): Promise<void>;
|
|
62
|
-
private secureDeviceMessagePayload;
|
|
63
|
-
private validateDeviceMessagePayload;
|
|
64
|
-
private readonly deviceMessagesSeen;
|
|
65
|
-
private readonly deviceAliases;
|
|
66
|
-
private readonly deviceAliasesExpires;
|
|
67
56
|
sendDeviceMessage(partialMessage: Pick<IDeviceMessage, 'toDeviceId' | 'dataContextId' | 'payload'> & Partial<IDeviceMessage>): Promise<any>;
|
|
68
|
-
private forwardDeviceMessageAndReturnFirstResponse;
|
|
69
57
|
/**
|
|
70
58
|
* this is used for testing setup, it's not intended for normal use
|
|
71
59
|
*/
|
|
@@ -76,7 +64,5 @@ export declare class ConnectionManager {
|
|
|
76
64
|
pathType: string;
|
|
77
65
|
throughDeviceIds: string[];
|
|
78
66
|
};
|
|
79
|
-
cleanupExpiredAliases(): void;
|
|
80
67
|
dispose(): Promise<void>;
|
|
81
68
|
}
|
|
82
|
-
export {};
|
|
@@ -7,8 +7,9 @@ const chunk_download_manager_1 = require("../chunk-download-manager");
|
|
|
7
7
|
const machine_stats_1 = require("../machine-stats");
|
|
8
8
|
const sync_group_1 = require("../sync-group");
|
|
9
9
|
const connection_manager_priorities_1 = require("./connection-manager-priorities");
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const device_messages_1 = require("./device-messages");
|
|
11
|
+
var device_messages_2 = require("./device-messages");
|
|
12
|
+
Object.defineProperty(exports, "TTL0", { enumerable: true, get: function () { return device_messages_2.TTL0; } });
|
|
12
13
|
class ConnectionManager {
|
|
13
14
|
userContext;
|
|
14
15
|
changeTrackingTableFactory;
|
|
@@ -16,20 +17,20 @@ class ConnectionManager {
|
|
|
16
17
|
getGroupSecretKey;
|
|
17
18
|
// TODO set this based on device type (desktops and servers should be able to handle much more than the default of 30)
|
|
18
19
|
MAX_CONNECTIONS = peers_sdk_1.PeerDeviceConsts.MAX_CONNECTIONS;
|
|
19
|
-
MAX_CONNECTIONS_TO_FORWARD_MESSAGES_TO
|
|
20
|
-
|
|
20
|
+
get MAX_CONNECTIONS_TO_FORWARD_MESSAGES_TO() {
|
|
21
|
+
return this.deviceMessages.MAX_CONNECTIONS_TO_FORWARD_MESSAGES_TO;
|
|
22
|
+
}
|
|
23
|
+
set MAX_CONNECTIONS_TO_FORWARD_MESSAGES_TO(value) {
|
|
24
|
+
this.deviceMessages.MAX_CONNECTIONS_TO_FORWARD_MESSAGES_TO = value;
|
|
25
|
+
}
|
|
21
26
|
chunkDownloadManager;
|
|
27
|
+
deviceMessages;
|
|
22
28
|
syncGroups = {};
|
|
23
29
|
connectionStates = new Map();
|
|
24
30
|
deviceSyncConnections = new Map();
|
|
25
31
|
groupMemberSubscriptionCleanups = new Map();
|
|
26
32
|
allConnections = {};
|
|
27
|
-
cleanOldDeviceMessagesSeenInterval;
|
|
28
33
|
pingInterval;
|
|
29
|
-
cleanupExpiredAliasesInterval;
|
|
30
|
-
userConnectCodeOfferSubscription;
|
|
31
|
-
userConnectCodeAnswerSubscription;
|
|
32
|
-
deviceMessageHandlers = new Map();
|
|
33
34
|
connectionMetrics = new Map();
|
|
34
35
|
onConnectionAdded = (0, peers_sdk_1.observable)();
|
|
35
36
|
onConnectionRemoved = (0, peers_sdk_1.observable)();
|
|
@@ -40,128 +41,21 @@ class ConnectionManager {
|
|
|
40
41
|
this.localDevice = localDevice;
|
|
41
42
|
this.getGroupSecretKey = getGroupSecretKey;
|
|
42
43
|
this.chunkDownloadManager = new chunk_download_manager_1.ChunkDownloadManager(this);
|
|
44
|
+
this.deviceMessages = new device_messages_1.DeviceMessages({
|
|
45
|
+
userContext,
|
|
46
|
+
localDevice,
|
|
47
|
+
getGroupSecretKey,
|
|
48
|
+
getAllConnections: () => this.allConnections,
|
|
49
|
+
getConnectionStates: () => this.connectionStates,
|
|
50
|
+
getSyncGroups: () => this.syncGroups,
|
|
51
|
+
getPeerGroupDevice: (dataContext) => this.getPeerGroupDevice(dataContext),
|
|
52
|
+
onUserConnectExchangeFinished: (deviceId) => this.onUserConnectExchangeFinished(deviceId),
|
|
53
|
+
});
|
|
43
54
|
this.setupGroupsSubscriptions();
|
|
44
|
-
this.cleanOldDeviceMessagesSeenInterval = setInterval(() => {
|
|
45
|
-
this.cleanOldDeviceMessagesSeen();
|
|
46
|
-
}, 60 * 60 * 1000); // every hour
|
|
47
55
|
// Ping all connections every 15 seconds
|
|
48
56
|
this.pingInterval = setInterval(() => {
|
|
49
57
|
this.pingAllConnections();
|
|
50
58
|
}, 15_000);
|
|
51
|
-
// setup user connect code subscriptions
|
|
52
|
-
this.userConnectCodeOfferSubscription = peers_sdk_1.userConnectCodeOffer.subscribe(() => {
|
|
53
|
-
if ((0, peers_sdk_1.userConnectCodeOffer)()) {
|
|
54
|
-
const { alias } = (0, peers_sdk_1.parseConnectionCode)((0, peers_sdk_1.userConnectCodeOffer)());
|
|
55
|
-
this.sendDeviceMessage({
|
|
56
|
-
toDeviceId: 'device-alias',
|
|
57
|
-
dataContextId: '',
|
|
58
|
-
payload: alias,
|
|
59
|
-
ttl: 3,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
this.userConnectCodeAnswerSubscription = peers_sdk_1.userConnectCodeAnswer.subscribe(() => {
|
|
64
|
-
if ((0, peers_sdk_1.userConnectCodeAnswer)()) {
|
|
65
|
-
this.userConnectOffer((0, peers_sdk_1.userConnectCodeAnswer)());
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
this.cleanupExpiredAliasesInterval = setInterval(() => {
|
|
69
|
-
this.cleanupExpiredAliases();
|
|
70
|
-
}, 60_0000);
|
|
71
|
-
}
|
|
72
|
-
async userConnectOffer(userConnectCode) {
|
|
73
|
-
const { alias, secret } = (0, peers_sdk_1.parseConnectionCode)(userConnectCode);
|
|
74
|
-
const me = await (0, peers_sdk_1.getMe)();
|
|
75
|
-
const myUserConnectInfo = {
|
|
76
|
-
userId: me.userId,
|
|
77
|
-
name: me.name,
|
|
78
|
-
publicKey: me.publicKey,
|
|
79
|
-
publicBoxKey: me.publicBoxKey,
|
|
80
|
-
deviceId: this.userContext.deviceId(),
|
|
81
|
-
};
|
|
82
|
-
const response = await this.sendDeviceMessage({
|
|
83
|
-
toDeviceId: alias,
|
|
84
|
-
dataContextId: 'user-connect',
|
|
85
|
-
payload: (0, peers_sdk_1.encryptWithSecret)(myUserConnectInfo, secret),
|
|
86
|
-
});
|
|
87
|
-
if (response === exports.TTL0) {
|
|
88
|
-
(0, peers_sdk_1.userConnectStatus)('Error: Could not find device, check connection code and try again');
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
const remoteUserConnectInfo = (0, peers_sdk_1.decryptWithSecret)(response.payload, secret);
|
|
92
|
-
const usersTable = (0, peers_sdk_1.Users)(this.userContext.userDataContext);
|
|
93
|
-
await usersTable.save({
|
|
94
|
-
userId: remoteUserConnectInfo.userId,
|
|
95
|
-
name: remoteUserConnectInfo.name || remoteUserConnectInfo.userId,
|
|
96
|
-
publicKey: remoteUserConnectInfo.publicKey,
|
|
97
|
-
publicBoxKey: remoteUserConnectInfo.publicBoxKey,
|
|
98
|
-
}, { restoreIfDeleted: true, weakInsert: true });
|
|
99
|
-
const devicesTable = (0, peers_sdk_1.Devices)(this.userContext.userDataContext);
|
|
100
|
-
await devicesTable.save({
|
|
101
|
-
deviceId: remoteUserConnectInfo.deviceId,
|
|
102
|
-
userId: remoteUserConnectInfo.userId,
|
|
103
|
-
firstSeen: new Date(),
|
|
104
|
-
lastSeen: new Date(),
|
|
105
|
-
trustLevel: peers_sdk_1.TrustLevel.NewDevice,
|
|
106
|
-
}, { restoreIfDeleted: true, weakInsert: true });
|
|
107
|
-
this.onUserConnectExchangeFinished(remoteUserConnectInfo.deviceId);
|
|
108
|
-
(0, peers_sdk_1.userConnectStatus)(remoteUserConnectInfo.userId);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
async userConnectAnswer(message) {
|
|
112
|
-
try {
|
|
113
|
-
const userConnectCode = (0, peers_sdk_1.userConnectCodeOffer)();
|
|
114
|
-
if (!userConnectCode) {
|
|
115
|
-
throw new Error('Device is not waiting for a user connection, check connection code and try again');
|
|
116
|
-
}
|
|
117
|
-
const parsedCode = (0, peers_sdk_1.parseConnectionCode)(userConnectCode);
|
|
118
|
-
const remoteUserConnectInfo = (0, peers_sdk_1.decryptWithSecret)(message.payload, parsedCode.secret);
|
|
119
|
-
const usersTable = (0, peers_sdk_1.Users)(this.userContext.userDataContext);
|
|
120
|
-
await usersTable.save({
|
|
121
|
-
userId: remoteUserConnectInfo.userId,
|
|
122
|
-
name: remoteUserConnectInfo.name || remoteUserConnectInfo.userId,
|
|
123
|
-
publicKey: remoteUserConnectInfo.publicKey,
|
|
124
|
-
publicBoxKey: remoteUserConnectInfo.publicBoxKey,
|
|
125
|
-
}, { restoreIfDeleted: true, weakInsert: true });
|
|
126
|
-
const devicesTable = (0, peers_sdk_1.Devices)(this.userContext.userDataContext);
|
|
127
|
-
await devicesTable.save({
|
|
128
|
-
deviceId: remoteUserConnectInfo.deviceId,
|
|
129
|
-
userId: remoteUserConnectInfo.userId,
|
|
130
|
-
firstSeen: new Date(),
|
|
131
|
-
lastSeen: new Date(),
|
|
132
|
-
trustLevel: peers_sdk_1.TrustLevel.NewDevice,
|
|
133
|
-
}, { restoreIfDeleted: true, weakInsert: true });
|
|
134
|
-
(0, peers_sdk_1.userConnectStatus)(remoteUserConnectInfo.userId);
|
|
135
|
-
const me = await (0, peers_sdk_1.getMe)();
|
|
136
|
-
const myUserConnectInfo = {
|
|
137
|
-
userId: me.userId,
|
|
138
|
-
name: me.name,
|
|
139
|
-
publicKey: me.publicKey,
|
|
140
|
-
publicBoxKey: me.publicBoxKey,
|
|
141
|
-
deviceId: this.userContext.deviceId(),
|
|
142
|
-
};
|
|
143
|
-
return {
|
|
144
|
-
statusCode: 200,
|
|
145
|
-
payload: (0, peers_sdk_1.encryptWithSecret)(myUserConnectInfo, parsedCode.secret),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
catch (err) {
|
|
149
|
-
return {
|
|
150
|
-
isError: true,
|
|
151
|
-
statusCode: 400,
|
|
152
|
-
statusMessage: `Error handling user-connect device message: ${err}`
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
cleanOldDeviceMessagesSeen() {
|
|
157
|
-
// clear out deviceMessageIds older than 24 hours
|
|
158
|
-
const twentyFourHours = 24 * 60 * 60 * 1000;
|
|
159
|
-
for (const deviceMessageId of Object.keys(this.deviceMessagesSeen)) {
|
|
160
|
-
const time = (0, peers_sdk_1.idTime)(deviceMessageId);
|
|
161
|
-
if (Date.now() - time > twentyFourHours) {
|
|
162
|
-
delete this.deviceMessagesSeen[deviceMessageId];
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
59
|
}
|
|
166
60
|
async pingAllConnections() {
|
|
167
61
|
// make sure all connections exist in `allConnections`
|
|
@@ -226,13 +120,7 @@ class ConnectionManager {
|
|
|
226
120
|
* @returns Unsubscribe function
|
|
227
121
|
*/
|
|
228
122
|
on(deviceMessageType, handler) {
|
|
229
|
-
|
|
230
|
-
throw new Error(`Handler already registered for device message type: ${deviceMessageType}`);
|
|
231
|
-
}
|
|
232
|
-
this.deviceMessageHandlers.set(deviceMessageType, handler);
|
|
233
|
-
return () => {
|
|
234
|
-
this.deviceMessageHandlers.delete(deviceMessageType);
|
|
235
|
-
};
|
|
123
|
+
return this.deviceMessages.on(deviceMessageType, handler);
|
|
236
124
|
}
|
|
237
125
|
getDeviceConnection(deviceId) {
|
|
238
126
|
return this.allConnections[deviceId];
|
|
@@ -481,311 +369,8 @@ class ConnectionManager {
|
|
|
481
369
|
}
|
|
482
370
|
return Promise.resolve();
|
|
483
371
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
// try to lookup public box key from device
|
|
487
|
-
if (!toPublicBoxKey && message.toDeviceId) {
|
|
488
|
-
const device = await (0, peers_sdk_1.Devices)(this.userContext.userDataContext).get(message.toDeviceId);
|
|
489
|
-
if (device) {
|
|
490
|
-
const user = await (0, peers_sdk_1.getUserById)(device.userId, { userContext: this.userContext });
|
|
491
|
-
if (user) {
|
|
492
|
-
toPublicBoxKey = user.publicBoxKey;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
// try to lookup public box key by user (it's not uncommon to send a message to user's personal data context)
|
|
497
|
-
if (!toPublicBoxKey) {
|
|
498
|
-
if (message.dataContextId === this.userContext.userId) {
|
|
499
|
-
toPublicBoxKey = this.localDevice.publicBoxKey;
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
const user = await (0, peers_sdk_1.getUserById)(message.dataContextId, { userContext: this.userContext });
|
|
503
|
-
if (user) {
|
|
504
|
-
toPublicBoxKey = user.publicBoxKey;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
// try to lookup public box key by group
|
|
509
|
-
if (!toPublicBoxKey) {
|
|
510
|
-
const group = await (0, peers_sdk_1.Groups)(this.userContext.userDataContext).get(message.dataContextId);
|
|
511
|
-
if (group) {
|
|
512
|
-
toPublicBoxKey = group.publicBoxKey;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
// sign and box (or just sign if we couldn't find a box key)
|
|
516
|
-
if (toPublicBoxKey) {
|
|
517
|
-
message.payload = this.localDevice.signAndBoxDataForKey(message.payload, toPublicBoxKey);
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
console.warn(`WARNING! Could not establish a publicBoxKey to encrypt message with, sending as signed plain text`, message.payload);
|
|
521
|
-
message.payload = this.localDevice.signObjectWithSecretKey(message.payload);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
async validateDeviceMessagePayload(message) {
|
|
525
|
-
if (!message.payload)
|
|
526
|
-
return;
|
|
527
|
-
const fromDevice = await (0, peers_sdk_1.Devices)(this.userContext.userDataContext).get(message.fromDeviceId);
|
|
528
|
-
const fromUser = fromDevice?.userId && await (0, peers_sdk_1.getUserById)(fromDevice.userId, { userContext: this.userContext }) || undefined;
|
|
529
|
-
if ((0, peers_sdk_1.isBoxedData)(message.payload)) {
|
|
530
|
-
try {
|
|
531
|
-
const fromBoxKey = message.payload.fromPublicKey;
|
|
532
|
-
if (fromUser && fromUser.publicBoxKey !== fromBoxKey) {
|
|
533
|
-
throw new Error(`Box key used does not match sending device's user`);
|
|
534
|
-
}
|
|
535
|
-
message.payload = this.localDevice.openBoxedAndSignedData(message.payload);
|
|
536
|
-
}
|
|
537
|
-
catch (err) {
|
|
538
|
-
// if I couldn't open it with mine, maybe it's boxed with the group's secret key
|
|
539
|
-
try {
|
|
540
|
-
const groupSecretKey = await this.getGroupSecretKey(message.dataContextId);
|
|
541
|
-
message.payload = (0, peers_sdk_1.openBoxWithSecretKey)(message.payload, groupSecretKey);
|
|
542
|
-
}
|
|
543
|
-
catch (err) {
|
|
544
|
-
throw new Error(`Could not open boxed data`, { cause: err });
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
try {
|
|
550
|
-
const fromPublicKey = message.payload.publicKey;
|
|
551
|
-
if (fromUser && fromUser.publicKey !== fromPublicKey) {
|
|
552
|
-
throw new Error(`public key of signature does not match sending device's user`);
|
|
553
|
-
}
|
|
554
|
-
(0, peers_sdk_1.isObjectSignatureValid)(message.payload);
|
|
555
|
-
}
|
|
556
|
-
catch (err) {
|
|
557
|
-
throw new Error(`Could not validate signature`, { cause: err });
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
deviceMessagesSeen = {};
|
|
562
|
-
deviceAliases = new Map();
|
|
563
|
-
deviceAliasesExpires = new Map();
|
|
564
|
-
async sendDeviceMessage(partialMessage) {
|
|
565
|
-
// Fill in defaults for optional fields
|
|
566
|
-
const message = {
|
|
567
|
-
deviceMessageId: partialMessage.deviceMessageId || (0, peers_sdk_1.newid)(),
|
|
568
|
-
fromDeviceId: partialMessage.fromDeviceId || this.userContext.deviceId(),
|
|
569
|
-
toDeviceId: partialMessage.toDeviceId,
|
|
570
|
-
dataContextId: partialMessage.dataContextId,
|
|
571
|
-
ttl: partialMessage.ttl ?? 5,
|
|
572
|
-
payload: partialMessage.payload,
|
|
573
|
-
hops: partialMessage.hops || [],
|
|
574
|
-
};
|
|
575
|
-
// don't process this again if I've already seen it
|
|
576
|
-
if (this.deviceMessagesSeen[message.deviceMessageId]) {
|
|
577
|
-
return ACK_DUP;
|
|
578
|
-
}
|
|
579
|
-
this.deviceMessagesSeen[message.deviceMessageId] = true;
|
|
580
|
-
// device-alias special logic
|
|
581
|
-
{
|
|
582
|
-
if (message.toDeviceId === 'device-alias' && message.payload) {
|
|
583
|
-
const alias = message.payload;
|
|
584
|
-
if (typeof alias !== 'string' || alias.length < 4 || alias.length > 12) {
|
|
585
|
-
return {
|
|
586
|
-
isError: true,
|
|
587
|
-
statusCode: 400,
|
|
588
|
-
statusMessage: 'Invalid alias',
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
if (this.deviceAliases.has(alias) && this.deviceAliases.get(alias) !== message.fromDeviceId) {
|
|
592
|
-
return {
|
|
593
|
-
isError: true,
|
|
594
|
-
statusCode: 400,
|
|
595
|
-
statusMessage: 'Alias already in use by another device',
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
this.deviceAliases.set(alias, message.fromDeviceId);
|
|
599
|
-
this.deviceAliasesExpires.set(alias, Date.now() + 600_000); // 10 minutes
|
|
600
|
-
message.ttl--;
|
|
601
|
-
if (message.ttl <= 0) {
|
|
602
|
-
return exports.TTL0;
|
|
603
|
-
}
|
|
604
|
-
// propagate the alias to all devices
|
|
605
|
-
return this.forwardDeviceMessageAndReturnFirstResponse(message, Object.values(this.allConnections));
|
|
606
|
-
}
|
|
607
|
-
const knownAlias = this.deviceAliases.get(message.toDeviceId);
|
|
608
|
-
if (knownAlias) {
|
|
609
|
-
message.toDeviceId = knownAlias;
|
|
610
|
-
}
|
|
611
|
-
// user-connect special logic
|
|
612
|
-
if (message.dataContextId === 'user-connect' && message.toDeviceId === this.userContext.deviceId()) {
|
|
613
|
-
return this.userConnectAnswer(message);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
// if this is the source device, assume we need to secure the message
|
|
617
|
-
if (message.hops.length === 0 && message.payload && message.dataContextId !== 'user-connect') {
|
|
618
|
-
try {
|
|
619
|
-
await this.secureDeviceMessagePayload(message);
|
|
620
|
-
}
|
|
621
|
-
catch (err) {
|
|
622
|
-
throw new Error(`Error while securing device message payload`, { cause: err });
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
// add this device to the list of hops
|
|
626
|
-
message.hops = [...message.hops, this.userContext.deviceId()];
|
|
627
|
-
const dataContextIds = [this.userContext.userId, ...this.userContext.groupIds()];
|
|
628
|
-
// Device Discovery - if there is no deviceId, we assume they are just looking for any device that supports the given dataContextId
|
|
629
|
-
// TODO revisit this to make sure we're not creating a bunch of unnecessary noise by using an empty toDeviceId
|
|
630
|
-
if (message.toDeviceId === '' && dataContextIds.includes(message.dataContextId) && message.fromDeviceId !== this.userContext.deviceId() && !message.payload) {
|
|
631
|
-
return {
|
|
632
|
-
statusCode: 200,
|
|
633
|
-
message: "OK",
|
|
634
|
-
hops: message.hops
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
// if it's meant for this device then process it
|
|
638
|
-
if (this.userContext.deviceId() === message.toDeviceId) {
|
|
639
|
-
try {
|
|
640
|
-
await this.validateDeviceMessagePayload(message);
|
|
641
|
-
}
|
|
642
|
-
catch (err) {
|
|
643
|
-
return {
|
|
644
|
-
isError: true,
|
|
645
|
-
statusCode: 400,
|
|
646
|
-
statusMessage: `Error while validating device message payload: ${err}`
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
// Check for device-level handler first (e.g., webrtc-signal)
|
|
650
|
-
if (message.payload?.type && this.deviceMessageHandlers.has(message.payload.type)) {
|
|
651
|
-
const handler = this.deviceMessageHandlers.get(message.payload.type);
|
|
652
|
-
return handler(message);
|
|
653
|
-
}
|
|
654
|
-
// check if this user has joined this group, if not return error
|
|
655
|
-
if (!dataContextIds.includes(message.dataContextId)) {
|
|
656
|
-
return {
|
|
657
|
-
isError: true,
|
|
658
|
-
statusCode: 400,
|
|
659
|
-
statusMessage: 'Device does not support specified dataContext: ' + message.dataContextId
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
const dataContext = this.userContext.getDataContext(message.dataContextId);
|
|
663
|
-
const syncDevice = this.getPeerGroupDevice(dataContext);
|
|
664
|
-
// give it to groupDevice to process it
|
|
665
|
-
return syncDevice.sendDeviceMessage(message);
|
|
666
|
-
}
|
|
667
|
-
// decrement ttl
|
|
668
|
-
message.ttl--;
|
|
669
|
-
if (message.ttl <= 0) {
|
|
670
|
-
return exports.TTL0;
|
|
671
|
-
}
|
|
672
|
-
// TODO maybe use hash chain to validate hopes are append only
|
|
673
|
-
// TODO maybe use hops to record paths for later use
|
|
674
|
-
// try to forward to the exact device
|
|
675
|
-
if (this.allConnections[message.toDeviceId]) {
|
|
676
|
-
const conn = this.allConnections[message.toDeviceId];
|
|
677
|
-
return conn.emit("sendDeviceMessage", message);
|
|
678
|
-
}
|
|
679
|
-
// if we don't have a direct connection to the device and the ttl is 1, we know it'll fail so short circuit
|
|
680
|
-
if (message.ttl <= 1) {
|
|
681
|
-
return exports.TTL0;
|
|
682
|
-
}
|
|
683
|
-
// before flooding the network make sure the message size isn't too big and TTL is reasonable
|
|
684
|
-
if (JSON.stringify(message).length > this.MAX_DEVICE_MESSAGE_SIZE) {
|
|
685
|
-
return {
|
|
686
|
-
isError: true,
|
|
687
|
-
statusCode: 413,
|
|
688
|
-
statusMessage: "Payload Too Large"
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
if (message.ttl > 4) {
|
|
692
|
-
throw new Error('Max TTL is 5 but message has TTL of ' + (message.ttl + 1));
|
|
693
|
-
}
|
|
694
|
-
// start with connections to all devices that are not in the current list of hops
|
|
695
|
-
let connectionsToTry = Array.from(this.connectionStates.keys())
|
|
696
|
-
.filter(c => !message.hops.includes(c.remoteDeviceInfo.deviceId));
|
|
697
|
-
const alreadySeenDeviceIds = (0, lodash_1.uniq)([this.userContext.deviceId(), ...Object.keys(this.allConnections), ...message.hops]);
|
|
698
|
-
// try to find an indirect connection from cached networkInfos (connection to a device that is then connected to target device)
|
|
699
|
-
for (const syncGroup of Object.values(this.syncGroups)) {
|
|
700
|
-
const pathToDevice = syncGroup.getPathToDevice(message.toDeviceId);
|
|
701
|
-
if (pathToDevice.throughDeviceIds.length) {
|
|
702
|
-
const indirectConnections = connectionsToTry.filter(c => pathToDevice.throughDeviceIds.includes(c.remoteDeviceInfo.deviceId))
|
|
703
|
-
// before slicing, sort by connections by the ones with the most indirect connections to other devices
|
|
704
|
-
// QUESTION? do we really want to purposely send to "busy" devices or would it be better to send to less busy devices that just happen to have the right path?
|
|
705
|
-
.sort((a, b) => {
|
|
706
|
-
const aRemoteDeviceIds = this.getConnectionIndirectRemoteDeviceIds(a, alreadySeenDeviceIds);
|
|
707
|
-
const bRemoteDeviceIds = this.getConnectionIndirectRemoteDeviceIds(b, alreadySeenDeviceIds);
|
|
708
|
-
return bRemoteDeviceIds.length - aRemoteDeviceIds.length;
|
|
709
|
-
})
|
|
710
|
-
.slice(0, 2); // in the unlikely event we have many indirect connections, trim them down
|
|
711
|
-
const response = await this.forwardDeviceMessageAndReturnFirstResponse(message, indirectConnections);
|
|
712
|
-
if ((response !== ACK_DUP && response !== exports.TTL0)) {
|
|
713
|
-
return response;
|
|
714
|
-
}
|
|
715
|
-
else if (indirectConnections.length > 1) {
|
|
716
|
-
// if we had multiple indirect connections and they all returned TTL0 (which may have stemmed from ACK_DUP),
|
|
717
|
-
// don't continue trying other connections, it's very likely the message has already been delivered
|
|
718
|
-
return response;
|
|
719
|
-
}
|
|
720
|
-
connectionsToTry = connectionsToTry.filter(c => !indirectConnections.includes(c));
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
// if we don't have a lot of connections just forward to them all
|
|
724
|
-
if (connectionsToTry.length <= this.MAX_CONNECTIONS_TO_FORWARD_MESSAGES_TO) {
|
|
725
|
-
return this.forwardDeviceMessageAndReturnFirstResponse(message, connectionsToTry);
|
|
726
|
-
}
|
|
727
|
-
// WARNING! at this point, we don't have a known path but we have a lot of connections
|
|
728
|
-
// we could end up choking this device or its network if we're not careful and it might time out anyway
|
|
729
|
-
// we want to find a good subset of connections that have the best chance of being connected to the device
|
|
730
|
-
// remove connections that don't have any new paths
|
|
731
|
-
// i.e. trim off this device's connections to leaf network devices, no point forwarding to them
|
|
732
|
-
connectionsToTry = connectionsToTry.filter(c => {
|
|
733
|
-
const remoteDeviceIds = this.getConnectionIndirectRemoteDeviceIds(c, alreadySeenDeviceIds);
|
|
734
|
-
return remoteDeviceIds.length > 0;
|
|
735
|
-
});
|
|
736
|
-
if (connectionsToTry.length <= this.MAX_CONNECTIONS_TO_FORWARD_MESSAGES_TO) {
|
|
737
|
-
return this.forwardDeviceMessageAndReturnFirstResponse(message, connectionsToTry);
|
|
738
|
-
}
|
|
739
|
-
// sort the connections by how many new paths they have (highest first) and whether they're in the target group
|
|
740
|
-
// example: if we allow 4 connections to be forwarded to, and we have 2 connections that are in the target group,
|
|
741
|
-
// those 2 will be prioritized and then the next 2 connections will be the ones with the highest number of new paths
|
|
742
|
-
// if we have more than 4 connections in the target group, we'll take the 4 with the highest number of new paths
|
|
743
|
-
const messageDataContextSyncGroup = this.syncGroups[message.dataContextId];
|
|
744
|
-
const groupConnectionDeviceIds = new Set((messageDataContextSyncGroup?.getConnections() || []).map(dc => dc.deviceId));
|
|
745
|
-
// TODO: change this to re-sort after each selection to account for overlap in indirect connections
|
|
746
|
-
connectionsToTry = connectionsToTry.sort((a, b) => {
|
|
747
|
-
// prioritize connections that are in the target group
|
|
748
|
-
const aIsGroupConnection = groupConnectionDeviceIds.has(a.remoteDeviceInfo.deviceId) ? 1 : 0;
|
|
749
|
-
const bIsGroupConnection = groupConnectionDeviceIds.has(b.remoteDeviceInfo.deviceId) ? 1 : 0;
|
|
750
|
-
if (aIsGroupConnection !== bIsGroupConnection) {
|
|
751
|
-
return bIsGroupConnection - aIsGroupConnection;
|
|
752
|
-
}
|
|
753
|
-
// for each connection, get the number of indirect connections it has to devices we haven't already seen
|
|
754
|
-
const aRemoteDeviceIds = this.getConnectionIndirectRemoteDeviceIds(a, alreadySeenDeviceIds);
|
|
755
|
-
const bRemoteDeviceIds = this.getConnectionIndirectRemoteDeviceIds(b, alreadySeenDeviceIds);
|
|
756
|
-
return bRemoteDeviceIds.length - aRemoteDeviceIds.length;
|
|
757
|
-
});
|
|
758
|
-
// finally, forward to the top connections
|
|
759
|
-
return this.forwardDeviceMessageAndReturnFirstResponse(message, connectionsToTry.slice(0, this.MAX_CONNECTIONS_TO_FORWARD_MESSAGES_TO));
|
|
760
|
-
}
|
|
761
|
-
forwardDeviceMessageAndReturnFirstResponse(message, connectionsToForwardTo) {
|
|
762
|
-
return new Promise((resolve) => {
|
|
763
|
-
const pendingDevices = new Set(connectionsToForwardTo.map(c => c.remoteDeviceInfo.deviceId));
|
|
764
|
-
if (pendingDevices.size === 0) {
|
|
765
|
-
return resolve(exports.TTL0);
|
|
766
|
-
}
|
|
767
|
-
let responseSent = false;
|
|
768
|
-
function checkResponse(deviceId, response) {
|
|
769
|
-
if (responseSent) {
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
pendingDevices.delete(deviceId);
|
|
773
|
-
if (response !== ACK_DUP && response !== exports.TTL0) {
|
|
774
|
-
responseSent = true;
|
|
775
|
-
resolve(response);
|
|
776
|
-
}
|
|
777
|
-
if (pendingDevices.size === 0) {
|
|
778
|
-
responseSent = true;
|
|
779
|
-
resolve(exports.TTL0);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
for (const conn of connectionsToForwardTo) {
|
|
783
|
-
const deviceId = conn.remoteDeviceInfo.deviceId;
|
|
784
|
-
conn.emit("sendDeviceMessage", message)
|
|
785
|
-
.then(response => checkResponse(deviceId, response))
|
|
786
|
-
.catch(err => checkResponse(deviceId, err));
|
|
787
|
-
}
|
|
788
|
-
});
|
|
372
|
+
sendDeviceMessage(partialMessage) {
|
|
373
|
+
return this.deviceMessages.sendDeviceMessage(partialMessage);
|
|
789
374
|
}
|
|
790
375
|
/**
|
|
791
376
|
* this is used for testing setup, it's not intended for normal use
|
|
@@ -825,21 +410,7 @@ class ConnectionManager {
|
|
|
825
410
|
};
|
|
826
411
|
}
|
|
827
412
|
getConnectionIndirectRemoteDeviceIds(connection, excludeDeviceIds) {
|
|
828
|
-
|
|
829
|
-
const allSyncGroups = Object.values(this.syncGroups);
|
|
830
|
-
const connectedIds = new Set();
|
|
831
|
-
for (const syncDevice of allSyncGroups) {
|
|
832
|
-
syncDevice.reportRemoteDeviceConnectedIds(remoteDeviceId, connectedIds);
|
|
833
|
-
}
|
|
834
|
-
if (excludeDeviceIds?.length) {
|
|
835
|
-
for (const excludeId of excludeDeviceIds) {
|
|
836
|
-
connectedIds.delete(excludeId);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
// remove self which will always be there but we should never care about (hmmm, this might confuse things since it should be there but never will be?)
|
|
840
|
-
// MAYBE: use this as a way to detect out-of-date network info caches if it's _not_ there?
|
|
841
|
-
connectedIds.delete(this.userContext.deviceId());
|
|
842
|
-
return Array.from(connectedIds);
|
|
413
|
+
return this.deviceMessages.getConnectionIndirectRemoteDeviceIds(connection, excludeDeviceIds);
|
|
843
414
|
}
|
|
844
415
|
getPathToDevice(deviceId) {
|
|
845
416
|
if (this.allConnections[deviceId]) {
|
|
@@ -861,24 +432,12 @@ class ConnectionManager {
|
|
|
861
432
|
throughDeviceIds: [],
|
|
862
433
|
};
|
|
863
434
|
}
|
|
864
|
-
cleanupExpiredAliases() {
|
|
865
|
-
const now = Date.now();
|
|
866
|
-
for (const [alias, expires] of this.deviceAliasesExpires) {
|
|
867
|
-
if (expires < now) {
|
|
868
|
-
this.deviceAliases.delete(alias);
|
|
869
|
-
this.deviceAliasesExpires.delete(alias);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
435
|
async dispose() {
|
|
874
|
-
this.
|
|
875
|
-
this.userConnectCodeAnswerSubscription.dispose();
|
|
436
|
+
this.deviceMessages.dispose();
|
|
876
437
|
for (const syncGroup of Object.values(this.syncGroups)) {
|
|
877
438
|
await syncGroup?.dispose();
|
|
878
439
|
}
|
|
879
|
-
clearInterval(this.cleanOldDeviceMessagesSeenInterval);
|
|
880
440
|
clearInterval(this.pingInterval);
|
|
881
|
-
clearInterval(this.cleanupExpiredAliasesInterval);
|
|
882
441
|
const connections = Array.from(this.connectionStates.keys());
|
|
883
442
|
for (const connection of connections) {
|
|
884
443
|
await this.removeConnection(connection);
|