@matter/general 0.16.1 → 0.16.2-alpha.0-20260114-d3127faee

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 (98) hide show
  1. package/dist/cjs/net/ServerAddress.d.ts +52 -14
  2. package/dist/cjs/net/ServerAddress.d.ts.map +1 -1
  3. package/dist/cjs/net/ServerAddress.js +41 -6
  4. package/dist/cjs/net/ServerAddress.js.map +2 -2
  5. package/dist/cjs/net/ServerAddressSet.d.ts +65 -0
  6. package/dist/cjs/net/ServerAddressSet.d.ts.map +1 -0
  7. package/dist/cjs/net/ServerAddressSet.js +144 -0
  8. package/dist/cjs/net/ServerAddressSet.js.map +6 -0
  9. package/dist/cjs/net/dns-sd/MdnsSocket.d.ts +40 -0
  10. package/dist/cjs/net/dns-sd/MdnsSocket.d.ts.map +1 -0
  11. package/dist/cjs/net/dns-sd/MdnsSocket.js +164 -0
  12. package/dist/cjs/net/dns-sd/MdnsSocket.js.map +6 -0
  13. package/dist/cjs/net/dns-sd/index.d.ts +7 -0
  14. package/dist/cjs/net/dns-sd/index.d.ts.map +1 -0
  15. package/dist/cjs/net/dns-sd/index.js +24 -0
  16. package/dist/cjs/net/dns-sd/index.js.map +6 -0
  17. package/dist/cjs/net/index.d.ts +2 -0
  18. package/dist/cjs/net/index.d.ts.map +1 -1
  19. package/dist/cjs/net/index.js +2 -0
  20. package/dist/cjs/net/index.js.map +1 -1
  21. package/dist/cjs/util/Abort.d.ts +9 -0
  22. package/dist/cjs/util/Abort.d.ts.map +1 -1
  23. package/dist/cjs/util/Abort.js +20 -0
  24. package/dist/cjs/util/Abort.js.map +1 -1
  25. package/dist/cjs/util/Heap.d.ts +84 -0
  26. package/dist/cjs/util/Heap.d.ts.map +1 -0
  27. package/dist/cjs/util/Heap.js +286 -0
  28. package/dist/cjs/util/Heap.js.map +6 -0
  29. package/dist/cjs/util/Observable.d.ts +29 -6
  30. package/dist/cjs/util/Observable.d.ts.map +1 -1
  31. package/dist/cjs/util/Observable.js +40 -6
  32. package/dist/cjs/util/Observable.js.map +1 -1
  33. package/dist/cjs/util/Promises.d.ts +3 -0
  34. package/dist/cjs/util/Promises.d.ts.map +1 -1
  35. package/dist/cjs/util/Promises.js +33 -3
  36. package/dist/cjs/util/Promises.js.map +2 -2
  37. package/dist/cjs/util/Set.d.ts.map +1 -1
  38. package/dist/cjs/util/Set.js +14 -8
  39. package/dist/cjs/util/Set.js.map +1 -1
  40. package/dist/cjs/util/index.d.ts +1 -0
  41. package/dist/cjs/util/index.d.ts.map +1 -1
  42. package/dist/cjs/util/index.js +1 -0
  43. package/dist/cjs/util/index.js.map +1 -1
  44. package/dist/esm/net/ServerAddress.d.ts +52 -14
  45. package/dist/esm/net/ServerAddress.d.ts.map +1 -1
  46. package/dist/esm/net/ServerAddress.js +41 -6
  47. package/dist/esm/net/ServerAddress.js.map +2 -2
  48. package/dist/esm/net/ServerAddressSet.d.ts +65 -0
  49. package/dist/esm/net/ServerAddressSet.d.ts.map +1 -0
  50. package/dist/esm/net/ServerAddressSet.js +124 -0
  51. package/dist/esm/net/ServerAddressSet.js.map +6 -0
  52. package/dist/esm/net/dns-sd/MdnsSocket.d.ts +40 -0
  53. package/dist/esm/net/dns-sd/MdnsSocket.d.ts.map +1 -0
  54. package/dist/esm/net/dns-sd/MdnsSocket.js +149 -0
  55. package/dist/esm/net/dns-sd/MdnsSocket.js.map +6 -0
  56. package/dist/esm/net/dns-sd/index.d.ts +7 -0
  57. package/dist/esm/net/dns-sd/index.d.ts.map +1 -0
  58. package/dist/esm/net/dns-sd/index.js +7 -0
  59. package/dist/esm/net/dns-sd/index.js.map +6 -0
  60. package/dist/esm/net/index.d.ts +2 -0
  61. package/dist/esm/net/index.d.ts.map +1 -1
  62. package/dist/esm/net/index.js +2 -0
  63. package/dist/esm/net/index.js.map +1 -1
  64. package/dist/esm/util/Abort.d.ts +9 -0
  65. package/dist/esm/util/Abort.d.ts.map +1 -1
  66. package/dist/esm/util/Abort.js +20 -0
  67. package/dist/esm/util/Abort.js.map +1 -1
  68. package/dist/esm/util/Heap.d.ts +84 -0
  69. package/dist/esm/util/Heap.d.ts.map +1 -0
  70. package/dist/esm/util/Heap.js +266 -0
  71. package/dist/esm/util/Heap.js.map +6 -0
  72. package/dist/esm/util/Observable.d.ts +29 -6
  73. package/dist/esm/util/Observable.d.ts.map +1 -1
  74. package/dist/esm/util/Observable.js +40 -6
  75. package/dist/esm/util/Observable.js.map +1 -1
  76. package/dist/esm/util/Promises.d.ts +3 -0
  77. package/dist/esm/util/Promises.d.ts.map +1 -1
  78. package/dist/esm/util/Promises.js +33 -3
  79. package/dist/esm/util/Promises.js.map +2 -2
  80. package/dist/esm/util/Set.d.ts.map +1 -1
  81. package/dist/esm/util/Set.js +14 -8
  82. package/dist/esm/util/Set.js.map +1 -1
  83. package/dist/esm/util/index.d.ts +1 -0
  84. package/dist/esm/util/index.d.ts.map +1 -1
  85. package/dist/esm/util/index.js +1 -0
  86. package/dist/esm/util/index.js.map +1 -1
  87. package/package.json +2 -2
  88. package/src/net/ServerAddress.ts +93 -19
  89. package/src/net/ServerAddressSet.ts +225 -0
  90. package/src/net/dns-sd/MdnsSocket.ts +203 -0
  91. package/src/net/dns-sd/index.ts +7 -0
  92. package/src/net/index.ts +2 -0
  93. package/src/util/Abort.ts +25 -0
  94. package/src/util/Heap.ts +332 -0
  95. package/src/util/Observable.ts +74 -10
  96. package/src/util/Promises.ts +61 -4
  97. package/src/util/Set.ts +15 -8
  98. package/src/util/index.ts +1 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2026 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { AddressStatus, ServerAddress } from "./ServerAddress.js";
8
+
9
+ /**
10
+ * A set of server addresses ordered by a comparator.
11
+ */
12
+ export interface ServerAddressSet<T extends ServerAddress> {
13
+ /**
14
+ * Add an address.
15
+ *
16
+ * If the address alreay exists, returns the existing address to facilitate comparison by value. If not, returns
17
+ * the input address.
18
+ */
19
+ add(address: T): T;
20
+
21
+ /**
22
+ * Delete an address.
23
+ */
24
+ delete(address: T): boolean;
25
+
26
+ /**
27
+ * Replace the stored addresses.
28
+ */
29
+ replace(newAddresses: Iterable<T>): void;
30
+
31
+ /**
32
+ * Test for existence of address.
33
+ */
34
+ has(address: T): boolean;
35
+
36
+ /**
37
+ * The number of addresses.
38
+ */
39
+ size: number;
40
+
41
+ /**
42
+ * Iterate.
43
+ *
44
+ * If you replace addresses during iteration only addresses not already produced will be covered by remaining
45
+ * iterations.
46
+ */
47
+ [Symbol.iterator](): Iterator<T>;
48
+ }
49
+
50
+ /**
51
+ * Create a new {@link ServerAddressSet}.
52
+ */
53
+ export function ServerAddressSet<T extends ServerAddress>(
54
+ initial?: Iterable<T>,
55
+ comparator = ServerAddressSet.compareDesirability,
56
+ ) {
57
+ let version = 0;
58
+ let addresses: undefined | Map<string, T>;
59
+
60
+ const set: ServerAddressSet<T> = {
61
+ add(address: T) {
62
+ version++;
63
+ const key = ServerAddress.urlFor(address);
64
+ const existing = addresses?.get(key);
65
+ if (existing) {
66
+ return existing;
67
+ }
68
+
69
+ if (!addresses) {
70
+ addresses = new Map();
71
+ }
72
+
73
+ addresses.set(key, address);
74
+ return address;
75
+ },
76
+
77
+ delete(address: T) {
78
+ version++;
79
+ return addresses?.delete(ServerAddress.urlFor(address)) ?? false;
80
+ },
81
+
82
+ has(address: T) {
83
+ return addresses?.has(ServerAddress.urlFor(address)) ?? false;
84
+ },
85
+
86
+ replace(newAddresses) {
87
+ version++;
88
+ const oldAddresses = addresses;
89
+ addresses = new Map();
90
+ for (const address of newAddresses) {
91
+ const key = ServerAddress.urlFor(address);
92
+ addresses.set(key, oldAddresses?.get(key) ?? address);
93
+ }
94
+ },
95
+
96
+ get size() {
97
+ return addresses?.size ?? 0;
98
+ },
99
+
100
+ *[Symbol.iterator]() {
101
+ const tried = new Set<string>();
102
+
103
+ all: while (true) {
104
+ const currentVersion = version;
105
+ const ordered = addresses
106
+ ? [...addresses.entries()].map(([, address]) => address).sort(comparator)
107
+ : [];
108
+
109
+ for (const address of ordered) {
110
+ // Skip duplicates or addresses we've tried with previous sets
111
+ const key = ServerAddress.urlFor(address);
112
+ if (tried.has(key)) {
113
+ continue;
114
+ }
115
+
116
+ yield address;
117
+
118
+ // Restart iteration if the underlying address set changed
119
+ if (currentVersion !== version) {
120
+ continue all;
121
+ }
122
+ }
123
+
124
+ break;
125
+ }
126
+ },
127
+ };
128
+
129
+ if (initial) {
130
+ set.replace(initial);
131
+ }
132
+
133
+ return set;
134
+ }
135
+
136
+ export namespace ServerAddressSet {
137
+ export interface Comparator<T extends ServerAddress = ServerAddress> {
138
+ (addr1: T, addr2: T): number;
139
+ }
140
+
141
+ /**
142
+ * Update a list of addresses with health information from another list.
143
+ */
144
+ export function copyHealth<T extends ServerAddress>(targetAddresses: Iterable<T>, sourceAddresses: Iterable<T>) {
145
+ const output = [...targetAddresses];
146
+
147
+ nextTarget: for (let i = 0; i < output.length; i++) {
148
+ const target = output[i];
149
+ for (const source of sourceAddresses) {
150
+ if (ServerAddress.isEqual(target, source)) {
151
+ if (source.healthyAt !== undefined || source.unhealthyAt !== undefined) {
152
+ output[i] = { ...target, healthyAt: source.healthyAt, unhealthyAt: source.unhealthyAt };
153
+ }
154
+ continue nextTarget;
155
+ }
156
+ }
157
+ }
158
+
159
+ return output;
160
+ }
161
+
162
+ /**
163
+ * Compare the "desirability" of two addresses for communication.
164
+ */
165
+ export function compareDesirability(a: ServerAddress, b: ServerAddress) {
166
+ const value = ServerAddress.selectionPreferenceOf(a) - ServerAddress.selectionPreferenceOf(b);
167
+
168
+ if (value) {
169
+ return value;
170
+ }
171
+
172
+ // Compare health
173
+ const relativeHealth = compareHealth(a, b);
174
+ if (relativeHealth) {
175
+ return relativeHealth;
176
+ }
177
+
178
+ // Compare priority if known for both addresses
179
+ if (a.priority !== undefined && b.priority !== undefined) {
180
+ return b.priority - a.priority;
181
+ }
182
+
183
+ return 0;
184
+ }
185
+
186
+ /**
187
+ * Compare the health of two addresses.
188
+ *
189
+ * Returns a negative number if a is healthier, positive if b is healthier and 0 if assessment is neutral.
190
+ */
191
+ export function compareHealth(a: AddressStatus, b: AddressStatus) {
192
+ const ha = ServerAddress.healthOf(a);
193
+ const hb = ServerAddress.healthOf(b);
194
+
195
+ // If a is unhealthy, check b's unhealth
196
+ if (ha.unhealthyAt) {
197
+ if (hb.unhealthyAt) {
198
+ // Prefer smaller "unhealthy at" value
199
+ return ha.unhealthyAt - hb.unhealthyAt;
200
+ }
201
+
202
+ // Prefer b as it is healthy or unused
203
+ return 1;
204
+ }
205
+
206
+ // If b is unhealthy and a is not; prefer b
207
+ if (hb.unhealthyAt) {
208
+ return -1;
209
+ }
210
+
211
+ // If a is known healthy, check b's health
212
+ if (ha.healthyAt) {
213
+ if (hb.healthyAt) {
214
+ // Prefer greater health value
215
+ return hb.healthyAt - ha.healthyAt;
216
+ }
217
+
218
+ // Prefer a as b has not been used
219
+ return -1;
220
+ }
221
+
222
+ // No preference
223
+ return 0;
224
+ }
225
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2026 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {
8
+ DnsCodec,
9
+ DnsMessage,
10
+ DnsMessagePartiallyPreEncoded,
11
+ DnsMessageType,
12
+ DnsMessageTypeFlag,
13
+ MAX_MDNS_MESSAGE_SIZE,
14
+ } from "#codec/DnsCodec.js";
15
+ import { Diagnostic } from "#log/Diagnostic.js";
16
+ import { Logger } from "#log/Logger.js";
17
+ import { MatterAggregateError } from "#MatterError.js";
18
+ import { Network } from "#net/Network.js";
19
+ import { UdpMulticastServer } from "#net/udp/UdpMulticastServer.js";
20
+ import { Bytes } from "#util/Bytes.js";
21
+ import { Lifetime } from "#util/Lifetime.js";
22
+ import { AsyncObservable, BasicObservable } from "#util/Observable.js";
23
+ import { MaybePromise } from "#util/Promises.js";
24
+
25
+ const logger = Logger.get("MdnsListener");
26
+
27
+ /**
28
+ * Manages the UDP socket for other components that implement MDNS logic.
29
+ */
30
+ export class MdnsSocket {
31
+ #socket: UdpMulticastServer;
32
+ #handlers?: Set<PromiseLike<void>>;
33
+ #isClosed = false;
34
+ #receipt: AsyncObservable<[message: MdnsSocket.Message]> = new BasicObservable(
35
+ error => logger.error("Unhandled error in MDNS listener", error),
36
+ true,
37
+ );
38
+
39
+ static async create(
40
+ network: Network,
41
+ options?: { enableIpv4?: boolean; netInterface?: string; lifetime?: Lifetime.Owner },
42
+ ) {
43
+ const { enableIpv4 = true, netInterface, lifetime } = options ?? {};
44
+ const socket = new MdnsSocket(
45
+ await UdpMulticastServer.create({
46
+ lifetime,
47
+ network,
48
+ netInterface,
49
+ broadcastAddressIpv4: enableIpv4 ? MdnsSocket.BROADCAST_IPV4 : undefined,
50
+ broadcastAddressIpv6: MdnsSocket.BROADCAST_IPV6,
51
+ listeningPort: MdnsSocket.BROADCAST_PORT,
52
+ }),
53
+ );
54
+ return socket;
55
+ }
56
+
57
+ constructor(socket: UdpMulticastServer) {
58
+ this.#socket = socket;
59
+ socket.onMessage(this.#handleMessage.bind(this));
60
+ }
61
+
62
+ get network() {
63
+ return this.#socket.network;
64
+ }
65
+
66
+ get supportsIpv4() {
67
+ return this.#socket.supportsIpv4;
68
+ }
69
+
70
+ get netInterface() {
71
+ return this.#socket.netInterface;
72
+ }
73
+
74
+ get receipt() {
75
+ return this.#receipt;
76
+ }
77
+
78
+ async send(message: Partial<DnsMessage> & { messageType: DnsMessageType }, intf?: string, unicastDest?: string) {
79
+ const { messageType } = message;
80
+ // When we send Queries that are too long they need to have the Truncated flag set
81
+ const truncatedMessageType = DnsMessageType.isQuery(messageType)
82
+ ? messageType | DnsMessageTypeFlag.TC
83
+ : messageType;
84
+
85
+ const chunk: DnsMessagePartiallyPreEncoded = {
86
+ transactionId: 0,
87
+ queries: [],
88
+ authorities: [],
89
+
90
+ ...message,
91
+
92
+ answers: [],
93
+ additionalRecords: [],
94
+ };
95
+
96
+ // Note - for size calculations we assume queries are relatively small. We only split answers across messages
97
+ let encodedChunkWithoutAnswers = DnsCodec.encode(chunk);
98
+ let chunkSize = encodedChunkWithoutAnswers.byteLength;
99
+
100
+ // Add answers, splitting message as necessary
101
+ for (const answer of message.answers ?? []) {
102
+ const answerEncoded = DnsCodec.encodeRecord(answer);
103
+
104
+ if (chunkSize + answerEncoded.byteLength > MAX_MDNS_MESSAGE_SIZE) {
105
+ if (chunk.answers.length === 0) {
106
+ // The first answer is already too big, log at least a warning
107
+ logger.warn(
108
+ `MDNS message with ${Diagnostic.json(
109
+ chunk.queries,
110
+ )} is too big to fit into a single MDNS message. Send anyway, but please report!`,
111
+ );
112
+ }
113
+
114
+ // New answer does not fit anymore, send out the message
115
+ // When sending a query, we set the Truncated flag to indicate more answers are available
116
+ await this.#send(
117
+ {
118
+ ...chunk,
119
+ messageType: truncatedMessageType,
120
+ },
121
+ intf,
122
+ unicastDest,
123
+ );
124
+
125
+ // Reset the message, length counter and included answers to count for next message
126
+ if (chunk.queries.length) {
127
+ chunk.queries.length = 0;
128
+ encodedChunkWithoutAnswers = DnsCodec.encode(chunk);
129
+ }
130
+ chunk.answers.length = 0;
131
+ chunkSize = encodedChunkWithoutAnswers.byteLength + answerEncoded.byteLength;
132
+ } else {
133
+ chunkSize += answerEncoded.byteLength;
134
+ }
135
+
136
+ chunk.answers.push(answerEncoded);
137
+ }
138
+
139
+ // Add "additional records"... We include these but only if they fit
140
+ const additionalRecords = message.additionalRecords ?? [];
141
+ for (const additionalRecord of additionalRecords) {
142
+ const additionalRecordEncoded = DnsCodec.encodeRecord(additionalRecord);
143
+ chunkSize += additionalRecordEncoded.byteLength;
144
+ if (chunkSize > MAX_MDNS_MESSAGE_SIZE) {
145
+ break;
146
+ }
147
+ chunk.additionalRecords.push(additionalRecordEncoded);
148
+ }
149
+
150
+ await this.#send(chunk, intf, unicastDest);
151
+ }
152
+
153
+ async #send(message: DnsMessagePartiallyPreEncoded, intf?: string, unicastDest?: string) {
154
+ await this.#socket.send(DnsCodec.encode(message), intf, unicastDest);
155
+ }
156
+
157
+ async close() {
158
+ this.#isClosed = true;
159
+ await this.#socket.close();
160
+ if (this.#handlers) {
161
+ await MatterAggregateError.allSettled(this.#handlers);
162
+ }
163
+ }
164
+
165
+ #handleMessage(bytes: Bytes, sourceIp: string, sourceIntf: string) {
166
+ // Ignore if closed
167
+ if (this.#isClosed) {
168
+ return;
169
+ }
170
+
171
+ // Parse
172
+ const parsed = DnsCodec.decode(bytes);
173
+
174
+ // Skip unparseable
175
+ if (parsed === undefined) {
176
+ return;
177
+ }
178
+
179
+ let promise = this.#receipt.emit({
180
+ ...parsed,
181
+ sourceIp,
182
+ sourceIntf,
183
+ }) as MaybePromise;
184
+
185
+ if (MaybePromise.is(promise)) {
186
+ if (this.#handlers === undefined) {
187
+ this.#handlers = new Set();
188
+ }
189
+ promise = Promise.resolve(promise).finally(() => this.#handlers?.delete(promise as PromiseLike<void>));
190
+ }
191
+ }
192
+ }
193
+
194
+ export namespace MdnsSocket {
195
+ export interface Message extends DnsMessage {
196
+ sourceIp: string;
197
+ sourceIntf: string;
198
+ }
199
+
200
+ export const BROADCAST_IPV4 = "224.0.0.251";
201
+ export const BROADCAST_IPV6 = "ff02::fb";
202
+ export const BROADCAST_PORT = 5353;
203
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2026 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ export * from "./MdnsSocket.js";
package/src/net/index.ts CHANGED
@@ -7,10 +7,12 @@
7
7
  export * from "./AppAddress.js";
8
8
  export * from "./Channel.js";
9
9
  export * from "./ConnectionlessTransport.js";
10
+ export * from "./dns-sd/index.js";
10
11
  export * from "./http/index.js";
11
12
  export * from "./mock/index.js";
12
13
  export * from "./mqtt/index.js";
13
14
  export * from "./Network.js";
14
15
  export * from "./RetrySchedule.js";
15
16
  export * from "./ServerAddress.js";
17
+ export * from "./ServerAddressSet.js";
16
18
  export * from "./udp/index.js";
package/src/util/Abort.ts CHANGED
@@ -96,6 +96,13 @@ export class Abort extends Callable<[reason?: Error]> implements AbortController
96
96
  return Abort.race(this, ...promises);
97
97
  }
98
98
 
99
+ /**
100
+ * Race with throw on abort.
101
+ */
102
+ async attempt<T>(...promises: Array<T | PromiseLike<T>>) {
103
+ return Abort.attempt(this, ...promises);
104
+ }
105
+
99
106
  /**
100
107
  * Free resources.
101
108
  *
@@ -115,6 +122,12 @@ export class Abort extends Callable<[reason?: Error]> implements AbortController
115
122
  this.close();
116
123
  }
117
124
 
125
+ if(condition?: unknown, reason?: Error) {
126
+ if (condition) {
127
+ this.abort(reason);
128
+ }
129
+ }
130
+
118
131
  get aborted() {
119
132
  return this.signal.aborted;
120
133
  }
@@ -262,6 +275,18 @@ export namespace Abort {
262
275
  return SafePromise.race(promises);
263
276
  }
264
277
 
278
+ /**
279
+ * Race with throw on abort.
280
+ */
281
+ export async function attempt<T>(signal: Signal | undefined, ...promises: Array<T | PromiseLike<T>>) {
282
+ if (signal && "signal" in signal) {
283
+ signal = signal.signal;
284
+ }
285
+ const result = await race(signal, ...promises);
286
+ signal?.throwIfAborted();
287
+ return result as Awaited<T>;
288
+ }
289
+
265
290
  /**
266
291
  * Perform abortable sleep.
267
292
  */