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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@webqit/observer

v3.2.0

Published

A simple set of functions for intercepting and observing JavaScript objects and arrays.

Downloads

1,421

Readme

The Observer API

MotivationOverviewDocumentationPolyfillGetting InvolvedLicense

Observe and intercept operations on arbitrary JavaScript objects and arrays using a utility-first, general-purpose reactivity API! This API re-explores the unique design of the Object.observe() API and takes a stab at what could be a unifying API over related but disparate things like Object.observe(), Reflect APIs, and the "traps" API (proxy traps)!

Observer API is an upcoming proposal!

Motivation

Tracking mutations on JavaScript objects has historically relied on "object wrapping" techniques with ES6 Proxies, and on "property mangling" techniques with getters and setters. Besides how the first poses an object identity problem and the second, an interoperability problem, there is also much inflexibility in the programming model that each enables!

This is discussed extensively in the introductory blog post

We find a design precedent to object observability in the Object.observe() API, which at one time checked all the boxes and touched the very pain points we have today! The idea with the new Observer API is to re-explore that unique design with a more wholistic approach that considers the broader subject of Reactive Programming in JavaScript!

Status

  • Working implementation via a polyfill
  • Integral to the Quantum JS project
  • Actively developed
  • Open to contributions

An Overview

The Observer API is a set of utility functions - notably, the Observer.observe() and Observer.intercept() methods - for all things object observability.

Looking for [email protected]?

Method: Observer.observe()

Observe mutations on arbitrary objects or arrays!

// An object
const obj = {};
// Mtation observer on an object
const abortController = Observer.observe( obj, inspect );
// An array
const arr = [];
// Mtation observer on an array
const abortController = Observer.observe( arr, inspect );

Changes are delivered synchronously - as they happen.

// The change handler
function inspect( mutations ) {
    mutations.forEach( mutation => {
        console.log( mutation.type, mutation.key, mutation.value, mutation.oldValue );
    } );
}

--> Stop observing at any time by calling abort() on the returned abortController:

// Remove listener
abortController.abort();

└ And you can provide your own Abort Signal instance:

// Providing an AbortSignal
const abortController = new AbortController;
Observer.observe( obj, inspect, { signal: abortController.signal } );
// Abort at any time
abortController.abort();

--> Where listeners initiate nested observers (child observers), leverage "AbortSignal-cascading" to tie child observers to parent observer's lifecycle:

// Parent - 
const abortController = Observer.observe( obj, ( mutations, flags ) => {

    // Child
    Observer.observe( obj, inspect, { signal: flags.signal } ); // <<<---- AbortSignal-cascading

    // Child
    Observer.observe( obj, inspect, { signal: flags.signal } ); // <<<---- AbortSignal-cascading

} );

"Child" observers get automatically aborted at parent's "next turn", and at parent's own abortion!

--> Use the options.diff parameter to ignore mutation events whose current value is same as previous value:

// Parent - 
const abortController = Observer.observe( obj, mutations => {
  console.log( m.type, m.value, m.oldValue );
}, { diff: true } );
obj.property = 'Same value';
obj.property = 'Same value';

Observer is called only on the first update!

Concept: Mutation APIs

In addition to making literal operations, you can also programmatically mutate properties of an object using the Reflect-like set of operators; each operation will be reported by observers:

// A single "set" operation on an object
Observer.set( obj, 'prop0', 'value0' );
Observer.defineProperty( obj, 'prop1', { get: () => 'value1' } );
Observer.deleteProperty( obj, 'prop2' );
// A single "set" operation on an array
Observer.set( arr, 0, 'item0' ); // Array [ 'item0' ]
Observer.deleteProperty( arr, 0 ); // Array [ <1 empty slot> ]

In the polyfill, object observability doesn't work with literal operations. Beware non-reactive operations:

// Literal object operators
delete obj.prop0;
obj.prop3 = 'value3';
// Array methods
arr.push( 'item3' );
arr.pop();

--> Enable reactivity on specific properties with literal object accessors - using the Observer.accessorize() method:

// Accessorize all current enumerable properties
Observer.accessorize( obj );
// Accessorize specific properties (existing or new)
Observer.accessorize( obj, [ 'prop0', 'prop1', 'prop2' ] );

// Make reactive UPDATES
obj.prop0 = 'value0';
obj.prop1 = 'value1';
obj.prop2 = 'value2';
// Accessorize all current indexes
Observer.accessorize( arr );
// Accessorize specific indexes (existing or new)
Observer.accessorize( arr, [ 0, 1, 2 ] );

// Make reactive UPDATES
arr[ 0 ] = 'item0';
arr[ 1 ] = 'item1';
arr[ 2 ] = 'item2';

// Bonus reactivity with array methods that re-index existing items
arr.unshift( 'new-item0' );
arr.shift();

In the polyfill, object observability doesn't work with literal operations. Beware non-reactive operations:

// The delete operator and object properties that haven't been accessorized
delete obj.prop0;
obj.prop3 = 'value3';
// Array methods that do not re-index existing items
arr.push( 'item0' );
arr.pop();

--> Enable reactivity on arbitray properties with Proxies - using the Observer.proxy() method:

// Obtain a reactive Proxy for an object
const $obj = Observer.proxy( obj );

// Make reactive operations
$obj.prop1 = 'value1';
$obj.prop4 = 'value4';
$obj.prop8 = 'value8';

// With the delete operator
delete $obj.prop0;
// Obtain a reactive Proxy for an array
const $arr = Observer.proxy( arr );

// Make reactive operations
$arr[ 0 ] = 'item0';
$arr[ 1 ] = 'item1';
$arr[ 2 ] = 'item2';

// With an instance method
$arr.push( 'item3' );

And no problem if you end up nesting the approaches.

// 'value1'-->obj
Observer.accessorize( obj, [ 'prop0', 'prop1', 'prop2', ] );
obj.prop1 = 'value1';

// 'value1'-->$obj-->obj
let $obj = Observer.proxy( obj );
$obj.prop1 = 'value1';

// 'value1'-->set()-->$obj-->obj
Observer.set( $obj, 'prop1', 'value1' );

--> "Restore" accessorized properties to their normal state by calling the unaccessorize() method:

Observer.unaccessorize( obj, [ 'prop1', 'prop6', 'prop10' ] );

--> "Reproduce" original objects from Proxies obtained via Observer.proxy() by calling the unproxy() method:

obj = Observer.unproxy( $obj );

Concept: Paths

Observe "a property" at a path in an object tree:

const obj = {
  level1: {
    level2: 'level2-value',
  },
};
const path = Observer.path( 'level1', 'level2' );
Observer.observe( obj, path, m => {
  console.log( m.type, m.path, m.value, m.isUpdate );
} );
Observer.set( obj.level1, 'level2', 'level2-new-value' );

| type | path | value | isUpdate | | ---- | ---- | ----- | -------- | | set | [ level1, level2, ] | level2-new-value | true |

And the initial tree structure can be whatever:

// A tree structure that is yet to be built
const obj = {};
const path = Observer.path( 'level1', 'level2', 'level3', 'level4' );
Observer.observe( obj, path, m => {
  console.log( m.type, m.path, m.value, m.isUpdate );
} );

Now, any operation that changes what "the value" at the path resolves to - either by tree extension or tree truncation - will fire our listener:

Observer.set( obj, 'level1', { level2: {}, } );

| type | path | value | isUpdate | | ---- | ---- | ----- | -------- | | set | [ level1, level2, level3, level4, ] | undefined | false |

Meanwhile, this next one completes the tree, and the listener reports a value at its observed path:

Observer.set( obj.level1, 'level2', { level3: { level4: 'level4-value', }, } );

| type | path | value | isUpdate | | ---- | ---- | ----- | -------- | | set | [ level1, level2, level3, level4, ] | level4-value | false |

--> Use the event's context property to inspect the parent event if you were to find the exact point at which mutation happened in the path in an audit trail:

let context = m.context;
console.log(context);

And up again one level until the root event:

let parentContext = context.context;
console.log(parentContext);

--> Observe trees that are built asynchronously! Where a promise is encountered along the path, further access is paused until promise resolves:

Observer.set( obj.level1, 'level2', Promise.resolve( { level3: { level4: 'level4-value', }, } ) );

Concept: Batch Mutations

Make multiple mutations at a go, and they'll be correctly delivered as a batch to observers!

// Batch operations on an object
Observer.set( obj, {
    prop0: 'value0',
    prop1: 'value1',
    prop2: 'value2',
} );
Observer.defineProperties( obj, {
    prop0: { value: 'value0' },
    prop1: { value: 'value1' },
    prop2: { get: () => 'value2' },
} );
Observer.deleteProperties( obj, [ 'prop0', 'prop1', 'prop2' ] );
// Batch operations on an array
Observer.set( arr, {
    '0': 'item0',
    '1': 'item1',
    '2': 'item2',
} );
Object.proxy( arr ).push( 'item3', 'item4', 'item5', );
Object.proxy( arr ).unshift( 'new-item0' );
Object.proxy( arr ).splice( 0 );

--> Use the Observer.batch() to batch multiple arbitrary mutations - whether related or not:

Observer.batch( arr, async () => {
    Observer.set( arr, 0, 'item0' ); // Array [ 'item0' ]
    await somePromise();
    Observer.set( arr, 2, 'item2' ); // Array [ 'item0', <1 empty slot>, 'item2' ]
} );

Method calls on a proxied instance - e.g. Object.proxy( arr ).splice( 0 ) - also follow this strategy.

Method: Observer.intercept()

Intercept operations on any object or array before they happen! This helps you extend standard operations on an object - Observer.set(), Observer.deleteProperty(), etc - using Proxy-like traps.

Below, we intercept all "set" operations for an HTTP URL then transform it to an HTTPS URL.

const setTrap = ( operation, previous, next ) => {
    if ( operation.key === 'url' && operation.value.startsWith( 'http:' ) ) {
        operation.value = operation.value.replace( 'http:', 'https:' );
    }
    return next();
};
Observer.intercept( obj, 'set', setTrap );

Now, only the first of the following will fly as-is.

// Not transformed
Observer.set( obj, 'url', 'https://webqit.io' );

// Transformed
Observer.set( obj, 'url', 'http://webqit.io' );

And below, we intercept all "get" operations for a certain value to trigger a network fetch behind the scenes.

const getTrap = ( operation, previous, next ) => {
    if ( operation.key === 'token' ) {
        return next( fetch( tokenUrl ) );
    }
    return next();
};
Observer.intercept( obj, 'get', getTrap );

And all of that can go into one "traps" object:

Observer.intercept( obj, {
    get: getTrap,
    set: setTrap,
    deleteProperty: deletePropertyTrap,
    defineProperty: definePropertyTrap,
    ownKeys: ownKeysTrap,
    has: hasTrap,
    // etc
} );

Documentation

Visit the docs for full details - including Reflect API Supersets, Timing and Batching, API Reference, etc.

The Polyfill

The Observer API is being developed as something to be used today - via a polyfill. The polyfill features all of what's documented - with limitations in the area of making mutations: you can only make mutations using the Mutation APIs.

<script src="https://unpkg.com/@webqit/observer/dist/main.js"></script>

4.4 kB min + gz | 13.9 KB min

// Obtain the APIs
const Observer = window.webqit.Observer;
npm i @webqit/observer
// Import
import Observer from '@webqit/observer';;

Getting Involved

All forms of contributions are welcome at this time. For example, implementation details are all up for discussion. And here are specific links:

License

MIT.