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

@gullerya/object-observer

v7.0.0

Published

object-observer utility provides simple means to (deeply) observe specified object/array changes; implemented via native Proxy; changes delivered in a synchronous way

Downloads

3,061

Readme

npm GitHub

Quality pipeline Codecov Codacy

object-observer

object-observer provides a deep observation of a changes performed on an object/array graph.

For a high-level overview of how the library is put together see the architecture doc.

Main aspects and features:

  • implemented via native Proxy (revokable)
  • observation is 'deep', yielding changes from a sub-graphs too
  • nested objects of the observable graph are observables too
  • changes delivered in a synchronous way by default, asynchronous delivery is optionally available as per Observable configuration; more details here
  • observed changes may optionally be filtered via the Filter class; more details here
  • original objects are cloned while turned into Observables
    • circular references are nullified in the clone
  • observed mutations on plain objects:
    • property assignment (obj.x = v, obj['x'] = v)
    • property deletion (delete obj.x)
  • array specifics:
    • indexed assignment (arr[i] = v), length assignment, and delete arr[i] are observed (same traps as for plain objects)
    • intrinsic Array mutation methods observed: pop, push, shift, unshift, reverse, sort, fill, splice, copyWithin
    • massive mutations delivered in a single callback, usually as an array of atomic changes
  • typed array specifics:
    • indexed assignment (ta[i] = v) is observed (same trap as for plain objects); length is fixed
    • intrinsic TypedArray mutation methods observed: reverse, sort, fill, set, copyWithin
    • massive mutations delivered in a single callback, usually as an array of atomic changes
  • intrinsic mutation methods of Map, WeakMap, Set, WeakSet (set, delete) etc are not observed (see this issue for more details)
  • following host objects (and their extensions) are skipped from cloning / turning into observables: Date

Supported: CHROMElast 2 versions | FIREFOXlast 2 versions | EDGElast 2 versions | SAFARIlast 2 versions | NODE JS 24.15.0+

Performance report can be found here.

Changelog is here.

Preview

For a preview/playground you are welcome to:

  • CodePen - Observable.from() flavor
  • CodePen - new ObjectObserver() flavor

Install

Use regular npm install @gullerya/object-observer --save-prod to use the library from your local environment.

ES module:

import { Observable } from '@gullerya/object-observer';

CJS flavor:

const { Observable } = require('@gullerya/object-observer');

Huge thanks to seidelmartin providing the CJS build while greatly improving the build code overall along the way!

CDN (most suggested, when possible):

import { Observable } from 'https://libs.gullerya.com/object-observer/x.y.z/object-observer.min.js';

Replace the x.y.z with the desired version, one of the listed in the changelog.

CDN features:

  • security:
    • HTTPS only
    • intergrity checksums for SRI
  • performance
    • highly available (with many geo spread edges)
    • agressive caching setup

Full details about CDN usage and example are found here.

API

Library implements Observable API as it is defined here.

There is also a 'DOM-like' API flavor - constructable ObjectObserver. This API is resonating with DOM's MutationObserver, ResizeObserver etc from the syntax perspective. Under the hood it uses the same Observable mechanics. Read docs about this API flavor here.

object-observer is cross-instance operable. Observables created by different instances of the library will still be detected correctly as such and handled correctly by any of the instances.

Security

Security policy is described here. If/when any concern raised, please follow the process.

Examples

Objects
const
    order = { type: 'book', pid: 102, ammount: 5, remark: 'remove me' },
    observableOrder = Observable.from(order);

Observable.observe(observableOrder, changes => {
    changes.forEach(change => {
        console.log(change);
    });
});


observableOrder.ammount = 7;
//  { type: 'update', path: ['ammount'], value: 7, oldValue: 5, object: observableOrder }


observableOrder.address = {
    street: 'Str 75',
    apt: 29
};
//  { type: "insert", path: ['address'], value: { ... }, object: observableOrder }


observableOrder.address.apt = 30;
//  { type: "update", path: ['address','apt'], value: 30, oldValue: 29, object: observableOrder.address }


delete observableOrder.remark;
//  { type: "delete", path: ['remark'], oldValue: 'remove me', object: observableOrder }

Object.assign(observableOrder, { amount: 1, remark: 'less is more' }, { async: true });
//  - by default the changes below would be delivered in a separate callback
//  - due to async use, they are delivered as a batch in a single callback
//  { type: 'update', path: ['ammount'], value: 1, oldValue: 7, object: observableOrder }
//  { type: 'insert', path: ['remark'], value: 'less is more', object: observableOrder }
Arrays
let a = [ 1, 2, 3, 4, 5 ],
    observableA = Observable.from(a);

Observable.observe(observableA, changes => {
    changes.forEach(change => {
        console.log(change);
    });
});


//  observableA = [ 1, 2, 3, 4, 5 ]
observableA.pop();
//  { type: 'delete', path: [4], value: undefined, oldValue: 5, object: observableA }


//  now observableA = [ 1, 2, 3, 4 ]
//  following operation will cause a single callback to the observer with an array of 2 changes in it)
observableA.push('a', 'b');
//  { type: 'insert', path: [4], value: 'a', oldValue: undefined, object: observableA }
//  { type: 'insert', path: [5], value: 'b', oldValue: undefined, object: observableA }


//  now observableA = [1, 2, 3, 4, 'a', 'b']
observableA.shift();
//  { type: 'delete', path: [0] value: undefined, oldValue: 1, object: observableA }


//  now observableA = [ 2, 3, 4, 'a', 'b' ]
//  following operation will cause a single callback to the observer with an array of 2 changes in it)
observableA.unshift('x', 'y');
//  { type: 'insert', path: [0], value: 'x', oldValue: undefined, object: observableA }
//  { type: 'insert', path: [1], value: 'y', oldValue: undefined, object: observableA }


//  now observableA = [ 2, 3, 4, 'a', 'b' ]
observableA.reverse();
//  { type: 'reverse', path: [], object: observableA } (see below and exampe of this event for nested array)


//  now observableA = [ 'b', 'a', 4, 3, 2 ]
observableA.sort();
//  { type: 'shuffle', path: [], object: observableA } (see below and exampe of this event for nested array)


//  observableA = [ 2, 3, 4, 'a', 'b' ]
observableA.fill(0, 0, 1);
//  { type: 'update', path: [0], value: 0, oldValue: 2, object: observableA }


//  observableA = [ 0, 3, 4, 'a', 'b' ]
//  the following operation will cause a single callback to the observer with an array of 2 changes in it)
observableA.splice(0, 1, 'x', 'y');
//  { type: 'update', path: [0], value: 'x', oldValue: 0, object: observableA }
//  { type: 'insert', path: [1], value: 'y', oldValue: undefined, object: observableA }


let customer = { orders: [ ... ] },
    oCustomer = Observable.from(customer);

//  sorting the orders array, pay attention to the path in the event
oCustomer.orders.sort();
//  { type: 'shuffle', path: ['orders'], object: oCustomer.orders }


oCustomer.orders.reverse();
//  { type: 'reverse', path: ['orders'], object: oCustomer.orders }

Arrays notes: Some of array operations are effectively moving/reindexing the whole array (shift, unshift, splice, reverse, sort). In cases of massive changes touching presumably the whole array I took a pessimistic approach with a special non-detailed events: 'reverse' for reverse, 'shuffle' for sort. The rest of these methods I'm handling in an optimistic way delivering the changes that are directly related to the method invocation, while leaving out the implicit outcomes like reindexing of the rest of the Array.

Observation options

object-observer allows to filter the events delivered to each callback/listener via an optional filters array — each element MUST be a Filter instance. Multiple filters compose as logical AND (each filter narrows the result).

In the examples below assume that callback = changes => {...}.

import { Observable, Filter } from '@gullerya/object-observer';

let user = {
        firstName: 'Aya',
        lastName: 'Guller',
        address: {
            city: 'of mountaineers',
            street: 'of the top ridges',
            block: 123,
            extra: {
                data: {}
            }
        }
    },
    oUser = Observable.from(user);

//  exact paths
//
//  going to observe ONLY the changes of 'firstName' or 'address.city'
Observable.observe(oUser, callback, { filters: [Filter.exactPaths(['firstName', 'address.city'])] });

//  direct children of 'address' (city, street, block, extra) — and REVERSE/SHUFFLE at 'address'
Observable.observe(oUser, callback, { filters: [Filter.directChildrenOf('address')] });

//  all changes from 'address' and deeper
Observable.observe(oUser, callback, { filters: [Filter.pathsStartWith('address')] });

//  custom predicate
Observable.observe(oUser, callback, { filters: [Filter.custom(cs => cs.filter(c => c.type === 'update'))] });
Validators

object-observer allows to veto mutations before they are applied via an optional validators array supplied to Observable.from. Each element MUST be a Validator instance. Validators are attached at tree creation — they apply to the whole observable graph, not per observer.

A validator receives the prospective Change[] (with rooted paths and the raw, un-observified new value) BEFORE the underlying target is mutated. Throwing from a validator aborts the mutation: the target is left unchanged and observers are not invoked. Multiple validators compose as logical AND with short-circuit — the first throw stops the chain.

import { Observable, Validator } from '@gullerya/object-observer';

const immutableMarker = Validator.custom(changes => {
    for (const c of changes) {
        if (c.oldValue === 'immutable') {
            throw new Error(`change at '${c.pathAsString}' rejected: oldValue is immutable`);
        }
    }
});

const oo = Observable.from({ a: 'immutable' }, { validators: [immutableMarker] });
oo.a = 'other';    //  throws; oo.a still === 'immutable'; no observer fired

Not yet supported: validators do not currently run for Array.prototype.splice, Array.prototype.fill, and Array.prototype.copyWithin (and their TypedArray counterparts where applicable). These mutations proceed as before without validator consultation. Support is planned for a subsequent release.