npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

tinkerhub-discovery

v0.6.0

Published

Standardized API for service discovery and publishing

Downloads

20,013

Readme

tinkerhub-discovery

This library contains base classes for building and consuming service discovery mechanisms using JavaScript and TypeScript.

It is intended to be used to implement some sort of discovery mechanism, such as those for SSDP and MDNS.

The library is made available via NPM:

npm install tinkerhub-discovery

API

The basic functionality of a discovery instance is exposed via the type ServiceDiscovery:

const discovery = new DiscoveryInstanceHere();

// Fetch services available
for(const service of discovery.services) {
  // Do something with a service found
}

// Get or find services
const viaId = discovery.get('idOfService');
const singleViaFilter = discovery.find(service => /* filter code here */);
const allViaFilter = discovery.find(service => service.someData === 'test');

// Listen to when services become available or unavailable
discovery.onAvailable(service => /* service is now available */);
discovery.onUnavailable(service => /* service is no longer available */);

// Some discoveries support updates
discovery.onUpdate((service, previousService) => /* service has been updated */)

// When done using a discovery it should be destroyed
await discovery.destroy();

Filtered, mapping and merging services

Filtering services

Discovery instances can be filtered, which provides a live filtered view that supports events:

const filtered = discovery.filter(service => /* return true to include in filtered view */);

// Listen to events as with the main discovery instance
filtered.onAvailable(service => ...);

// Destroy the filtered view and its parent
await filtered.destroy();

// Alternative to destroy - release the view, but do not destroy the parent
await filtered.release();

Mapping services into more specific types

Similar to filtering it is possible to map services into another object, as long as the mapped object contains an identifier:

const mapped = discovery.map(service => new CustomService(service));

Mapping supports returning promises to perform asynchronous functions during mapping:

const mapped = discovery.map(async service => await loadService(service));

Mappers may return null if they do not wish to map a service, in which case the discovery acts as it was filtered out.

Mapping has an advanced mode where it's possible to optionally react to updated and unavailable services:

const mapped = discovery.map({
  create: service => new CustomService(service),

  update: ({ service, previousService, previousMappedService }) => {
    /*
     * `service` points to the updated service to map
     * `previousService` is the previous version of the service to map
     * `previousMappedService` is what `create` or `update` mapped to previously
     * 
     * Either:
     * 
     * 1) Return null/undefined to remove the service
     * 2) Return the previously mapped service
     * 3) Return a new mapped service
     * 
     */
  },

  destroy: mappedService => /* perform some destruction of the mapped service */
})

Merging services from multiple discoveries

It's possible to create a merged view that returns services from multiple discoveries. Such services will be merged using their identifier and the first service seen will be returned for as long as it is valid.

// Merge two discoveries together
const discovery = firstDiscovery.and(secondDiscovery);

// To combine more than two discoveries use `MergedDiscovery` directly
import { MergedDiscovery } from 'tinkerhub-discovery';
new MergedDiscovery([ firstDiscovery, secondDiscovery, thirdDiscovery ]);

// Destroy the discovery and the merged discoveries
await discovery.destroy();

// Release this discovery without destroying the merged discoveries
await discovery.release();

Combining filtering and mapping

Combining filtering and mapping can make it easy to find and create a more specific API for a service, like this example that looks for Philips Hue bridges:

import { SSDPDiscovery } from 'tinkerhub-ssdp';

const discovery = new SSDPDiscovery()
  .filter(service => service.headers['HUE-BRIDGEID'])
  .map(service => new HueBridge(service));

discovery.onAvailable(hueBridge => /* instance of HueBridge is available */);

// To shutdown discovery (will destroy the root discovery, the filtered and mapped view)
await discovery.destroy();

Manual discovery

If you have a need to keep a manually updated list of services, it's possible to create an instance of ManualDiscovery and add/remove services as needed:

import { ManualServiceDiscovery } from 'tinkerhub-discovery';

const discovery = new ManualServiceDiscovery<ServiceType>();

// Add services
discovery.add(new ServiceType(...));

// Remove services
discovery.remove('idOfService');
discovery.remove(serviceInstance);

// Set the exact services available
discovery.set([ new ServiceType(...) ]);

Building a custom discovery

There are a few ways to build a custom discovery, with the two main ways being:

  • Event-based discovery, services are added or removed when changed
  • Sync-based discovery, services are discovered in bulk

A very basic example would be this discovery that listens for incoming UDP packets and just adds them as services:

const dgram = require('dgram');
const { BasicServiceDiscovery } = require('tinkerhub-discovery');

class CustomDiscovery extends BasicServiceDiscovery {
  constructor() {
    super('custom');

    this.socket = dgram.createSocket('udp4');
    this.socket.bind();
    this.socket.on(12345, '224.1.1.1', 'listening', () => {
      this.socket.addMembership('224.1.1.1');
      this.socket.setBroadcast(true);
    });
    this.socket.on('message', msg => {
      // Parse incoming message here
      const service = {
        id: extractSomeSortOfId(msg),
        message: msg
      };

      // Add it to the list of services
      this.updateService(service);
    });
  }

  destroy() {
    this.socket.destroy();

    super.destroy();
  }
}

The above discovery would never remove any services, but extending ExpiringServiceDiscovery would activate time based and remove services based on when they were last seen:

const dgram = require('dgram');
const { ExpiringServiceDiscovery } = require('tinkerhub-discovery');

class CustomDiscovery extends ExpiringServiceDiscovery {
  constructor() {
    super('custom', {
      expirationTime: 60*1000 /* milliseconds */
    });

    this.socket = dgram.createSocket('udp4');
    this.socket.bind();
    this.socket.on(12345, '224.1.1.1', 'listening', () => {
      this.socket.addMembership('224.1.1.1');
      this.socket.setBroadcast(true);
    });
    this.socket.on('message', msg => {
      // Parse incoming message here
      const service = {
        id: extractSomeSortOfId(msg),
        message: msg
      };

      // Add it to the list of services
      this.updateService(service);
    });
  }

  destroy() {
    this.socket.destroy();

    super.destroy();
  }
}