Skip to content

webqit/observer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Observer API – Mutation-Based Reactivity for JavaScript

npm version npm downloads bundle 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 retired Object.observe() API and unifies that with the rest of JavaScript's metaprogramming APIs: Proxies, Reflect, Object!

Observer comes as one little API for all things object observability. (Only ~5.8KiB min|zip)

const obj = {};

// Observe all property changes
Observer.observe(obj, (mutations) => {
  mutations.forEach(mutation => {
    console.log(`${mutation.type}: ${mutation.key} = ${mutation.value}`);
  });
});

Observer.set(obj, 'count', 5);
Observer.deleteProperty(obj, 'oldProp');

Tip

Reactivity is anchored on its programmtic APIs like .set(), .deleteProperty(), but you also get reactivity over literal JavaScript operations β€” obj.prop = value, delete obj.prop, etc. β€” by means of the accessorize() and proxy() methods covered just ahead.

For full-fledged Imperative Reactive Programming, you want to see the Quantum JS project.


Looking for [email protected]?

This documentation is for [email protected]. For the previous version, see [email protected].

Table of Contents

Why Observer?

JavaScript is inherently a mutable language but lacks a built-in way to observe said mutations. When you do obj.prop = value or delete obj.prop, there's no mechanism to detect those changes.

The Problem:

const state = { count: 0, items: [] };

// No way to observe/intercept these mutations in JavaScript
state.count = 5;
state.items.push('new item');
delete state.oldProp;

// No way to detect these changes

This limitation in the language has long created a blindspot β€” and a weakness β€” for reactive systems. Consequently:

  • reactive frameworks (like React, Vue) learned to forbid mutability

  • immutability became the default workaround. You don't mutate, you create a new object each time:

    state = { ...state, count: 6 };
    state = { ...state, count: 7 };
    state = { ...state, count: 8 };
    state = { ...state, items: [...state.items, 'new item 1'] };
    state = { ...state, items: [...state.items, 'new item 2'] };
    state = { ...state, items: [...state.items, 'new item 3'] };

    Because this is generally hard to follow, frameworks typically enforce immutability by means of strong design constraints.

    Outside of a framework, you get standalone immutability libraries (like Immer, or Immutable.js back in the day) that as well try to simulate an immutable world, where data is never changed, only replaced.

  • mutation gets a bad rap

Using the Observer API:

By enabling observability at the object/array level, the Observer API effectively solves reactivity for a mutable world. The Result is mutation-based reactivity as a first-class concept in JavaScript. Consequently:

  • you are able to weild the full power of mutability in programming to your advantage
  • you are able to make sense of a mutable world β€” and integrate with it β€” rather than struggle with it

Quick Start

Install from NPM or include from a CDN.

Installation

npm install @webqit/observer
import Observer from '@webqit/observer';

CDN

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

Basic Usage

import Observer from '@webqit/observer';

const user = { name: 'John', items: [] };

// Watch for changes
const controller = Observer.observe(user, (mutations) => {
  mutations.forEach(mutation => {
    console.log(`Changed ${mutation.key}: ${mutation.oldValue} β†’ ${mutation.value}`);
  });
});

Observer.set(user, 'name', 'Jane');
Observer.set(user, 'age', 26);

// Stop watching
controller.abort();

Working with Arrays

const items = ['apple', 'banana'];

Observer.observe(items, (mutations) => {
  console.log('Array changed:', mutations);
});

// Use programmatic APIs for mutations
Observer.set(items, 0, 'grape');
Observer.set(items, 2, 'orange');

// Reactive method calls
Observer.apply(items.push, items, ['new item']);
Observer.proxy(state.items).push('new item')

Intercepting Operations

// Transform values before they're set
Observer.intercept(user, 'set', (operation, previous, next) => {
  if (operation.key === 'email') {
    operation.value = operation.value.toLowerCase();
  }
  return next();
});

Observer.set(user, 'email', '[email protected]'); // Becomes '[email protected]'

Key Features

Core Reactivity

  • πŸ”„ Real-time Observability: Watch object and array changes as they happen
  • ⚑ Synchronous Updates: Changes are delivered synchronously, not batched
  • 🎯 Granular Control: Watch specific properties, paths; even wildcards
  • 🌳 Deep Path Watching: Observe nested properties or entire object tree

Advanced Capabilities

  • πŸ›‘οΈ Operation Interception: Transform, validate, or block operations before execution
  • πŸ”— Traps Pipeline: Compose multiple interceptors for complex behavior
  • πŸ“¦ Atomic Batching: Batch multiple changes into single atomic operation
  • πŸ”„ Object Mirroring: Create reactive synchronization between objects

Developer Experience

  • πŸ”§ Utility-First API: Clean, functional design with consistent patterns
  • πŸ“± Universal Support: Works in browsers, Node.js, and all JavaScript environments
  • πŸ”Œ Standard Integration: Built on AbortSignal, Reflect API, and Proxy standards
  • πŸ“Š Lightweight: Only ~5.8KB min+gz with zero dependencies

Ecosystem Integrations

The Observer API is enabling a shared protocol for mutation-based reactivity across the ecosystem:

Uses Observer API under the hood to operate as a full-fledged reactive runtime. Quantum enables Imperative Reactive Programming by leveraging Observer's reactivity foundation to make ordinary JavaScript code reactive.

🌐 OOHTML

Uses Observer API to underpin dynamic, reactive UIs. OOHTML enables live data binding between UI and app state, automatically updating the DOM when your data changes.

⚑ Webflo

Uses Observer API to underpin Live Objects as a first-class concept. Live Objects in Webflo lets you send dynamic state from your server to the UI with reactivity over the wire.

πŸ”— LinkedQL

Uses Observer API to underpin Live Objects as a first-class concept. Live Objects in LinkedQL lets you have query results as self-updating result sets.

API Reference

Observer.observe(target, callback, options?)

Observe changes on an object or array. Returns an AbortController instance for lifecycle management.

Basic Usage:

const obj = {};

const controller = Observer.observe(obj, (mutations) => {
  mutations.forEach(mutation => {
    console.log(`${mutation.type}: ${mutation.key} = ${mutation.value}`);
  });
});

// Changes are delivered synchronously
Observer.set(obj, 'name', 'Bob');
Observer.set(obj, 'age', 30);

// Stop observing
controller.abort();

Alternative Method Shapes:

// Watch specific properties
Observer.observe(obj, ['name', 'email'], callback);

// Watch a single property
Observer.observe(obj, 'name', callback);

// Watch all properties (default)
Observer.observe(obj, callback);

Options:

  • signal: A custom AbortSignal instance that control lifecycle
  • diff: Only fire for values that actually changed
  • recursions: Controls recursion handling ('inject', 'force-sync', 'force-async')
  • withPropertyDescriptors: Include property descriptor information

Abort Signals

Observer returns a standard AbortSignal instance for managing observer lifecycle.

// Returns AbortController for lifecycle management
const controller = Observer.observe(obj, callback);
controller.abort(); // Stop observing

// Provide your own AbortSignal
const abortController = new AbortController();
Observer.observe(obj, callback, { signal: abortController.signal });
abortController.abort(); // Stop observing

Or, you can provide your own:

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

Lifecycle Signals

Each lifecycle event fired carries its own Abort Signal that automatically aborts at the end of its turn β€” just when the next event fires. They're useful for tying other parts of the system to just the given event's lifecycle. For example, lifecycle signals enable parent-child observer relationships where child observers automatically abort when their parent aborts. Leverage this to simplify hierarchical observer patterns.

// Parent observer with lifecycle management
const parentController = Observer.observe(obj, (mutations, flags) => {
  // Child observers automatically abort when parent aborts
  Observer.observe(obj, childCallback, { signal: flags.signal });
  
  // Multiple child observers tied to parent lifecycle
  Observer.observe(obj, anotherCallback, { signal: flags.signal });
});

// All child observers abort when parent aborts
parentController.abort();

Parity Table

Observer API Object.observe() (Deprecated)
Signature .observe(target, callback, options?) .observe(target, callback, acceptList?)
Return Value AbortController (lifecycle management) undefined (no lifecycle management)
Additional Features AbortSignal integration, path watching, batch/atomic operations, synchronous event model, etc. Basic object observation, asynchronous event model (deprecated)

Observer.intercept(target, operation, handler, options?)

Intercept operations before they happen. Can intercept individual operations or multiple operations at once.

Single Operation Interception:

Intercept individual operations to transform, validate, or block them before they execute.

// Transform values before they're set
Observer.intercept(obj, 'set', (operation, previous, next) => {
  if (operation.key === 'email') {
    operation.value = operation.value.toLowerCase();
  }
  return next();
});

Observer.set(obj, 'email', '[email protected]'); // Becomes '[email protected]'

Multiple Operations Interception:

Intercept multiple operations simultaneously to create comprehensive behavior modifications.

const options = {};
Observer.intercept(obj, {
  set: (operation, previous, next) => {
    if (operation.key === 'email') {
      operation.value = operation.value.toLowerCase();
    }
    return next();
  },
  get: (operation, previous, next) => {
    if (operation.key === 'token') {
      return next(fetchToken());
    }
    return next();
  },
  deleteProperty: (operation, previous, next) => {
    if (operation.key === 'password') {
      console.log('Password deletion blocked');
      return false; // Block the operation
    }
    return next();
  }
}, options);

Traps Pipeline:

Multiple interceptors can intercept same operation, and these will operate like a middleware pipeline where each interceptor uses next() to advance the operation to subsequent interceptors in the pipeline.

// First interceptor: Transform email to lowercase
Observer.intercept(obj, 'get', (operation, previous, next) => {
  if (operation.key === 'email') {
    const result = next();
    return result ? result.toLowerCase() : result;
  }
  return next();
});

// Second interceptor: Add validation
Observer.intercept(obj, 'get', (operation, previous, next) => {
  if (operation.key === 'email') {
    const result = next();
    if (result && !result.includes('@')) {
      throw new Error('Invalid email format');
    }
    return result;
  }
  return next();
});

// Now when accessing email, both interceptors run in sequence:
// 1. First: transforms to lowercase
// 2. Second: validates format
// Result: '[email protected]' β†’ '[email protected]' β†’ validation passes

Interceptable Operations

  • set - Property assignment
  • get - Property access
  • has - Property existence check
  • ownKeys - Object key enumeration
  • deleteProperty - Property deletion
  • defineProperty - Property definition
  • getOwnPropertyDescriptor - Property descriptor access

Parity Table

Observer API Proxy Traps
Signature .intercept(target, operation, handler, options?)
.intercept(target, { [operation]: handler[, ...]}, options?)
new Proxy(target, { [operation]: handler[, ...] })
Return Value undefined (registration) Proxy (wrapped object)
Additional Features Traps pipeline, composable interceptors Single trap per operation, no composability

Observer.set(target, key, value, options?)

Set properties reactively using a programmatic mutation API. Triggers observers and can be intercepted via Observer.intercept().

Basic Usage:

Observer.set(obj, 'name', 'Alice');
Observer.set(arr, 0, 'first item');

Alternative Method Shapes:

// Set multiple properties at once
Observer.set(obj, {
  name: 'Alice',
  age: 25,
  email: '[email protected]'
});

// Set with receiver context
Observer.set(obj, 'name', 'Alice', receiver);

Usage Patterns

// Reactive state updates
Observer.set(state, 'loading', true);
Observer.set(state, 'data', responseData);

// Array operations
Observer.set(items, 0, 'new item');
Observer.set(items, items.length, 'append item');

// Nested property updates
Observer.set(obj, Observer.path('user', 'profile', 'name'), 'Alice');

Parity Table

Observer API Reflect API
Signature .set(target, key, value, options?) .set(target, key, value)
Return Value boolean (success) boolean (success)
Additional Features Triggers observers, interceptable Standard property setting

Observer.get(target, key, options?)

Get properties using a programmatic API. Can be intercepted via Observer.intercept() to provide computed values or transformations.

Basic Usage:

const value = Observer.get(obj, 'name');
const nested = Observer.get(obj, 'user.profile.name');

Scenario: Computed Properties:

// Intercept to provide computed values
Observer.intercept(obj, 'get', (operation, previous, next) => {
  if (operation.key === 'fullName') {
    return `${obj.firstName} ${obj.lastName}`;
  }
  return next();
});

Observer.get(obj, 'fullName'); // "John Doe" (computed on-the-fly)

Parity Table

Observer API Reflect API
Signature .get(target, key, options?) .get(target, key)
Return Value any (property value) any (property value)
Additional Features Interceptable for computed values Standard property access

Observer.has(target, key, options?)

Check if a property exists on an object. Can be intercepted via Observer.intercept() to hide or reveal properties dynamically.

Basic Usage:

Observer.has(obj, 'name'); // true/false
Observer.has(obj, 'user.profile.name'); // nested property check

Scenario: Property Hiding:

// Intercept to hide sensitive properties
Observer.intercept(obj, 'has', (operation, previous, next) => {
  if (operation.key === 'password') {
    return false; // Hide password from property checks
  }
  return next();
});

Observer.has(obj, 'password'); // false (hidden from checks)

Parity Table

Observer API Reflect API
Signature .has(target, key, options?) .has(target, key)
Return Value boolean (exists) boolean (exists)
Additional Features Interceptable for property hiding Standard property existence check

Observer.ownKeys(target, options?)

Get all own property keys of an object. Can be intercepted via Observer.intercept() to filter or transform the key list.

Basic Usage:

Observer.ownKeys(obj); // ['name', 'email', 'age']

Scenario: Key Filtering:

// Intercept to filter out sensitive keys
Observer.intercept(obj, 'ownKeys', (operation, previous, next) => {
  const keys = next();
  return keys.filter(key => key !== 'password');
});

Observer.ownKeys(obj); // ['name', 'email'] (password filtered out)

Parity Table

Observer API Reflect API Object API
Signature .ownKeys(target, options?) .ownKeys(target) .keys(obj)
Return Value string[] (keys) string[] (keys) string[] (keys)
Additional Features Interceptable for key filtering Standard key enumeration Standard key enumeration

Observer.deleteProperty(target, key, options?)

Delete properties reactively using a programmatic mutation API.

Basic Usage:

Observer.deleteProperty(obj, 'oldProp');
Observer.deleteProperty(arr, 0);

Parity Table

Observer API Reflect API
Signature .deleteProperty(target, key, options?) .deleteProperty(target, key)
Return Value boolean (success) boolean (success)
Additional Features Triggers observers, interceptable Standard property deletion

Observer.deleteProperties(target, keys, options?)

Delete multiple properties at once.

Observer.deleteProperties(obj, ['oldProp1', 'oldProp2', 'tempProp']);

Parity Table

Observer API No Direct Equivalent
Signature .deleteProperties(target, keys, options?) No batch delete in standard APIs
Return Value boolean[] (success array) N/A
Additional Features Triggers observers, interceptable N/A

Observer.defineProperty(target, key, descriptor, options?)

Define properties reactively using the programmatic mutation API.

Basic Usage:

Observer.defineProperty(obj, 'computed', { 
  get: () => obj.value * 2 
});

Parity Table

Observer API Reflect API Object API
Signature .defineProperty(target, key, descriptor, options?) .defineProperty(target, key, descriptor) .defineProperty(obj, key, descriptor)
Return Value boolean (success) boolean (success) object (modified object)
Additional Features Triggers observers, interceptable Standard property definition Standard property definition

Observer.defineProperties(target, descriptors, options?)

Define multiple properties at once.

Observer.defineProperties(obj, {
  name: { value: 'Alice', writable: true },
  email: { value: '[email protected]', writable: true },
  age: { value: 25, writable: true }
});

Parity Table

Observer API Object API
Signature .defineProperties(target, descriptors, options?) .defineProperties(obj, descriptors)
Return Value boolean (success) object (modified object)
Additional Features Triggers observers, interceptable Standard property definition

Observer.accessorize(target, properties?, options?)

Make properties reactive for direct assignment.

const obj = { age: null };

// Make all CURRENT properties reactive
Observer.accessorize(obj);

// Make specific properties reactive
Observer.accessorize(obj, ['name', 'email']);

// Now direct assignment works
obj.name = 'Alice';
obj.email = '[email protected]';

Parity Table

Observer API No Direct Equivalent
Signature .accessorize(target, properties?, options?) No direct equivalent in standard APIs
Return Value undefined (modification) N/A
Additional Features Makes properties reactive for direct assignment N/A

Observer.unaccessorize(target, properties?)

Restore accessorized properties to their normal state.

// Restore specific properties
Observer.unaccessorize(obj, ['name', 'email'], options?);

// Restore all accessorized properties
Observer.unaccessorize(obj);

Parity Table

Observer API No Direct Equivalent
Signature .unaccessorize(target, properties?, options?) No direct equivalent in standard APIs
Return Value undefined (modification) N/A
Additional Features Restores accessorized properties to normal state N/A

Observer.proxy(target, options?)

Create a reactive proxy of any object to get automatic reactivity and interceptibility over on-the-fly operations.

Basic Usage:

const $obj = Observer.proxy(obj);

// All operations are reactive
$obj.name = 'Alice';             // Triggers observers
$obj.newProp = 'value';          // Triggers observers
delete $obj.oldProp;             // Triggers observers

// Array methods are reactive
$arr.push('item1', 'item2');     // Triggers observers
$arr[0] = 'newValue';            // Triggers observers

Nested Operations (Requires chainable: true)

Use chainable: true to interact with deeply nested objects as proxy instances too. By default, .proxy() doesn't perform deep wrapping - nested objects are returned as plain objects. Chainable mode enables automatic proxying of nested objects at the point they're accessed, allowing nested operations to trigger observers.

const $obj = Observer.proxy(obj, { chainable: true });

// Nested objects are automatically proxied when accessed
const $user = $obj.user;           // Returns a proxied object
$user.name = 'Alice';              // Triggers observers
$user.profile.theme = 'dark';      // Triggers observers

// Array methods return proxied arrays
const $filtered = $obj.items.filter(x => x.active); // Returns proxied array
$filtered.push('newItem');         // Triggers observers

// Direct nested access also works
$obj.users[0].name = 'Bob';        // Triggers observers
$obj.data.splice(0, 1);            // Triggers observers

Membrane Mode

Membranes ensure that the same proxy instance is returned across multiple .proxy() calls for the same object. When combined with chainable: true, membranes also ensure consistent proxy identity for nested objects.

// Create membrane for consistent proxy identity
const $obj1 = Observer.proxy(obj, { membrane: 'userData' });
const $obj2 = Observer.proxy(obj, { membrane: 'userData' });

// Same proxy instance returned
console.log($obj1 === $obj2); // true

// Root operations are reactive
$obj1.name = 'Alice';                  // Triggers observers
$obj2.email = '[email protected]';    // Triggers observers (same proxy)

// When combined with chainable: true
const $obj3 = Observer.proxy(obj, { membrane: 'userData', chainable: true });
const $user1 = $obj3.user;
const $user2 = $obj3.user;

// Same nested proxy instance returned
console.log($user1 === $user2); // true
$user1.name = 'Alice';          // Triggers observers

How Membranes Work

  • Root Object Identity - Same root object always returns the same proxy instance across multiple .proxy() calls
  • Membrane References - Uses a reference system to ensure consistent proxy identity
  • Nested Object Identity - When combined with chainable: true, ensures same nested objects return same proxy instances
  • Performance - Only creates one proxy per object (root or nested)
  • Consistency - Maintains referential equality for both root and nested objects

Membrane vs Chainable Object Identity

const obj = { user: { name: 'Alice' }, items: ['item1'] };

// MEMBRANE: Same root object = same proxy instance
const $obj1 = Observer.proxy(obj, { membrane: 'test' });
const $obj2 = Observer.proxy(obj, { membrane: 'test' });
console.log($obj1 === $obj2); // true - same root proxy

// Nested objects are NOT automatically proxied (without chainable)
const user1 = $obj1.user; // Plain object, not proxied
const user2 = $obj2.user; // Plain object, not proxied
console.log(user1 === user2); // true - same plain object

// CHAINABLE: Auto-proxies nested objects when accessed
const $obj = Observer.proxy(obj, { chainable: true });
const $user1 = $obj.user; // Proxied object
const $user2 = $obj.user; // Different proxy instance
console.log($user1 !== $user2); // true - different proxy instances

// MEMBRANE + CHAINABLE: Consistent nested proxy identity
const $obj3 = Observer.proxy(obj, { membrane: 'test', chainable: true });
const $user3 = $obj3.user; // Proxied object
const $user4 = $obj3.user; // Same proxy instance
console.log($user3 === $user4); // true - same nested proxy

Real-World Usage Patterns

Scenario: Form Handling:

const $form = Observer.proxy(formData, { membrane: 'form' });
$form.name = 'John';             // Auto-save, validation
$form.email = '[email protected]'; // Auto-save, validation
$form.tags.push('urgent');       // Auto-save, validation

Scenario: State Management:

const $state = Observer.proxy(appState, { chainable: true });
$state.user.isLoggedIn = true;   // UI updates
$state.cart.items.push(product); // UI updates
$state.getUser().profile.theme = 'dark'; // UI updates (chainable)

Formal Arguments

// Basic proxy
const $obj = Observer.proxy(obj);

// Proxy with options
const $obj = Observer.proxy(obj, {
  membrane: 'userData',        // Auto-proxy nested objects
  chainable: true              // Auto-wrap returned objects
});

// Proxy with custom extension
const $obj = Observer.proxy(obj, {}, (traps) => {
  // Extend proxy traps
  traps.get = (target, key, receiver) => {
    if (key === 'computed') {
      return target.firstName + ' ' + target.lastName;
    }
    return traps.get(target, key, receiver);
  };
  return traps;
});

Proxy Features (Summary)

  • Literal syntax - Use normal JavaScript operations
  • Array methods - All array methods are reactive
  • Property access - All property operations are reactive
  • Nested operations - Works with deeply nested objects
  • Dynamic properties - Supports computed property names
  • Method chaining - Array methods can be chained
  • Membrane support - Auto-proxy nested objects
  • Chainable operations - Auto-wrap returned values
  • Custom traps - Extend proxy behavior
  • Namespace isolation - Separate observer namespaces

Parity Table

Observer API Proxy API
Signature .proxy(target, options?) new Proxy(target, handlers)
Return Value Proxy (reactive proxy) Proxy (standard proxy)
Additional Features Built-in reactivity, membrane, chainable Manual trap implementation required

Observer.unproxy(target, options?)

Get the original object from a proxy.

const $obj = Observer.proxy(obj);
const original = Observer.unproxy($obj); // Returns original obj

Parity Table

Observer API No Direct Equivalent
Signature .unproxy(target) No direct equivalent in standard APIs
Return Value object (original object) N/A
Additional Features Extracts original object from Observer proxy N/A

Observer.path(...segments)

Create path arrays for deep property observation. Path watching enables observing changes at specific nested paths within object trees, including non-existent paths that are created dynamically.

Basic Usage:

// Watch deep paths
const path = Observer.path('user', 'profile', 'settings');
Observer.observe(obj, path, (mutation) => {
  console.log(`Deep change: ${mutation.path} = ${mutation.value}`);
});

Usage Patterns

// Form validation
const path = Observer.path('form', 'user', 'email');
Observer.observe(form, path, (mutation) => {
  validateEmail(mutation.value);
});

// State management
const path = Observer.path('app', 'user', 'preferences', 'theme');
Observer.observe(state, path, (mutation) => {
  updateTheme(mutation.value);
});

// Configuration watching
const path = Observer.path('config', 'api', 'endpoint');
Observer.observe(config, path, (mutation) => {
  updateApiEndpoint(mutation.value);
});

Path Features (Summary)

  • Watches paths that are created dynamically
  • Uses an array syntax to avoid conflicting with property names with dots
  • Returns mutation context for audit trails

Observer.any()

Create a wildcard directive for matching any property or array index in path patterns. Wildcards enable flexible observation of dynamic data structures where you need to watch changes at any index or property name.

Basic Usage:

// Watch any user at any index
const path = Observer.path('users', Observer.any(), 'name');
Observer.observe(obj, path, (mutation) => {
  console.log(`User name changed: ${mutation.path} = ${mutation.value}`);
});

Advanced Compositions:

Combine multiple wildcards to create powerful observation patterns for complex data structures. This enables watching changes across dynamic arrays, nested objects, and varying property names.

// Multiple wildcards in sequence
const path = Observer.path('sections', Observer.any(), 'items', Observer.any(), 'name');
// Matches: sections[0].items[1].name, sections[2].items[0].name, etc.

// Wildcard at different levels
const path = Observer.path('app', 'users', Observer.any(), 'profile', 'settings', Observer.any());
// Matches: app.users[0].profile.settings[theme], app.users[1].profile.settings[language], etc.

// Wildcard with specific properties
const path = Observer.path('data', Observer.any(), 'metadata', 'version');
// Matches: data[item1].metadata.version, data[item2].metadata.version, etc.

Observer.subtree()

Create a subtree directive for watching all changes from a specific level down infinitely. Subtree watching enables comprehensive observation of complex nested data structures without needing to specify every possible path.

Basic Usage:

// Watch all changes from this level down
Observer.observe(obj, Observer.subtree(), (mutation) => {
  console.log(`Any change: ${mutation.path} = ${mutation.value}`);
});

Advanced Compositions:

// Subtree after specific path
const path = Observer.path('app', 'users', Observer.subtree());
// Watches: app.users.name, app.users.profile.theme, app.users.settings.notifications, etc.

// Subtree with wildcards
const path = Observer.path('sections', Observer.any(), Observer.subtree());
// Watches: sections[0].title, sections[0].items[1].name, sections[1].config.theme, etc.

// Multiple subtrees
const path = Observer.path('app', 'users', Observer.any(), 'profile', Observer.subtree());
// Watches: app.users[0].profile.name, app.users[0].profile.settings.theme, etc.

// Subtree at root level
Observer.observe(obj, Observer.subtree(), (mutation) => {
  // Watches EVERY change in the entire object tree
});

Real-World Usage Patterns

// E-commerce: Watch any product in any category
const path = Observer.path('store', 'categories', Observer.any(), 'products', Observer.any(), Observer.subtree());
// Triggers for: store.categories[electronics].products[laptop].price
//              store.categories[books].products[novel].title
//              store.categories[clothing].products[shirt].sizes[large]
// Multi-tenant: Watch any user's data in any organization
const path = Observer.path('orgs', Observer.any(), 'users', Observer.any(), Observer.subtree());
// Triggers for: orgs[company1].users[alice].profile.name
//              orgs[company2].users[bob].settings.theme
// Content Management: Watch any page in any section
const path = Observer.path('cms', 'sections', Observer.any(), 'pages', Observer.any(), Observer.subtree());
// Triggers for: cms.sections[blog].pages[post1].content
//              cms.sections[news].pages[article].metadata.tags

Observer.batch(target, callback, options?)

Batch multiple operations together. Batched operations ensure atomicity - all changes are delivered as a single event to observers, preventing partial updates and ensuring data consistency.

Basic Usage:

// Batch multiple changes
Observer.batch(obj, () => {
  Observer.set(obj, 'name', 'Alice');
  Observer.set(obj, 'email', '[email protected]');
  Observer.deleteProperty(obj, 'age');
});

// All changes are delivered as a single batch to observers

Observer.map(source, target, options?)

Create reactive mirrors between objects β€” changes in source automatically sync to target. Object mirroring enables automatic data flow between different parts of your application, keeping them synchronized without manual intervention.

Basic Usage:

const source = { name: 'Alice', age: 25 };
const target = {};

// Create reactive mirror
const controller = Observer.map(source, target);

// Changes in source automatically sync to target
Observer.set(source, 'name', 'Bob');
console.log(target.name); // 'Bob'

// Stop mirroring
controller.abort();

Alternative Method Shapes:

// Mirror with options
Observer.map(source, target, {
  only: ['name', 'email'], // Only mirror specific properties
  except: ['password'], // Exclude specific properties
  spread: true, // Spread array elements
  onlyEnumerable: false // Include non-enumerable properties
});

// Mirror with namespace
Observer.map(source, target, { namespace: 'user' });

Usage Patterns

// State synchronization
const appState = { user: { name: 'Alice' } };
const uiState = {};
Observer.map(appState, uiState);

// Form data mirroring
const formData = { name: '', email: '' };
const validationState = {};
Observer.map(formData, validationState);

// Array synchronization
const sourceArray = [1, 2, 3];
const targetArray = [];
Observer.map(sourceArray, targetArray, { spread: true });

Other Methods

Mentioned here for completeness, Observer also provides these utility methods:

  • Observer.apply(target, thisArg, args) - Apply functions reactively
  • Observer.construct(target, args) - Construct objects reactively
  • Observer.getOwnPropertyDescriptor(target, key) - Get property descriptors reactively
  • Observer.getPrototypeOf(target) - Get prototype reactively
  • Observer.setPrototypeOf(target, prototype) - Set prototype reactively
  • Observer.isExtensible(target) - Check extensibility reactively
  • Observer.preventExtensions(target) - Prevent extensions reactively

Extended Documentation

Contributing

We welcome contributions! Here's how to get involved:

License

MIT

Sponsor this project

  •  

Contributors 2

  •  
  •