Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/webqit/observer
A simple set of functions for intercepting and observing JavaScript objects and arrays.
https://github.com/webqit/observer
array-observe events interception object-observe observer reflection
Last synced: about 1 month ago
JSON representation
A simple set of functions for intercepting and observing JavaScript objects and arrays.
- Host: GitHub
- URL: https://github.com/webqit/observer
- Owner: webqit
- License: mit
- Created: 2020-03-23T23:09:42.000Z (almost 5 years ago)
- Default Branch: master
- Last Pushed: 2024-08-02T06:51:05.000Z (5 months ago)
- Last Synced: 2024-11-19T12:52:29.867Z (about 1 month ago)
- Topics: array-observe, events, interception, object-observe, observer, reflection
- Language: JavaScript
- Homepage:
- Size: 982 KB
- Stars: 21
- Watchers: 3
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# The Observer API
**[Motivation](#motivation) • [Overview](#an-overview) • [Documentation](#documentation) • [Polyfill](#the-polyfill) • [Getting Involved](#getting-involved) • [License](#license)**
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()`](https://web.dev/es7-observe/) API and takes a stab at what could be **a unifying API** over *related but disparate* things like `Object.observe()`, [Reflect](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), and on "property mangling" techniques with [getters and setters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). 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](https://dev.to/oxharris/re-exploring-reactivity-and-introducing-the-observer-api-and-reflex-functions-4h70)
We find a design precedent to object observability in the [`Object.observe()`](https://web.dev/es7-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](https://github.com/webqit/quantum-js)
+ 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.
This is documentation for [email protected]
Looking for [`[email protected]`](https://github.com/webqit/observer/tree/v1.7.6)?
### Method: `Observer.observe()`Observe mutations on arbitrary objects or arrays!
```js
// An object
const obj = {};
// Mtation observer on an object
const abortController = Observer.observe( obj, inspect );
``````js
// An array
const arr = [];
// Mtation observer on an array
const abortController = Observer.observe( arr, inspect );
```└ *Changes are delivered [**synchronously**](https://github.com/webqit/observer/wiki/#timing-and-batching) - as they happen.*
```js
// 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*:
```js
// Remove listener
abortController.abort();
```└ And you can provide your own [Abort Signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) instance:
```js
// Providing an AbortSignal
const abortController = new AbortController;
Observer.observe( obj, inspect, { signal: abortController.signal } );
``````js
// 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:
```js
// 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:
```js
// Parent -
const abortController = Observer.observe( obj, mutations => {
console.log( m.type, m.value, m.oldValue );
}, { diff: true } );
``````js
obj.property = 'Same value';
``````js
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect#static_methods)-like* set of operators; each operation will be reported by observers:
```js
// A single "set" operation on an object
Observer.set( obj, 'prop0', 'value0' );
Observer.defineProperty( obj, 'prop1', { get: () => 'value1' } );
Observer.deleteProperty( obj, 'prop2' );
``````js
// A single "set" operation on an array
Observer.set( arr, 0, 'item0' ); // Array [ 'item0' ]
Observer.deleteProperty( arr, 0 ); // Array [ <1 empty slot> ]
```Polyfill limitations
In the polyfill, object observability doesn't work with literal operations. **Beware non-reactive operations**:
```js
// Literal object operators
delete obj.prop0;
obj.prop3 = 'value3';
``````js
// Array methods
arr.push( 'item3' );
arr.pop();
```**-->** Enable reactivity on *specific* properties with literal *object accessors* - using the `Observer.accessorize()` method:
```js
// 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';
``````js
// 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();
```Polyfill limitations
In the polyfill, object observability doesn't work with literal operations. **Beware non-reactive operations**:
```js
// The delete operator and object properties that haven't been accessorized
delete obj.prop0;
obj.prop3 = 'value3';
``````js
// 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:
```js
// 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;
``````js
// 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.*
```js
// '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:
```js
Observer.unaccessorize( obj, [ 'prop1', 'prop6', 'prop10' ] );
```**-->** "Reproduce" original objects from Proxies obtained via `Observer.proxy()` by calling the `unproxy()` method:
```js
obj = Observer.unproxy( $obj );
```#### Concept: *Paths*
Observe "a property" at a path in an object tree:
```js
const obj = {
level1: {
level2: 'level2-value',
},
};
``````js
const path = Observer.path( 'level1', 'level2' );
Observer.observe( obj, path, m => {
console.log( m.type, m.path, m.value, m.isUpdate );
} );
``````js
Observer.set( obj.level1, 'level2', 'level2-new-value' );
```Console
| type | path | value | isUpdate |
| ---- | ---- | ----- | -------- |
| `set` | [ `level1`, `level2`, ] | `level2-new-value` | `true` |└ *And the initial tree structure can be whatever*:
```js
// A tree structure that is yet to be built
const obj = {};
``````js
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*:
```js
Observer.set( obj, 'level1', { level2: {}, } );
```Console
| 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*:
```js
Observer.set( obj.level1, 'level2', { level3: { level4: 'level4-value', }, } );
```Console
| 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:
```js
let context = m.context;
console.log(context);
```└ *And up again one level until the root event*:
```js
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:
```js
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!
```js
// 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' ] );
``````js
// 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:
```js
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.*
```js
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.*
```js
// 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.*
```js
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:*
```js
Observer.intercept( obj, {
get: getTrap,
set: setTrap,
deleteProperty: deletePropertyTrap,
defineProperty: definePropertyTrap,
ownKeys: ownKeysTrap,
has: hasTrap,
// etc
} );
```## Documentation
Visit the [docs](https://github.com/webqit/observer/wiki) for full details - including [Reflect API Supersets](https://github.com/webqit/observer/wiki#featuring-reflect-api-supersets), [Timing and Batching](https://github.com/webqit/observer/wiki#timing-and-batching), [API Reference](https://github.com/webqit/observer/wiki#putting-it-all-together), 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](#concept-mutation-apis).
Load from a CDN
```html
```
> `4.4` kB min + gz | `13.9` KB min [↗](https://bundlephobia.com/package/@webqit/observer)
```js
// Obtain the APIs
const Observer = window.webqit.Observer;
```Install from NPM
```bash
npm i @webqit/observer
``````js
// 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:
+ [Project](https://github.com/webqit/observer)
+ [Documentation](https://github.com/webqit/observer/wiki)
+ [Discusions](https://github.com/webqit/observer/discussions)
+ [Issues](https://github.com/webqit/observer/issues)## License
MIT.