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 🙏

© 2026 – Pkg Stats / Ryan Hefner

yoctopus

v1.2.1

Published

A plugin-based WebSocket server framework for Node.js. Yoctopus provides a modular architecture where functionality is organized into plugins that define event listeners, endpoints, and connection-scoped settings.

Readme

Yoctopus

A plugin-based WebSocket server framework for Node.js. Yoctopus provides a modular architecture where functionality is organized into plugins that define event listeners, endpoints, and connection-scoped settings.

Table of Contents

Features

  • Plugin Architecture: Modular design where each plugin can define endpoints, settings, and event listeners
  • Connection-Scoped State: Each connection has its own data store with typed, validated settings
  • Request Validation: Built-in validation for request formats (string, number, boolean, array, object, date, email, hostname, custom)
  • Endpoint Requirements: Conditionally allow/deny endpoint access based on connection state
  • Lifecycle Events: Hook into connection open, close, request received, response prepared/sent
  • Cross-Plugin Communication: Plugins can read each other's settings and listen for changes
  • Real-time Notifications: Push messages to clients via the notify function
  • Multicast Support: Send messages to filtered subsets of connections
  • SSL/TLS Support: Built-in HTTPS/WSS support

Installation

npm install yoctopus

Quick Start

const Yoctopus = require('yoctopus')

const server = new Yoctopus({ port: 3000 })

// Add a simple plugin
server.plugin({
    name: 'greeter',
    endpoints: {
        hello: {
            read: {
                requestFormat: {
                    name: 'string'
                },
                handler: ({ request }) => ({
                    message: `Hello, ${request.data.name}!`
                })
            }
        }
    }
})

server.start()

A client can now connect and send:

{
    "meta": { "id": "1", "endpoint": "greeter:hello", "method": "read" },
    "data": { "name": "Alice" }
}

And receive:

{
    "meta": { "id": "1", "endpoint": "greeter:hello", "method": "read" },
    "data": { "message": "Hello, Alice!" }
}

Core Concepts

Server Initialization

const server = new Yoctopus({
    port,                   // Port to host the server on
    keyPath,                // Path to SSL key (optional, enables HTTPS)
    certPath,               // Path to SSL certificate (optional)
    caPath,                 // Path to SSL CA (optional)
    allowedOrigins,         // Array of allowed origins, e.g. ['https://example.com'] or ['*']
    pingInterval,           // Milliseconds between ping checks (default: 250)
    maxLostPings,           // Max unanswered pings before disconnect (default: 3)
    staticEndpointArguments // Object passed to all handlers
})

// Add plugins
server.plugin(pluginConfig, pluginOptions)

// Start the server
await server.start()

Plugins

Plugins are the building blocks of Yoctopus. Each plugin can define:

  • name: Unique identifier (required)
  • settings: Connection-scoped variables with type validation
  • endpoints: Request handlers
  • listeners: Event handlers for lifecycle and setting changes
  • passToListeners: Functions/data to pass to other plugins' handlers
{
    name: 'my-plugin',
    
    settings: {
        'userId': 'string',
        'isAdmin': 'bool',
        'loginCount': 'number'
    },
    
    listeners: {
        server: {
            onConnectionOpened: ({ connectionSettings, socket, notify, connectionId }) => {},
            onConnectionClosed: ({ connectionSettings, notify, connectionId }) => {},
            onRequestReceived: ({ request, connectionSettings, notify, data }) => {},
            onResponsePrepared: ({ request, connectionSettings, notify, data }) => {},
            onResponseSent: ({ request, connectionSettings, notify, data }) => {},
            onConnectionChanged: ({ connectionSettings, notify }) => {}
        },
        plugins: {
            'other-plugin': {
                'settingName': ({ value, setting, connectionId }) => {
                    // Called when another plugin's setting changes
                }
            }
        }
    },
    
    endpoints: {
        'endpoint-name': {
            'method': {
                requirements: { 'plugin.setting': expectedValue },
                requestFormat: { 'key': 'type' },
                handler: async ({ request, connectionSettings, notify, log, connectionId }) => {
                    return { result: 'data' }
                }
            }
        }
    },
    
    passToListeners: {
        someFunction: () => {},
        someData: { key: 'value' }
    }
}

Endpoints

Endpoints are organized by name and method. The method typically represents the type of operation:

endpoints: {
    user: {
        create: {
            requestFormat: {
                name: 'string',
                email: 'email',
                age: 'number'
            },
            handler: async ({ request, connectionSettings }) => {
                const { name, email, age } = request.data
                // Create user...
                return { id: 'user-123', name, email }
            }
        },
        read: {
            requestFormat: {
                id: 'string'
            },
            handler: async ({ request }) => {
                // Fetch user...
                return { id: request.data.id, name: 'Alice' }
            }
        },
        update: {
            requirements: { 'auth.authenticated': true },
            handler: async ({ request, connectionSettings }) => {
                // Update user...
            }
        },
        delete: {
            requirements: { 'auth.isAdmin': true },
            handler: async ({ request }) => {
                // Delete user...
            }
        }
    }
}

Endpoint Aliases

By default, endpoints are accessed with plugin-name:endpoint format. You can create aliases for cleaner access:

server.plugin({
    name: 'fb-auth',
    endpoints: { user: { read: { handler: () => {} } } }
}, {
    endpointAliases: {
        'user': 'account'  // Access as 'account' instead of 'fb-auth:user'
    }
})

Connection Settings

Each connection has a data store where plugins can store typed, validated settings.

Declaring Settings

{
    name: 'auth',
    settings: {
        'authenticated': 'bool',
        'userId': 'string',
        'roles': 'array',
        'metadata': 'object',
        'loginCount': 'number'
    }
}

Reading Settings

Use connectionSettings.get(pluginName, settingKey) to read any plugin's settings:

handler: async ({ connectionSettings }) => {
    const userId = connectionSettings.get('auth', 'userId')
    const isAdmin = connectionSettings.get('auth', 'roles')?.includes('admin')
    const port = connectionSettings.get('yoctopus', 'port')
    return { userId, isAdmin, port }
}

Writing Settings

Method 1: Setter (Legacy)

handler: async ({ connectionSettings }) => {
    connectionSettings.set('authenticated', true)
    connectionSettings.set('userId', 'user-123')
    return { success: true }
}

Method 2: Return Value (Recommended)

handler: async () => ({
    settings: {
        authenticated: true,
        userId: 'user-123'
    },
    response: {
        success: true,
        message: 'Logged in'
    }
})

The return value method is cleaner and makes settings changes explicit. You can return:

  • Both settings and response: { settings: {...}, response: {...} }
  • Only settings: { settings: {...} } (response defaults to {})
  • Only response: { response: {...} } (no settings changed)
  • Direct value (legacy): return someValue (treated as response)

Listening to Setting Changes

Plugins can react when another plugin's settings change:

{
    name: 'logger',
    listeners: {
        plugins: {
            'auth': {
                'authenticated': ({ value, setting, connectionId }) => {
                    console.log(`Connection ${connectionId} auth changed to ${value}`)
                }
            }
        }
    }
}

Lifecycle Events

| Event | When Called | Use Case | |-------|-------------|----------| | onConnectionOpened | New WebSocket connection established | Initialize connection state, authenticate | | onRequestReceived | Request received, before processing | Logging, rate limiting, request transformation | | onResponsePrepared | Response ready, before sending | Response transformation, logging | | onResponseSent | Response sent to client | Analytics, cleanup | | onConnectionClosed | Connection closed | Cleanup, logging | | onConnectionChanged | Any setting changed on connection | Cross-plugin coordination |

listeners: {
    server: {
        onConnectionOpened: async ({ socket, notify, connectionSettings, connectionId }) => {
            console.log(`New connection: ${connectionId}`)
            // Access request headers via socket.upgradeReq.headers
        },
        onRequestReceived: async ({ request, data }) => {
            console.log(`Request to ${request.meta.endpoint}`)
            // Return modified data to transform the request
            return { ...data, timestamp: Date.now() }
        },
        onResponsePrepared: async ({ data }) => {
            // Return modified data to transform the response
            return { ...data, serverTime: Date.now() }
        },
        onConnectionClosed: async ({ connectionId }) => {
            console.log(`Connection closed: ${connectionId}`)
        }
    }
}

Request Format Validation

Define expected request data types. Requests that don't match are rejected automatically.

requestFormat: {
    name: 'string',              // Must be a string
    age: 'number',               // Must be a number
    isActive: 'bool',            // Must be a boolean
    tags: 'array',               // Must be an array
    metadata: 'object',          // Must be an object
    email: 'email',              // Must be valid email format
    website: 'hostname',         // Must be valid hostname
    birthdate: 'date',           // Must be ISO8601 date string
    status: ['active', 'inactive', 'pending'],  // Union type - must be one of these
    validator: (value) => value > 0  // Custom validator function
}

For nested validation:

requestFormat: {
    user: {
        name: 'string',
        address: {
            street: 'string',
            city: 'string',
            zip: 'string'
        }
    }
}

Requirements

Restrict endpoint access based on connection state:

endpoints: {
    admin: {
        dashboard: {
            requirements: {
                'auth.authenticated': true,
                'auth.isAdmin': true
            },
            handler: () => ({ dashboardData: '...' })
        }
    },
    profile: {
        read: {
            requirements: {
                'auth.authenticated': true
            },
            handler: ({ connectionSettings }) => {
                return { userId: connectionSettings.get('auth', 'userId') }
            }
        }
    }
}

Requirements can also be async functions:

requirements: {
    'custom': async ({ connectionSettings, log }) => {
        const userId = connectionSettings.get('auth', 'userId')
        return await checkUserPermissions(userId)
    }
}

Bundled Plugins

yoctopus (Core Plugin)

Automatically installed. Provides basic connection metadata.

Settings:

  • label (string): Human-readable connection label
  • isSecure (bool): Whether connection is over HTTPS/WSS
  • port (number): Server port
  • remoteAddress (string): Client IP address

Endpoints:

  • yoctopus:label.update - Set a label for the connection
// Client can label their connection
{ 
    "meta": { "id": "1", "endpoint": "yoctopus:label", "method": "update" },
    "data": "Mobile App Connection"
}

multicast

Send messages to filtered subsets of connections based on their store data.

Usage:

// In any endpoint handler
handler: async ({ multicast }) => {
    // Send to all connections where auth.isAdmin is true
    multicast(
        { auth: { isAdmin: true } },
        { type: 'admin-alert', data: { message: 'System update' } }
    )
    
    // Send to connections with specific userId
    multicast(
        { auth: { userId: 'user-123' } },
        { type: 'personal-notification', data: { ... } }
    )
    
    return { sent: true }
}

api

Exposes the server's endpoint API to clients. Useful for auto-generating documentation or building dynamic clients.

Endpoints:

  • api:api.read - Get all available endpoints and their request formats
  • api:api.subscribe - Subscribe to API changes
  • api:api.unsubscribe - Unsubscribe from API changes
// Client request
{ "meta": { "id": "1", "endpoint": "api:api", "method": "read" }, "data": {} }

// Response
{
    "meta": { ... },
    "data": {
        "greeter:hello": {
            "read": { "requestFormat": { "name": "string" } }
        },
        "user": {
            "create": { "requestFormat": { "name": "string", "email": "email" } },
            "read": { "requestFormat": { "id": "string" } }
        }
    }
}

sequelize-change-tracker

Requires: npm install sequelize sequelize-change-tracker sqlite3 (or your DB driver)

Real-time database change notifications. Automatically notifies connected clients when Sequelize models are created, updated, or deleted.

Setup:

const Yoctopus = require('yoctopus')
const { Sequelize, DataTypes } = require('sequelize')
const changeTrackerPlugin = require('yoctopus/plugins/sequelize-change-tracker')

// Setup Sequelize
const sequelize = new Sequelize('sqlite::memory:')

const User = sequelize.define('User', {
    name: DataTypes.STRING,
    email: DataTypes.STRING,
    password: DataTypes.STRING  // Sensitive - won't be exposed
})

const Post = sequelize.define('Post', {
    title: DataTypes.STRING,
    content: DataTypes.TEXT,
    authorId: DataTypes.INTEGER
})

await sequelize.sync()

// Setup Yoctopus
const server = new Yoctopus({ port: 3000 })

const { tracker, plugin } = changeTrackerPlugin({
    models: [User, Post],
    server: server,
    autoSubscribeTo: ['Post'],  // Auto-subscribe all connections to Post changes
    publicFieldsPerModel: {
        User: ['id', 'name'],   // Only expose id and name (not email, password)
        Post: ['id', 'title', 'content', 'authorId']
    }
})

server.plugin(plugin)
await server.start()

How it works:

  1. Auto-subscription: When a client connects, they're automatically subscribed to models listed in autoSubscribeTo
  2. Field filtering: Only fields in publicFieldsPerModel are sent to clients (protects sensitive data)
  3. Cleanup: Subscriptions are automatically removed when connections close

Client notifications:

When database changes occur, subscribed clients receive:

{
    "meta": { "method": "notify", "type": "data-changed" },
    "data": {
        "operation": "create",
        "model": "Post",
        "fields": {
            "id": 1,
            "title": "Hello World",
            "content": "My first post",
            "authorId": 42
        }
    }
}

Operations: create, update, delete

Manual subscriptions:

You can allow clients to subscribe to specific models via endpoints:

server.plugin({
    name: 'subscriptions',
    endpoints: {
        subscribe: {
            create: {
                requestFormat: { model: 'string' },
                handler: ({ request, changeTracker, connectionId }) => {
                    changeTracker.addSubscription({
                        modelName: request.data.model,
                        subscriptionId: connectionId
                    })
                    return { subscribed: request.data.model }
                }
            }
        },
        unsubscribe: {
            delete: {
                requestFormat: { model: 'string' },
                handler: ({ request, changeTracker, connectionId }) => {
                    changeTracker.removeSubscription({
                        modelName: request.data.model,
                        subscriptionId: connectionId
                    })
                    return { unsubscribed: request.data.model }
                }
            }
        }
    }
})

Instance-specific subscriptions:

Subscribe to changes on specific records:

// Subscribe to a specific post's changes
changeTracker.addSubscription({
    modelName: 'Post',
    subscriptionId: connectionId,
    instanceId: 42  // Only notified when Post with id=42 changes
})

Client Communication

Message Format

All messages use JSON with this structure:

Request:

{
    "meta": {
        "id": "unique-request-id",
        "endpoint": "plugin:endpoint",
        "method": "read|create|update|delete|..."
    },
    "data": { ... }
}

Response:

{
    "meta": {
        "id": "unique-request-id",
        "endpoint": "plugin:endpoint",
        "method": "read",
        "status": "ok"
    },
    "data": { ... }
}

Error Response:

{
    "meta": {
        "id": "unique-request-id",
        "endpoint": "plugin:endpoint",
        "method": "read",
        "status": "error"
    },
    "error": "Error message or array of errors"
}

Server Notification (push):

{
    "meta": {
        "method": "notify",
        "type": "notification-type"
    },
    "data": { ... }
}

Sending Notifications

From any handler:

handler: async ({ notify }) => {
    // Send a notification to this connection
    notify({ type: 'alert', data: { message: 'Hello!' } })
    
    return { result: 'ok' }
}

Advanced Topics

Binary Message Handling

Handle raw binary WebSocket messages:

const server = new Yoctopus({
    port: 3000,
    binaryHandler: async ({ buffer, connectionSettings, connectionId, notify }) => {
        // Process binary data
        const processed = processBuffer(buffer)
        notify({ type: 'binary-processed', data: { size: buffer.length } })
    },
    binaryHandlerRequirements: {
        'auth.authenticated': true  // Only authenticated users can send binary
    }
})

Static Endpoint Arguments

Pass shared resources to all handlers:

const db = new Database()
const cache = new Redis()

const server = new Yoctopus({
    port: 3000,
    staticEndpointArguments: {
        db,
        cache,
        config: { maxItems: 100 }
    }
})

// In handlers:
handler: async ({ db, cache, config }) => {
    const items = await db.query('SELECT * FROM items LIMIT ?', [config.maxItems])
    return { items }
}

Dynamic Origin Control

const server = new Yoctopus({
    port: 3000,
    allowedOrigins: ['https://myapp.com']
})

// Later, update allowed origins
server.allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com']

Subscribing to Logs

const unsubscribe = server.subscribeToLogs((logEntry) => {
    console.log(`[${logEntry.level}] ${logEntry.message}`)
    // Send to external logging service
})

// Later
unsubscribe()

Connection Register

Access all active connections:

// Get all connection IDs
const connectionIds = Object.keys(server.connectionRegister)

// Access a specific connection
const connection = server.connectionRegister[connectionId]
connection.notify({ type: 'direct-message', data: {...} })
connection.close()

// Export connection state
const state = connection.exportStore()

Error Handling

const server = new Yoctopus({
    port: 3000,
    onError: (error) => {
        console.error('WebSocket server error:', error)
        // Handle error, potentially restart server
    }
})

                             *#(##((((###/                                 
                          (/*(***,*/(/,****##                              
                        ,(*((((,*,,,,,,,,,,/(#/                            
                       (/**((//*////..,,,//*/((%                           
                      *(//*,,,,,/////..,,,,**////                          
                      %#/////*,,.//*,,.,,//(*((/#                          
                      %(((///,,,...,/....///((((%                          
                      #(##/,,,,//.......,,//*((#%                          
                      %%%%(//,/,,....//..,,*(%%#%                          
                       &&@%(*,,,,./...,,,.,/#&&%                           
                        %#*%%%/,/*,//**,*%%#,(%                            
                //(/(   ///#@@@(#/,,**(/@@@(*/*   (/(//                    
                */(///* #@%.&@@@%*,*,,#@&(@@@@# ,(////*                    
         /*/*/ #%%%/%/, /@@@@@@(%/,,,,#/@@&&@@/ ,/#/#%%# *//*/             
        /*(% .. %##/#(%   (%#%%****,*,,*###%(   %(%*%%% ...##*/            
       .(/#*     (#**(%.    /(/,(/,,,./,/(*     #(/*/#     (#//.           
        ,*(#/     #(//(#/((*(,,,./...,*,,/*,#%#(###(#     #/(**            
        (#//*(//    ###%(,***(///....//....,,,*####    **(/**((            
          (////(/*((((/(**(.,///,,.....////*,**/(((/,*(*/***(              
             (//*//**/((**/*,,,,.,.....*////..*(((//(/***/                 
                ((,%#%((*,,,*.,////......,,,,.*/(%#%,/(                    
             (/#/#  &//**/,*,,,*///....*/,,,,,,,,/&  (*#/(                 
             (/(/  /##*(#((**(/,*,,.,,,/////(((/*(/,  *///                 
            #/%//&%(#((*   ((/***/**/*****(*   *(*/(#&*/%(#                
             (%/(//*///,   ,%/*/((* ,#(//(%.   ,((/////*%/                 
             #//(///*/     /#(((#     (/*(#*     (/***/*/#                 
                       ((#**/((##     #(**/**#(#                           
                       (##/&/(/#,     .(/#/@/(#(                           
                      #((%/&%###       #(#&&*%((#                          
                      /((*(%#(#%       ((%(*##(                          
                       *((%/%%&.       .&%&/%((*.                          
                        ,(#(/(           (//#(,