@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.
@@ -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 declare const TTL0 = "TTL0";
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
- MAX_DEVICE_MESSAGE_SIZE: number;
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
- exports.TTL0 = "TTL0";
11
- const ACK_DUP = "ACK_DUP";
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 = 6;
20
- MAX_DEVICE_MESSAGE_SIZE = 64 * 1024; // 64 KB
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
- if (this.deviceMessageHandlers.has(deviceMessageType)) {
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
- async secureDeviceMessagePayload(message) {
485
- let toPublicBoxKey = ''; //message.toPublicBoxKey;
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
- const remoteDeviceId = connection.remoteDeviceInfo.deviceId;
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.userConnectCodeOfferSubscription.dispose();
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);