@matter/protocol 0.12.0-alpha.0-20241220-ad0dea627 → 0.12.0-alpha.0-20241220-c66941cd7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/cjs/certificate/CertificateAuthority.js +2 -2
  2. package/dist/cjs/certificate/CertificateAuthority.js.map +1 -1
  3. package/dist/cjs/interaction/InteractionMessenger.d.ts +1 -1
  4. package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
  5. package/dist/cjs/interaction/InteractionMessenger.js +7 -6
  6. package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
  7. package/dist/cjs/mdns/MdnsScanner.d.ts +2 -4
  8. package/dist/cjs/mdns/MdnsScanner.d.ts.map +1 -1
  9. package/dist/cjs/mdns/MdnsScanner.js +130 -51
  10. package/dist/cjs/mdns/MdnsScanner.js.map +2 -2
  11. package/dist/cjs/mdns/MdnsServer.d.ts +1 -1
  12. package/dist/cjs/mdns/MdnsServer.d.ts.map +1 -1
  13. package/dist/cjs/mdns/MdnsServer.js +20 -8
  14. package/dist/cjs/mdns/MdnsServer.js.map +1 -1
  15. package/dist/cjs/peer/ControllerDiscovery.js +1 -1
  16. package/dist/cjs/peer/ControllerDiscovery.js.map +1 -1
  17. package/dist/cjs/peer/PeerSet.d.ts.map +1 -1
  18. package/dist/cjs/peer/PeerSet.js +27 -10
  19. package/dist/cjs/peer/PeerSet.js.map +2 -2
  20. package/dist/cjs/protocol/MessageExchange.d.ts +5 -0
  21. package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
  22. package/dist/cjs/protocol/MessageExchange.js.map +1 -1
  23. package/dist/esm/certificate/CertificateAuthority.js +2 -2
  24. package/dist/esm/certificate/CertificateAuthority.js.map +1 -1
  25. package/dist/esm/interaction/InteractionMessenger.d.ts +1 -1
  26. package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
  27. package/dist/esm/interaction/InteractionMessenger.js +7 -6
  28. package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
  29. package/dist/esm/mdns/MdnsScanner.d.ts +2 -4
  30. package/dist/esm/mdns/MdnsScanner.d.ts.map +1 -1
  31. package/dist/esm/mdns/MdnsScanner.js +130 -51
  32. package/dist/esm/mdns/MdnsScanner.js.map +2 -2
  33. package/dist/esm/mdns/MdnsServer.d.ts +1 -1
  34. package/dist/esm/mdns/MdnsServer.d.ts.map +1 -1
  35. package/dist/esm/mdns/MdnsServer.js +20 -8
  36. package/dist/esm/mdns/MdnsServer.js.map +1 -1
  37. package/dist/esm/peer/ControllerDiscovery.js +1 -1
  38. package/dist/esm/peer/ControllerDiscovery.js.map +1 -1
  39. package/dist/esm/peer/PeerSet.d.ts.map +1 -1
  40. package/dist/esm/peer/PeerSet.js +27 -10
  41. package/dist/esm/peer/PeerSet.js.map +2 -2
  42. package/dist/esm/protocol/MessageExchange.d.ts +5 -0
  43. package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
  44. package/dist/esm/protocol/MessageExchange.js.map +1 -1
  45. package/package.json +6 -6
  46. package/src/certificate/CertificateAuthority.ts +2 -2
  47. package/src/interaction/InteractionMessenger.ts +9 -5
  48. package/src/mdns/MdnsScanner.ts +190 -59
  49. package/src/mdns/MdnsServer.ts +24 -8
  50. package/src/peer/ControllerDiscovery.ts +1 -1
  51. package/src/peer/PeerSet.ts +34 -11
  52. package/src/protocol/MessageExchange.ts +6 -0
@@ -70,6 +70,12 @@ type OperationalDeviceRecordWithExpire = Omit<OperationalDevice, "addresses"> &
70
70
  addresses: Map<string, MatterServerRecordWithExpire>; // Override addresses type to include expiration
71
71
  };
72
72
 
73
+ type StructuredDnsAnswers = {
74
+ operational?: Record<number, DnsRecord<any>[]>; // Operational Matter device records by recordType
75
+ commissionable?: Record<number, DnsRecord<any>[]>; // Commissionable Matter device records by recordType
76
+ addresses?: Record<string, DnsRecord<any>[]>; // IP Address records by name
77
+ };
78
+
73
79
  /** The initial number of seconds between two announcements. MDNS specs require 1-2 seconds, so lets use the middle. */
74
80
  const START_ANNOUNCE_INTERVAL_SECONDS = 1.5;
75
81
 
@@ -96,7 +102,7 @@ export class MdnsScanner implements Scanner {
96
102
  );
97
103
  }
98
104
 
99
- readonly #activeAnnounceQueries = new Map<string, { queries: DnsQuery[]; answers: DnsRecord<any>[] }>();
105
+ readonly #activeAnnounceQueries = new Map<string, { queries: DnsQuery[]; answers: StructuredDnsAnswers }>();
100
106
  #queryTimer?: Timer;
101
107
  #nextAnnounceIntervalSeconds = START_ANNOUNCE_INTERVAL_SECONDS;
102
108
 
@@ -108,6 +114,7 @@ export class MdnsScanner implements Scanner {
108
114
  resolver: () => void;
109
115
  timer?: Timer;
110
116
  resolveOnUpdatedRecords: boolean;
117
+ cancelResolver?: (value: void) => void;
111
118
  }
112
119
  >();
113
120
  readonly #periodicTimer: Timer;
@@ -141,7 +148,9 @@ export class MdnsScanner implements Scanner {
141
148
  this.#queryTimer?.stop();
142
149
  const allQueries = Array.from(this.#activeAnnounceQueries.values());
143
150
  const queries = allQueries.flatMap(({ queries }) => queries);
144
- const answers = allQueries.flatMap(({ answers }) => answers);
151
+ const answers = allQueries.flatMap(({ answers }) =>
152
+ Object.values(answers).flatMap(answer => Object.values(answer).flatMap(records => records)),
153
+ );
145
154
 
146
155
  this.#queryTimer = Time.getTimer("MDNS discovery", this.#nextAnnounceIntervalSeconds * 1000, () =>
147
156
  this.#sendQueries(),
@@ -210,7 +219,7 @@ export class MdnsScanner implements Scanner {
210
219
  * Set new DnsQuery records to the list of active queries to discover devices in the network and start sending them
211
220
  * out. When entry already exists the query is overwritten and answers are always added.
212
221
  */
213
- #setQueryRecords(queryId: string, queries: DnsQuery[], answers: DnsRecord<any>[] = []) {
222
+ #setQueryRecords(queryId: string, queries: DnsQuery[], answers: StructuredDnsAnswers = {}) {
214
223
  const activeExistingQuery = this.#activeAnnounceQueries.get(queryId);
215
224
  if (activeExistingQuery) {
216
225
  const { queries: existingQueries } = activeExistingQuery;
@@ -231,7 +240,7 @@ export class MdnsScanner implements Scanner {
231
240
  return;
232
241
  }
233
242
  queries = [...newQueries, ...existingQueries];
234
- answers = [...activeExistingQuery.answers, ...answers];
243
+ answers = this.#combineStructuredAnswers(activeExistingQuery.answers, answers);
235
244
  }
236
245
  this.#activeAnnounceQueries.set(queryId, { queries, answers });
237
246
  logger.debug(`Set ${queries.length} query records for query ${queryId}: ${Logger.toJSON(queries)}`);
@@ -241,7 +250,9 @@ export class MdnsScanner implements Scanner {
241
250
  }
242
251
 
243
252
  #getActiveQueryEarlierAnswers() {
244
- return Array.from(this.#activeAnnounceQueries.values()).flatMap(({ answers }) => answers);
253
+ return this.#combineStructuredAnswers(
254
+ ...[...this.#activeAnnounceQueries.values()].map(({ answers }) => answers),
255
+ );
245
256
  }
246
257
 
247
258
  /**
@@ -314,13 +325,21 @@ export class MdnsScanner implements Scanner {
314
325
  * Registers a deferred promise for a specific queryId together with a timeout and return the promise.
315
326
  * The promise will be resolved when the timer runs out latest.
316
327
  */
317
- async #registerWaiterPromise(queryId: string, timeoutSeconds?: number, resolveOnUpdatedRecords = true) {
328
+ async #registerWaiterPromise(
329
+ queryId: string,
330
+ timeoutSeconds?: number,
331
+ resolveOnUpdatedRecords = true,
332
+ cancelResolver?: (value: void) => void,
333
+ ) {
318
334
  const { promise, resolver } = createPromise<void>();
319
335
  const timer =
320
336
  timeoutSeconds !== undefined
321
- ? Time.getTimer("MDNS timeout", timeoutSeconds * 1000, () => this.#finishWaiter(queryId, true)).start()
337
+ ? Time.getTimer("MDNS timeout", timeoutSeconds * 1000, () => {
338
+ cancelResolver?.();
339
+ this.#finishWaiter(queryId, true);
340
+ }).start()
322
341
  : undefined;
323
- this.#recordWaiters.set(queryId, { resolver, timer, resolveOnUpdatedRecords });
342
+ this.#recordWaiters.set(queryId, { resolver, timer, resolveOnUpdatedRecords, cancelResolver });
324
343
  logger.debug(
325
344
  `Registered waiter for query ${queryId} with ${
326
345
  timeoutSeconds !== undefined ? `timeout ${timeoutSeconds} seconds` : "no timeout"
@@ -399,6 +418,9 @@ export class MdnsScanner implements Scanner {
399
418
 
400
419
  cancelCommissionableDeviceDiscovery(identifier: CommissionableDeviceIdentifiers, resolvePromise = true) {
401
420
  const queryId = this.#buildCommissionableQueryIdentifier(identifier);
421
+ const { cancelResolver } = this.#recordWaiters.get(queryId) ?? {};
422
+ // Mark as cancelled to not loop further in discovery, if cancel resolver is used
423
+ cancelResolver?.();
402
424
  this.#finishWaiter(queryId, resolvePromise);
403
425
  }
404
426
 
@@ -613,10 +635,8 @@ export class MdnsScanner implements Scanner {
613
635
  }
614
636
 
615
637
  /**
616
- * Discovers commissionable devices based on a defined identifier and returns the first found entries. If already a
617
- * @param identifier
618
- * @param callback
619
- * @param timeoutSeconds
638
+ * Discovers commissionable devices based on a defined identifier and returns the first found entries.
639
+ * If an own cancelSignal promise is used the discovery can only be cancelled via this signal!
620
640
  */
621
641
  async findCommissionableDevicesContinuously(
622
642
  identifier: CommissionableDeviceIdentifiers,
@@ -625,17 +645,26 @@ export class MdnsScanner implements Scanner {
625
645
  cancelSignal?: Promise<void>,
626
646
  ): Promise<CommissionableDevice[]> {
627
647
  const discoveredDevices = new Set<string>();
628
- const now = Time.nowMs();
629
648
 
630
- const discoveryEndTime = timeoutSeconds ? now + timeoutSeconds * 1000 : undefined;
649
+ const discoveryEndTime = timeoutSeconds ? Time.nowMs() + timeoutSeconds * 1000 : undefined;
631
650
  const queryId = this.#buildCommissionableQueryIdentifier(identifier);
632
651
  this.#setQueryRecords(queryId, this.#getCommissionableQueryRecords(identifier));
633
652
 
653
+ let queryResolver: ((value: void) => void) | undefined;
654
+ if (cancelSignal === undefined) {
655
+ const { promise, resolver } = createPromise<void>();
656
+ cancelSignal = promise;
657
+ queryResolver = resolver;
658
+ }
659
+
634
660
  let canceled = false;
635
661
  cancelSignal?.then(
636
662
  () => {
637
663
  canceled = true;
638
- this.#finishWaiter(queryId, true);
664
+ if (queryResolver === undefined) {
665
+ // Always finish when cancelSignal parameter was used, else cancelling is done separately
666
+ this.#finishWaiter(queryId, true);
667
+ }
639
668
  },
640
669
  cause => {
641
670
  logger.error("Unexpected error canceling commissioning", cause);
@@ -653,12 +682,12 @@ export class MdnsScanner implements Scanner {
653
682
 
654
683
  let remainingTime;
655
684
  if (discoveryEndTime !== undefined) {
656
- const remainingTime = Math.ceil((discoveryEndTime - now) / 1000);
685
+ remainingTime = discoveryEndTime - Time.nowMs();
657
686
  if (remainingTime <= 0) {
658
687
  break;
659
688
  }
660
689
  }
661
- await this.#registerWaiterPromise(queryId, remainingTime, false);
690
+ await this.#registerWaiterPromise(queryId, remainingTime, false, queryResolver);
662
691
  }
663
692
  return this.#getCommissionableDeviceRecords(identifier);
664
693
  }
@@ -681,6 +710,99 @@ export class MdnsScanner implements Scanner {
681
710
  );
682
711
  }
683
712
 
713
+ /** Converts the discovery data into a structured format for performant access. */
714
+ #structureAnswers(...answersList: DnsRecord<any>[][]): StructuredDnsAnswers {
715
+ const structuredAnswers: StructuredDnsAnswers = {};
716
+
717
+ answersList.forEach(answers =>
718
+ answers.forEach(answer => {
719
+ const { name, recordType } = answer;
720
+ if (name.endsWith(MATTER_SERVICE_QNAME)) {
721
+ structuredAnswers.operational = structuredAnswers.operational ?? {};
722
+ structuredAnswers.operational[recordType] = structuredAnswers.operational[recordType] ?? [];
723
+ structuredAnswers.operational[recordType].push(answer);
724
+ } else if (name.endsWith(MATTER_COMMISSION_SERVICE_QNAME)) {
725
+ structuredAnswers.commissionable = structuredAnswers.commissionable ?? {};
726
+ structuredAnswers.commissionable[recordType] = structuredAnswers.commissionable[recordType] ?? [];
727
+ structuredAnswers.commissionable[recordType].push(answer);
728
+ } else if (recordType === DnsRecordType.A || recordType === DnsRecordType.AAAA) {
729
+ structuredAnswers.addresses = structuredAnswers.addresses ?? {};
730
+ structuredAnswers.addresses[name] = structuredAnswers.addresses[name] ?? [];
731
+ structuredAnswers.addresses[name].push(answer);
732
+ }
733
+ }),
734
+ );
735
+
736
+ return structuredAnswers;
737
+ }
738
+
739
+ #combineStructuredAnswers(...answersList: StructuredDnsAnswers[]): StructuredDnsAnswers {
740
+ const combinedAnswers: {
741
+ operational?: Record<number, Map<string, DnsRecord<any>>>;
742
+ commissionable?: Record<number, Map<string, DnsRecord<any>>>;
743
+ addresses?: Record<string, DnsRecord<any>[]>;
744
+ } = {};
745
+
746
+ for (const answers of answersList) {
747
+ if (answers.operational) {
748
+ combinedAnswers.operational = combinedAnswers.operational ?? {};
749
+ for (const [recordType, records] of Object.entries(answers.operational) as unknown as [
750
+ number,
751
+ DnsRecord<any>[],
752
+ ][]) {
753
+ combinedAnswers.operational[recordType] = combinedAnswers.operational[recordType] ?? new Map();
754
+ records.forEach(record => {
755
+ combinedAnswers.operational![recordType].set(record.name, record);
756
+ });
757
+ }
758
+ }
759
+ if (answers.commissionable) {
760
+ combinedAnswers.commissionable = combinedAnswers.commissionable ?? {};
761
+ for (const [recordType, records] of Object.entries(answers.commissionable) as unknown as [
762
+ number,
763
+ DnsRecord<any>[],
764
+ ][]) {
765
+ combinedAnswers.commissionable[recordType] =
766
+ combinedAnswers.commissionable[recordType] ?? new Map();
767
+ records.forEach(record => {
768
+ combinedAnswers.commissionable![recordType].set(record.name, record);
769
+ });
770
+ }
771
+ }
772
+ if (answers.addresses) {
773
+ combinedAnswers.addresses = combinedAnswers.addresses ?? {};
774
+ for (const [name, records] of Object.entries(answers.addresses) as unknown as [
775
+ string,
776
+ DnsRecord<any>[],
777
+ ][]) {
778
+ combinedAnswers.addresses[name] = combinedAnswers.addresses[name] ?? [];
779
+ // IP address consolidation happens when they are selected for a device
780
+ combinedAnswers.addresses[name].push(...records);
781
+ }
782
+ }
783
+ }
784
+
785
+ return {
786
+ operational: combinedAnswers.operational
787
+ ? Object.fromEntries(
788
+ Object.entries(combinedAnswers.operational).map(([recordType, records]) => [
789
+ recordType,
790
+ Array.from(records.values()),
791
+ ]),
792
+ )
793
+ : undefined,
794
+ commissionable: combinedAnswers.commissionable
795
+ ? Object.fromEntries(
796
+ Object.entries(combinedAnswers.commissionable).map(([recordType, records]) => [
797
+ recordType,
798
+ Array.from(records.values()),
799
+ ]),
800
+ )
801
+ : undefined,
802
+ addresses: combinedAnswers.addresses,
803
+ };
804
+ }
805
+
684
806
  /**
685
807
  * Main method to handle all incoming DNS messages.
686
808
  * It will parse the message and check if it contains relevant discovery records.
@@ -692,58 +814,62 @@ export class MdnsScanner implements Scanner {
692
814
  if (message.messageType !== DnsMessageType.Response && message.messageType !== DnsMessageType.TruncatedResponse)
693
815
  return;
694
816
 
695
- const answers = [...message.answers, ...message.additionalRecords];
817
+ const answers = this.#structureAnswers([...message.answers, ...message.additionalRecords]);
696
818
 
819
+ const formerAnswers = this.#getActiveQueryEarlierAnswers();
697
820
  // Check if we got operational discovery records and handle them
698
- if (this.#handleOperationalRecords(answers, this.#getActiveQueryEarlierAnswers(), netInterface)) return;
821
+ this.#handleOperationalRecords(answers, formerAnswers, netInterface);
699
822
 
700
823
  // Else check if we got commissionable discovery records and handle them
701
- this.#handleCommissionableRecords(answers, this.#getActiveQueryEarlierAnswers(), netInterface);
824
+ this.#handleCommissionableRecords(answers, formerAnswers, netInterface);
702
825
  }
703
826
 
704
827
  #handleIpRecords(
705
- answers: DnsRecord<any>[],
828
+ answers: StructuredDnsAnswers[],
706
829
  target: string,
707
830
  netInterface: string,
708
831
  ): { value: string; ttl: number }[] {
709
- const ipRecords = answers.filter(
710
- ({ name, recordType }) =>
711
- ((recordType === DnsRecordType.A && this.#enableIpv4) || recordType === DnsRecordType.AAAA) &&
712
- name === target,
713
- );
714
- return (ipRecords as DnsRecord<string>[]).map(({ value, ttl }) => ({
715
- value: value.startsWith("fe80::") ? `${value}%${netInterface}` : value,
716
- ttl,
717
- }));
832
+ const ipRecords = answers
833
+ .flatMap(answer => answer.addresses?.[target] ?? [])
834
+ .filter(
835
+ ({ recordType }) =>
836
+ recordType === DnsRecordType.AAAA || (recordType === DnsRecordType.A && this.#enableIpv4),
837
+ );
838
+
839
+ // If an IP is included multiple times we only keep the latest record
840
+ const collectedIps = new Map<string, { value: string; ttl: number }>();
841
+ ipRecords.forEach(record => {
842
+ const { value, ttl } = record as DnsRecord<string>;
843
+ if (value.startsWith("fe80::")) {
844
+ collectedIps.set(value, { value: `${value}%${netInterface}`, ttl });
845
+ } else {
846
+ collectedIps.set(value, { value, ttl });
847
+ }
848
+ });
849
+ return [...collectedIps.values()];
718
850
  }
719
851
 
720
- #handleOperationalRecords(answers: DnsRecord<any>[], formerAnswers: DnsRecord<any>[], netInterface: string) {
721
- let recordsHandled = false;
852
+ #handleOperationalRecords(
853
+ answers: StructuredDnsAnswers,
854
+ formerAnswers: StructuredDnsAnswers,
855
+ netInterface: string,
856
+ ) {
722
857
  // Does the message contain data for an operational service?
723
- const operationalTxtRecords = answers.filter(
724
- ({ name, recordType }) => recordType === DnsRecordType.TXT && name.endsWith(MATTER_SERVICE_QNAME),
725
- );
726
- if (operationalTxtRecords.length) {
727
- operationalTxtRecords.forEach(record => this.#handleOperationalTxtRecord(record, netInterface));
728
- recordsHandled = true;
729
- }
858
+ if (!answers.operational) return;
730
859
 
731
- let operationalSrvRecords = answers.filter(
732
- ({ name, recordType }) => recordType === DnsRecordType.SRV && name.endsWith(MATTER_SERVICE_QNAME),
733
- );
734
- if (!operationalSrvRecords.length) {
735
- operationalSrvRecords = formerAnswers.filter(
736
- ({ name, recordType }) => recordType === DnsRecordType.SRV && name.endsWith(MATTER_SERVICE_QNAME),
737
- );
860
+ const operationalTxtRecords = answers.operational[DnsRecordType.TXT] ?? [];
861
+ operationalTxtRecords.forEach(record => this.#handleOperationalTxtRecord(record, netInterface));
862
+
863
+ let operationalSrvRecords = answers.operational[DnsRecordType.SRV] ?? [];
864
+ if (!operationalSrvRecords.length && formerAnswers.operational) {
865
+ operationalSrvRecords = formerAnswers.operational[DnsRecordType.SRV] ?? [];
738
866
  }
739
867
 
740
868
  if (operationalSrvRecords.length) {
741
869
  operationalSrvRecords.forEach(record =>
742
870
  this.#handleOperationalSrvRecord(record, answers, formerAnswers, netInterface),
743
871
  );
744
- recordsHandled = true;
745
872
  }
746
- return recordsHandled;
747
873
  }
748
874
 
749
875
  #handleOperationalTxtRecord(record: DnsRecord<any>, netInterface: string) {
@@ -791,8 +917,8 @@ export class MdnsScanner implements Scanner {
791
917
 
792
918
  #handleOperationalSrvRecord(
793
919
  record: DnsRecord<any>,
794
- answers: DnsRecord<any>[],
795
- formerAnswers: DnsRecord<any>[],
920
+ answers: StructuredDnsAnswers,
921
+ formerAnswers: StructuredDnsAnswers,
796
922
  netInterface: string,
797
923
  ) {
798
924
  const {
@@ -813,7 +939,7 @@ export class MdnsScanner implements Scanner {
813
939
  return true;
814
940
  }
815
941
 
816
- const ips = this.#handleIpRecords([...answers, ...formerAnswers], target, netInterface);
942
+ const ips = this.#handleIpRecords([formerAnswers, answers], target, netInterface);
817
943
  const deviceExisted = this.#operationalDeviceRecords.has(matterName);
818
944
  const device = this.#operationalDeviceRecords.get(matterName) ?? {
819
945
  deviceIdentifier: matterName,
@@ -861,18 +987,23 @@ export class MdnsScanner implements Scanner {
861
987
  return true;
862
988
  }
863
989
 
864
- #handleCommissionableRecords(answers: DnsRecord<any>[], formerAnswers: DnsRecord<any>[], netInterface: string) {
990
+ #handleCommissionableRecords(
991
+ answers: StructuredDnsAnswers,
992
+ formerAnswers: StructuredDnsAnswers,
993
+ netInterface: string,
994
+ ) {
865
995
  // Does the message contain a SRV record for an operational service we are interested in?
866
- let commissionableRecords = answers.filter(({ name }) => name.endsWith(MATTER_COMMISSION_SERVICE_QNAME));
867
- if (!commissionableRecords.length) {
868
- commissionableRecords = formerAnswers.filter(({ name }) => name.endsWith(MATTER_COMMISSION_SERVICE_QNAME));
869
- if (!commissionableRecords.length) return;
996
+ let commissionableRecords = answers.commissionable ?? {};
997
+ if (!commissionableRecords[DnsRecordType.SRV]?.length && !commissionableRecords[DnsRecordType.TXT]?.length) {
998
+ commissionableRecords = formerAnswers.commissionable ?? {};
999
+ if (!commissionableRecords[DnsRecordType.SRV]?.length && !commissionableRecords[DnsRecordType.TXT]?.length)
1000
+ return;
870
1001
  }
871
1002
 
872
1003
  const queryMissingDataForInstances = new Set<string>();
873
1004
 
874
1005
  // First process the TXT records
875
- const txtRecords = commissionableRecords.filter(({ recordType }) => recordType === DnsRecordType.TXT);
1006
+ const txtRecords = commissionableRecords[DnsRecordType.TXT] ?? [];
876
1007
  for (const record of txtRecords) {
877
1008
  const { name, ttl } = record;
878
1009
  if (ttl === 0) {
@@ -916,7 +1047,7 @@ export class MdnsScanner implements Scanner {
916
1047
  }
917
1048
 
918
1049
  // We got SRV records for the instance ID, so we know the host name now and can collect the IP addresses
919
- const srvRecords = commissionableRecords.filter(({ recordType }) => recordType === DnsRecordType.SRV);
1050
+ const srvRecords = commissionableRecords[DnsRecordType.SRV] ?? [];
920
1051
  for (const record of srvRecords) {
921
1052
  const storedRecord = this.#commissionableDeviceRecords.get(record.name);
922
1053
  if (storedRecord === undefined) continue;
@@ -934,7 +1065,7 @@ export class MdnsScanner implements Scanner {
934
1065
 
935
1066
  const recordExisting = storedRecord.addresses.size > 0;
936
1067
 
937
- const ips = this.#handleIpRecords([...answers, ...formerAnswers], target, netInterface);
1068
+ const ips = this.#handleIpRecords([formerAnswers, answers], target, netInterface);
938
1069
  if (ips.length > 0) {
939
1070
  for (const { value: ip, ttl } of ips) {
940
1071
  if (ttl === 0) {
@@ -61,6 +61,7 @@ export class MdnsServer {
61
61
  15 * 60 * 1000 /* 15mn - also matches maximum commissioning window time. */,
62
62
  );
63
63
  readonly #recordLastSentAsMulticastAnswer = new Map<string, number>();
64
+ readonly #recordLastSentAsUnicastAnswer = new Map<string, number>();
64
65
 
65
66
  readonly #network: Network;
66
67
  readonly #multicastServer: UdpMulticastServer;
@@ -75,8 +76,8 @@ export class MdnsServer {
75
76
  this.#netInterface = netInterface;
76
77
  }
77
78
 
78
- buildDnsRecordKey(record: DnsRecord<any>, netInterface?: string) {
79
- return `${record.name}-${record.recordClass}-${record.recordType}-${netInterface}`;
79
+ buildDnsRecordKey(record: DnsRecord<any>, netInterface?: string, unicastTarget?: string) {
80
+ return `${record.name}-${record.recordClass}-${record.recordType}-${netInterface}-${unicastTarget}`;
80
81
  }
81
82
 
82
83
  buildTypePortKey(type: AnnouncementType, port: number) {
@@ -128,30 +129,42 @@ export class MdnsServer {
128
129
 
129
130
  const now = Time.nowMs();
130
131
  let uniCastResponse = queries.filter(query => !query.uniCastResponse).length === 0;
131
- const answersTimeSinceLastMultiCast = answers.map(answer => ({
132
+ const answersTimeSinceLastSent = answers.map(answer => ({
132
133
  timeSinceLastMultiCast:
133
134
  now -
134
135
  (this.#recordLastSentAsMulticastAnswer.get(this.buildDnsRecordKey(answer, netInterface)) ?? 0),
136
+ timeSinceLastUniCast:
137
+ now -
138
+ (this.#recordLastSentAsUnicastAnswer.get(this.buildDnsRecordKey(answer, netInterface, remoteIp)) ??
139
+ 0),
135
140
  ttl: answer.ttl,
136
141
  }));
137
142
  if (
138
143
  uniCastResponse &&
139
- answersTimeSinceLastMultiCast.filter(
144
+ answersTimeSinceLastSent.some(
140
145
  ({ timeSinceLastMultiCast, ttl }) => timeSinceLastMultiCast > (ttl / 4) * 1000,
141
- ).length > 0
146
+ )
142
147
  ) {
143
148
  // If the query is for unicast response, still send as multicast if they were last sent as multicast longer then 1/4 of their ttl
144
149
  uniCastResponse = false;
145
150
  }
146
151
  if (!uniCastResponse) {
147
- answers = answers.filter(
148
- (_, index) => answersTimeSinceLastMultiCast[index].timeSinceLastMultiCast > 1000,
149
- );
152
+ answers = answers.filter((_, index) => answersTimeSinceLastSent[index].timeSinceLastMultiCast > 1000);
150
153
  if (answers.length === 0) continue; // Nothing to send
151
154
 
152
155
  answers.forEach(answer =>
153
156
  this.#recordLastSentAsMulticastAnswer.set(this.buildDnsRecordKey(answer, netInterface), now),
154
157
  );
158
+ } else {
159
+ answers = answers.filter((_, index) => answersTimeSinceLastSent[index].timeSinceLastUniCast > 1000);
160
+ if (answers.length === 0) continue; // Nothing to send
161
+
162
+ answers.forEach(answer =>
163
+ this.#recordLastSentAsUnicastAnswer.set(
164
+ this.buildDnsRecordKey(answer, netInterface, remoteIp),
165
+ now,
166
+ ),
167
+ );
155
168
  }
156
169
 
157
170
  this.#sendRecords(
@@ -290,6 +303,7 @@ export class MdnsServer {
290
303
  );
291
304
  await this.#records.clear();
292
305
  this.#recordLastSentAsMulticastAnswer.clear();
306
+ this.#recordLastSentAsUnicastAnswer.clear();
293
307
  }
294
308
 
295
309
  async setRecordsGenerator(
@@ -299,12 +313,14 @@ export class MdnsServer {
299
313
  ) {
300
314
  await this.#records.clear();
301
315
  this.#recordLastSentAsMulticastAnswer.clear();
316
+ this.#recordLastSentAsUnicastAnswer.clear();
302
317
  this.#recordsGenerator.set(this.buildTypePortKey(type, hostPort), generator);
303
318
  }
304
319
 
305
320
  async close() {
306
321
  await this.#records.close();
307
322
  this.#recordLastSentAsMulticastAnswer.clear();
323
+ this.#recordLastSentAsUnicastAnswer.clear();
308
324
  await this.#multicastServer.close();
309
325
  }
310
326
 
@@ -193,7 +193,7 @@ export class ControllerDiscovery {
193
193
  while (true) {
194
194
  logger.debug(
195
195
  `Server addresses to try: ${Array.from(addresses)
196
- .map(([addressString, { device }]) => `${device?.DN}: ${addressString}`)
196
+ .map(([addressString, { device }]) => `${addressString}${device?.DN ? ` (${device.DN})` : ""}`)
197
197
  .join(",")}`,
198
198
  );
199
199
 
@@ -82,7 +82,8 @@ export interface DiscoveryOptions {
82
82
  interface RunningDiscovery {
83
83
  type: NodeDiscoveryType;
84
84
  promises?: (() => Promise<MessageChannel>)[];
85
- timer?: Timer;
85
+ stopTimerFunc?: (() => void) | undefined;
86
+ mdnsScanner?: MdnsScanner;
86
87
  }
87
88
 
88
89
  /**
@@ -356,9 +357,8 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
356
357
  }
357
358
 
358
359
  async close() {
359
- const mdnsScanner = this.#scanners.scannerFor(ChannelType.UDP) as MdnsScanner | undefined;
360
- for (const [address, { timer }] of this.#runningPeerDiscoveries.entries()) {
361
- timer?.stop();
360
+ for (const [address, { stopTimerFunc, mdnsScanner }] of this.#runningPeerDiscoveries.entries()) {
361
+ stopTimerFunc?.();
362
362
 
363
363
  // This ends discovery without triggering promises
364
364
  mdnsScanner?.cancelOperationalDeviceDiscovery(this.#sessions.fabricFor(address), address.nodeId, false);
@@ -469,21 +469,26 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
469
469
 
470
470
  const discoveryPromises = new Array<() => Promise<MessageChannel>>();
471
471
  let reconnectionPollingTimer: Timer | undefined;
472
+ let stopTimerFunc: (() => void) | undefined;
472
473
 
473
- if (operationalAddress !== undefined) {
474
+ const lastOperationalAddress = this.#getLastOperationalAddress(address);
475
+ if (lastOperationalAddress !== undefined) {
474
476
  // Additionally to general discovery we also try to poll the formerly known operational address
475
477
  if (requestedDiscoveryType === NodeDiscoveryType.FullDiscovery) {
476
478
  const { promise, resolver, rejecter } = createPromise<MessageChannel>();
477
479
 
480
+ logger.debug(
481
+ `Starting reconnection polling for ${serverAddressToString(lastOperationalAddress)} (Interval ${RECONNECTION_POLLING_INTERVAL_MS / 1000}s)`,
482
+ );
478
483
  reconnectionPollingTimer = Time.getPeriodicTimer(
479
484
  "Controller reconnect",
480
485
  RECONNECTION_POLLING_INTERVAL_MS,
481
486
  async () => {
482
487
  try {
483
- logger.debug(`Polling for device at ${serverAddressToString(operationalAddress)} ...`);
488
+ logger.debug(`Polling for device at ${serverAddressToString(lastOperationalAddress)} ...`);
484
489
  const result = await this.#reconnectKnownAddress(
485
490
  address,
486
- operationalAddress,
491
+ lastOperationalAddress,
487
492
  discoveryData,
488
493
  );
489
494
  if (result !== undefined && reconnectionPollingTimer?.isRunning) {
@@ -509,6 +514,11 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
509
514
  },
510
515
  ).start();
511
516
 
517
+ stopTimerFunc = () => {
518
+ reconnectionPollingTimer?.stop();
519
+ reconnectionPollingTimer = undefined;
520
+ rejecter(new NoResponseTimeoutError("Reconnection polling cancelled"));
521
+ };
512
522
  discoveryPromises.push(() => promise);
513
523
  }
514
524
  }
@@ -521,8 +531,8 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
521
531
  timeoutSeconds,
522
532
  timeoutSeconds === undefined,
523
533
  );
524
- const { timer } = this.#runningPeerDiscoveries.get(address) ?? {};
525
- timer?.stop();
534
+ const { stopTimerFunc } = this.#runningPeerDiscoveries.get(address) ?? {};
535
+ stopTimerFunc?.();
526
536
  this.#runningPeerDiscoveries.delete(address);
527
537
 
528
538
  const { result } = await ControllerDiscovery.iterateServerAddresses(
@@ -551,10 +561,13 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
551
561
  this.#runningPeerDiscoveries.set(address, {
552
562
  type: requestedDiscoveryType,
553
563
  promises: discoveryPromises,
554
- timer: reconnectionPollingTimer,
564
+ stopTimerFunc,
565
+ mdnsScanner,
555
566
  });
556
567
 
557
- return await anyPromise(discoveryPromises).finally(() => this.#runningPeerDiscoveries.delete(address));
568
+ return await anyPromise(discoveryPromises).finally(() => {
569
+ this.#runningPeerDiscoveries.delete(address);
570
+ });
558
571
  }
559
572
 
560
573
  async #reconnectKnownAddress(
@@ -713,6 +726,16 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
713
726
  };
714
727
  }
715
728
  await this.#store.updatePeer(peer);
729
+
730
+ // If we got a new channel and have a running discovery we can end it
731
+ if (this.#runningPeerDiscoveries.has(address)) {
732
+ logger.info(`Found ${address} during discovery, cancel discovery.`);
733
+ // We are currently discovering this node, so we need to update the discovery data
734
+ const { mdnsScanner } = this.#runningPeerDiscoveries.get(address) ?? {};
735
+
736
+ // This ends discovery and triggers the promises
737
+ mdnsScanner?.cancelOperationalDeviceDiscovery(this.#sessions.fabricFor(address), address.nodeId, true);
738
+ }
716
739
  }
717
740
 
718
741
  #getLastOperationalAddress(address: PeerAddress) {
@@ -49,6 +49,12 @@ export type ExchangeSendOptions = {
49
49
  */
50
50
  expectAckOnly?: boolean;
51
51
 
52
+ /**
53
+ * If the message is part of a multiple message interaction, this flag indicates that it is not allowed
54
+ * to establish a new exchange
55
+ */
56
+ multipleMessageInteraction?: boolean;
57
+
52
58
  /**
53
59
  * Defined an expected processing time by the responder for the message. This is used to calculate the final
54
60
  * timeout for responses together with the normal retransmission logic when MRP is used.