@matter/protocol 0.12.0-alpha.0-20250108-7ae2a767d → 0.12.0-alpha.0-20250110-6349da2f0

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 (54) hide show
  1. package/dist/cjs/interaction/ServerSubscription.d.ts.map +1 -1
  2. package/dist/cjs/interaction/ServerSubscription.js +7 -3
  3. package/dist/cjs/interaction/ServerSubscription.js.map +1 -1
  4. package/dist/cjs/mdns/MdnsBroadcaster.d.ts +3 -3
  5. package/dist/cjs/mdns/MdnsBroadcaster.d.ts.map +1 -1
  6. package/dist/cjs/mdns/MdnsBroadcaster.js +55 -31
  7. package/dist/cjs/mdns/MdnsBroadcaster.js.map +1 -1
  8. package/dist/cjs/mdns/MdnsScanner.d.ts.map +1 -1
  9. package/dist/cjs/mdns/MdnsScanner.js +226 -28
  10. package/dist/cjs/mdns/MdnsScanner.js.map +1 -1
  11. package/dist/cjs/mdns/MdnsServer.d.ts +5 -1
  12. package/dist/cjs/mdns/MdnsServer.d.ts.map +1 -1
  13. package/dist/cjs/mdns/MdnsServer.js +12 -7
  14. package/dist/cjs/mdns/MdnsServer.js.map +1 -1
  15. package/dist/cjs/peer/ControllerCommissioningFlow.d.ts.map +1 -1
  16. package/dist/cjs/peer/ControllerCommissioningFlow.js +35 -2
  17. package/dist/cjs/peer/ControllerCommissioningFlow.js.map +1 -1
  18. package/dist/cjs/protocol/DeviceAdvertiser.d.ts.map +1 -1
  19. package/dist/cjs/protocol/DeviceAdvertiser.js +6 -1
  20. package/dist/cjs/protocol/DeviceAdvertiser.js.map +1 -1
  21. package/dist/cjs/protocol/DeviceCommissioner.d.ts.map +1 -1
  22. package/dist/cjs/protocol/DeviceCommissioner.js +5 -1
  23. package/dist/cjs/protocol/DeviceCommissioner.js.map +1 -1
  24. package/dist/esm/interaction/ServerSubscription.d.ts.map +1 -1
  25. package/dist/esm/interaction/ServerSubscription.js +7 -3
  26. package/dist/esm/interaction/ServerSubscription.js.map +1 -1
  27. package/dist/esm/mdns/MdnsBroadcaster.d.ts +3 -3
  28. package/dist/esm/mdns/MdnsBroadcaster.d.ts.map +1 -1
  29. package/dist/esm/mdns/MdnsBroadcaster.js +56 -31
  30. package/dist/esm/mdns/MdnsBroadcaster.js.map +1 -1
  31. package/dist/esm/mdns/MdnsScanner.d.ts.map +1 -1
  32. package/dist/esm/mdns/MdnsScanner.js +226 -28
  33. package/dist/esm/mdns/MdnsScanner.js.map +1 -1
  34. package/dist/esm/mdns/MdnsServer.d.ts +5 -1
  35. package/dist/esm/mdns/MdnsServer.d.ts.map +1 -1
  36. package/dist/esm/mdns/MdnsServer.js +12 -7
  37. package/dist/esm/mdns/MdnsServer.js.map +1 -1
  38. package/dist/esm/peer/ControllerCommissioningFlow.d.ts.map +1 -1
  39. package/dist/esm/peer/ControllerCommissioningFlow.js +36 -3
  40. package/dist/esm/peer/ControllerCommissioningFlow.js.map +1 -1
  41. package/dist/esm/protocol/DeviceAdvertiser.d.ts.map +1 -1
  42. package/dist/esm/protocol/DeviceAdvertiser.js +6 -1
  43. package/dist/esm/protocol/DeviceAdvertiser.js.map +1 -1
  44. package/dist/esm/protocol/DeviceCommissioner.d.ts.map +1 -1
  45. package/dist/esm/protocol/DeviceCommissioner.js +5 -1
  46. package/dist/esm/protocol/DeviceCommissioner.js.map +1 -1
  47. package/package.json +6 -6
  48. package/src/interaction/ServerSubscription.ts +7 -3
  49. package/src/mdns/MdnsBroadcaster.ts +63 -35
  50. package/src/mdns/MdnsScanner.ts +264 -43
  51. package/src/mdns/MdnsServer.ts +20 -7
  52. package/src/peer/ControllerCommissioningFlow.ts +48 -3
  53. package/src/protocol/DeviceAdvertiser.ts +7 -1
  54. package/src/protocol/DeviceCommissioner.ts +11 -2
@@ -56,6 +56,7 @@ const MDNS_EXPIRY_GRACE_PERIOD_FACTOR = 1.05;
56
56
 
57
57
  type MatterServerRecordWithExpire = ServerAddressIp & Lifespan;
58
58
 
59
+ /** Type for commissionable Device records including Lifespan details. */
59
60
  type CommissionableDeviceRecordWithExpire = Omit<CommissionableDevice, "addresses"> &
60
61
  Lifespan & {
61
62
  addresses: Map<string, MatterServerRecordWithExpire>; // Override addresses type to include expiration
@@ -65,17 +66,27 @@ type CommissionableDeviceRecordWithExpire = Omit<CommissionableDevice, "addresse
65
66
  P?: number; // Additional Field for Product ID
66
67
  };
67
68
 
69
+ /** Type for operational Device records including Lifespan details. */
68
70
  type OperationalDeviceRecordWithExpire = Omit<OperationalDevice, "addresses"> &
69
71
  Lifespan & {
70
72
  addresses: Map<string, MatterServerRecordWithExpire>; // Override addresses type to include expiration
71
73
  };
72
74
 
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
75
+ /** Type for any DNS record with Lifespan (discoveredAt and ttl) details. */
76
+ type AnyDnsRecordWithExpiry = DnsRecord<any> & Lifespan;
77
+
78
+ /** Type for DNS answers with Address details structured for better direct access performance. */
79
+ type StructuredDnsAddressAnswers = {
80
+ addressesV4?: Record<string, Map<string, AnyDnsRecordWithExpiry>>; // IPv4 Address record by name and value (IP)
81
+ addressesV6?: Record<string, Map<string, AnyDnsRecordWithExpiry>>; // IPv6 Address record by name and value (IP)
77
82
  };
78
83
 
84
+ /** Type for DNS answers with Lifespan details structured for better direct access performance. */
85
+ type StructuredDnsAnswers = {
86
+ operational?: Record<number, AnyDnsRecordWithExpiry[]>; // Operational Matter device records by recordType
87
+ commissionable?: Record<number, AnyDnsRecordWithExpiry[]>; // Commissionable Matter device records by recordType
88
+ } & StructuredDnsAddressAnswers;
89
+
79
90
  /** The initial number of seconds between two announcements. MDNS specs require 1-2 seconds, so lets use the middle. */
80
91
  const START_ANNOUNCE_INTERVAL_SECONDS = 1.5;
81
92
 
@@ -102,12 +113,19 @@ export class MdnsScanner implements Scanner {
102
113
  );
103
114
  }
104
115
 
116
+ /** Active announces by queryId with queries and known answers */
105
117
  readonly #activeAnnounceQueries = new Map<string, { queries: DnsQuery[]; answers: StructuredDnsAnswers }>();
106
- #queryTimer?: Timer;
107
- #nextAnnounceIntervalSeconds = START_ANNOUNCE_INTERVAL_SECONDS;
108
118
 
119
+ /** Known IP addresses by network interface */
120
+ readonly #discoveredIpRecords = new Map<string, StructuredDnsAddressAnswers>();
121
+
122
+ /** Known operational device records by Matter Qname */
109
123
  readonly #operationalDeviceRecords = new Map<string, OperationalDeviceRecordWithExpire>();
124
+
125
+ /** Known commissionable device records by queryId */
110
126
  readonly #commissionableDeviceRecords = new Map<string, CommissionableDeviceRecordWithExpire>();
127
+
128
+ /** Waiters for specific queryIds to resolve a promise when a record is discovered */
111
129
  readonly #recordWaiters = new Map<
112
130
  string,
113
131
  {
@@ -117,9 +135,11 @@ export class MdnsScanner implements Scanner {
117
135
  cancelResolver?: (value: void) => void;
118
136
  }
119
137
  >();
138
+
139
+ #queryTimer?: Timer;
140
+ #nextAnnounceIntervalSeconds = START_ANNOUNCE_INTERVAL_SECONDS;
120
141
  readonly #periodicTimer: Timer;
121
142
  #closing = false;
122
-
123
143
  readonly #multicastServer: UdpMulticastServer;
124
144
  readonly #enableIpv4?: boolean;
125
145
 
@@ -149,7 +169,9 @@ export class MdnsScanner implements Scanner {
149
169
  const allQueries = Array.from(this.#activeAnnounceQueries.values());
150
170
  const queries = allQueries.flatMap(({ queries }) => queries);
151
171
  const answers = allQueries.flatMap(({ answers }) =>
152
- Object.values(answers).flatMap(answer => Object.values(answer).flatMap(records => records)),
172
+ Object.values(answers).flatMap(answer =>
173
+ Object.values(answer).flatMap(records => (Array.isArray(records) ? records : records.values())),
174
+ ),
153
175
  );
154
176
 
155
177
  this.#queryTimer = Time.getTimer("MDNS discovery", this.#nextAnnounceIntervalSeconds * 1000, () =>
@@ -249,9 +271,14 @@ export class MdnsScanner implements Scanner {
249
271
  this.#queryTimer = Time.getTimer("MDNS discovery", 0, () => this.#sendQueries()).start();
250
272
  }
251
273
 
252
- #getActiveQueryEarlierAnswers() {
274
+ /**
275
+ * Combines the known answers from all active queries and the known IP addresses from the network
276
+ * interface into one data package
277
+ */
278
+ #getActiveQueryEarlierAnswers(netInterface: string) {
253
279
  return this.#combineStructuredAnswers(
254
280
  ...[...this.#activeAnnounceQueries.values()].map(({ answers }) => answers),
281
+ this.#discoveredIpRecords.get(netInterface) ?? {},
255
282
  );
256
283
  }
257
284
 
@@ -714,21 +741,38 @@ export class MdnsScanner implements Scanner {
714
741
  #structureAnswers(...answersList: DnsRecord<any>[][]): StructuredDnsAnswers {
715
742
  const structuredAnswers: StructuredDnsAnswers = {};
716
743
 
744
+ const discoveredAt = Time.nowMs();
717
745
  answersList.forEach(answers =>
718
746
  answers.forEach(answer => {
719
747
  const { name, recordType } = answer;
720
748
  if (name.endsWith(MATTER_SERVICE_QNAME)) {
721
749
  structuredAnswers.operational = structuredAnswers.operational ?? {};
722
750
  structuredAnswers.operational[recordType] = structuredAnswers.operational[recordType] ?? [];
723
- structuredAnswers.operational[recordType].push(answer);
751
+ structuredAnswers.operational[recordType].push({
752
+ discoveredAt,
753
+ ...answer,
754
+ });
724
755
  } else if (name.endsWith(MATTER_COMMISSION_SERVICE_QNAME)) {
725
756
  structuredAnswers.commissionable = structuredAnswers.commissionable ?? {};
726
757
  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);
758
+ structuredAnswers.commissionable[recordType].push({
759
+ discoveredAt,
760
+ ...answer,
761
+ });
762
+ } else if (recordType === DnsRecordType.AAAA) {
763
+ structuredAnswers.addressesV6 = structuredAnswers.addressesV6 ?? {};
764
+ structuredAnswers.addressesV6[name] = structuredAnswers.addressesV6[name] ?? new Map();
765
+ structuredAnswers.addressesV6[name].set(answer.value, {
766
+ discoveredAt,
767
+ ...answer,
768
+ });
769
+ } else if (this.#enableIpv4 && recordType === DnsRecordType.A) {
770
+ structuredAnswers.addressesV4 = structuredAnswers.addressesV4 ?? {};
771
+ structuredAnswers.addressesV4[name] = structuredAnswers.addressesV4[name] ?? new Map();
772
+ structuredAnswers.addressesV4[name].set(answer.value, {
773
+ discoveredAt,
774
+ ...answer,
775
+ });
732
776
  }
733
777
  }),
734
778
  );
@@ -737,10 +781,12 @@ export class MdnsScanner implements Scanner {
737
781
  }
738
782
 
739
783
  #combineStructuredAnswers(...answersList: StructuredDnsAnswers[]): StructuredDnsAnswers {
784
+ // Special type for easier combination of answers
740
785
  const combinedAnswers: {
741
- operational?: Record<number, Map<string, DnsRecord<any>>>;
742
- commissionable?: Record<number, Map<string, DnsRecord<any>>>;
743
- addresses?: Record<string, DnsRecord<any>[]>;
786
+ operational?: Record<number, Map<string, AnyDnsRecordWithExpiry>>;
787
+ commissionable?: Record<number, Map<string, AnyDnsRecordWithExpiry>>;
788
+ addressesV4?: Record<string, Map<string, AnyDnsRecordWithExpiry>>;
789
+ addressesV6?: Record<string, Map<string, AnyDnsRecordWithExpiry>>;
744
790
  } = {};
745
791
 
746
792
  for (const answers of answersList) {
@@ -748,11 +794,18 @@ export class MdnsScanner implements Scanner {
748
794
  combinedAnswers.operational = combinedAnswers.operational ?? {};
749
795
  for (const [recordType, records] of Object.entries(answers.operational) as unknown as [
750
796
  number,
751
- DnsRecord<any>[],
797
+ AnyDnsRecordWithExpiry[],
752
798
  ][]) {
753
799
  combinedAnswers.operational[recordType] = combinedAnswers.operational[recordType] ?? new Map();
754
800
  records.forEach(record => {
755
- combinedAnswers.operational![recordType].set(record.name, record);
801
+ const existingRecord = combinedAnswers.operational![recordType].get(record.name);
802
+ if (existingRecord && existingRecord.discoveredAt < record.discoveredAt) {
803
+ if (record.ttl === 0) {
804
+ combinedAnswers.operational![recordType].delete(record.name);
805
+ } else {
806
+ combinedAnswers.operational![recordType].set(record.name, record);
807
+ }
808
+ }
756
809
  });
757
810
  }
758
811
  }
@@ -760,24 +813,58 @@ export class MdnsScanner implements Scanner {
760
813
  combinedAnswers.commissionable = combinedAnswers.commissionable ?? {};
761
814
  for (const [recordType, records] of Object.entries(answers.commissionable) as unknown as [
762
815
  number,
763
- DnsRecord<any>[],
816
+ AnyDnsRecordWithExpiry[],
764
817
  ][]) {
765
818
  combinedAnswers.commissionable[recordType] =
766
819
  combinedAnswers.commissionable[recordType] ?? new Map();
767
820
  records.forEach(record => {
768
- combinedAnswers.commissionable![recordType].set(record.name, record);
821
+ const existingRecord = combinedAnswers.commissionable![recordType].get(record.name);
822
+ if (existingRecord && existingRecord.discoveredAt < record.discoveredAt) {
823
+ if (record.ttl === 0) {
824
+ combinedAnswers.commissionable![recordType].delete(record.name);
825
+ } else {
826
+ combinedAnswers.commissionable![recordType].set(record.name, record);
827
+ }
828
+ }
769
829
  });
770
830
  }
771
831
  }
772
- if (answers.addresses) {
773
- combinedAnswers.addresses = combinedAnswers.addresses ?? {};
774
- for (const [name, records] of Object.entries(answers.addresses) as unknown as [
832
+ if (answers.addressesV6) {
833
+ combinedAnswers.addressesV6 = combinedAnswers.addressesV6 ?? {};
834
+ for (const [name, records] of Object.entries(answers.addressesV6) as unknown as [
775
835
  string,
776
- DnsRecord<any>[],
836
+ Map<string, AnyDnsRecordWithExpiry>,
777
837
  ][]) {
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);
838
+ combinedAnswers.addressesV6[name] = combinedAnswers.addressesV6[name] ?? new Map();
839
+ Object.values(records).forEach(record => {
840
+ const existingRecord = combinedAnswers.addressesV6![name].get(record.value);
841
+ if (existingRecord && existingRecord.discoveredAt < record.discoveredAt) {
842
+ if (record.ttl === 0) {
843
+ combinedAnswers.addressesV6![name].delete(name);
844
+ } else {
845
+ combinedAnswers.addressesV6![name].set(name, record);
846
+ }
847
+ }
848
+ });
849
+ }
850
+ }
851
+ if (this.#enableIpv4 && answers.addressesV4) {
852
+ combinedAnswers.addressesV4 = combinedAnswers.addressesV4 ?? {};
853
+ for (const [name, records] of Object.entries(answers.addressesV4) as unknown as [
854
+ string,
855
+ Map<string, AnyDnsRecordWithExpiry>,
856
+ ][]) {
857
+ combinedAnswers.addressesV4[name] = combinedAnswers.addressesV4[name] ?? new Map();
858
+ Object.values(records).forEach(record => {
859
+ const existingRecord = combinedAnswers.addressesV4![name].get(record.value);
860
+ if (existingRecord && existingRecord.discoveredAt < record.discoveredAt) {
861
+ if (record.ttl === 0) {
862
+ combinedAnswers.addressesV4![name].delete(name);
863
+ } else {
864
+ combinedAnswers.addressesV4![name].set(name, record);
865
+ }
866
+ }
867
+ });
781
868
  }
782
869
  }
783
870
  }
@@ -799,8 +886,11 @@ export class MdnsScanner implements Scanner {
799
886
  ]),
800
887
  );
801
888
  }
802
- if (combinedAnswers.addresses) {
803
- result.addresses = combinedAnswers.addresses;
889
+ if (combinedAnswers.addressesV6) {
890
+ result.addressesV6 = combinedAnswers.addressesV6;
891
+ }
892
+ if (this.#enableIpv4 && combinedAnswers.addressesV4) {
893
+ result.addressesV4 = combinedAnswers.addressesV4;
804
894
  }
805
895
 
806
896
  return result;
@@ -819,12 +909,77 @@ export class MdnsScanner implements Scanner {
819
909
 
820
910
  const answers = this.#structureAnswers([...message.answers, ...message.additionalRecords]);
821
911
 
822
- const formerAnswers = this.#getActiveQueryEarlierAnswers();
912
+ const formerAnswers = this.#getActiveQueryEarlierAnswers(netInterface);
823
913
  // Check if we got operational discovery records and handle them
824
914
  this.#handleOperationalRecords(answers, formerAnswers, netInterface);
825
915
 
826
916
  // Else check if we got commissionable discovery records and handle them
827
917
  this.#handleCommissionableRecords(answers, formerAnswers, netInterface);
918
+
919
+ this.#updateIpRecords(answers, netInterface);
920
+ }
921
+
922
+ /**
923
+ * Update the discovered matter relevant IP records with the new data from the DNS message.
924
+ */
925
+ #updateIpRecords(answers: StructuredDnsAnswers, netInterface: string) {
926
+ const interfaceRecords = this.#discoveredIpRecords.get(netInterface);
927
+ if (interfaceRecords === undefined) {
928
+ return;
929
+ }
930
+ let updated = false;
931
+ if (answers.addressesV6) {
932
+ for (const [target, ipAddresses] of Object.entries(answers.addressesV6)) {
933
+ if (interfaceRecords.addressesV6?.[target] !== undefined) {
934
+ for (const [ip, record] of Object.entries(ipAddresses)) {
935
+ if (record.ttl === 0) {
936
+ interfaceRecords.addressesV6[target].delete(ip);
937
+ } else {
938
+ interfaceRecords.addressesV6[target].set(ip, record);
939
+ }
940
+ updated = true;
941
+ }
942
+ }
943
+ }
944
+ }
945
+ if (this.#enableIpv4 && answers.addressesV4) {
946
+ for (const [target, ipAddresses] of Object.entries(answers.addressesV4)) {
947
+ if (interfaceRecords.addressesV4?.[target] !== undefined) {
948
+ for (const [ip, record] of Object.entries(ipAddresses)) {
949
+ if (record.ttl === 0) {
950
+ interfaceRecords.addressesV4[target].delete(ip);
951
+ } else {
952
+ interfaceRecords.addressesV4[target].set(ip, record);
953
+ }
954
+ updated = true;
955
+ }
956
+ }
957
+ }
958
+ }
959
+ if (updated) {
960
+ this.#discoveredIpRecords.set(netInterface, interfaceRecords);
961
+ }
962
+ }
963
+
964
+ /**
965
+ * Register Matter relevant IP records for later usage.
966
+ */
967
+ #registerIpRecords(ipAddresses: AnyDnsRecordWithExpiry[], netInterface: string) {
968
+ const interfaceRecords = this.#discoveredIpRecords.get(netInterface) ?? {};
969
+ for (const record of ipAddresses) {
970
+ const { recordType, name, value: ip, ttl } = record as DnsRecord<string>;
971
+ if (ttl === 0) continue; // Skip records with ttl=0
972
+ if (recordType === DnsRecordType.AAAA) {
973
+ interfaceRecords.addressesV6 = interfaceRecords.addressesV6 ?? {};
974
+ interfaceRecords.addressesV6[name] = interfaceRecords.addressesV6[name] ?? new Map();
975
+ interfaceRecords.addressesV6[name].set(ip, record);
976
+ } else if (this.#enableIpv4 && recordType === DnsRecordType.A) {
977
+ interfaceRecords.addressesV4 = interfaceRecords.addressesV4 ?? {};
978
+ interfaceRecords.addressesV4[name] = interfaceRecords.addressesV4[name] ?? new Map();
979
+ interfaceRecords.addressesV4[name].set(ip, record);
980
+ }
981
+ }
982
+ this.#discoveredIpRecords.set(netInterface, interfaceRecords);
828
983
  }
829
984
 
830
985
  #handleIpRecords(
@@ -832,12 +987,21 @@ export class MdnsScanner implements Scanner {
832
987
  target: string,
833
988
  netInterface: string,
834
989
  ): { value: string; ttl: number }[] {
835
- const ipRecords = answers
836
- .flatMap(answer => answer.addresses?.[target] ?? [])
837
- .filter(
838
- ({ recordType }) =>
839
- recordType === DnsRecordType.AAAA || (recordType === DnsRecordType.A && this.#enableIpv4),
840
- );
990
+ const ipRecords = new Array<AnyDnsRecordWithExpiry>();
991
+ answers.forEach(answer => {
992
+ if (answer.addressesV6?.[target]) {
993
+ ipRecords.push(...answer.addressesV6[target].values());
994
+ }
995
+ if (this.#enableIpv4 && answer.addressesV4?.[target]) {
996
+ ipRecords.push(...answer.addressesV4[target].values());
997
+ }
998
+ });
999
+ if (ipRecords.length === 0) {
1000
+ return [];
1001
+ }
1002
+
1003
+ // Remember the IP records for later Matter usage
1004
+ this.#registerIpRecords(ipRecords, netInterface); // Register for potential later usage
841
1005
 
842
1006
  // If an IP is included multiple times we only keep the latest record
843
1007
  const collectedIps = new Map<string, { value: string; ttl: number }>();
@@ -950,6 +1114,7 @@ export class MdnsScanner implements Scanner {
950
1114
  discoveredAt,
951
1115
  ttl: ttl * 1000,
952
1116
  };
1117
+ const ipsInitiallyEmpty = device.addresses.size === 0;
953
1118
  const { addresses } = device;
954
1119
  if (ips.length > 0) {
955
1120
  for (const { value: ip, ttl } of ips) {
@@ -967,9 +1132,9 @@ export class MdnsScanner implements Scanner {
967
1132
  addresses.set(address.ip, address);
968
1133
  }
969
1134
  device.addresses = addresses;
970
- if (!this.#operationalDeviceRecords.has(matterName)) {
1135
+ if (ipsInitiallyEmpty) {
971
1136
  logger.debug(
972
- `Added IPs for operational device ${matterName} to cache (interface ${netInterface}):`,
1137
+ `Added ${addresses.size} IPs for operational device ${matterName} to cache (interface ${netInterface}):`,
973
1138
  ...MdnsScanner.deviceAddressDiagnostics(addresses),
974
1139
  );
975
1140
  }
@@ -1066,7 +1231,7 @@ export class MdnsScanner implements Scanner {
1066
1231
  continue;
1067
1232
  }
1068
1233
 
1069
- const recordExisting = storedRecord.addresses.size > 0;
1234
+ const recordAddressesKnown = storedRecord.addresses.size > 0;
1070
1235
 
1071
1236
  const ips = this.#handleIpRecords([formerAnswers, answers], target, netInterface);
1072
1237
  if (ips.length > 0) {
@@ -1099,6 +1264,11 @@ export class MdnsScanner implements Scanner {
1099
1264
  `Requesting IP addresses for commissionable device ${record.name} (interface ${netInterface}).`,
1100
1265
  );
1101
1266
  this.#setQueryRecords(queryId, queries, answers);
1267
+ } else if (!recordAddressesKnown) {
1268
+ logger.debug(
1269
+ `Added ${storedRecord.addresses.size} IPs for commissionable device ${record.name} to cache (interface ${netInterface}):`,
1270
+ ...MdnsScanner.deviceAddressDiagnostics(storedRecord.addresses),
1271
+ );
1102
1272
  }
1103
1273
  if (storedRecord.addresses.size === 0) continue;
1104
1274
 
@@ -1106,7 +1276,7 @@ export class MdnsScanner implements Scanner {
1106
1276
  if (queryId === undefined) continue;
1107
1277
 
1108
1278
  queryMissingDataForInstances.delete(record.name); // No need to query anymore, we have anything we need
1109
- this.#finishWaiter(queryId, true, recordExisting);
1279
+ this.#finishWaiter(queryId, true, recordAddressesKnown);
1110
1280
  }
1111
1281
 
1112
1282
  // We have to query for the SRV records for the missing commissionable devices where we only had TXT records
@@ -1199,10 +1369,61 @@ export class MdnsScanner implements Scanner {
1199
1369
  });
1200
1370
  }
1201
1371
  if (now > expires && !addresses.size) {
1202
- // device expired and also has no adresses anymore
1372
+ // device expired and also has no addresses anymore
1203
1373
  this.#commissionableDeviceRecords.delete(recordKey);
1204
1374
  }
1205
1375
  });
1376
+
1377
+ [...this.#activeAnnounceQueries.values()].forEach(({ answers }) => this.#expireStructuredAnswers(answers, now));
1378
+
1379
+ this.#discoveredIpRecords.forEach(answers => this.#expireStructuredAnswers(answers, now));
1380
+ }
1381
+
1382
+ #expireStructuredAnswers(data: StructuredDnsAnswers, now: number) {
1383
+ if (data.operational) {
1384
+ Object.keys(data.operational).forEach(recordType => {
1385
+ const type = parseInt(recordType);
1386
+ data.operational![type] = data.operational![type].filter(
1387
+ ({ discoveredAt, ttl }) => now < discoveredAt + this.#effectiveTTL(ttl * 1000),
1388
+ );
1389
+ if (data.operational![type].length === 0) {
1390
+ delete data.operational![type];
1391
+ }
1392
+ });
1393
+ }
1394
+ if (data.commissionable) {
1395
+ Object.keys(data.commissionable).forEach(recordType => {
1396
+ const type = parseInt(recordType);
1397
+ data.commissionable![type] = data.commissionable![type].filter(
1398
+ ({ discoveredAt, ttl }) => now < discoveredAt + this.#effectiveTTL(ttl * 1000),
1399
+ );
1400
+ if (data.commissionable![type].length === 0) {
1401
+ delete data.commissionable![type];
1402
+ }
1403
+ });
1404
+ }
1405
+ if (data.addressesV6) {
1406
+ Object.keys(data.addressesV6).forEach(name => {
1407
+ for (const [ip, { discoveredAt, ttl }] of data.addressesV6![name].entries()) {
1408
+ if (now < discoveredAt + this.#effectiveTTL(ttl * 1000)) continue; // not expired yet
1409
+ data.addressesV6![name].delete(ip);
1410
+ }
1411
+ if (data.addressesV6![name].size === 0) {
1412
+ delete data.addressesV6![name];
1413
+ }
1414
+ });
1415
+ }
1416
+ if (data.addressesV4) {
1417
+ Object.keys(data.addressesV4).forEach(name => {
1418
+ for (const [ip, { discoveredAt, ttl }] of data.addressesV4![name].entries()) {
1419
+ if (now < discoveredAt + this.#effectiveTTL(ttl * 1000)) continue; // not expired yet
1420
+ data.addressesV4![name].delete(ip);
1421
+ }
1422
+ if (data.addressesV4![name].size === 0) {
1423
+ delete data.addressesV4![name];
1424
+ }
1425
+ });
1426
+ }
1206
1427
  }
1207
1428
 
1208
1429
  static discoveryDataDiagnostics(data: DiscoveryData) {
@@ -115,14 +115,14 @@ export class MdnsServer {
115
115
  : [];
116
116
  if (knownAnswers.length > 0) {
117
117
  for (const knownAnswersRecord of knownAnswers) {
118
- answers = answers.filter(record => !isDeepEqual(record, knownAnswersRecord));
118
+ answers = answers.filter(record => !isDeepEqual(record, knownAnswersRecord, true));
119
119
  if (answers.length === 0) break; // Nothing to send
120
120
  }
121
121
  if (answers.length === 0) continue; // Nothing to send
122
122
  if (additionalRecords.length > 0) {
123
123
  for (const knownAnswersRecord of knownAnswers) {
124
124
  additionalRecords = additionalRecords.filter(
125
- record => !isDeepEqual(record, knownAnswersRecord),
125
+ record => !isDeepEqual(record, knownAnswersRecord, true),
126
126
  );
127
127
  }
128
128
  }
@@ -268,7 +268,8 @@ export class MdnsServer {
268
268
  ).catch(error => logger.error(error));
269
269
  }
270
270
 
271
- async expireAnnouncements(announcedNetPort?: number, type?: AnnouncementType) {
271
+ async expireAnnouncements(options?: { announcedNetPort?: number; type?: AnnouncementType; forInstance?: string }) {
272
+ const { announcedNetPort, type, forInstance: instanceToExpire } = options ?? {};
272
273
  await MatterAggregateError.allSettled(
273
274
  this.#records.keys().map(async netInterface => {
274
275
  const records = await this.#records.get(netInterface);
@@ -280,13 +281,25 @@ export class MdnsServer {
280
281
  portType !== this.buildTypePortKey(type, announcedNetPort)
281
282
  )
282
283
  continue;
283
- let instanceName: string | undefined;
284
- portTypeRecords.forEach(record => {
284
+ const recordsToProcess =
285
+ instanceToExpire !== undefined
286
+ ? portTypeRecords.filter(
287
+ ({ forInstance }) => forInstance !== undefined && instanceToExpire === forInstance,
288
+ )
289
+ : portTypeRecords;
290
+ const instanceSet = new Set<string>();
291
+ recordsToProcess.forEach(record => {
285
292
  record.ttl = 0;
286
- if (instanceName === undefined && record.recordType === DnsRecordType.TXT) {
287
- instanceName = record.name;
293
+ if (record.recordType === DnsRecordType.TXT) {
294
+ instanceSet.add(record.name);
288
295
  }
289
296
  });
297
+ const instanceName =
298
+ instanceSet.size > 1
299
+ ? "multiple"
300
+ : instanceSet.size === 1
301
+ ? Array.from(instanceSet.values())[0]
302
+ : "";
290
303
  logger.debug(
291
304
  `Expiring records`,
292
305
  Diagnostic.dict({
@@ -10,7 +10,7 @@ import { GeneralCommissioning } from "#clusters/general-commissioning";
10
10
  import { NetworkCommissioning } from "#clusters/network-commissioning";
11
11
  import { OperationalCredentials } from "#clusters/operational-credentials";
12
12
  import { TimeSynchronizationCluster } from "#clusters/time-synchronization";
13
- import { Bytes, ChannelType, Crypto, Logger, MatterError, Time, UnexpectedDataError, repackErrorAs } from "#general";
13
+ import { Bytes, ChannelType, Crypto, Logger, MatterError, repackErrorAs, Time, UnexpectedDataError } from "#general";
14
14
  import {
15
15
  ClusterId,
16
16
  ClusterType,
@@ -107,6 +107,9 @@ type CommissioningStep = {
107
107
 
108
108
  /** Logic function to execute */
109
109
  stepLogic: () => Promise<CommissioningStepResult>;
110
+
111
+ /** Optional flag to indicate that the failsafe timer should be rearmed in any case before this step. */
112
+ reArmFailsafe?: boolean;
110
113
  };
111
114
 
112
115
  /** Data that are collected initially or through the commissioning process and can be used also by other steps. */
@@ -194,9 +197,15 @@ export class ControllerCommissioningFlow {
194
197
  async executeCommissioning() {
195
198
  this.#sortSteps();
196
199
 
200
+ let failSafeTimerReArmedAfterPreviousStep = false;
197
201
  for (const step of this.#commissioningSteps) {
198
202
  logger.info(`Executing commissioning step ${step.stepNumber}.${step.subStepNumber}: ${step.name}`);
199
203
  try {
204
+ if (step.reArmFailsafe && !failSafeTimerReArmedAfterPreviousStep) {
205
+ logger.debug(`Re-Arming failsafe timer before executing step`);
206
+ await this.#armFailsafe();
207
+ }
208
+ failSafeTimerReArmedAfterPreviousStep = false;
200
209
  const result = await step.stepLogic();
201
210
  this.#setCommissioningStepResult(step, result);
202
211
 
@@ -222,6 +231,7 @@ export class ControllerCommissioningFlow {
222
231
  )}s elapsed since last arm failsafe, re-arming failsafe`,
223
232
  );
224
233
  await this.#armFailsafe();
234
+ failSafeTimerReArmedAfterPreviousStep = true;
225
235
  }
226
236
  }
227
237
 
@@ -369,6 +379,7 @@ export class ControllerCommissioningFlow {
369
379
  stepNumber: 12, // includes step 13
370
380
  subStepNumber: 2,
371
381
  name: "NetworkCommissioning.Wifi",
382
+ reArmFailsafe: true,
372
383
  stepLogic: () => this.#configureNetworkWifi(),
373
384
  });
374
385
  }
@@ -377,6 +388,7 @@ export class ControllerCommissioningFlow {
377
388
  stepNumber: 12, // includes step 13
378
389
  subStepNumber: 3,
379
390
  name: "NetworkCommissioning.Thread",
391
+ reArmFailsafe: true,
380
392
  stepLogic: () => this.#configureNetworkThread(),
381
393
  });
382
394
  }
@@ -390,6 +402,7 @@ export class ControllerCommissioningFlow {
390
402
  stepNumber: 14, // includes step 15 (CASE connection)
391
403
  subStepNumber: 1,
392
404
  name: "Reconnect",
405
+ reArmFailsafe: true,
393
406
  stepLogic: () => this.#reconnectWithDevice(),
394
407
  });
395
408
 
@@ -1149,13 +1162,45 @@ export class ControllerCommissioningFlow {
1149
1162
  *
1150
1163
  */
1151
1164
  async #reconnectWithDevice() {
1152
- logger.debug("Reconnecting with device ...");
1165
+ const isConcurrentFlow = this.#collectedCommissioningData.supportsConcurrentConnection !== false;
1166
+
1167
+ logger.debug(`Reconnecting with device with ${isConcurrentFlow ? "concurrent" : "non-concurrent"} flow ...`);
1168
+
1169
+ // Reconnection with discovery could take longer then the default failsafe time, so we need to
1170
+ // re-arm the failsafe when we are in a concurrent commissioning flow also in parallel to
1171
+ // the operative reconnection
1172
+ // TODO: Check whats needed for non-concurrent commissioning flows (maybe arm initially longer?)
1173
+ const reArmFailsafeInterval = Time.getPeriodicTimer(
1174
+ "Re-Arm Failsafe during reconnect",
1175
+ this.#failSafeTimeMs / 2,
1176
+ () => {
1177
+ const now = Time.nowMs();
1178
+ if (this.#commissioningExpiryTime !== undefined && now < this.#commissioningExpiryTime) {
1179
+ logger.error(
1180
+ `Re-Arm Failsafe Timer during reconnect with device. Time left: ${Math.round((this.#commissioningExpiryTime - now) / 1000)}s`,
1181
+ );
1182
+ this.#armFailsafe().catch(error => {
1183
+ logger.error("Error while re-arming failsafe during reconnect", error);
1184
+ reArmFailsafeInterval.stop();
1185
+ });
1186
+ } else {
1187
+ // Stop as soon as we are over the maximum commissioning time
1188
+ reArmFailsafeInterval.stop();
1189
+ }
1190
+ },
1191
+ );
1192
+ if (isConcurrentFlow) {
1193
+ reArmFailsafeInterval.start();
1194
+ }
1195
+
1153
1196
  const transitionResult = await this.#transitionToCase(
1154
1197
  this.#interactionClient.address,
1155
1198
  // Assume concurrent connections are supported if not know (which should not be the case when we came here)
1156
- this.#collectedCommissioningData.supportsConcurrentConnection ?? true,
1199
+ isConcurrentFlow,
1157
1200
  );
1158
1201
 
1202
+ reArmFailsafeInterval.stop();
1203
+
1159
1204
  if (transitionResult === undefined) {
1160
1205
  logger.debug("CASE commissioning handled externally, terminating commissioning flow");
1161
1206
  return {