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

sequelize-change-tracker

v0.6.0

Published

Wrapper around Sequelize hooks to help distribute changes to specific listeners (subscriptions).

Readme

Sequelize Change Tracker

A reactive extension for Sequelize that enables subscribing to database changes on specific table rows, entire tables, or value-based ranges. Perfect for building real-time applications, live dashboards, virtual scrolling tables, or 3D world simulations.

Features

  • Specific Subscriptions — Track changes to individual database rows
  • Generic Subscriptions — Monitor all create/update/delete operations on a table
  • Range Subscriptions — Track changes within numeric/date ranges (sliding windows)
  • Multi-dimensional Ranges — Perfect for 3D viewports (x, y, z coordinates)
  • Bucket Subscriptions — Track changes matching discrete values (status = 'pending')
  • Cascading Notifications — Automatically notifies parent model subscribers when related models change
  • Automatic Subscription via Queries — Subscribe to instances directly through Sequelize query options
  • Event-Based Architecture — Built on Node.js EventEmitter for flexible integration

Installation

npm install sequelize-change-tracker

Quick Start

import { Sequelize, DataTypes } from 'sequelize';
import SequelizeChangeTracker from 'sequelize-change-tracker';

// Setup Sequelize
const sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:' });

const User = sequelize.define('User', {
    name: DataTypes.STRING,
    email: DataTypes.STRING
});

await sequelize.sync();

// Initialize the change tracker
const tracker = new SequelizeChangeTracker({ 
    models: [User] 
});

// Listen for changes
tracker.on('data-changed', (event) => {
    console.log(`${event.operation} on ${event.model}:`, event.instance);
    console.log('Notify subscriptions:', event.subscriptionIds);
});

// Create a user and track changes
const user = await User.create({ name: 'Alice', email: '[email protected]' });
tracker.addSubscription({ 
    modelName: 'User', 
    subscriptionId: 'user-alice', 
    instanceId: user.id 
});

// This update will trigger the 'data-changed' event
await user.update({ name: 'Alice Smith' });

API Reference

Constructor

new SequelizeChangeTracker({ models })

| Parameter | Type | Description | |-----------|------|-------------| | models | Array<Model> | Array of Sequelize models to track |

Instance Subscription Methods

addSubscription({ modelName, subscriptionId, instanceId? })

Add a subscription to track changes to specific instances or entire tables.

// Generic subscription — notified on ALL User creates/updates/deletes
tracker.addSubscription({ 
    modelName: 'User', 
    subscriptionId: 'ws-connection-123' 
});

// Specific subscription — notified only when this user changes
tracker.addSubscription({ 
    modelName: 'User', 
    subscriptionId: 'ws-connection-123', 
    instanceId: user.id 
});

removeSubscription({ modelName, subscriptionId, instanceId?, generic? })

Remove a specific subscription.

// Remove specific subscription
tracker.removeSubscription({ 
    modelName: 'User', 
    subscriptionId: 'ws-connection-123', 
    instanceId: user.id 
});

// Remove generic subscription
tracker.removeSubscription({ 
    modelName: 'User', 
    subscriptionId: 'ws-connection-123', 
    generic: true 
});

Range Subscription Methods (Sliding Windows)

Perfect for virtual scrolling tables, paginated views, or any UI that shows a "window" into data.

setRangeSubscription({ modelName, subscriptionId, windowId, ranges })

Set or update a range subscription. Calling with the same windowId replaces the previous subscription (sliding window behavior).

// Subscribe to orders with amount between 100-500
tracker.setRangeSubscription({
    modelName: 'Order',
    subscriptionId: 'ws-123',
    windowId: 'main-table',
    ranges: [
        { field: 'amount', min: 100, max: 500 }
    ]
});

// User scrolls — slide the window
tracker.setRangeSubscription({
    modelName: 'Order',
    subscriptionId: 'ws-123',
    windowId: 'main-table',  // Same windowId = replaces previous
    ranges: [
        { field: 'amount', min: 400, max: 800 }
    ]
});

addRangeSubscription({ modelName, subscriptionId, ranges })

Add a static range subscription (accumulates, doesn't replace). Use for persistent filters.

// Always notify about high-value orders
tracker.addRangeSubscription({
    modelName: 'Order',
    subscriptionId: 'ws-123',
    ranges: [
        { field: 'amount', min: 10000, max: Infinity }
    ]
});

removeRangeSubscription({ subscriptionId, windowId })

Remove a specific window subscription.

tracker.removeRangeSubscription({ 
    subscriptionId: 'ws-123', 
    windowId: 'main-table' 
});

Multi-dimensional Range Subscriptions (3D Viewports)

For 3D worlds, games, or any multi-dimensional data, use multiple range conditions with AND logic.

// Subscribe to voxels within camera viewport
tracker.setRangeSubscription({
    modelName: 'Voxel',
    subscriptionId: 'player-1',
    windowId: 'camera',
    ranges: [
        { field: 'x', min: playerX - 50, max: playerX + 50 },
        { field: 'y', min: playerY - 50, max: playerY + 50 },
        { field: 'z', min: playerZ - 20, max: playerZ + 20 }
    ]
});

// Player moves — update viewport
function onPlayerMove(x, y, z) {
    tracker.setRangeSubscription({
        modelName: 'Voxel',
        subscriptionId: 'player-1',
        windowId: 'camera',
        ranges: [
            { field: 'x', min: x - 50, max: x + 50 },
            { field: 'y', min: y - 50, max: y + 50 },
            { field: 'z', min: z - 20, max: z + 20 }
        ]
    });
}

Bucket Subscription Methods

For discrete categorical values like status, type, or category.

addBucketSubscription({ modelName, subscriptionId, field, value?, values? })

Subscribe to changes where a field matches specific values.

// Single value
tracker.addBucketSubscription({
    modelName: 'Order',
    subscriptionId: 'ws-123',
    field: 'status',
    value: 'pending'
});

// Multiple values (OR logic)
tracker.addBucketSubscription({
    modelName: 'Order',
    subscriptionId: 'ws-123',
    field: 'status',
    values: ['pending', 'processing', 'review']
});

removeBucketSubscription({ modelName, subscriptionId, field })

Remove a bucket subscription.

tracker.removeBucketSubscription({
    modelName: 'Order',
    subscriptionId: 'ws-123',
    field: 'status'
});

Cleanup Methods

removeSubscriptionAllModels(subscriptionId)

Remove all subscriptions for a given subscription ID. Handles instance, generic, range, and bucket subscriptions.

// Client disconnected — clean up everything
tracker.removeSubscriptionAllModels('ws-connection-123');

destroy()

Clean up the tracker, removing all hooks and event listeners.

tracker.destroy();

Events

data-changed

Emitted when a tracked model changes.

tracker.on('data-changed', (event) => {
    // event.operation    — 'create' | 'update' | 'delete'
    // event.model        — Model name (e.g., 'User')
    // event.instance     — The changed instance data
    // event.changedFields — Array of field names that changed
    // event.subscriptionIds — Array of subscription IDs to notify
});

subscriptions-changed

Emitted when a subscription is added or updated.

tracker.on('subscriptions-changed', (event) => {
    // event.subscriptionId — The subscription ID
    // event.modelName      — Model name
    // event.type           — 'range-window' | 'range-static' | 'bucket' | undefined
    // event.windowId       — Window ID (for range-window)
    // event.ranges         — Range conditions (for range subscriptions)
    // event.field          — Field name (for bucket subscriptions)
    // event.values         — Bucket values (for bucket subscriptions)
});

Automatic Subscription via Query Options

Subscribe to instances automatically when querying by adding trackChanges to your Sequelize options:

// Subscribe when finding
const user = await User.findOne({ 
    where: { id: userId },
    trackChanges: { subscriptionId: 'ws-connection-123' }
});

// Subscribe when creating
const newUser = await User.create(
    { name: 'Bob' },
    { trackChanges: { subscriptionId: 'ws-connection-123' }}
);

// Subscribe to multiple instances
const users = await User.findAll({ 
    trackChanges: { subscriptionId: 'ws-connection-123' }
});

Use Case Examples

Virtual Scrolling Table

// As user scrolls through a sorted table
function onScroll(visibleRows) {
    const minDate = visibleRows[0].createdAt;
    const maxDate = visibleRows[visibleRows.length - 1].createdAt;
    
    tracker.setRangeSubscription({
        modelName: 'Order',
        subscriptionId: connectionId,
        windowId: 'orders-table',
        ranges: [
            { field: 'createdAt', min: minDate, max: maxDate }
        ]
    });
}

3D Voxel World

const Voxel = sequelize.define('Voxel', {
    x: DataTypes.INTEGER,
    y: DataTypes.INTEGER,
    z: DataTypes.INTEGER,
    material: DataTypes.STRING
});

const tracker = new SequelizeChangeTracker({ models: [Voxel] });

// Route changes to players
tracker.on('data-changed', (event) => {
    if (event.model === 'Voxel') {
        for (const playerId of event.subscriptionIds) {
            sendToPlayer(playerId, {
                type: 'voxel-update',
                operation: event.operation,
                voxel: event.instance
            });
        }
    }
});

// When player connects
function onPlayerConnect(playerId, position) {
    tracker.setRangeSubscription({
        modelName: 'Voxel',
        subscriptionId: playerId,
        windowId: 'camera',
        ranges: [
            { field: 'x', min: position.x - 50, max: position.x + 50 },
            { field: 'y', min: position.y - 50, max: position.y + 50 },
            { field: 'z', min: position.z - 20, max: position.z + 20 }
        ]
    });
}

// When player moves
function onPlayerMove(playerId, position) {
    tracker.setRangeSubscription({
        modelName: 'Voxel',
        subscriptionId: playerId,
        windowId: 'camera',
        ranges: [
            { field: 'x', min: position.x - 50, max: position.x + 50 },
            { field: 'y', min: position.y - 50, max: position.y + 50 },
            { field: 'z', min: position.z - 20, max: position.z + 20 }
        ]
    });
}

// When player disconnects
function onPlayerDisconnect(playerId) {
    tracker.removeSubscriptionAllModels(playerId);
}

Real-Time Dashboard

// Main data table with date filter
tracker.setRangeSubscription({
    modelName: 'Order',
    subscriptionId: connectionId,
    windowId: 'main-table',
    ranges: [{ field: 'createdAt', min: startDate, max: endDate }]
});

// High-value orders chart (static, always active)
tracker.addRangeSubscription({
    modelName: 'Order',
    subscriptionId: connectionId,
    ranges: [{ field: 'amount', min: 10000, max: Infinity }]
});

// Pending orders alert panel
tracker.addBucketSubscription({
    modelName: 'Order',
    subscriptionId: connectionId,
    field: 'status',
    values: ['pending', 'review']
});

WebSocket Integration

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });
const tracker = new SequelizeChangeTracker({ models: [User, Order, Voxel] });
const clients = new Map();

tracker.on('data-changed', (event) => {
    for (const subscriptionId of event.subscriptionIds) {
        const client = clients.get(subscriptionId);
        if (client?.readyState === WebSocket.OPEN) {
            client.send(JSON.stringify({
                type: 'data-changed',
                model: event.model,
                operation: event.operation,
                data: event.instance
            }));
        }
    }
});

wss.on('connection', (ws) => {
    const connectionId = crypto.randomUUID();
    clients.set(connectionId, ws);
    
    ws.on('message', (message) => {
        const { action, ...params } = JSON.parse(message);
        
        switch (action) {
            case 'subscribe':
                tracker.addSubscription({ ...params, subscriptionId: connectionId });
                break;
            case 'setRange':
                tracker.setRangeSubscription({ ...params, subscriptionId: connectionId });
                break;
            case 'addBucket':
                tracker.addBucketSubscription({ ...params, subscriptionId: connectionId });
                break;
        }
    });
    
    ws.on('close', () => {
        tracker.removeSubscriptionAllModels(connectionId);
        clients.delete(connectionId);
    });
});

Subscription Types Summary

| Method | Behavior | Use Case | |--------|----------|----------| | addSubscription({ instanceId }) | Track specific row | Single record detail view | | addSubscription({}) | Track all table changes | Table-wide notifications | | setRangeSubscription | Sliding window (upsert) | Virtual scrolling, 3D viewports | | addRangeSubscription | Static range (accumulate) | Persistent value filters | | addBucketSubscription | Discrete values | Status/category filters |

Supported Associations

The tracker understands these Sequelize association types for cascading notifications:

  • HasOne
  • HasMany
  • BelongsTo
  • BelongsToMany

Limitations

  • bulkUpdate and bulkDestroy operations do not trigger notifications (Sequelize limitation — these hooks don't receive instance data)