Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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.

License

NotificationsYou must be signed in to change notification settings

ElliotNB/observable-slim

Repository files navigation

Build StatusCoverage StatusMonthly 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. It safely handles circular references, ensures fast O(1) performance through WeakMap/WeakSet identity tables, and keeps paths accurate even when arrays reorder. You can send updates immediately or batch them for smoother UI rendering. It also efficiently manages proxies, lets you pause or validate updates, and cleans up memory safely when data is removed. Lightweight (~6 KB minified), dependency-free, and memory-safe, it's well-suited for UI data binding, state management, audit trails, debugging tools, telemetry/profiling, or collaborative editing using CRDT or OT algorithms.

Table of Contents

Design (Deep Dive)

Curious about the underlying architecture and implementation? Seedocs/design.md for the problem model, core algorithms, complexity, invariants, cycle-safe instrumentation, cross-proxy fan-out, 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.

Features

  • Deep observation of objects and arrays (all nested children)
  • Structured change records with:type,property,currentPath (dot notation),jsonPointer (RFC6901),previousValue,newValue,target,proxy
  • Batched notifications withdomDelay (boolean or ms number)
  • Multiple proxies per target with safe cross-proxy propagation
  • Pause/resume observers andpause/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 atest hook (flushCleanup()) to run pending cleanups immediately
  • TypeScript declarations included (observable-slim.d.ts)

Installation

Browser (UMD):

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

NPM (CommonJS):

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

ES Modules (via bundlers that can import CJS):

importObservableSlimfrom'observable-slim';

Quick Start

conststate={user:{name:'Ada'},todos:[]};constp=ObservableSlim.create(state,true,(changes)=>{// Array of change records batched on a small timeout when domDelay === trueconsole.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 mirrorstarget 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 anarray 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/enableobserver callbacks for the given proxy (no changes are blocked).

ObservableSlim.pauseChanges(proxy) /resumeChanges(proxy)

Disable/enablewrites 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)

Returntrue 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 (climbdepth levels; default1).

ObservableSlim.getPath(proxy, options)

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

  • options = { jsonPointer?: boolean } – whentrue, 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 to0 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 changedproxy:object,// proxy for the targetnewValue:any,previousValue?:any}

Usage Examples

Observer output:

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

consttest={};constp=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"]// }]deletep.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)

constp=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

constdata={foo:{bar:"bar"}};constp1=ObservableSlim.create(data,false,(c)=>console.log("p1",c));ObservableSlim.observe(p1,(c)=>console.log("p1-second",c));constp2=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

constp=ObservableSlim.create({x:0},false,(c)=>console.log("obs",c));ObservableSlim.pause(p);p.x=1;// no observer callbacksObservableSlim.resume(p);ObservableSlim.pauseChanges(p);p.x=2;// observer fires, but underlying target is NOT updatedconsole.log(p.x);// still 0ObservableSlim.resumeChanges(p);p.x=3;// observer fires and target is updated

Introspection helpers and Symbols

conststate={a:{b:1}};constproxy=ObservableSlim.create(state,false,()=>{});console.log(ObservableSlim.isProxy(proxy));// trueconsole.log(ObservableSlim.getTarget(proxy)===state);// trueconstchild=proxy.a;console.log(ObservableSlim.getParent(child)===proxy);// parent proxyconsole.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]);// trueconsole.log(proxy[TARGET]===state);// trueconsole.log(child[PARENT](1)===proxy);// trueconsole.log(child[PATH]);// 'a'

Remove an observable

constp=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 ES2015Proxy,WeakMap andSymbol support.

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

Polyfills cannot fully emulateProxy; 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 thed.ts file for TypeScript declarations.
  • Build (minified):npm run build emits.cjs,.mjs,.js and.d.ts artifacts into thedist folder.

Contributing

Issues and PRs are welcome! Please:

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

License

MIT


Migration from legacy magic properties

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

Legacy (deprecated)New API
proxy.__isProxyObservableSlim.isProxy(proxy) orproxy[ObservableSlim.symbols.IS_PROXY]
proxy.__getTargetObservableSlim.getTarget(proxy) orproxy[ObservableSlim.symbols.TARGET]
proxy.__getParent(depth?)ObservableSlim.getParent(proxy, depth) orproxy[ObservableSlim.symbols.PARENT](depth)
proxy.__getPathObservableSlim.getPath(proxy) orproxy[ObservableSlim.symbols.PATH]

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

About

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.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp