@collight/dns-sd
Version:
A DNS Service Discovery implementation in modern TypeScript
1 lines • 11.7 kB
Source Map (JSON)
{"version":3,"file":"Registry.cjs","sources":["../../../src/registry/Registry.ts"],"sourcesContent":["import { RecordType } from 'dns-packet';\nimport { ResponsePacket } from 'multicast-dns';\n\nimport { debug, MDNSServer, nameEquals } from '../utils';\nimport { Service, ServiceOptions } from './Service';\n\n// MARK: Registry\nexport class Registry {\n static MAX_PROBE_AUTO_RESOLVE_ATTEMPTS = 10;\n static REANNOUNCE_MAX_MS: number = 60 * 60 * 1000;\n static REANNOUNCE_FACTOR = 3;\n\n private server: MDNSServer;\n private startedServices: Service[] = [];\n\n constructor(server: MDNSServer) {\n this.server = server;\n }\n\n /**\n * Creates a new `Service` instance with the given options,\n * and links it to this registry.\n *\n * @param options - The configuration options for the service.\n * @returns A new `Service` instance.\n */\n makeService(options: ServiceOptions): Service {\n const service = new Service(options);\n service.registry = this;\n return service;\n }\n\n /**\n * Publishes a service to the network using the given options.\n * This includes optional probing and sending announcement packets.\n *\n * @param options - The configuration options for the service.\n * @returns A promise that resolves with the published `Service`.\n */\n async publish(options: ServiceOptions): Promise<Service> {\n const service = this.makeService(options);\n await service.start();\n return service;\n }\n\n /**\n * Stops and unpublishes all currently running services.\n *\n * Sends goodbye messages and clears internal service state.\n */\n async unpublishAll(): Promise<void> {\n await this.goodbye(this.startedServices).finally(() => {\n this.startedServices = [];\n });\n }\n\n /**\n * Marks all services as destroyed without unpublishing them.\n *\n * Used when permanently shutting down the registry and disable future operations.\n */\n destroy(): void {\n for (const service of this.startedServices) {\n service.destroyed = true;\n }\n }\n\n /**\n * Internal handler invoked when a service is started.\n *\n * This will initiate probing (if enabled) and trigger the initial announcement.\n *\n * @internal\n * @param service - The service being started.\n * @param options - Optional configuration to override probing behavior.\n */\n async onServiceStart(service: Service): Promise<void> {\n this.startedServices.push(service);\n\n if (service.probe) {\n if (service.probeAutoResolve) {\n // Auto-resolve conflicts by renaming service on probe conflict\n const originalName = service.name;\n const fqdnSuffix = service.fqdn.substring(originalName.length);\n let attempt = 1;\n\n while (attempt <= Registry.MAX_PROBE_AUTO_RESOLVE_ATTEMPTS) {\n const exists = await this.probe(service);\n if (!exists) break;\n\n attempt++;\n service.name = `${originalName} (${attempt})`;\n service.fqdn = service.name + fqdnSuffix;\n }\n\n if (attempt > Registry.MAX_PROBE_AUTO_RESOLVE_ATTEMPTS) {\n console.error(\n new Error(`Failed to resolve name conflicts after ${Registry.MAX_PROBE_AUTO_RESOLVE_ATTEMPTS} attempts.`),\n );\n void service.stop();\n return;\n }\n } else {\n // Normal probe once, stop if conflict detected\n const exists = await this.probe(service);\n if (exists) {\n void service.stop();\n console.error(new Error('Service name is already in use on the network'));\n return;\n }\n }\n }\n\n this.announce(service);\n return new Promise<void>(resolve => {\n service.on('up', resolve);\n });\n }\n\n /**\n * Internal handler invoked when a service is stopped.\n *\n * Sends goodbye messages and updates internal state.\n *\n * @internal\n * @param service - The service being stopped.\n */\n async onServiceStop(service: Service): Promise<void> {\n await this.goodbye([service]).finally(() => {\n const index = this.startedServices.indexOf(service);\n if (index !== -1) {\n this.startedServices.splice(index, 1);\n }\n });\n }\n\n // MARK: private\n /**\n * Probes the network to detect service name conflicts before announcing.\n *\n * Follows RFC 6762 §8.1–8.3 to ensure uniqueness by sending multiple queries\n * and listening for conflicting responses.\n *\n * @param service - The service to probe for potential name conflicts.\n */\n private async probe(service: Service): Promise<boolean> {\n let sent = false;\n let retries = 0;\n let timer: NodeJS.Timeout;\n\n return new Promise(resolve => {\n const send = (): void => {\n // abort if the service have or is being stopped in the meantime\n if (!service.started || service.destroyed) {\n return;\n }\n\n this.server.mdns.query(service.fqdn, 'ANY' as RecordType, error => {\n if (error) {\n console.warn('Error during probing:', error);\n }\n sent = true;\n ++retries;\n timer = setTimeout(retries < 3 ? send : done, 250).unref();\n });\n };\n\n const done = (exists = false): void => {\n this.server.mdns.off('response', onresponse);\n clearTimeout(timer);\n resolve(exists);\n };\n\n const onresponse = (packet: ResponsePacket): void => {\n /**\n * Apparently conflicting Multicast DNS responses received *before* the first probe packet is sent\n * MUST be silently ignored.\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-8.2 | Simultaneous Probe Tiebreaking}\n */\n if (!sent) {\n return;\n }\n if (\n packet.answers.some(a => nameEquals(a.name, service.fqdn)) ||\n packet.additionals.some(a => nameEquals(a.name, service.fqdn))\n ) {\n done(true);\n }\n };\n\n this.server.mdns.on('response', onresponse);\n setTimeout(send, Math.random() * 250);\n });\n }\n\n /**\n * Announces a newly registered service to the network.\n *\n * Sends initial responses immediately, then exponentially spaced re-announcements\n * (3s, 9s, 27s...) up to one hour intervals per RFC 6762 §8.3.\n *\n * @param service - The service to announce.\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-8.3 | Announcing}\n */\n private announce(service: Service): void {\n const records = service.getRecords();\n\n this.server.register(records);\n\n let delay = 1000;\n const broadcast = (): void => {\n if (!service.started || service.destroyed) return;\n\n debug('mdns broadcast:', records);\n this.server.mdns.respond(records, error => {\n if (error) {\n console.warn('Error during announcement:', error);\n }\n\n if (!service.published) {\n service.published = true;\n service.emit('up');\n }\n\n delay = delay * Registry.REANNOUNCE_FACTOR;\n if (delay < Registry.REANNOUNCE_MAX_MS && !service.destroyed) {\n setTimeout(broadcast, delay).unref();\n }\n });\n };\n\n broadcast();\n }\n\n /**\n * Send goodbye messages for a list of services and unregistering them.\n *\n * Each record's TTL is set to 0 to indicate that the service is going offline.\n *\n * @param services - Array of services to remove from the network.\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-10.1 | Goodbye Packets}\n */\n private async goodbye(services: Service[]): Promise<void> {\n const publishedServices = services.filter(s => s.published);\n if (publishedServices.length === 0) {\n return;\n }\n\n const records = publishedServices.flatMap(service => service.getRecords().map(record => ({ ...record, ttl: 0 })));\n if (records.length === 0) {\n return;\n }\n\n this.server.unregister(records);\n\n return new Promise<void>((resolve, reject) => {\n debug('mdns goodbye:', records);\n this.server.mdns.respond(records, error => {\n for (const service of publishedServices) {\n service.published = false;\n service.emit('down');\n }\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n}\n"],"names":["_Registry","server","options","service","Service","originalName","fqdnSuffix","attempt","resolve","index","sent","retries","timer","send","error","done","exists","onresponse","packet","a","nameEquals","records","delay","broadcast","debug","services","publishedServices","record","reject","Registry"],"mappings":"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"}