bonjour-hap 3.10.2 → 3.10.3-beta.1

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/index.d.ts CHANGED
@@ -1,57 +1,144 @@
1
- import { SocketOptions } from "dgram";
1
+ import { RemoteInfo, Socket } from "dgram";
2
2
 
3
- export type BonjourFindOptions = {
4
- type: string;
5
- subtypes?: string[];
3
+ /**
4
+ * Options accepted by the underlying multicast-dns responder.
5
+ * Forwarded by `Bonjour(options)` straight into `multicast-dns`.
6
+ */
7
+ export interface MulticastOptions {
8
+ /** Multicast port. Defaults to 5353. */
9
+ port?: number;
10
+ /** Socket type. Defaults to 'udp4'. */
11
+ type?: "udp4" | "udp6";
12
+ /** Multicast IP address. Defaults to 224.0.0.251 for udp4. Required for udp6. */
13
+ ip?: string;
14
+ /** Alias for `ip`. */
15
+ host?: string;
16
+ /** Interface name. Required for IPv6 multicast. */
17
+ interface?: string;
18
+ /** Whether to allow address reuse. Defaults to true. */
19
+ reuseAddr?: boolean;
20
+ /** Pre-created dgram socket. */
21
+ socket?: Socket;
22
+ /** Whether to join the multicast group. Defaults to true. */
23
+ multicast?: boolean;
24
+ /** Multicast TTL. Defaults to 255. */
25
+ ttl?: number;
26
+ /** Multicast loopback. Defaults to true. */
27
+ loopback?: boolean;
28
+ /** Bind address, or `false` to skip binding. */
29
+ bind?: string | false;
30
+ }
31
+
32
+ export interface BonjourFindOptions {
33
+ /** If omitted, performs a wildcard search across all service types. */
34
+ type?: string;
35
+ /** Defaults to 'tcp'. */
6
36
  protocol?: "tcp" | "udp";
7
- txt?: Record<string, any>;
8
- };
37
+ /** Filter to a specific service instance name. */
38
+ name?: string;
39
+ /** Options forwarded to the TXT record decoder. */
40
+ txt?: { binary?: boolean };
41
+ }
9
42
 
43
+ /**
44
+ * A service discovered via {@link Bonjour.find} / {@link Bonjour.findOne}.
45
+ *
46
+ * Populated from incoming SRV/TXT/A/AAAA records. Services without a matching
47
+ * SRV record are filtered out before being emitted, so all SRV-derived fields
48
+ * are guaranteed to be present on emitted services.
49
+ */
10
50
  export interface BonjourService {
11
51
  name: string;
52
+ fqdn: string;
12
53
  type: string;
54
+ subtypes: string[];
13
55
  protocol: "tcp" | "udp";
14
56
  host: string;
15
57
  port: number;
58
+ referer: RemoteInfo;
16
59
  addresses: string[];
17
- txt: Record<string, string>;
18
- rawTxt?: Buffer;
60
+ /** Decoded TXT record, if a TXT record was received. */
61
+ txt?: Record<string, string>;
62
+ /** Raw TXT record blocks, if a TXT record was received. */
63
+ rawTxt?: Buffer[];
19
64
  }
20
65
 
21
- export interface MulticastOptions extends SocketOptions {
22
- multicastAddress?: string;
23
- multicastInterface?: string;
24
- multicastTTL?: number;
25
- reuseAddr?: boolean;
26
- }
66
+ export interface Browser {
67
+ /** Currently-known services. Updated as services come up, change, or go down. */
68
+ services: BonjourService[];
27
69
 
28
- export class Browser {
29
70
  start(): void;
30
71
  stop(): void;
72
+ /** Send a fresh PTR query to refresh the service list. */
31
73
  update(): void;
32
- on(event: "up" | "down", listener: (service: BonjourService) => void): this;
74
+
75
+ on(event: "up" | "down" | "update", listener: (service: BonjourService) => void): this;
33
76
  }
34
77
 
35
- export class Advertisement {
36
- stop(): void;
78
+ /**
79
+ * A service published via {@link Bonjour.publish}. Returned to the caller so
80
+ * they can update the TXT record, stop, or destroy the advertisement.
81
+ */
82
+ export interface Advertisement {
83
+ name: string;
84
+ type: string;
85
+ protocol: "tcp" | "udp";
86
+ host: string;
87
+ port: number;
88
+ fqdn: string;
89
+ subtypes: string[] | null;
90
+ txt: Record<string, string> | null;
91
+ /** True once the initial announcement has been confirmed. */
92
+ published: boolean;
93
+
37
94
  start(): void;
38
- update(txt: Record<string, string>): void;
95
+ /**
96
+ * Stop advertising. The callback is required if the service has not been
97
+ * activated yet (the runtime calls it synchronously in that case).
98
+ */
99
+ stop(callback?: () => void): void;
100
+ /** Tear down the service and remove all listeners. */
101
+ destroy(): void;
102
+ /** Replace the TXT record. If `silent` is true, the new record is registered but not re-announced. */
103
+ updateTxt(txt: Record<string, string>, silent?: boolean): void;
104
+
105
+ /** Emitted once after the first successful announcement. */
106
+ on(event: "up", listener: () => void): this;
107
+ /** Emitted if the service name is already in use on the network. */
39
108
  on(event: "error", listener: (err: Error) => void): this;
40
109
  }
41
110
 
42
111
  export interface PublishOptions {
43
112
  name: string;
44
113
  type: string;
45
- subtypes?: string[];
46
114
  port: number;
47
115
  host?: string;
48
- txt?: Record<string, string>;
49
116
  protocol?: "tcp" | "udp";
50
- }
117
+ subtypes?: string[];
118
+ txt?: Record<string, string>;
119
+ /** Probe for name conflicts before announcing. Defaults to true. */
120
+ probe?: boolean;
51
121
 
52
- export default class Bonjour {
53
- constructor(options?: MulticastOptions);
122
+ /**
123
+ * Adds a meta enumeration record (`_services._dns-sd._udp.local`) when announcing.
124
+ * Only safe to enable when a single service is advertised on the responder, otherwise
125
+ * removing one service will break enumeration for the others.
126
+ */
127
+ addUnsafeServiceEnumerationRecord?: boolean;
54
128
 
129
+ /**
130
+ * Restricts the service to be advertised on the specified IP addresses or interface names.
131
+ * Interface names and addresses can be mixed.
132
+ * If an interface name is given, ANY address on that interface is advertised.
133
+ * If an IP address is given, only that address is advertised.
134
+ */
135
+ restrictedAddresses?: string[];
136
+
137
+ /** If true, the service will not advertise IPv6 (AAAA) records. */
138
+ disabledIpv6?: boolean;
139
+ }
140
+
141
+ export interface Bonjour {
55
142
  publish(options: PublishOptions): Advertisement;
56
143
  unpublishAll(callback?: () => void): void;
57
144
  find(
@@ -60,7 +147,26 @@ export default class Bonjour {
60
147
  ): Browser;
61
148
  findOne(
62
149
  options: BonjourFindOptions,
63
- callback: (service: BonjourService) => void
150
+ callback?: (service: BonjourService) => void
64
151
  ): Browser;
65
- destroy(): void;
152
+ /**
153
+ * Tear down the responder. Goodbye records are broadcast for every
154
+ * published service before the underlying mdns socket is closed.
155
+ * The optional callback fires once teardown is complete.
156
+ */
157
+ destroy(callback?: () => void): void;
158
+ /**
159
+ * Emitted when the underlying mdns socket reports an error or an
160
+ * outgoing response fails. If no listener is attached, the error is
161
+ * logged to `console.warn` so it does not crash the process.
162
+ */
163
+ on(event: "error", listener: (err: Error) => void): this;
164
+ }
165
+
166
+ export interface BonjourFactory {
167
+ new (options?: MulticastOptions): Bonjour;
168
+ (options?: MulticastOptions): Bonjour;
66
169
  }
170
+
171
+ declare const Bonjour: BonjourFactory;
172
+ export default Bonjour;
package/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const util = require('util')
4
+ const EventEmitter = require('events').EventEmitter
3
5
  const Registry = require('./lib/Registry.js')
4
6
  const Server = require('./lib/Server.js')
5
7
  const Browser = require('./lib/Browser.js')
@@ -7,11 +9,22 @@ const Browser = require('./lib/Browser.js')
7
9
  function Bonjour (opts) {
8
10
  if (!(this instanceof Bonjour)) { return new Bonjour(opts) }
9
11
 
12
+ EventEmitter.call(this)
13
+
10
14
  this._server = new Server(opts)
11
15
  this._registry = new Registry(this._server)
16
+ this._server.on('error', err => {
17
+ if (this.listenerCount('error') > 0) {
18
+ this.emit('error', err)
19
+ } else {
20
+ console.warn('bonjour-hap:', err.message || err)
21
+ }
22
+ })
12
23
  }
13
24
 
14
- Bonjour.prototype = {
25
+ util.inherits(Bonjour, EventEmitter)
26
+
27
+ Object.assign(Bonjour.prototype, {
15
28
  publish: function (opts) {
16
29
  return this._registry.publish(opts)
17
30
  },
@@ -33,10 +46,12 @@ Bonjour.prototype = {
33
46
  return browser
34
47
  },
35
48
 
36
- destroy: function () {
37
- this._registry.destroy()
38
- this._server.mdns.destroy()
49
+ destroy: function (cb) {
50
+ this._registry.destroy(() => {
51
+ this._server.mdns.destroy()
52
+ if (cb) cb()
53
+ })
39
54
  }
40
- }
55
+ })
41
56
 
42
57
  module.exports = Bonjour
package/lib/Browser.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const util = require('util')
4
4
  const EventEmitter = require('events').EventEmitter
5
5
  const serviceName = require('multicast-dns-service-types')
6
+ const deepEqual = require('fast-deep-equal')
6
7
  const dnsEqual = require('./utils/dnsEqual')
7
8
  const dnsTxt = require('./utils/txtDecoder')
8
9
 
@@ -35,7 +36,7 @@ function Browser (mdns, opts, onup) {
35
36
  this._mdns = mdns
36
37
  this._onresponse = null
37
38
  this._serviceMap = {}
38
- this._txt = dnsTxt(opts.txt)
39
+ this._txt = dnsTxt(opts && opts.txt)
39
40
 
40
41
  if (!opts || !opts.type) {
41
42
  this._name = WILDCARD
@@ -67,7 +68,7 @@ Browser.prototype.start = function () {
67
68
  this._onresponse = function (packet, rinfo) {
68
69
  if (self._wildcard) {
69
70
  packet.answers.forEach(function (answer) {
70
- if (answer.type !== 'PTR' || answer.name !== self._name || answer.name in nameMap) return
71
+ if (answer.type !== 'PTR' || !dnsEqual(answer.name, self._name) || answer.data in nameMap) return
71
72
  nameMap[answer.data] = true
72
73
  self._mdns.query(answer.data, 'PTR')
73
74
  })
@@ -123,10 +124,19 @@ Browser.prototype._updateService = function (service) {
123
124
  return false
124
125
  })
125
126
  if (!cachedService) return
127
+ if (!serviceChanged(cachedService, service)) return
126
128
  this.services[index] = service
127
129
  this.emit('update', service)
128
130
  }
129
131
 
132
+ function serviceChanged (a, b) {
133
+ return a.host !== b.host ||
134
+ a.port !== b.port ||
135
+ !deepEqual(a.addresses, b.addresses) ||
136
+ !deepEqual(a.subtypes, b.subtypes) ||
137
+ !deepEqual(a.txt, b.txt)
138
+ }
139
+
130
140
  Browser.prototype._removeService = function (fqdn) {
131
141
  let service, index
132
142
  this.services.some(function (s, i) {
package/lib/Prober.js CHANGED
@@ -30,7 +30,7 @@ Prober.prototype = {
30
30
 
31
31
  start: function () {
32
32
  this.mdns.on('response', this.bound)
33
- setTimeout(this.try.bind(this), Math.random() * 250)
33
+ setTimeout(this.try.bind(this), Math.random() * 250).unref()
34
34
  },
35
35
 
36
36
  try: function () {
package/lib/Registry.js CHANGED
@@ -26,8 +26,13 @@ Registry.prototype = {
26
26
  this._services = []
27
27
  },
28
28
 
29
- destroy: function () {
30
- for (let i = 0; i < this._services.length; i++) { this._services[i].destroy() }
29
+ destroy: function (cb) {
30
+ const services = this._services.slice()
31
+ this._services = []
32
+ this._tearDown(services, () => {
33
+ for (let i = 0; i < services.length; i++) { services[i].destroy() }
34
+ if (cb) cb()
35
+ })
31
36
  },
32
37
 
33
38
  /**
@@ -46,7 +51,7 @@ Registry.prototype = {
46
51
 
47
52
  const records = services.map(function (service) {
48
53
  service.deactivate()
49
- const records = service._records(true)
54
+ const records = service._records()
50
55
  records.forEach(function (record) {
51
56
  record.ttl = 0 // prepare goodbye message
52
57
  })
package/lib/Server.js CHANGED
@@ -1,17 +1,23 @@
1
1
  'use strict'
2
2
 
3
+ const util = require('util')
4
+ const EventEmitter = require('events').EventEmitter
3
5
  const multicastdns = require('multicast-dns')
4
6
  const dnsEqual = require('./utils/dnsEqual')
5
7
  const helpers = require('./helpers.js')
6
8
 
7
9
  const Server = function (opts) {
10
+ EventEmitter.call(this)
8
11
  this.mdns = multicastdns(opts)
9
12
  this.mdns.setMaxListeners(0)
10
13
  this.registry = {}
11
14
  this.mdns.on('query', this._respondToQuery.bind(this))
15
+ this.mdns.on('error', err => this.emit('error', err))
12
16
  }
13
17
 
14
- Server.prototype = {
18
+ util.inherits(Server, EventEmitter)
19
+
20
+ Object.assign(Server.prototype, {
15
21
  _respondToQuery: function (query) {
16
22
  for (let i = 0; i < query.questions.length; i++) {
17
23
  const question = query.questions[i]
@@ -24,7 +30,7 @@ Server.prototype = {
24
30
  ? Object.keys(this.registry).map(this._recordsFor.bind(this, name)).flat()
25
31
  : this._recordsFor(name, type)
26
32
 
27
- if (answers.length === 0) return
33
+ if (answers.length === 0) continue
28
34
 
29
35
  // generate the additionals section
30
36
  let additionals = []
@@ -57,7 +63,7 @@ Server.prototype = {
57
63
  answers,
58
64
  additionals
59
65
  }, err => {
60
- if (err) throw err // TODO: Handle this (if no callback is given, the error will be ignored)
66
+ if (err) this.emit('error', err)
61
67
  })
62
68
  }
63
69
  },
@@ -89,7 +95,7 @@ Server.prototype = {
89
95
  if (!(type in this.registry)) { continue }
90
96
 
91
97
  this.registry[type] = this.registry[type].filter(r => {
92
- return r.name !== record.name
98
+ return !dnsEqual(r.name, record.name)
93
99
  })
94
100
  }
95
101
  },
@@ -103,6 +109,6 @@ Server.prototype = {
103
109
  })
104
110
  }
105
111
 
106
- }
112
+ })
107
113
 
108
114
  module.exports = Server
package/lib/Service.js CHANGED
@@ -83,7 +83,7 @@ const proto = {
83
83
 
84
84
  stop: function (cb) {
85
85
  if (!this._activated) {
86
- cb()
86
+ if (cb) cb()
87
87
  return
88
88
  }
89
89
 
@@ -109,19 +109,25 @@ const proto = {
109
109
  if (this.timer) { clearTimeout(this.timer) }
110
110
 
111
111
  this.delay = 1000
112
+ this._emitAnnounce(silent)
113
+ },
114
+
115
+ _emitAnnounce: function (silent) {
116
+ if (this._destroyed) { return }
112
117
  this.emit('service-announce-request', this.packet, silent || false, this.onAnnounceComplete.bind(this))
113
118
  },
114
119
 
115
120
  onAnnounceComplete: function () {
121
+ if (this._destroyed || !this._activated) return
122
+
116
123
  if (!this.published) {
117
- this._activated = true // not sure if this is needed here
118
124
  this.published = true
119
125
  this.emit('up')
120
126
  }
121
127
 
122
128
  this.delay = this.delay * REANNOUNCE_FACTOR
123
- if (this.delay < REANNOUNCE_MAX_MS && !this._destroyed && this._activated) {
124
- this.timer = setTimeout(this.announce.bind(this), this.delay).unref()
129
+ if (this.delay < REANNOUNCE_MAX_MS) {
130
+ this.timer = setTimeout(this._emitAnnounce.bind(this), this.delay).unref()
125
131
  } else {
126
132
  this.timer = undefined
127
133
  this.delay = undefined
@@ -145,12 +151,12 @@ const proto = {
145
151
  this.published = false
146
152
  },
147
153
 
148
- _records: function (teardown) {
154
+ _records: function () {
149
155
  const records = [this._rrPtr(), this._rrSrv(), this._rrTxt()]
150
156
 
151
157
  records.push(...this._addressRecords())
152
158
 
153
- if (!teardown && this.addUnsafeServiceEnumerationRecord) {
159
+ if (this.addUnsafeServiceEnumerationRecord) {
154
160
  records.push(this._rrMetaPtr())
155
161
  }
156
162
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bonjour-hap",
3
- "version": "3.10.2",
3
+ "version": "3.10.3-beta.1",
4
4
  "description": "A Bonjour/Zeroconf implementation in pure JavaScript (for HAP)",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",