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

@phpdot/statedb

v1.0.0

Published

Lightweight, zero-dependency reactive database for JavaScript and TypeScript. MongoDB-style queries, update operators, indexes, schema validation, and a built-in watcher system for real-time UI reactivity. Optional localStorage/sessionStorage persistence.

Readme

StateDB

npm version license tests zero deps gzip size

Lightweight, zero-dependency reactive database for JavaScript and TypeScript. MongoDB-style queries, update operators, indexes, schema validation, and a built-in watcher system for real-time UI reactivity. Optional localStorage/sessionStorage persistence.

Built for developers who think in collections, queries, and CRUD — not observables, reducers, or stores.

Why StateDB?

  • You need client-side data with a familiar query syntax (MongoDB-style)
  • You want reactive UI updates without React/Vue/Angular
  • You want to drop in a single <script> tag and start working
  • You don't want to learn RxJS, Redux, or a new state management paradigm
  • You're building with Livewire, HTMX, Alpine, or vanilla JS and need structured client-side state

Install

npm install @phpdot/statedb

CDN

<!-- Minified (~20KB) -->
<script src="https://unpkg.com/@phpdot/statedb/dist/index.min.js"></script>

<!-- Non-minified (readable) -->
<script src="https://unpkg.com/@phpdot/statedb/dist/index.js"></script>

<script>
  const db = new StateDB('my-app');
</script>

ESM / CJS

// ESM
import { StateDB } from '@phpdot/statedb';

// CJS
const { StateDB } = require('@phpdot/statedb');

Quick Start

import { StateDB } from '@phpdot/statedb';

// Create a database and collection
const db = new StateDB('mydb');
const users = db.createCollection('users');

// Insert documents
users.insertOne({ name: 'Omar', age: 30, role: 'admin' });
users.insertMany([
  { name: 'Ali', age: 25, role: 'user' },
  { name: 'Sara', age: 28, role: 'admin' },
]);

// Query with MongoDB operators
const admins = users.find({ role: 'admin' }).sort({ age: -1 }).toArray();
// [{ name: 'Omar', age: 30, ... }, { name: 'Sara', age: 28, ... }]

// Update with operators
users.updateOne({ name: 'Ali' }, { $set: { role: 'admin' }, $inc: { age: 1 } });

// React to changes
users.watch((event, docs) => {
  console.log(event, docs); // 'update', [{ name: 'Ali', ... }]
});

// Delete
users.deleteMany({ role: 'user' });

Features

CRUD Operations

const users = db.createCollection('users');

// Insert
const doc = users.insertOne({ name: 'Omar', age: 30 });
// → { __inc: 1, name: 'Omar', age: 30 }

const docs = users.insertMany([{ name: 'Ali' }, { name: 'Sara' }]);

// Find
users.find().toArray();                          // all documents
users.find({ role: 'admin' }).toArray();         // with query
users.findOne({ name: 'Omar' });                 // single document
users.findOne({ __inc: 1 });                     // by ID (O(1) lookup)

// Helpers
users.count({ role: 'admin' });                  // count matching
users.exists({ name: 'Omar' });                  // true/false
users.first();                                   // first document
users.last();                                    // last document

// Update
users.updateOne({ name: 'Omar' }, { $set: { age: 31 } });
users.updateMany({ role: 'user' }, { $set: { active: true } });
users.replaceOne({ name: 'Omar' }, { name: 'Omar H', age: 31 });
users.upsertOne({ email: '[email protected]' }, { name: 'New', email: '[email protected]' });

// Delete
users.deleteOne({ name: 'Omar' });
users.deleteMany({ active: false });
users.drop();                                    // remove all documents

Every document gets an auto-incrementing __inc field used as the primary key. This is never reused, even after deletes.


Query Operators

Full MongoDB-style query syntax with dot notation for nested fields.

| Comparison | Logical | Array | Element | Evaluation | |-----------|---------|-------|---------|------------| | $eq $ne | $and | $all | $exists | $regex | | $gt $gte | $or | $elemMatch | $type | | | $lt $lte | $not | $size | | | | $in $nin | $nor | | | |

// Comparison
users.find({ age: { $gt: 25, $lte: 35 } });

// Logical
users.find({ $or: [{ role: 'admin' }, { age: { $gte: 30 } }] });
users.find({ $and: [{ active: true }, { $not: { role: 'guest' } }] });

// Array
users.find({ tags: { $all: ['js', 'ts'] } });
users.find({ scores: { $elemMatch: { $gte: 90 } } });
users.find({ tags: { $size: 3 } });

// Element
users.find({ email: { $exists: true } });
users.find({ age: { $type: 'number' } });

// Regex
users.find({ name: { $regex: /^om/i } });

// Inclusion
users.find({ status: { $in: ['active', 'pending'] } });
users.find({ role: { $nin: ['banned'] } });

// Dot notation (nested fields)
users.find({ 'address.city': 'Dubai' });
users.find({ 'profile.settings.theme': 'dark' });

Update Operators

| Field | Numeric | Array | |-------|---------|-------| | $set $unset | $inc $mul | $push $pop | | $rename | $min $max | $pull $pullAll | | $currentDate | | $addToSet |

// Set fields (supports dot notation)
users.updateOne({ name: 'Omar' }, {
  $set: { 'address.city': 'Dubai', active: true },
});

// Increment / multiply
users.updateOne({ __inc: 1 }, { $inc: { loginCount: 1 } });
users.updateOne({ __inc: 1 }, { $mul: { price: 1.1 } });

// Min / max (only updates if new value is lower/higher)
users.updateOne({ __inc: 1 }, { $min: { lowest: 5 }, $max: { highest: 100 } });

// Remove fields
users.updateOne({ __inc: 1 }, { $unset: { tempField: '' } });

// Rename fields
users.updateOne({ __inc: 1 }, { $rename: { oldName: 'newName' } });

// Set current date
users.updateOne({ __inc: 1 }, { $currentDate: { updatedAt: true } });

// Array: push
users.updateOne({ __inc: 1 }, { $push: { tags: 'new' } });
users.updateOne({ __inc: 1 }, {
  $push: { tags: { $each: ['a', 'b'], $position: 0 } },
});

// Array: add unique
users.updateOne({ __inc: 1 }, { $addToSet: { tags: 'unique' } });
users.updateOne({ __inc: 1 }, { $addToSet: { tags: { $each: ['a', 'b'] } } });

// Array: remove
users.updateOne({ __inc: 1 }, { $pull: { tags: 'old' } });
users.updateOne({ __inc: 1 }, { $pullAll: { tags: ['a', 'b'] } });
users.updateOne({ __inc: 1 }, { $pop: { tags: 1 } });   // last
users.updateOne({ __inc: 1 }, { $pop: { tags: -1 } });  // first

// Simple merge (no operators — merges fields directly)
users.updateOne({ __inc: 1 }, { name: 'Omar H', age: 31 });

Chainable Queries

users.find({ active: true })
  .sort({ age: -1 })          // sort descending
  .skip(10)                    // pagination offset
  .limit(5)                    // max results
  .project({ name: 1, age: 1 }) // include only these fields
  .toArray();                  // execute and return array

// Iteration helpers
users.find().forEach((doc) => console.log(doc));
users.find().map((doc) => doc.name);     // ['Omar', 'Ali', ...]
users.find().filter((doc) => doc.age > 25);
users.find().count();
users.find().first();
users.find().last();

Projection

// Include mode — only return specified fields (+ __inc always included)
users.find({}, { name: 1, email: 1 }).toArray();
// [{ __inc: 1, name: 'Omar', email: '[email protected]' }]

// Exclude mode — return all fields except specified
users.find({}, { password: 0, secret: 0 }).toArray();

// Works with dot notation
users.find({}, { 'address.city': 1 }).toArray();

Watchers (Reactivity)

Subscribe to data changes. Watchers fire synchronously after every mutation — ideal for re-rendering UI.

// Watch all changes
const id = users.watch((event, docs, prevDocs) => {
  // event:    'insert' | 'update' | 'delete' | 'drop'
  // docs:     affected documents (current state)
  // prevDocs: previous state (null for insert/drop)
  console.log(event, docs);
});

// Watch specific operations only
users.watch(callback, { ops: ['insert', 'delete'] });

// Stop watching
users.unwatch(id);       // remove specific watcher
users.unwatch();         // remove all watchers

Real-world example: reactive UI

<ul id="user-list"></ul>

<script>
  const db = new StateDB('app');
  const users = db.createCollection('users');

  // Re-render whenever data changes
  users.watch(function() {
    var list = document.getElementById('user-list');
    list.textContent = '';
    users.find().sort({ name: 1 }).toArray().forEach(function(u) {
      var li = document.createElement('li');
      li.textContent = u.name + ' (' + u.age + ')';
      list.appendChild(li);
    });
  });

  // These automatically trigger the watcher
  users.insertOne({ name: 'Omar', age: 30 });
  users.updateOne({ name: 'Omar' }, { $inc: { age: 1 } });
</script>

Schema Validation

Define schemas when creating collections. Documents are validated on every insert and update.

const users = db.createCollection('users', {
  schema: {
    name:  { type: String, required: true, minLength: 2, maxLength: 50 },
    email: { type: String, required: true, match: /^.+@.+\..+$/ },
    age:   { type: Number, min: 0, max: 150 },
    role:  { type: String, enum: ['admin', 'user', 'guest'], default: 'user' },
    tags:  { type: Array, of: String },
    bio:   { type: String },
    active:{ type: Boolean, default: true },
    joined:{ type: Date, default: () => new Date() },
  },
});

Schema rules

| Rule | Type | Description | |------|------|-------------| | type | String Number Boolean Date Array Object | Type checking | | required | boolean | Must be present on insert | | default | value or () => value | Applied on insert if field is missing | | min / max | number | For Number fields | | minLength / maxLength | number | For String fields | | enum | array | Allowed values | | match | RegExp | Pattern match for String fields | | of | type constructor | Element type for Array fields |

// Defaults are applied automatically
users.insertOne({ name: 'Omar', email: '[email protected]' });
// → { name: 'Omar', email: '[email protected]', role: 'user', active: true, joined: Date, __inc: 1 }

// Validation errors throw
users.insertOne({ name: 'A' });
// Error: Validation error: name must be at least 2 characters

users.insertOne({ age: 'not a number' });
// Error: Validation error: age must be of type Number

Capped collections

Automatically remove the oldest documents when the collection reaches a maximum size. Useful for logs, recent activity, or chat messages.

const logs = db.createCollection('logs', {
  schema: {},
  capped: true,
  max: 1000,
});

// When the 1001st doc is inserted, the oldest is removed automatically

Indexes

Create indexes for fast lookups. Queries automatically use the best available index.

// Single field index
users.createIndex('email', { unique: true });

// Compound index (multiple fields)
users.createIndex({ country: 1, city: 1 });

// Queries use indexes automatically
users.findOne({ email: '[email protected]' });       // IXSCAN (fast)
users.find({ country: 'UAE', city: 'Dubai' });    // uses compound index
users.find({ country: 'UAE' });                   // uses compound index (prefix match)
users.find({ city: 'Dubai' });                    // COLLSCAN (city is not the prefix)

// Manage indexes
users.getIndexes();          // ['__inc', 'email', 'country_city']
users.dropIndex('email');

Explain

Inspect how a query is executed — whether it uses an index or a full collection scan.

const explain = users.find({ email: '[email protected]' }).explain();

console.log(explain.queryPlanner.winningPlan);
// { stage: 'FETCH', inputStage: { stage: 'IXSCAN', indexName: 'email' } }

console.log(explain.executionStats);
// { nReturned: 1, totalDocsExamined: 1, indexUsed: true, executionTimeMs: 0.02 }

Hooks (Middleware)

Run functions before or after operations. Pre-hooks can modify data or abort the operation.

// Add timestamps automatically
users.pre('insert', (doc) => {
  doc.createdAt = new Date();
  doc.updatedAt = new Date();
});

users.pre('update', (query, update) => {
  update.$set = update.$set || {};
  update.$set.updatedAt = new Date();
});

// Abort an operation (return false)
users.pre('delete', (query) => {
  const doc = users.findOne(query);
  if (doc.role === 'admin') return false; // prevent deleting admins
});

// Post-hooks for side effects
users.post('insert', (doc) => {
  console.log('New user:', doc.name);
});

users.post('update', (result, query, update) => {
  console.log(`Modified ${result.modifiedCount} documents`);
});

users.post('delete', (result, query) => {
  console.log(`Deleted ${result.deletedCount} documents`);
});

// Remove hooks
users.removePre('insert');              // remove all pre-insert hooks
users.removePre('insert', specificFn);  // remove specific hook
users.removePost('update');

Persistence

Optionally save data to localStorage or sessionStorage. Data auto-saves on every mutation (debounced) and auto-loads on initialization.

const db = new StateDB('mydb', {
  persistent: true,
  storage: 'local',   // 'local' (default) or 'session'
  debounce: 100,       // ms between saves (default: 100)
});

// Data auto-saves after mutations
// Data auto-loads when StateDB is created

// Manual controls
db.save();   // force save now
db.load();   // force reload from storage
db.flush();  // clear storage completely

Export / Import

Move data in and out of StateDB.

// Export — clean data without __inc (for APIs, downloads)
const data = db.export();
// { users: [{ name: 'Omar', age: 30 }, { name: 'Ali', age: 25 }] }

// Dump — full data with __inc and counters (for backups)
const backup = db.dump();
// { users: { documents: [{ __inc: 1, name: 'Omar', ... }], inc: 3 } }

// Import clean data (assigns new __inc values)
db.import({ users: [{ name: 'Omar' }, { name: 'Ali' }] });

// Restore from dump (preserves __inc values)
db.restore(backup);

Browser Usage (IIFE)

StateDB works anywhere JavaScript runs. For browsers without a build tool, use the IIFE bundle:

<script src="https://unpkg.com/@phpdot/statedb/dist/index.min.js"></script>
<script>
  var db = new StateDB('app');
  var todos = db.createCollection('todos');

  // Reactive rendering
  todos.watch(function() {
    var list = document.getElementById('list');
    list.textContent = '';
    todos.find().sort({ __inc: -1 }).toArray().forEach(function(t) {
      var li = document.createElement('li');
      li.textContent = t.text;
      list.appendChild(li);
    });
  });

  todos.insertOne({ text: 'Hello StateDB', done: false });
</script>

All exports are available on the global StateDB object:

var db = new StateDB('app');            // main class
StateDB.Collection;                      // Collection class
StateDB.Query;                           // Query engine
StateDB.Schema;                          // Schema class
StateDB.Index;                           // Index class
StateDB.Watcher;                         // Watcher class

Performance

Benchmarked on Node v22 with 10,000 documents. Run npm run bench to reproduce.

Insert

| Operation | Time | Throughput | |-----------|------|------------| | insertOne x10,000 | 3.2ms | 3.1M ops/s | | insertMany x10,000 | 1.0ms | 10.4M docs/s |

Find

| Operation | Time (1,000 queries) | Per query | |-----------|---------------------|-----------| | find({}).toArray() | 0.09ms | ~0.09us | | find({ role: "admin" }) | 22ms | ~22us | | find({ age: { $gt: 40 } }) | 53ms | ~53us | | find({ $and + $or }) | 167ms | ~167us | | find().sort().skip().limit() | 854ms | ~854us |

Find One

| Operation | Time (5,000 lookups) | Per lookup | |-----------|---------------------|------------| | findOne({ __inc }) primary key | 0.05ms | ~0.01us | | findOne({ email }) full scan | 160ms | ~32us | | findOne({ email }) indexed | 1.1ms | ~0.22us | | Index speedup | | 145x |

Update

| Operation | Time (1,000 updates) | Per update | |-----------|---------------------|-----------| | updateOne $set (by ID) | 0.3ms | ~0.3us | | updateOne $inc (by ID) | 0.5ms | ~0.5us | | updateMany (~2K docs) x10 | 4.0ms | ~0.4ms/batch |

Delete

| Operation | Time | |-----------|------| | deleteOne x5,000 (one by one) | 2.5ms | | deleteMany (4,000 matched) | 0.8ms |

Watchers

| Scenario | Time | |----------|------| | insertOne x5,000 + 1 watcher | 2.2ms | | updateOne x1,000 + 10 watchers | 1.1ms |

Schema

| Scenario | Time (10,000 inserts) | Overhead | |----------|----------------------|----------| | Without schema | 1.9ms | - | | With schema (4 validated fields) | 7.7ms | ~4x |


API Reference

StateDB

new StateDB(name?: string, options?: StateDBOptions)

| Option | Type | Default | Description | |--------|------|---------|-------------| | persistent | boolean | false | Enable auto-save to storage | | storage | 'local' \| 'session' | 'local' | Storage backend | | debounce | number | 100 | Save debounce in ms |

| Method | Returns | Description | |--------|---------|-------------| | createCollection(name, options?) | Collection | Create or get collection | | getCollection(name) | Collection \| null | Get existing collection | | listCollections() | string[] | List collection names | | dropCollection(name) | boolean | Drop a collection | | drop() | boolean | Drop all collections | | save() | boolean | Force save to storage | | load() | boolean | Force load from storage | | flush() | boolean | Clear storage | | export() | ExportData | Export clean data (no __inc) | | dump() | DumpData | Export with metadata | | import(data) | boolean | Import clean data | | restore(dump) | boolean | Restore from dump |

Collection

db.createCollection(name: string, options?: CollectionOptions)

| Option | Type | Description | |--------|------|-------------| | schema | SchemaDefinition | Field validation rules | | capped | boolean | Enable capped collection | | max | number | Max documents (capped only) |

CRUD

| Method | Returns | Description | |--------|---------|-------------| | insertOne(doc) | Document \| null | Insert one document | | insertMany(docs) | Document[] | Insert multiple documents | | find(query?, projection?) | QueryResult | Find documents | | findOne(query?) | Document \| null | Find one document | | count(query?) | number | Count matching documents | | exists(query?) | boolean | Check if any match exists | | first() | Document \| null | First document in collection | | last() | Document \| null | Last document in collection | | updateOne(query, update) | UpdateResult | Update first match | | updateMany(query, update) | UpdateResult | Update all matches | | replaceOne(query, replacement) | UpdateResult | Replace entire document | | upsertOne(query, doc) | UpdateResult | Update or insert | | deleteOne(query) | DeleteResult | Delete first match | | deleteMany(query) | DeleteResult | Delete all matches | | drop() | DeleteResult | Remove all documents |

Indexes

| Method | Returns | Description | |--------|---------|-------------| | createIndex(fields, options?) | string | Create index (returns name) | | dropIndex(name) | boolean | Remove an index | | getIndexes() | string[] | List all index names |

Watchers

| Method | Returns | Description | |--------|---------|-------------| | watch(callback, options?) | number | Subscribe to changes (returns ID) | | unwatch(id?) | void | Unsubscribe (by ID, or all) |

Hooks

| Method | Returns | Description | |--------|---------|-------------| | pre(op, callback) | this | Add pre-hook (return false to abort) | | post(op, callback) | this | Add post-hook | | removePre(op, callback?) | this | Remove pre-hook(s) | | removePost(op, callback?) | this | Remove post-hook(s) |

QueryResult

Returned by find(). All methods except toArray(), map(), filter(), count(), first(), last(), and explain() are chainable.

| Method | Returns | Description | |--------|---------|-------------| | sort(spec) | this | Sort: { field: 1 } (asc) or { field: -1 } (desc) | | skip(n) | this | Skip first n results | | limit(n) | this | Limit to n results | | project(spec) | this | Include ({ name: 1 }) or exclude ({ password: 0 }) fields | | toArray() | Document[] | Execute and return results | | forEach(fn) | this | Iterate over results | | map(fn) | any[] | Map results | | filter(fn) | Document[] | Filter results | | count() | number | Number of results | | first() | Document \| null | First result | | last() | Document \| null | Last result | | explain() | ExplainResult | Query execution details |

TypeScript Types

All types are exported for use in your code:

import type {
  Document,
  QueryFilter,
  QueryOperators,
  UpdateSpec,
  UpdateResult,
  DeleteResult,
  SchemaDefinition,
  CollectionOptions,
  StateDBOptions,
  WatcherEvent,
  WatcherCallback,
  ProjectionSpec,
  SortSpec,
  ExplainResult,
} from '@phpdot/statedb';

Examples

The examples/ folder contains working demos:

  • basic.html — Minimal getting-started example
  • todo-demo.html — Todo app with watchers and live operation log
  • api-demo.html — Index and query explain demo with external API data

Run locally:

npx serve .
# Open http://localhost:3000/examples/todo-demo.html

Development

# Install
npm install

# Build (ESM + CJS + IIFE + types)
npm run build

# Run tests (133 tests)
npm test

# Run benchmarks
npm run bench

# Watch mode
npm run dev

Project Structure

src/
  index.ts          Entry point (exports)
  browser.ts        IIFE entry (attaches classes to StateDB global)
  StateDB.ts        Database class, persistence
  Collection.ts     Collection class, CRUD delegation
  Query.ts          Query engine, compiled matchers
  DbIndex.ts        Index (single + compound, unique)
  Schema.ts         Schema validation
  Watcher.ts        Reactive event system
  types.ts          TypeScript interfaces
  operations/
    insert.ts       insertOne, insertMany
    find.ts         find, findOne, QueryResult
    update.ts       updateOne, updateMany, operators
    delete.ts       deleteOne, deleteMany, drop
tests/              Vitest test files (133 tests)
benchmarks/         Performance benchmarks
examples/           HTML demos
dist/               Build output
  index.mjs         ESM
  index.cjs         CJS
  index.js          IIFE (non-minified)
  index.min.js      IIFE (minified, ~20KB)
  index.d.ts        TypeScript declarations
  index.d.mts       TypeScript declarations (ESM)

License

MIT - Omar Hamdan

GitHub | npm