syndicate-js/packages/driver-mdns/src/index.js

122 lines
4.5 KiB
JavaScript

//---------------------------------------------------------------------------
// @syndicate-lang/driver-mdns, mDNS support for Syndicate.
// Copyright (C) 2016-2018 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//---------------------------------------------------------------------------
// TODO: support other than the default (usually ".local") domain.
const { Observe, currentFacet, genUuid, Dataspace } = require("@syndicate-lang/core");
const S = activate require("@syndicate-lang/driver-streams-node");
const { TimeLaterThan } = activate require("@syndicate-lang/driver-timer");
const bonjour = require('bonjour')();
assertion type Service(name, serviceType) = Symbol.for("mdns-service");
assertion type Publish(svc, hostName, port, txtData) = Symbol.for("mdns-publish");
assertion type Published(svc, hostName, port, txtData) = Symbol.for("mdns-published");
assertion type Discovered(svc, hostName, port, txtData, address) = Symbol.for("mdns-discovered");
// ^ Avahi gave us "family" and "interfaceName", too, but `bonjour` doesn't.
assertion type WildcardBrowserActive() = Symbol.for("-mdns-wildcard-browser-active");
export {
Service,
Publish,
Published,
Discovered,
};
function parseServiceType(serviceType) {
const splitType = /_([^_]+)\._([^_]+)/.exec(serviceType);
if (!splitType) {
throw new Error('Cannot parse serviceType + ' + JSON.stringify(serviceType))
}
return [splitType[1], splitType[2]];
}
spawn named 'driver/mdns-publish' {
during Observe(Published($svc, $hostName, $port, $txtData)) {
assert Publish(svc, hostName, port, txtData);
}
// TODO: The `bonjour` module is limited to publishing a single TXT
// record with key-value strings. It'd be nice to support the full
// range of TXT content allowed by the mDNS and DNS RFCs, including
// non-key-value-formatted data and multiple TXT records.
during Publish($svc(Service($name, $serviceType)), $hostName, $port, $txtData)
spawn named ['bonjour', svc] {
const options = {};
options.name = name;
if (hostName !== null) options.host = hostName;
options.port = port;
[options.type, options.protocol] = parseServiceType(serviceType);
options.txt = txtData.toJS();
console.log('publish', options);
const service = bonjour.publish(options);
on stop service.stop();
field this.established = false;
assert Published(svc, hostName, port, txtData) when (this.established);
service.on('up', Dataspace.wrapExternal(() => { this.established = true; }));
service.on('error', Dataspace.wrapExternal((err) => { throw err; }));
}
}
spawn named 'driver/mdns-browse' {
during Observe(Discovered(Service(_, $serviceType), _, _, _, _)) {
const options = {};
if (typeof serviceType === 'string') {
[options.type, options.protocol] = parseServiceType(serviceType);
}
const browser = bonjour.find(options);
on stop browser.stop();
// field this.nextUpdate = +(new Date());
// on asserted TimeLaterThan(this.nextUpdate) {
// console.log('updating');
// browser.update();
// }
const eachRecord = (service, cb) => {
const svc = Service(service.name, '_' + service.type + '._' + service.protocol);
for (const addr of service.addresses) {
cb(Discovered(svc, service.host, service.port, service.txt, addr));
}
// const now = +(new Date());
// if (this.nextUpdate < now) {
// this.nextUpdate = now + 200;
// }
};
browser.on('up', Dataspace.wrapExternal((service) => {
eachRecord(service, (a) => {
currentFacet().actor.adhocAssert(a)
});
}));
browser.on('down', Dataspace.wrapExternal((service) => {
eachRecord(service, (a) => {
currentFacet().actor.adhocRetract(a)
});
}));
if (typeof serviceType !== 'string') {
assert WildcardBrowserActive();
} else {
stop on asserted WildcardBrowserActive();
}
}
}