@matter/general 0.16.8-alpha.0-20260125-38e62bc3e → 0.16.8-alpha.0-20260127-65e1b40e2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/net/ServerAddress.d.ts.map +1 -1
- package/dist/cjs/net/ServerAddress.js +2 -1
- package/dist/cjs/net/ServerAddress.js.map +1 -1
- package/dist/cjs/net/dns-sd/DnssdName.d.ts +56 -0
- package/dist/cjs/net/dns-sd/DnssdName.d.ts.map +1 -0
- package/dist/cjs/net/dns-sd/DnssdName.js +193 -0
- package/dist/cjs/net/dns-sd/DnssdName.js.map +6 -0
- package/dist/cjs/net/dns-sd/DnssdNames.d.ts +77 -0
- package/dist/cjs/net/dns-sd/DnssdNames.d.ts.map +1 -0
- package/dist/cjs/net/dns-sd/DnssdNames.js +238 -0
- package/dist/cjs/net/dns-sd/DnssdNames.js.map +6 -0
- package/dist/cjs/net/dns-sd/DnssdSolicitor.d.ts +80 -0
- package/dist/cjs/net/dns-sd/DnssdSolicitor.d.ts.map +1 -0
- package/dist/cjs/net/dns-sd/DnssdSolicitor.js +212 -0
- package/dist/cjs/net/dns-sd/DnssdSolicitor.js.map +6 -0
- package/dist/cjs/net/dns-sd/IpService.d.ts +73 -0
- package/dist/cjs/net/dns-sd/IpService.d.ts.map +1 -0
- package/dist/cjs/net/dns-sd/IpService.js +329 -0
- package/dist/cjs/net/dns-sd/IpService.js.map +6 -0
- package/dist/cjs/net/dns-sd/IpServiceResolution.d.ts +16 -0
- package/dist/cjs/net/dns-sd/IpServiceResolution.d.ts.map +1 -0
- package/dist/cjs/net/dns-sd/IpServiceResolution.js +162 -0
- package/dist/cjs/net/dns-sd/IpServiceResolution.js.map +6 -0
- package/dist/cjs/net/dns-sd/IpServiceStatus.d.ts +58 -0
- package/dist/cjs/net/dns-sd/IpServiceStatus.d.ts.map +1 -0
- package/dist/cjs/net/dns-sd/IpServiceStatus.js +191 -0
- package/dist/cjs/net/dns-sd/IpServiceStatus.js.map +6 -0
- package/dist/cjs/net/dns-sd/index.d.ts +6 -0
- package/dist/cjs/net/dns-sd/index.d.ts.map +1 -1
- package/dist/cjs/net/dns-sd/index.js +6 -0
- package/dist/cjs/net/dns-sd/index.js.map +1 -1
- package/dist/cjs/net/udp/UdpInterface.js +1 -1
- package/dist/cjs/net/udp/UdpInterface.js.map +1 -1
- package/dist/esm/net/ServerAddress.d.ts.map +1 -1
- package/dist/esm/net/ServerAddress.js +2 -1
- package/dist/esm/net/ServerAddress.js.map +1 -1
- package/dist/esm/net/dns-sd/DnssdName.d.ts +56 -0
- package/dist/esm/net/dns-sd/DnssdName.d.ts.map +1 -0
- package/dist/esm/net/dns-sd/DnssdName.js +173 -0
- package/dist/esm/net/dns-sd/DnssdName.js.map +6 -0
- package/dist/esm/net/dns-sd/DnssdNames.d.ts +77 -0
- package/dist/esm/net/dns-sd/DnssdNames.d.ts.map +1 -0
- package/dist/esm/net/dns-sd/DnssdNames.js +218 -0
- package/dist/esm/net/dns-sd/DnssdNames.js.map +6 -0
- package/dist/esm/net/dns-sd/DnssdSolicitor.d.ts +80 -0
- package/dist/esm/net/dns-sd/DnssdSolicitor.d.ts.map +1 -0
- package/dist/esm/net/dns-sd/DnssdSolicitor.js +192 -0
- package/dist/esm/net/dns-sd/DnssdSolicitor.js.map +6 -0
- package/dist/esm/net/dns-sd/IpService.d.ts +73 -0
- package/dist/esm/net/dns-sd/IpService.d.ts.map +1 -0
- package/dist/esm/net/dns-sd/IpService.js +309 -0
- package/dist/esm/net/dns-sd/IpService.js.map +6 -0
- package/dist/esm/net/dns-sd/IpServiceResolution.d.ts +16 -0
- package/dist/esm/net/dns-sd/IpServiceResolution.d.ts.map +1 -0
- package/dist/esm/net/dns-sd/IpServiceResolution.js +142 -0
- package/dist/esm/net/dns-sd/IpServiceResolution.js.map +6 -0
- package/dist/esm/net/dns-sd/IpServiceStatus.d.ts +58 -0
- package/dist/esm/net/dns-sd/IpServiceStatus.d.ts.map +1 -0
- package/dist/esm/net/dns-sd/IpServiceStatus.js +171 -0
- package/dist/esm/net/dns-sd/IpServiceStatus.js.map +6 -0
- package/dist/esm/net/dns-sd/index.d.ts +6 -0
- package/dist/esm/net/dns-sd/index.d.ts.map +1 -1
- package/dist/esm/net/dns-sd/index.js +6 -0
- package/dist/esm/net/dns-sd/index.js.map +1 -1
- package/dist/esm/net/udp/UdpInterface.js +1 -1
- package/dist/esm/net/udp/UdpInterface.js.map +1 -1
- package/package.json +2 -2
- package/src/net/ServerAddress.ts +2 -1
- package/src/net/dns-sd/DnssdName.ts +252 -0
- package/src/net/dns-sd/DnssdNames.ts +208 -0
- package/src/net/dns-sd/DnssdSolicitor.ts +231 -0
- package/src/net/dns-sd/IpService.ts +346 -0
- package/src/net/dns-sd/IpServiceResolution.ts +134 -0
- package/src/net/dns-sd/IpServiceStatus.ts +212 -0
- package/src/net/dns-sd/index.ts +6 -0
- package/src/net/udp/UdpInterface.ts +1 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2026 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DnsRecordType, SrvRecordValue } from "#codec/DnsCodec.js";
|
|
8
|
+
import { Diagnostic } from "#log/Diagnostic.js";
|
|
9
|
+
import { AddressLifespan, ServerAddressUdp } from "#net/ServerAddress.js";
|
|
10
|
+
import { ServerAddressSet } from "#net/ServerAddressSet.js";
|
|
11
|
+
import { Duration } from "#time/Duration.js";
|
|
12
|
+
import { Time } from "#time/Time.js";
|
|
13
|
+
import { Abort } from "#util/Abort.js";
|
|
14
|
+
import { AsyncObservable, AsyncObservableValue, ObserverGroup } from "#util/Observable.js";
|
|
15
|
+
import { DnssdName } from "./DnssdName.js";
|
|
16
|
+
import { DnssdNames } from "./DnssdNames.js";
|
|
17
|
+
import { IpServiceStatus } from "./IpServiceStatus.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A service addressable by IP that updates as {@link DnssdNames} change.
|
|
21
|
+
*/
|
|
22
|
+
export class IpService {
|
|
23
|
+
readonly #name: DnssdName;
|
|
24
|
+
readonly #via: string;
|
|
25
|
+
readonly #names: DnssdNames;
|
|
26
|
+
readonly #observers = new ObserverGroup(this);
|
|
27
|
+
readonly #services = new Map<string, Service>();
|
|
28
|
+
readonly #changed = new AsyncObservable<[]>();
|
|
29
|
+
readonly #addresses = ServerAddressSet<ServerAddressUdp>();
|
|
30
|
+
#status = new IpServiceStatus(this);
|
|
31
|
+
#notified?: Promise<void>;
|
|
32
|
+
|
|
33
|
+
constructor(name: string, via: string, names: DnssdNames) {
|
|
34
|
+
this.#name = names.get(name);
|
|
35
|
+
this.#names = names;
|
|
36
|
+
this.#via = Diagnostic.via(via);
|
|
37
|
+
this.#observers.on(this.#name, this.#onServiceChanged);
|
|
38
|
+
|
|
39
|
+
for (const record of this.#name.records) {
|
|
40
|
+
const service = serviceOf(record);
|
|
41
|
+
if (!service) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.#updateService(record.ttl, service);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The DNS-SD name.
|
|
51
|
+
*/
|
|
52
|
+
get name() {
|
|
53
|
+
return this.#name;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Other DNS-SD names.
|
|
58
|
+
*/
|
|
59
|
+
get names() {
|
|
60
|
+
return this.#names;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Identifier used for logging.
|
|
65
|
+
*/
|
|
66
|
+
get via() {
|
|
67
|
+
return this.#via;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Status details of the service.
|
|
72
|
+
*/
|
|
73
|
+
get status() {
|
|
74
|
+
return this.#status;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Release resources.
|
|
79
|
+
*/
|
|
80
|
+
async close() {
|
|
81
|
+
this.#observers.close();
|
|
82
|
+
await this.#status.close();
|
|
83
|
+
if (this.#notified) {
|
|
84
|
+
await this.#notified;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Known addresses.
|
|
90
|
+
*/
|
|
91
|
+
get addresses() {
|
|
92
|
+
return this.#addresses;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Values from TXT records.
|
|
97
|
+
*/
|
|
98
|
+
get parameters() {
|
|
99
|
+
return this.#name.parameters;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Emits when the service changes.
|
|
104
|
+
*/
|
|
105
|
+
get changed() {
|
|
106
|
+
return this.#changed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
map<T>(fn: (addr: ServerAddressUdp) => T): T[] {
|
|
110
|
+
return [...this.addresses].map(fn);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Stream address updates, starting with initial set of addresses.
|
|
115
|
+
*
|
|
116
|
+
* Outputs addresses in priority order so the stream may be used directly for establishing new connections.
|
|
117
|
+
*
|
|
118
|
+
* If no addresses are present, triggers discovery using standard MDNS backoff schedule.
|
|
119
|
+
*/
|
|
120
|
+
async *addressChanges({
|
|
121
|
+
abort,
|
|
122
|
+
order = ServerAddressSet.compareDesirability,
|
|
123
|
+
}: {
|
|
124
|
+
abort?: AbortSignal;
|
|
125
|
+
order?: ServerAddressSet.Comparator;
|
|
126
|
+
ipv4?: boolean;
|
|
127
|
+
} = {}): AsyncGenerator<{ kind: "add" | "delete"; address: ServerAddressUdp }> {
|
|
128
|
+
let knownAddresses = new Set<ServerAddressUdp>();
|
|
129
|
+
|
|
130
|
+
// Implement change detection
|
|
131
|
+
const dirty = new AsyncObservableValue<[isDirty: boolean]>();
|
|
132
|
+
using _changed = this.changed.use(() => dirty.emit(true));
|
|
133
|
+
|
|
134
|
+
loop: while (true) {
|
|
135
|
+
// Collect and order addresses; do not use return from resolve() to avoid race condition with dirty
|
|
136
|
+
// observation
|
|
137
|
+
dirty.emit(false);
|
|
138
|
+
const addresses = ServerAddressSet(this.addresses, order);
|
|
139
|
+
|
|
140
|
+
// Enqueue new addresses
|
|
141
|
+
let changes = new Array<IpService.AddressChange>();
|
|
142
|
+
const oldKnownAddresses = knownAddresses;
|
|
143
|
+
knownAddresses = new Set();
|
|
144
|
+
for (const address of addresses) {
|
|
145
|
+
knownAddresses.add(address);
|
|
146
|
+
|
|
147
|
+
if (oldKnownAddresses.has(address)) {
|
|
148
|
+
oldKnownAddresses.delete(address);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
changes.push({ kind: "add", address });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Enqueue deleted addresses
|
|
156
|
+
if (oldKnownAddresses.size) {
|
|
157
|
+
const deletedAddresses = [...oldKnownAddresses.values()];
|
|
158
|
+
const deletes = deletedAddresses.map(address => ({ kind: "delete", address }) as const);
|
|
159
|
+
changes = [...deletes, ...changes];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Output
|
|
163
|
+
for (const change of changes) {
|
|
164
|
+
yield change;
|
|
165
|
+
|
|
166
|
+
// Abort if aborted
|
|
167
|
+
if (Abort.is(abort)) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Restart if changed
|
|
172
|
+
if (dirty.value) {
|
|
173
|
+
continue loop;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// All addresses emitted; wait for change
|
|
178
|
+
await Abort.race(abort, dirty);
|
|
179
|
+
if (Abort.is(abort)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#onServiceChanged = async ({ updated, deleted }: DnssdName.Changes) => {
|
|
186
|
+
if (updated) {
|
|
187
|
+
for (const record of updated) {
|
|
188
|
+
const service = serviceOf(record);
|
|
189
|
+
if (service) {
|
|
190
|
+
this.#updateService(record.ttl, service);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (deleted) {
|
|
196
|
+
for (const record of deleted) {
|
|
197
|
+
const service = serviceOf(record);
|
|
198
|
+
if (service) {
|
|
199
|
+
this.#deleteService(service);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
#updateService(ttl: Duration, { target, port, priority, weight }: SrvRecordValue) {
|
|
206
|
+
const key = hostKeyOf(target, port);
|
|
207
|
+
let service = this.#services.get(key);
|
|
208
|
+
|
|
209
|
+
if (service) {
|
|
210
|
+
service.discoveredAt = Time.nowMs;
|
|
211
|
+
service.ttl = ttl;
|
|
212
|
+
service.port = port;
|
|
213
|
+
service.priority = priority;
|
|
214
|
+
service.weight = weight;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
service = {
|
|
219
|
+
name: this.#names.get(target),
|
|
220
|
+
discoveredAt: Time.nowMs,
|
|
221
|
+
ttl,
|
|
222
|
+
port,
|
|
223
|
+
priority,
|
|
224
|
+
weight,
|
|
225
|
+
onChange: changes => this.#onAddressChanged(service!, changes),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
this.#observers.on(service.name, service.onChange);
|
|
229
|
+
|
|
230
|
+
this.#onAddressChanged(service, { name: service.name, updated: [...service.name.records] });
|
|
231
|
+
|
|
232
|
+
this.#services.set(key, service);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#deleteService({ target, port }: SrvRecordValue) {
|
|
236
|
+
const key = hostKeyOf(target, port);
|
|
237
|
+
const service = this.#services.get(key);
|
|
238
|
+
|
|
239
|
+
if (!service) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.#services.delete(key);
|
|
244
|
+
|
|
245
|
+
this.#observers.off(service.name, service.onChange);
|
|
246
|
+
|
|
247
|
+
this.#onAddressChanged(service, { name: service.name, deleted: [...service.name.records] });
|
|
248
|
+
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#onAddressChanged = (service: Service, { updated, deleted }: DnssdName.Changes) => {
|
|
253
|
+
if (updated) {
|
|
254
|
+
for (const record of updated) {
|
|
255
|
+
const addr = addressOf(record);
|
|
256
|
+
if (addr) {
|
|
257
|
+
this.#updateAddress(service, addr);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (deleted) {
|
|
262
|
+
for (const record of deleted) {
|
|
263
|
+
const addr = addressOf(record);
|
|
264
|
+
if (addr) {
|
|
265
|
+
this.#deleteAddress(service, addr);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
#updateAddress(service: Service, ip: string) {
|
|
272
|
+
const address: ServerAddressUdp = { type: "udp", ip, port: service.port };
|
|
273
|
+
|
|
274
|
+
if (this.#addresses.has(address)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.#addresses.add(address);
|
|
279
|
+
|
|
280
|
+
// Set status to reachable any time we add a new address
|
|
281
|
+
this.#status.isReachable = true;
|
|
282
|
+
|
|
283
|
+
this.#notify();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#deleteAddress(service: Service, ip: string) {
|
|
287
|
+
const address: ServerAddressUdp = { type: "udp", ip, port: service.port };
|
|
288
|
+
|
|
289
|
+
if (!this.#addresses.has(address)) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.#addresses.delete(address);
|
|
294
|
+
|
|
295
|
+
this.#notify();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#notify = () => {
|
|
299
|
+
if (this.#notified) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// We notify asynchronously so changes coalesce
|
|
304
|
+
this.#notified = this.#emitNotification();
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
async #emitNotification() {
|
|
308
|
+
await Time.sleep("discovery service coalescence", 0);
|
|
309
|
+
this.#notified = undefined;
|
|
310
|
+
await this.changed.emit();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export namespace IpService {
|
|
315
|
+
export interface AddressChange {
|
|
316
|
+
kind: "add" | "delete";
|
|
317
|
+
address: ServerAddressUdp;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
interface Service extends AddressLifespan {
|
|
322
|
+
name: DnssdName;
|
|
323
|
+
priority: number;
|
|
324
|
+
weight: number;
|
|
325
|
+
port: number;
|
|
326
|
+
onChange(changes: DnssdName.Changes): void;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function serviceOf(record: DnssdName.Record) {
|
|
330
|
+
if (record.recordType !== DnsRecordType.SRV) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return record.value;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function hostKeyOf(name: string, port: number) {
|
|
338
|
+
return `${name}:${port}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function addressOf(record: DnssdName.Record) {
|
|
342
|
+
if (record.recordType !== DnsRecordType.A && record.recordType !== DnsRecordType.AAAA) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
return record.value;
|
|
346
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2026 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DnsRecordType } from "#codec/DnsCodec.js";
|
|
8
|
+
import { ServerAddressSet } from "#net/ServerAddressSet.js";
|
|
9
|
+
import { Abort } from "#util/Abort.js";
|
|
10
|
+
import { isIPv4, isIPv6 } from "#util/Ip.js";
|
|
11
|
+
import { BasicMultiplex } from "#util/Multiplex.js";
|
|
12
|
+
import { ObserverGroup } from "#util/Observable.js";
|
|
13
|
+
import { BasicSet } from "#util/Set.js";
|
|
14
|
+
import { DnssdName } from "./DnssdName.js";
|
|
15
|
+
import type { DnssdSolicitor } from "./DnssdSolicitor.js";
|
|
16
|
+
import { IpService } from "./IpService.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Discovers new IP addresses for an {@link IpService}.
|
|
20
|
+
*
|
|
21
|
+
* This primarily involves sending queries for SRV records using {@link DnssdSolicitor#discover}. We also query for
|
|
22
|
+
* A and AAAA records for any SRV target hostnames for which we do not know IP addresses.
|
|
23
|
+
*
|
|
24
|
+
* Runs until aborted or we discover a new IP address (we ignore existing addresses).
|
|
25
|
+
*/
|
|
26
|
+
export async function IpServiceResolution(service: IpService, abort: AbortSignal, ipv4 = true) {
|
|
27
|
+
using localAbort = new Abort({ abort });
|
|
28
|
+
await using workers = new BasicMultiplex();
|
|
29
|
+
using observers = new ObserverGroup();
|
|
30
|
+
|
|
31
|
+
// Target names for SRV records. We report this to the solicitor as associated records and initiate discovery on
|
|
32
|
+
// any name that has no IP records
|
|
33
|
+
const hosts = new BasicSet<DnssdName>();
|
|
34
|
+
|
|
35
|
+
// We record an abort function for any hostname (SRV target) we are discovering because it has no known IPs
|
|
36
|
+
let hostResolvers: undefined | Map<DnssdName, Abort>;
|
|
37
|
+
|
|
38
|
+
// Resolve hosts with no IPs
|
|
39
|
+
observers.on(hosts.added, name => {
|
|
40
|
+
// Skip if host has IP records
|
|
41
|
+
if (
|
|
42
|
+
[...name.records].find(
|
|
43
|
+
record => record.recordType === DnsRecordType.AAAA || record.recordType === DnsRecordType.A,
|
|
44
|
+
)
|
|
45
|
+
) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Begin resolving
|
|
50
|
+
if (!hostResolvers) {
|
|
51
|
+
hostResolvers = new Map();
|
|
52
|
+
}
|
|
53
|
+
const hostAbort = new Abort({ abort: localAbort });
|
|
54
|
+
hostResolvers.set(name, hostAbort);
|
|
55
|
+
workers.add(
|
|
56
|
+
service.names.solicitor
|
|
57
|
+
.discover({
|
|
58
|
+
name,
|
|
59
|
+
recordTypes: ipv4 ? [DnsRecordType.A, DnsRecordType.AAAA] : [DnsRecordType.AAAA],
|
|
60
|
+
abort: hostAbort,
|
|
61
|
+
})
|
|
62
|
+
.finally(hostAbort.close.bind(hostAbort)),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Stop resolving hosts when deleted
|
|
67
|
+
observers.on(hosts.deleted, name => {
|
|
68
|
+
const abortHost = hostResolvers?.get(name);
|
|
69
|
+
if (!abortHost) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
hostResolvers?.delete(name);
|
|
74
|
+
abortHost();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Resolve any initial hosts without records
|
|
78
|
+
for (const record of service.name.records) {
|
|
79
|
+
if (record.recordType !== DnsRecordType.SRV) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
hosts.add(service.names.get(record.name));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Wire the service to a.) stop discovery when we discover a new address, and b.) update known hosts
|
|
87
|
+
const existingAddresses = ServerAddressSet(service.addresses);
|
|
88
|
+
observers.on(service.changed, () => {
|
|
89
|
+
// Detect new address which means discovery is complete
|
|
90
|
+
for (const address of service.addresses) {
|
|
91
|
+
if (!ipv4 && isIPv4(address.ip)) {
|
|
92
|
+
// Ignore ipv4 if ipv4 is unused
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!existingAddresses.has(address) && (ipv4 || isIPv6(address.ip))) {
|
|
97
|
+
// Address discovered; we're done
|
|
98
|
+
localAbort();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add/remove hosts as necessary
|
|
103
|
+
const srvs = [...service.name.records].filter(record => record.recordType === DnsRecordType.SRV);
|
|
104
|
+
const newHostnames = new Set(srvs.map(record => record.value.target));
|
|
105
|
+
for (const hostname of newHostnames) {
|
|
106
|
+
// Host is newly added; add to associated names
|
|
107
|
+
hosts.add(service.names.get(hostname));
|
|
108
|
+
}
|
|
109
|
+
for (const name of hosts) {
|
|
110
|
+
if (newHostnames.has(name.qname)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Host is no longer targeted; remove from associated names
|
|
115
|
+
hosts.delete(name);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Begin discovering SVC records
|
|
120
|
+
workers.add(
|
|
121
|
+
service.names.solicitor.discover({
|
|
122
|
+
abort: localAbort,
|
|
123
|
+
name: service.name,
|
|
124
|
+
recordTypes: [DnsRecordType.SRV],
|
|
125
|
+
|
|
126
|
+
get associatedNames() {
|
|
127
|
+
return hosts;
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Run until aborted, either because we discovered a new IP or input abort was signaled
|
|
133
|
+
await localAbort;
|
|
134
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2026 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Diagnostic } from "#log/Diagnostic.js";
|
|
8
|
+
import { Logger } from "#log/Logger.js";
|
|
9
|
+
import { AbortedError } from "#MatterError.js";
|
|
10
|
+
import { ServerAddress } from "#net/ServerAddress.js";
|
|
11
|
+
import { Time } from "#time/Time.js";
|
|
12
|
+
import { Timestamp } from "#time/Timestamp.js";
|
|
13
|
+
import { Abort } from "#util/Abort.js";
|
|
14
|
+
import { asError } from "#util/Error.js";
|
|
15
|
+
import { BasicSet } from "#util/Set.js";
|
|
16
|
+
import type { IpService } from "./IpService.js";
|
|
17
|
+
import { IpServiceResolution } from "./IpServiceResolution.js";
|
|
18
|
+
|
|
19
|
+
const logger = Logger.get("IpServiceStatus");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Tracks status of an {@link IpService}, logs state changes and manages service discovery.
|
|
23
|
+
*/
|
|
24
|
+
export class IpServiceStatus {
|
|
25
|
+
#service: IpService;
|
|
26
|
+
#isReachable = false;
|
|
27
|
+
#connecting = new BasicSet<PromiseLike<boolean>>();
|
|
28
|
+
#resolveAbort?: Abort;
|
|
29
|
+
#resolving?: Promise<void>;
|
|
30
|
+
#connectionInitiatedAt?: Timestamp;
|
|
31
|
+
#lastReceiptAt?: Timestamp;
|
|
32
|
+
|
|
33
|
+
constructor(service: IpService) {
|
|
34
|
+
this.#service = service;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async close() {
|
|
38
|
+
this.#stopResolving();
|
|
39
|
+
if (this.#resolving) {
|
|
40
|
+
await this.#resolving;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Is the service actively connecting?
|
|
46
|
+
*
|
|
47
|
+
* This is true so long as a promise passed to {@link connecting} is unresolved.
|
|
48
|
+
*/
|
|
49
|
+
get isConnecting() {
|
|
50
|
+
return this.#connecting.size > 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Is the service currently reachable?
|
|
55
|
+
*
|
|
56
|
+
* This value is writable. If you set {@link isReachable} to false and {@link isConnecting} is true, the service
|
|
57
|
+
* enters discovery mode and begins active solicitation so long as neither condition changes.
|
|
58
|
+
*
|
|
59
|
+
* The service sets {@link isReachable} to true automatically if:
|
|
60
|
+
*
|
|
61
|
+
* - It discovers a new (previously unknown) address, or
|
|
62
|
+
*
|
|
63
|
+
* - The input promise to {@link connecting} resolves to true
|
|
64
|
+
*
|
|
65
|
+
* The service sets {@link isReachable} to false automatically if the input promise to {@link connecting} resolves
|
|
66
|
+
* to false.
|
|
67
|
+
*/
|
|
68
|
+
get isReachable() {
|
|
69
|
+
return this.#isReachable;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Are we actively performing MDNS discovery for the service?
|
|
74
|
+
*/
|
|
75
|
+
get isResolving() {
|
|
76
|
+
return this.#resolving !== undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
set isReachable(isReachable: boolean) {
|
|
80
|
+
if (this.#isReachable === isReachable) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.#isReachable = isReachable;
|
|
85
|
+
|
|
86
|
+
if (isReachable) {
|
|
87
|
+
this.#maybeStopResolving();
|
|
88
|
+
} else {
|
|
89
|
+
this.#maybeStartResolving();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get lastReceiptAt(): Timestamp | undefined {
|
|
94
|
+
return this.#lastReceiptAt;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
set lastReceiptAt(time: Timestamp) {
|
|
98
|
+
this.#lastReceiptAt = time;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get connectionInitiatedAt() {
|
|
102
|
+
return this.#connectionInitiatedAt;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Register a new connection attempt.
|
|
107
|
+
*
|
|
108
|
+
* If {@link result} resolves as true the service is marked as reachable. If {@link result} resolves as false
|
|
109
|
+
* reachability is not modified.
|
|
110
|
+
*
|
|
111
|
+
* If {@link result} throws an error other than {@link AbortedError}, the service is marked as unreachable and if
|
|
112
|
+
* the error logged.
|
|
113
|
+
*
|
|
114
|
+
* {@link isConnecting} will be true until {@link result} resolves.
|
|
115
|
+
*/
|
|
116
|
+
connecting(result: PromiseLike<boolean>) {
|
|
117
|
+
logger.debug(this.#service.via, "Connecting");
|
|
118
|
+
|
|
119
|
+
result.then(
|
|
120
|
+
returned => {
|
|
121
|
+
this.#connecting.delete(result);
|
|
122
|
+
|
|
123
|
+
if (!this.#connecting.size) {
|
|
124
|
+
this.#connectionInitiatedAt = undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (returned) {
|
|
128
|
+
this.isReachable = true;
|
|
129
|
+
|
|
130
|
+
logger.info(this.#service.via, "Connected");
|
|
131
|
+
} else {
|
|
132
|
+
logger.debug(this.#service.via, "Connect attempt aborted");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.#maybeStopResolving();
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
error => {
|
|
139
|
+
this.#connecting.delete(result);
|
|
140
|
+
|
|
141
|
+
if (!(error instanceof AbortedError)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
logger.error(this.#service.via, "Connection error:", asError(error));
|
|
146
|
+
|
|
147
|
+
this.#isReachable = false;
|
|
148
|
+
|
|
149
|
+
this.#maybeStartResolving();
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
this.#connectionInitiatedAt = Time.nowMs;
|
|
154
|
+
this.#connecting.add(result);
|
|
155
|
+
|
|
156
|
+
this.#maybeStartResolving();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#maybeStartResolving() {
|
|
160
|
+
if (this.#isReachable || !this.isConnecting || this.#resolveAbort) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const numAddresses = this.#service.addresses.size;
|
|
165
|
+
|
|
166
|
+
let why;
|
|
167
|
+
|
|
168
|
+
switch (numAddresses) {
|
|
169
|
+
case 0:
|
|
170
|
+
why = "need address";
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case 1:
|
|
174
|
+
why = "address is unreachable";
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
default:
|
|
178
|
+
why = `${numAddresses} known addresses are unreachable`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
logger.info(this.#service.via, "Resolving", Diagnostic.weak(`(${why})`));
|
|
182
|
+
|
|
183
|
+
this.#resolveAbort = new Abort();
|
|
184
|
+
this.#resolving = IpServiceResolution(this.#service, this.#resolveAbort).finally(() => {
|
|
185
|
+
if (this.#resolveAbort?.aborted === false) {
|
|
186
|
+
const addresses = [...this.#service.addresses].map(ServerAddress.urlFor);
|
|
187
|
+
logger.debug(this.#service.via, `Resolved as ${addresses.join(", ")}`);
|
|
188
|
+
}
|
|
189
|
+
this.#resolveAbort?.close();
|
|
190
|
+
this.#resolveAbort = undefined;
|
|
191
|
+
this.#resolving = undefined;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#maybeStopResolving() {
|
|
196
|
+
if (!this.#isReachable || this.isConnecting || !this.#resolveAbort) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.#stopResolving();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#stopResolving() {
|
|
204
|
+
if (!this.#resolveAbort) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.#resolveAbort();
|
|
209
|
+
|
|
210
|
+
logger.debug(this.#service.via, "Stopped resolving");
|
|
211
|
+
}
|
|
212
|
+
}
|
package/src/net/dns-sd/index.ts
CHANGED
|
@@ -4,4 +4,10 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
export * from "./DnssdName.js";
|
|
8
|
+
export * from "./DnssdNames.js";
|
|
9
|
+
export * from "./DnssdSolicitor.js";
|
|
10
|
+
export * from "./IpService.js";
|
|
11
|
+
export * from "./IpServiceResolution.js";
|
|
12
|
+
export * from "./IpServiceStatus.js";
|
|
7
13
|
export * from "./MdnsSocket.js";
|
|
@@ -86,7 +86,7 @@ export class UdpConnection implements IpNetworkChannel<Bytes> {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
get name() {
|
|
89
|
-
return `${this.type}
|
|
89
|
+
return `${this.type}://${this.#peerAddress.includes(":") ? `[${this.#peerAddress}]` : this.#peerAddress}:${this.#peerPort}`;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
get networkAddress(): ServerAddressUdp {
|