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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@zdr-tools/zdr-entities

v1.8.0

Published

Reactive entity framework with properties that automatically track changes and notify when they update.

Readme

@zdr-tools/zdr-entities

Reactive entity framework with properties that automatically track changes and notify when they update.

Installation

npm install @zdr-tools/zdr-entities

Usage

This package lets you build reactive data models where properties automatically notify listeners when they change. Think of it like a more powerful version of React state that works outside of React.

Basic Property Example

import { PropEventBroker, AdvancedEventEmitter } from '@zdr-tools/zdr-entities';

const emitter = new AdvancedEventEmitter();
const userName = new PropEventBroker(emitter, 'John');

// Listen for changes
userName.register(({ value }) => {
  console.log('Name changed to:', value);
});

userName.set('Jane'); // Logs: "Name changed to: Jane"

Building Entities

import { Entity, textMinLength } from '@zdr-tools/zdr-entities';

class User extends Entity {
  private _name = this.createPropEventBroker('name', '', {
    validators: [textMinLength(2)]
  });
  private _email = this.createPropEventBroker('email', '');
  private _age = this.createPropEventBroker('age', 0);

  constructor(id?: string) {
    super(id);
  }

  getEntityName() { return 'User'; }

  get name() { return this._name; }
  get email() { return this._email; }
  get age() { return this._age; }
}

const user = new User();
user.name.set('John');
user.email.set('[email protected]');
user.age.set(25);

// Check if entity has unsaved changes
console.log(user.isChanged()); // true

// Save changes
user.commit();
console.log(user.isChanged()); // false

Core Classes

EventBroker<T>

Manages events for a specific type of data. This is the foundation everything else builds on.

const broker = new EventBroker<string>(emitter);
const unregister = broker.register(data => console.log(data));
broker.emit('hello');
unregister();

| Method | Parameters | Return Type | Description | |--------|------------|-------------|-------------| | register | callback: (data: T) => void | () => void | Listen for events, ensures callback only registered once, returns unregister function | | registerOnce | callback: (data: T) => void | () => void | Listen once, auto-unregisters after first call | | emit | data: T | void | Send event to all listeners | | unRegister | callback: (data: T) => void | void | Remove specific callback |

PropEventBroker<T>

A property that holds a value and notifies when it changes. Can include validation.

const age = new PropEventBroker(emitter, 25, {
  validators: [{
    validator: (val: number) => val >= 0 ? undefined : { result: 'Must be positive' }
  }]
});

age.set(-5); // Won't change due to validation
console.log(age.isValid()); // false

| Method | Parameters | Return Type | Description | |--------|------------|-------------|-------------| | get | - | T | Get current value | | set | value: T, options?: PropSetOptions | boolean | Set new value, returns true if changed | | isValid | - | boolean | Check if value passes validation | | getViolations | - | V[] \| undefined | Get validation errors | | serialize | - | string \| undefined | Serialize value to string | | getMetadataValue | key: string | unknown \| undefined | Get validator metadata |

PropSetOptions:

| Property | Type | Optional | Description | |----------|------|----------|-------------| | silent | boolean | Yes | Don't emit change event when true |

ReadablePropEventBroker<T>

Same as PropEventBroker but read-only (no set method).

| Method | Parameters | Return Type | Description | |--------|------------|-------------|-------------| | get | - | T | Get current value | | isValid | - | boolean | Check if value passes validation | | getViolations | - | V[] \| undefined | Get validation errors | | serialize | - | string \| undefined | Serialize value to string | | getMetadataValue | key: string | unknown \| undefined | Get validator metadata |

RestorablePropEventBroker<T>

Property that can track changes and restore to previous values.

const email = new RestorablePropEventBroker(emitter, '[email protected]');
email.commit(); // Save current value

email.set('[email protected]');
console.log(email.isChanged()); // true

email.restore(); // Go back to saved value
console.log(email.get()); // '[email protected]'

| Method | Parameters | Return Type | Description | |--------|------------|-------------|-------------| | get | - | T | Get current value | | set | value: T, options?: RestorablePropSetOptions | boolean | Set new value, returns true if changed | | isValid | - | boolean | Check if value passes validation | | getViolations | - | V[] \| undefined | Get validation errors | | isChanged | - | boolean | Check if different from saved value | | commit | - | void | Save current value | | restore | - | void | Go back to saved value | | resetToDefault | - | void | Reset to initial default value | | getStoredValue | - | T | Get committed value |

RestorablePropSetOptions extends PropSetOptions:

| Property | Type | Optional | Description | |----------|------|----------|-------------| | silent | boolean | Yes | Don't emit change event when true | | commit | boolean | Yes | Set value and immediately commit it |

Entity

Base class for creating reactive entities with multiple properties and collections.

class Product extends Entity {
  private _name = this.createPropEventBroker('name', '');
  private _price = this.createPropEventBroker('price', 0);

  constructor(id?: string) {
    super(id);
  }

  getEntityName() { return 'Product'; }
  get name() { return this._name; }
  get price() { return this._price; }
}

| Method | Parameters | Return Type | Description | |--------|------------|-------------|-------------| | getId | - | string | Get entity ID | | setId | newId: string | void | Set entity ID | | checkId | id: string | boolean | Check if entity matches ID | | isNew | - | boolean | Check if entity has no real ID yet | | isChanged | - | boolean | Check if any properties changed | | commit | - | void | Save all property changes | | restore | - | void | Restore all properties | | getChangedProps | - | IEntityChangedProps[] | Get list of what changed | | getChangeablePropsCount | - | number | Get count of changeable properties |

Protected Methods:

| Method | Parameters | Return Type | Description | |--------|------------|-------------|-------------| | createPropEventBroker | propName: string, initialValue: T, options?: IPropEventBrokerOptions<T, V> | IRestorablePropEventBroker<T, V> | Create a reactive property | | createEntityCollection | name: string, initialItems: T[] | IEntityCollection<T> | Create an unordered collection | | createOrderedEntityCollection | name: string, initialItems: T[] | IOrderedEntityCollection<T> | Create an ordered collection |

Events:

| Property | Type | Description | |----------|------|-------------| | propertyChanged | IEventBroker<IPropertyChangedEventData> | Fires when any property changes | | idChanged | IEventBroker<IdChangedEventData> | Fires when entity ID changes |

EntityCollection<T>

Holds multiple entities and notifies when items are added/removed.

const users = new EntityCollection<User>('Users', []);

users.itemsAdded.register(({ items }) => {
  console.log('Added users:', items.length);
});

users.addItems([user1, user2]);
const allUsers = users.getItems();

| Method | Parameters | Return Type | Description | |--------|------------|-------------|-------------| | addItems | items: T[], options?: AddItemsOptions | void | Add multiple entities | | getItems | - | T[] | Get all entities | | getItem | id: string | T \| undefined | Get entity by ID | | removeItem | id: string | T | Remove and return entity | | isEmpty | - | boolean | Check if empty | | removeAllItems | - | void | Clear all items | | replaceAllItems | items: T[] | void | Replace all items | | getNewItems | - | T[] | Get uncommitted items | | getOldItems | - | T[] | Get committed items |

AddItemsOptions:

| Property | Type | Optional | Description | |----------|------|----------|-------------| | silent | boolean | Yes | Don't emit events when true |

Events:

| Property | Type | Description | |----------|------|-------------| | itemsAdded | IEventBroker<IItemsEventData<T>> | When items are added | | itemRemoved | IEventBroker<IItemEventData<T>> | When item is removed | | itemChanged | IEventBroker<IItemEventData<T>> | When item properties change | | itemIdChanged | IEventBroker<IItemIdChangedEventData<T>> | When item ID changes | | collectionChanged | IEventBroker | When collection changes |

OrderedEntityCollection<T>

Like EntityCollection but maintains explicit ordering.

const orderedUsers = new OrderedEntityCollection<User>('Users', []);
orderedUsers.addItems([user1, user2]);
orderedUsers.moveItem(user1.getId(), 1); // Move to second position

const firstUser = orderedUsers.getItemAt(0);

Additional Methods (extends EntityCollection):

| Method | Parameters | Return Type | Description | |--------|------------|-------------|-------------| | moveItem | itemId: string, newIndex: number | void | Change item position | | getItemAt | index: number | T \| undefined | Get item by position | | getItemOrderIndex | itemId: string | number | Get item's index | | sort | compareFn: (a: T, b: T) => number | void | Sort the collection | | getPagedItems | page: Paging | T[] | Get paginated items | | addItems | items: T[], options?: OrderedAddItemsOptions | void | Add items at specific position |

OrderedAddItemsOptions extends AddItemsOptions:

| Property | Type | Optional | Description | |----------|------|----------|-------------| | silent | boolean | Yes | Don't emit events when true | | startIndex | number | Yes | Position to start adding items |

Additional Properties:

| Property | Type | Description | |----------|------|-------------| | order | IRestorablePropEventBroker<string[]> | Reactive property managing item order |

Built-in Validators

| Validator | Parameters | Description | |-----------|------------|-------------| | textMinLength | minLength: number | Ensures text is at least a certain length | | textMaxLength | maxLength: number | Ensures text doesn't exceed a length | | isDefined | - | Ensures value is not null or undefined |

const name = new PropEventBroker(emitter, '', {
  validators: [
    isDefined(),
    textMinLength(2),
    textMaxLength(50)
  ]
});

Examples

Complete User Management

class UserList extends Entity {
  private _users = this.createEntityCollection('users', []);
  private _searchTerm = this.createPropEventBroker('searchTerm', '');

  constructor() {
    super();
  }

  getEntityName() { return 'UserList'; }
  get users() { return this._users; }
  get searchTerm() { return this._searchTerm; }

  addUser(name: string, email: string) {
    const user = new User();
    user.name.set(name);
    user.email.set(email);
    this.users.addItems([user]);
  }

  getFilteredUsers() {
    const term = this.searchTerm.get().toLowerCase();
    return this.users.getItems().filter(user =>
      user.name.get().toLowerCase().includes(term)
    );
  }
}

const userList = new UserList();

// Listen for any changes
userList.propertyChanged.register(() => {
  console.log('Something in the user list changed');
});

userList.addUser('John', '[email protected]');
userList.searchTerm.set('jo'); // Triggers change event

Testing

All classes have comprehensive fake implementations for testing. First, install either @zdr-tools/zdr-jest-setup or @zdr-tools/zdr-vitest-setup.

import { FakeEntityBuilder, FakesFactory } from '@zdr-tools/zdr-entities/fakes';

// Using builder pattern
const fakeUser = new FakeEntityBuilder()
  .withId('test-user')
  .withIsChanged(false)
  .build();

// Using factory for convenience
const fakeProp = FakesFactory.createPropEventBroker('initial value');

expect(fakeUser.getId()).toBe('test-user');
expect(fakeProp.get()).toBe('initial value');

For comprehensive testing documentation including all fakes, builders, and examples, see Testing.md.

License

MIT