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

observable-slim

v0.2.2

Published

Observable Slim is a singleton that utilizes ES6 Proxies to observe changes made to an object and any nested children of that object. It is intended to assist with state management and one-way data binding.

Readme

:eyes: Observable Slim

Build Status Coverage Status Monthly Downloads

https://github.com/elliotnb/observable-slim

A small, dependency-free library that watches deep changes in plain JavaScript objects/arrays and tells you exactly what changed.

Observable Slim mirrors JavaScript objects and arrays using ES2015 Proxy, letting you observe deeply nested changes with clear notifications using dotted paths and RFC-6901 JSON Pointers. Engineered for performance, it features linear O(N) initialization and lazy path generation, allowing it to scale efficiently even on deep state trees. It safely handles circular references, ensures fast O(1) lookups via WeakMap identity tables, and keeps paths accurate even when arrays reorder. You can batch updates for smoother UI rendering, pause/resume observers, and rely on automatic garbage collection for removed data. Lightweight (~6 KB minified), dependency-free, and memory-safe.

Table of Contents

Design (Deep Dive)

Curious about the underlying architecture and implementation? See docs/design.md for the problem model, core algorithms, complexity, invariants, cycle-safe instrumentation, cross-proxy fan-out, lazy path generation (linked-list lineage), reachability-based teardown, and correctness arguments. It's optional reading, but helpful if you want to understand how the internals stay fast and memory-safe. For validation of scaling/time complexity performance, please see bench/results.md for the most recent benchmarking run.

Features

  • Deep observation of objects and arrays (all nested children)
  • High performance creation with O(N) linear scaling, suitable for deeply nested state trees.
  • Structured change records with: type, property, currentPath (dot notation), jsonPointer (RFC6901), previousValue, newValue, target, proxy
  • Batched notifications with domDelay (boolean or ms number)
  • Multiple proxies per target with safe cross-proxy propagation
  • Pause/resume observers and pause/resume changes (dry-run validation)
  • Accurate array length tracking using WeakMap bookkeeping
  • Introspection helpers: isProxy, getTarget, getParent, getPath
  • Advanced symbol capabilities for collision-free internals
  • Configurable orphan-cleanup scheduler (configure({ cleanupDelayMs })) and a test hook (flushCleanup()) to run pending cleanups immediately
  • TypeScript declarations included (observable-slim.d.ts)

Installation

Browser (UMD):

<script src="https://unpkg.com/observable-slim"></script>
<script>
  const state = { hello: "world" };
  const proxy = ObservableSlim.create(state, false, (changes) => console.log(changes));
</script>

NPM (CommonJS):

npm install observable-slim --save
const ObservableSlim = require('observable-slim');

ES Modules (via bundlers that can import CJS):

import ObservableSlim from 'observable-slim';

Quick Start

const state = { user: { name: 'Ada' }, todos: [] };
const p = ObservableSlim.create(state, true, (changes) => {
  // Array of change records batched on a small timeout when domDelay === true
  console.log(changes);
});

p.todos.push({ title: 'Write tests', done: false });
p.user.name = 'Ada Lovelace';

API

ObservableSlim.create(target, domDelay, observer?)

Create a new Proxy that mirrors target and observes all deep changes.

  • target: object (required) – plain object/array to observe.
  • domDelay: boolean|number (required) – true to batch notifications on ~10ms timeout; false to notify synchronously; number > 0 to use a custom delay (ms).
  • observer(changes): function (optional) – receives an array of change records.
  • Returns: the Proxy.

Note: Passing an existing Proxy produced by Observable Slim is supported; the underlying original target will be used to avoid nested proxying.

ObservableSlim.observe(proxy, observer)

Attach an additional observer to an existing proxy. Observers are called with arrays of change records.

ObservableSlim.pause(proxy) / resume(proxy)

Temporarily disable/enable observer callbacks for the given proxy (no changes are blocked).

ObservableSlim.pauseChanges(proxy) / resumeChanges(proxy)

Disable/enable writes to the underlying target while still issuing change records. Useful for approval flows or validations.

ObservableSlim.remove(proxy)

Detach all observers and bookkeeping for the given proxy and its nested proxies created for the same root observable.

ObservableSlim.isProxy(obj)

Return true if the argument is a Proxy created by Observable Slim.

ObservableSlim.getTarget(proxy)

Return the original target object behind a Proxy created by Observable Slim.

ObservableSlim.getParent(proxy, depth=1)

Return the parent object of a proxy relative to the top-level observable (climb depth levels; default 1).

ObservableSlim.getPath(proxy, options)

Return the path string of a proxy relative to its root observable.

  • options = { jsonPointer?: boolean } – when true, return RFC6901 pointer (e.g., /foo/0/bar); otherwise dot path (e.g., foo.0.bar).

Advanced: ObservableSlim.symbols

For advanced users who need capability-style access without relying on public helpers, the library exposes collision-free Symbols:

  • ObservableSlim.symbols.IS_PROXY – brand symbol; proxy[IS_PROXY] === true
  • ObservableSlim.symbols.TARGET – unwrap symbol; proxy[TARGET] === originalObject
  • ObservableSlim.symbols.PARENT – function symbol; proxy[PARENT](depth) returns the ancestor
  • ObservableSlim.symbols.PATH – path symbol; proxy[PATH] returns the dot path

Symbols are not enumerable and won’t collide with user properties. Prefer the public helpers for most use cases.

ObservableSlim.configure(options)

Configure behavior that affects all observables created in this runtime.

  • options.cleanupDelayMs: number – delay (ms) used by the orphan-cleanup scheduler. Set to 0 to run cleanups eagerly; increase to coalesce more work.
ObservableSlim.configure({ cleanupDelayMs: 25 });

ObservableSlim.flushCleanup()

Force any pending orphan cleanups to run immediately. Useful in tests for deterministic timing; safe to call in production if you need to flush scheduled cleanups now.

ObservableSlim.flushCleanup();

Change Record Shape

Every notification contains an array of objects like:

{
  type: 'add' | 'update' | 'delete',
  property: string,
  currentPath: string,   // e.g. "foo.0.bar"
  jsonPointer: string,   // e.g. "/foo/0/bar"
  target: object,        // the concrete target that changed
  proxy: object,         // proxy for the target
  newValue: any,
  previousValue?: any
}

Usage Examples

Observer output:

Below, every mutation is followed by the array that your observer handler function receives:

const test = {};
const p = ObservableSlim.create(test, false, (changes) => {
  console.log(JSON.stringify(changes));
});

p.hello = "world";
// => [{
//   "type":"add","target":{"hello":"world"},"property":"hello",
//   "newValue":"world","currentPath":"hello","jsonPointer":"/hello",
//   "proxy":{"hello":"world"}
// }]

p.hello = "WORLD";
// => [{
//   "type":"update","target":{"hello":"WORLD"},"property":"hello",
//   "newValue":"WORLD","previousValue":"world",
//   "currentPath":"hello","jsonPointer":"/hello",
//   "proxy":{"hello":"WORLD"}
// }]

p.testing = {};
// => [{
//   "type":"add","target":{"hello":"WORLD","testing":{}},
//   "property":"testing","newValue":{},
//   "currentPath":"testing","jsonPointer":"/testing",
//   "proxy":{"hello":"WORLD","testing":{}}
// }]

p.testing.blah = 42;
// => [{
//   "type":"add","target":{"blah":42},"property":"blah","newValue":42,
//   "currentPath":"testing.blah","jsonPointer":"/testing/blah",
//   "proxy":{"blah":42}
// }]

p.arr = [];
// => [{
//   "type":"add","target":{"hello":"WORLD","testing":{"blah":42},"arr":[]},
//   "property":"arr","newValue":[],
//   "currentPath":"arr","jsonPointer":"/arr",
//   "proxy":{"hello":"WORLD","testing":{"blah":42},"arr":[]}
// }]

p.arr.push("hello world");
// => [{
//   "type":"add","target":["hello world"],"property":"0",
//   "newValue":"hello world","currentPath":"arr.0","jsonPointer":"/arr/0",
//   "proxy":["hello world"]
// }]

delete p.hello;
// => [{
//   "type":"delete","target":{"testing":{"blah":42},"arr":["hello world"]},
//   "property":"hello","newValue":null,"previousValue":"WORLD",
//   "currentPath":"hello","jsonPointer":"/hello",
//   "proxy":{"testing":{"blah":42},"arr":["hello world"]}
// }]

p.arr.splice(0,1);
// => [{
//   "type":"delete","target":[],"property":"0","newValue":null,
//   "previousValue":"hello world","currentPath":"arr.0","jsonPointer":"/arr/0",
//   "proxy":[]
// },{
//   "type":"update","target":[],"property":"length","newValue":0,
//   "previousValue":1,"currentPath":"arr.length","jsonPointer":"/arr/length",
//   "proxy":[]
// }]

Arrays in detail (push/unshift/pop/shift/splice)

const p = ObservableSlim.create({ arr: ["foo","bar"] }, false, (c) => console.log(JSON.stringify(c)));

p.arr.unshift("hello");
// 1) add index 2 moved -> implementation may record reindexes; canonical signal is:
// [{"type":"add","target":["hello","foo","bar"],"property":"0","newValue":"hello",
//   "currentPath":"arr.0","jsonPointer":"/arr/0","proxy":["hello","foo","bar"]}]

p.arr.pop();
// Deleting last element and updating length; commonly two records over one or two callbacks:
// [{"type":"delete","target":["hello","foo"],"property":"2","newValue":null,
//   "previousValue":"bar","currentPath":"arr.2","jsonPointer":"/arr/2","proxy":["hello","foo"]}]
// [{"type":"update","target":["hello","foo"],"property":"length","newValue":2,
//   "previousValue":3,"currentPath":"arr.length","jsonPointer":"/arr/length","proxy":["hello","foo"]}]

p.arr.splice(1,0,"X");
// Insert at index 1 and reindex subsequent items:
// [{"type":"add","target":["hello","X","foo"],"property":"1","newValue":"X",
//   "currentPath":"arr.1","jsonPointer":"/arr/1","proxy":["hello","X","foo"]}]

p.arr.shift();
// Move index 1 down to 0, delete old 1, update length. Typical sequence:
// [{"type":"update","target":["X","foo"],"property":"0","newValue":"X",
//   "previousValue":"hello","currentPath":"arr.0","jsonPointer":"/arr/0","proxy":["X","foo"]}]
// [{"type":"delete","target":["X","foo"],"property":"1","newValue":null,
//   "previousValue":"foo","currentPath":"arr.1","jsonPointer":"/arr/1","proxy":["X","foo"]}]
// [{"type":"update","target":["X","foo"],"property":"length","newValue":2,
//   "previousValue":3,"currentPath":"arr.length","jsonPointer":"/arr/length","proxy":["X","foo"]}]

Notes: Exact batching of array index/length signals can vary by engine and call path. The shapes above are representative and covered by the test suite (push, unshift, pop, shift, splice, and length tracking).

Multiple observers and multiple observables

const data = { foo: { bar: "bar" } };
const p1 = ObservableSlim.create(data, false, (c) => console.log("p1", c));
ObservableSlim.observe(p1, (c) => console.log("p1-second", c));

const p2 = ObservableSlim.create(data, false, (c) => console.log("p2", c));

p2.foo.bar = "baz"; // triggers both observers on p1 and p2

Pausing observers vs. pausing changes

const p = ObservableSlim.create({ x: 0 }, false, (c) => console.log("obs", c));

ObservableSlim.pause(p);
p.x = 1;     // no observer callbacks
ObservableSlim.resume(p);

ObservableSlim.pauseChanges(p);
p.x = 2;     // observer fires, but underlying target is NOT updated
console.log(p.x); // still 0
ObservableSlim.resumeChanges(p);
p.x = 3;     // observer fires and target is updated

Introspection helpers and Symbols

const state = { a: { b: 1 } };
const proxy = ObservableSlim.create(state, false, () => {});

console.log(ObservableSlim.isProxy(proxy));     // true
console.log(ObservableSlim.getTarget(proxy) === state); // true

const child = proxy.a;
console.log(ObservableSlim.getParent(child) === proxy);   // parent proxy
console.log(ObservableSlim.getPath(child));               // 'a'
console.log(ObservableSlim.getPath(child, { jsonPointer:true })); // '/a'

const { TARGET, IS_PROXY, PARENT, PATH } = ObservableSlim.symbols;
console.log(proxy[IS_PROXY]);      // true
console.log(proxy[TARGET] === state); // true
console.log(child[PARENT](1) === proxy); // true
console.log(child[PATH]);          // 'a'

Remove an observable

const p = ObservableSlim.create({ y: 1 }, false, () => console.log('called'));
ObservableSlim.remove(p);
p.y = 2; // no callbacks after removal

Limitations and Browser Support

This library requires native ES2015 Proxy, WeakMap and Symbol support.

  • ✅ Chrome 49+, Edge 12+, Firefox 18+, Opera 36+, Safari 10+ (per MDN guidance)
  • ❌ Internet Explorer: not supported

Polyfills cannot fully emulate Proxy; features like property addition/deletion and .length interception will not work under a polyfill.

TypeScript

Type declarations are published with the package (observable-slim.d.ts). Observer callbacks are strongly typed with the change record shape described above.

Development

  • Install deps: npm ci
  • Run tests: npm run test
  • Lint: npm run lint / npm run lint:fix to identify and correct code formatting.
  • Type declarations: npm run type generates the d.ts file for TypeScript declarations.
  • Build (minified): npm run build emits .cjs, .mjs, .js and .d.ts artifacts into the dist folder.

Contributing

Issues and PRs are welcome! Please:

  1. Write tests for behavioral changes.
  2. Keep the API surface small and predictable.
  3. Run npm run lint and npm run test before submitting.

License

MIT


Migration from legacy magic properties

Earlier versions exposed string-named magic fields (e.g., __isProxy, __getTarget, __getParent(), __getPath). These have been replaced by safer helpers and Symbols:

| Legacy (deprecated) | New API | | --------------------------- | ----------------------------------------------------------------------------------------- | | proxy.__isProxy | ObservableSlim.isProxy(proxy) or proxy[ObservableSlim.symbols.IS_PROXY] | | proxy.__getTarget | ObservableSlim.getTarget(proxy) or proxy[ObservableSlim.symbols.TARGET] | | proxy.__getParent(depth?) | ObservableSlim.getParent(proxy, depth) or proxy[ObservableSlim.symbols.PARENT](depth) | | proxy.__getPath | ObservableSlim.getPath(proxy) or proxy[ObservableSlim.symbols.PATH] |

The helpers are preferred for readability and to avoid re-entering traps unnecessarily.