UNPKG

@collight/dns-sd

Version:

A DNS Service Discovery implementation in modern TypeScript

1 lines 11.2 kB
{"version":3,"file":"Browser.cjs","sources":["../../../src/browser/Browser.ts"],"sourcesContent":["import { RemoteInfo } from 'dgram';\nimport { EventEmitter } from 'events';\nimport { MulticastDNS, ResponsePacket } from 'multicast-dns';\n\nimport { debug, nameEquals, ServiceType } from '../utils';\nimport { DiscoveredService } from './DiscoveredService';\n\n// MARK: BrowserFilter\nexport type BrowserFilter = {\n /**\n * Network protocol used by the service, either TCP or UDP.\n * @default 'tcp'\n */\n protocol: 'tcp' | 'udp';\n /**\n * The service type to browse for. This corresponds to the DNS-SD service type without the leading underscore.\n * @example 'http', 'ipp'\n */\n type: string;\n /**\n * Optional service subtypes to filter for. When provided, the browser targets this instance specifically.\n * @example ['foo', 'bar']\n */\n subtypes?: string[];\n\n /**\n * Optional specific instance name to filter for. When provided, the browser targets this instance specifically.\n * @example 'MyPrinter'\n */\n name?: string | RegExp;\n /**\n * Optional TXT record key-value pairs to filter discovered services.\n * Only services whose TXT records match all keys and values will be emitted.\n */\n txt?: Record<string, string | RegExp>;\n};\n\n// MARK: BrowserOptions\nexport interface BrowserOptions {\n filter?: BrowserFilter;\n}\n\ninterface BrowserEventMap {\n up: [service: DiscoveredService];\n down: [service: DiscoveredService];\n update: [curr: DiscoveredService, prev: DiscoveredService];\n}\n\n// MARK: Browser\n/**\n * Browses the local network for mDNS/DNS-SD service instances.\n *\n * Emits:\n * - `up`: when a new service appears.\n * - `down`: when a previously seen service sends a \"goodbye\" (TTL = 0).\n * - `update`: when an existing service has received new records\n */\nexport class Browser extends EventEmitter<BrowserEventMap> {\n static TLD = '.local';\n static WILDCARD = '_services._dns-sd._udp' + this.TLD;\n\n services: DiscoveredService[] = [];\n\n private mdns: MulticastDNS;\n private filter?: BrowserFilter;\n\n private onresponse?: (packet: ResponsePacket, rinfo: RemoteInfo) => void;\n\n constructor(mdns: MulticastDNS, options?: BrowserOptions) {\n super();\n\n this.mdns = mdns;\n this.filter = options?.filter;\n }\n\n private get queryNames(): string[] {\n if (!this.filter) {\n return [Browser.WILDCARD];\n }\n\n const { protocol, type, name, subtypes } = this.filter;\n const types =\n subtypes && subtypes.length > 0\n ? subtypes.map(subtype => new ServiceType(type, protocol, subtype).toString())\n : [new ServiceType(type, protocol).toString()];\n\n return types.map((typeStr: string) =>\n typeof name === 'string' ? `${name}.${typeStr}${Browser.TLD}` : `${typeStr}${Browser.TLD}`,\n );\n }\n\n /**\n * Starts the browser and begins listening for mDNS service announcements.\n * - Has no effect if the browser is already started.\n */\n start(): void {\n if (this.onresponse) return;\n\n this.onresponse = (packet: ResponsePacket, rinfo: RemoteInfo): void => {\n debug('mdns on response packet:', packet);\n\n // Handle goodbye records (TTL = 0) to remove offline services\n // See https://tools.ietf.org/html/rfc6762#section-8.4\n for (const answer of [...packet.answers, ...packet.additionals]) {\n if (answer.type === 'PTR' && answer.ttl === 0) {\n this.removeService(answer.data);\n }\n }\n\n // Discover new services from valid responses\n const services = DiscoveredService.fromResponse(packet, rinfo);\n if (services.length === 0) {\n if (!this.filter) {\n // Initial wildcard query will only give PTR, need to query each one manually\n for (const answer of packet.answers) {\n if (answer.type !== 'PTR' || answer.name !== Browser.WILDCARD) continue;\n debug('mdns wildcard query with:', answer.data);\n this.mdns.query(answer.data, 'PTR');\n }\n }\n return;\n }\n\n for (const service of services) {\n if (this.services.some(s => nameEquals(s.fqdn, service.fqdn))) {\n this.updateService(service);\n } else {\n this.addService(service);\n }\n }\n };\n\n this.mdns.on('response', this.onresponse);\n\n // trigger initial query\n this.update();\n }\n\n /**\n * Stops the browser from listening to mDNS service announcements.\n * - Has no effect if the browser is already stopped\n */\n stop(): void {\n if (!this.onresponse) return;\n\n this.mdns.removeListener('response', this.onresponse);\n this.onresponse = undefined;\n\n for (const service of this.services) {\n clearTimeout(service.ttlTimer);\n }\n this.services = [];\n }\n\n /**\n * Sends an active query to refresh the list of discovered services.\n * - Triggers a new PTR record query for the configured service name\n * - Causes mDNS responders to reply with their current service information\n * - Useful for manual refresh when you suspect stale service data\n */\n update(): void {\n // Actively query for service PTR records\n debug('mdns query with:', this.queryNames);\n for (const queryName of this.queryNames) {\n this.mdns.query(queryName, 'PTR');\n }\n }\n\n // MARK: private\n private match(service: DiscoveredService): boolean {\n if (!this.filter) {\n return true;\n }\n const { protocol, type, subtypes, name, txt } = this.filter;\n if (protocol !== service.protocol) {\n return false;\n }\n if (type !== service.type) {\n return false;\n }\n if (subtypes?.some(s => !service.subtypes.includes(s)) ?? false) {\n return false;\n }\n if (name !== undefined) {\n if (typeof name === 'string' && !nameEquals(name, service.name)) {\n return false;\n }\n if (name instanceof RegExp && !name.test(service.name)) {\n return false;\n }\n }\n if (txt) {\n for (const [key, value] of Object.entries(txt)) {\n if (service.txt?.[key] === undefined) {\n return false;\n }\n if (typeof value === 'string' && value !== service.txt[key]) {\n return false;\n }\n if (value instanceof RegExp && !value.test(service.txt[key])) {\n return false;\n }\n }\n }\n return true;\n }\n\n /**\n * TODO: {@link https://datatracker.ietf.org/doc/html/rfc6762#section-5.2 | Continuous Multicast DNS Querying}\n */\n private setupTTLTimer(service: DiscoveredService): void {\n if (service.ttl !== undefined) {\n service.ttlTimer = setTimeout(() => {\n if (service.expired) {\n this.removeService(service.fqdn);\n }\n }, service.ttl * 1000);\n }\n }\n\n private addService(service: DiscoveredService): void {\n if (!this.match(service)) {\n return;\n }\n\n this.services.push(service);\n\n this.setupTTLTimer(service);\n\n this.emit('up', service);\n }\n\n private updateService(service: DiscoveredService): void {\n // If the new updated service no longer matches the filter, remove the service\n if (!this.match(service)) {\n this.removeService(service.fqdn);\n return;\n }\n\n // Replace the old service instance\n for (let i = 0; i < this.services.length; ++i) {\n const s = this.services[i]!;\n if (nameEquals(s.fqdn, service.fqdn)) {\n clearTimeout(s.ttlTimer);\n this.services[i] = service;\n\n this.setupTTLTimer(service);\n\n this.emit('update', service, s);\n break;\n }\n }\n }\n\n private removeService(fqdn: string): void {\n for (let i = 0; i < this.services.length; ++i) {\n const s = this.services[i];\n if (s && nameEquals(s.fqdn, fqdn)) {\n this.services.splice(i, 1);\n clearTimeout(s.ttlTimer);\n this.emit('down', s);\n return;\n }\n }\n }\n}\n"],"names":["_Browser","EventEmitter","mdns","options","protocol","type","name","subtypes","subtype","ServiceType","typeStr","packet","rinfo","debug","answer","services","DiscoveredService","service","s","nameEquals","queryName","txt","key","value","i","fqdn","Browser"],"mappings":"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"}