@webqit/observer 3.8.2 → 3.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +918 -284
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,500 +1,1134 @@
1
- # The Observer API
1
+ # Observer
2
2
 
3
- <!-- BADGES/ -->
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![bundle][bundle-src]][bundle-href]
6
+ [![License][license-src]][license-href]
4
7
 
5
- <span class="badge-npmversion"><a href="https://npmjs.org/package/@webqit/observer" title="View this project on NPM"><img src="https://img.shields.io/npm/v/@webqit/observer.svg" alt="NPM version" /></a></span> <span class="badge-npmdownloads"><a href="https://npmjs.org/package/@webqit/observer" title="View this project on NPM"><img src="https://img.shields.io/npm/dm/@webqit/observer.svg" alt="NPM downloads" /></a></span>
8
+ 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 unifies that with related APIs like Reflect and Proxy "traps"!
6
9
 
7
- <!-- /BADGES -->
10
+ The Observer API comes as one little API for all things _object observability_. (Only `~5.8KiB min|zip`)
8
11
 
9
- **[Motivation](#motivation) • [Overview](#an-overview) • [Documentation](#documentation) • [Polyfill](#the-polyfill) • [Getting Involved](#getting-involved) • [License](#license)**
12
+ ```js
13
+ const state = {};
14
+
15
+ // Make mutations observable
16
+ Observer.observe(state, (mutations) => {
17
+ mutations.forEach(mutation => {
18
+ console.log(`${mutation.type}: ${mutation.key} = ${mutation.value}`);
19
+ // React to any mutation: set, delete, defineProperty, etc.
20
+ });
21
+ });
22
+
23
+ // Now these mutations are observable and reactive
24
+ Observer.set(state, 'count', 5);
25
+ Observer.deleteProperty(state, 'oldProp');
26
+ ```
10
27
 
11
- 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)!
28
+ > [!TIP]
29
+ > While reactivity is anchored on the programmtic APIs — `.set()`, `.deleteProperty()`, etc. — reactivity is also possible over literal JavaScript operations like `obj.prop = value`, `delete obj.prop` — by means of the `accessorize()` and `proxy()` methods covered just ahead.
30
+ >
31
+ > For full-fledged Imperative Reactive Programming, you may to see the [Quantum JS](https://github.com/webqit/quantum-js) project.
12
32
 
13
- Observer API is an upcoming proposal!
33
+ <details><summary>Looking for Observer@1.x?</summary>
14
34
 
15
- ## Motivation
35
+ This documentation is for Observer@2.x. For the previous version, see [Observer@1.x](https://github.com/webqit/observer/tree/v1.7.6).
16
36
 
17
- 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!
37
+ </details>
18
38
 
19
- 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)
39
+ - [Why Observer](#why-observer)
40
+ - [Quick Start](#quick-start)
41
+ - [Key Features](#key-features)
42
+ - [Ecosystem Integrations](#ecosystem-integrations)
43
+ - [API Reference](#api-reference)
44
+ - [Extended Documentation](#extended-documentation)
45
+ - [Contributing](#contributing)
20
46
 
21
- 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!
47
+ ## Why Observer?
22
48
 
23
- ## Status
49
+ 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.
24
50
 
25
- + Working implementation via a polyfill
26
- + Integral to the [Quantum JS project](https://github.com/webqit/quantum-js)
27
- + Actively developed
28
- + Open to contributions
51
+ **The Problem:**
29
52
 
30
- ## An Overview
53
+ ```js
54
+ const state = { count: 0, items: [] };
31
55
 
32
- The Observer API is a set of utility functions - notably, the `Observer.observe()` and `Observer.intercept()` methods - for all things object observability.
56
+ // No way to observe/intercept these mutations in JavaScript
57
+ state.count = 5;
58
+ state.items.push('new item');
59
+ delete state.oldProp;
33
60
 
34
- <details><summary>This is documentation for Observer@2.x</summary>
61
+ // No way to detect these changes
62
+ ```
35
63
 
36
- Looking for [`Observer@1.x`](https://github.com/webqit/observer/tree/v1.7.6)?
64
+ This limitation in the language has long created a **blindspot** — and a **weakness** — for reactive systems. Consequently:
37
65
 
38
- </details>
39
-
40
- ### Method: `Observer.observe()`
41
-
42
- Observe mutations on arbitrary objects or arrays!
66
+ + reactive frameworks (like React, Vue) learned to forbid mutability
67
+ + *immutability* became the default workaround. You don't mutate, you create a new object each time:
68
+
69
+ ```js
70
+ state = { ...state, count: 6 };
71
+ state = { ...state, count: 7 };
72
+ state = { ...state, count: 8 };
73
+ ```
43
74
 
44
75
  ```js
45
- // An object
46
- const obj = {};
47
- // Mtation observer on an object
48
- const abortController = Observer.observe( obj, inspect );
76
+ state = { ...state, items: [...state.items, 'new item 1'] };
77
+ state = { ...state, items: [...state.items, 'new item 2'] };
78
+ state = { ...state, items: [...state.items, 'new item 3'] };
79
+ ```
80
+
81
+ > Because this is generally hard to follow, frameworks typically enforce immutability via 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.
82
+
83
+ + mutation gets a bad rap
84
+
85
+ **Using the Observer API:**
86
+
87
+ By enabling observability at the object/array level, the Observer API effectively solves reactivity for a mutable world. Consequently:
88
+
89
+ + you are able to weild *the sheer power of mutability* in programming unappologetically
90
+ + you are able to make sense of a mutable world — and integrate with it — rather than see it as somethng to fight.
91
+
92
+ The Observer API collapses layers of complexity that reactive frameworks have built around immutability, bringing you back to the simplicity and power of direct mutation—but now with full observability.
93
+
94
+ **The Result:** Mutation-based reactivity as a first-class concept in JavaScript. Observer enables frameworks to embrace mutability instead of avoiding it — with [Quantum JS](https://github.com/webqit/quantum-js) taking this foundation to create imperative reactive programming.
95
+
96
+ ## Quick Start
97
+
98
+ Observer provides a simple API for watching object and array changes.
99
+
100
+ ### Installation
101
+
102
+ ```bash
103
+ npm install @webqit/observer
49
104
  ```
50
105
 
51
106
  ```js
52
- // An array
53
- const arr = [];
54
- // Mtation observer on an array
55
- const abortController = Observer.observe( arr, inspect );
107
+ import Observer from '@webqit/observer';
56
108
  ```
57
109
 
58
- *Changes are delivered [**synchronously**](https://github.com/webqit/observer/wiki/#timing-and-batching) - as they happen.*
110
+ ### CDN
59
111
 
60
- ```js
61
- // The change handler
62
- function inspect( mutations ) {
63
- mutations.forEach( mutation => {
64
- console.log( mutation.type, mutation.key, mutation.value, mutation.oldValue );
65
- } );
66
- }
112
+ ```html
113
+ <script src="https://unpkg.com/@webqit/observer/dist/main.js"></script>
114
+ <script>
115
+ const Observer = window.webqit.Observer;
116
+ </script>
67
117
  ```
68
118
 
69
- **-->** Stop observing at any time by calling `abort()` on the returned *abortController*:
119
+ ### Basic Usage
70
120
 
71
121
  ```js
72
- // Remove listener
73
- abortController.abort();
122
+ import Observer from '@webqit/observer';
123
+
124
+ const user = { name: 'John', items: [] };
125
+
126
+ // Watch for changes
127
+ const controller = Observer.observe(user, (mutations) => {
128
+ mutations.forEach(mutation => {
129
+ console.log(`Changed ${mutation.key}: ${mutation.oldValue} → ${mutation.value}`);
130
+ });
131
+ });
132
+
133
+ // Make changes using programmatic APIs
134
+ Observer.set(user, 'name', 'Jane');
135
+ Observer.set(user, 'age', 26);
136
+
137
+ // Stop watching
138
+ controller.abort();
74
139
  ```
75
140
 
76
- And you can provide your own [Abort Signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) instance:
141
+ ### Working with Arrays
77
142
 
78
143
  ```js
79
- // Providing an AbortSignal
80
- const abortController = new AbortController;
81
- Observer.observe( obj, inspect, { signal: abortController.signal } );
144
+ const items = ['apple', 'banana'];
145
+
146
+ Observer.observe(items, (mutations) => {
147
+ console.log('Array changed:', mutations);
148
+ });
149
+
150
+ // Use programmatic APIs for mutations
151
+ Observer.set(items, 0, 'grape');
152
+ Observer.set(items, 2, 'orange');
153
+
154
+ // Reactive method calls
155
+ Observer.apply(items.push, items, ['new item']); // Observer.proxy(state.items).push('new item')
82
156
  ```
83
157
 
158
+ ### Intercepting Operations
159
+
84
160
  ```js
85
- // Abort at any time
86
- abortController.abort();
161
+ // Transform values before they're set
162
+ Observer.intercept(user, 'set', (operation, previous, next) => {
163
+ if (operation.key === 'email') {
164
+ operation.value = operation.value.toLowerCase();
165
+ }
166
+ return next();
167
+ });
168
+
169
+ Observer.set(user, 'email', 'JOHN@EXAMPLE.COM'); // Becomes 'john@example.com'
87
170
  ```
88
171
 
89
- **-->** Where listeners initiate nested observers (child observers), leverage "AbortSignal-cascading" to tie child observers to parent observer's lifecycle:
172
+ ## Key Features
173
+
174
+ ### **Core Reactivity**
175
+ - **🔄 Real-time Observation**: Watch object and array changes as they happen
176
+ - **⚡ Synchronous Updates**: Changes are delivered immediately, not batched
177
+ - **🎯 Granular Control**: Watch specific properties, paths, or entire objects
178
+ - **🌳 Deep Path Watching**: Observe nested properties with wildcards and subtrees
90
179
 
180
+ ### **Advanced Capabilities**
181
+ - **🛡️ Operation Interception**: Transform, validate, or block operations before execution
182
+ - **📦 Atomic Batching**: Group multiple changes into single events
183
+ - **🔄 Object Mirroring**: Create reactive synchronization between objects
184
+ - **🔗 Traps Pipeline**: Compose multiple interceptors for complex behavior
185
+
186
+ ### **Developer Experience**
187
+ - **🔧 Utility-First API**: Clean, functional design with consistent patterns
188
+ - **📱 Universal Support**: Works in browsers, Node.js, and all JavaScript environments
189
+ - **🔌 Standard Integration**: Built on AbortSignal, Reflect API, and Proxy standards
190
+ - **📊 Lightweight**: Only ~5.8KB min+gz with zero dependencies
191
+
192
+ ## Ecosystem Integrations
193
+
194
+ The Observer API is enabling a shared protocol of mutation-based reactivity across the ecosystem:
195
+
196
+ ### **🚀 [Quantum Runtime](https://github.com/webqit/quantum-js)**
197
+ 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.
198
+
199
+ ### **🌐 [OOHTML](https://github.com/webqit/oohtml)**
200
+ Uses Observer API to underpin **reactive HTML templating**. OOHTML leverages Observer's reactivity layer to create dynamic, data-driven templates with seamless integration between data and presentation.
201
+
202
+ ### **⚡ [Webflo](https://github.com/webqit/webflo)**
203
+ Uses Observer API to underpin **Live Objects** as a first-class concept. Webflo leverages Observer's reactivity foundation to enable real-time data synchronization across client and server.
204
+
205
+ ### **🔗 [LinkedQL](https://github.com/linked-db/linked-ql)**
206
+ Uses Observer API to underpin **Live Objects** as a first-class concept. LinkedQL leverages Observer's reactivity foundation to provide reactive database operations with real-time data synchronization.
207
+
208
+ ## API Reference
209
+
210
+ - [Observer.observe()](#observerobservertarget-callback-options)
211
+ - [Observer.intercept()](#observerintercepttarget-operation-handler-options)
212
+ - [Observer.set()](#observersettarget-key-value-options)
213
+ - [Observer.get()](#observergettarget-key-options)
214
+ - [Observer.has()](#observerhastarget-key-options)
215
+ - [Observer.ownKeys()](#observerownkeystarget-options)
216
+ - [Observer.deleteProperty()](#observerdeletepropertytarget-key-options)
217
+ - [Observer.deleteProperties()](#observerdeletepropertiestarget-keys-options)
218
+ - [Observer.defineProperty()](#observerdefinepropertytarget-key-descriptor-options)
219
+ - [Observer.defineProperties()](#observerdefinepropertiestarget-descriptors-options)
220
+ - [Observer.accessorize()](#observeraccessorizetarget-properties-options)
221
+ - [Observer.unaccessorize()](#observerunaccessorizetarget-properties-options)
222
+ - [Observer.proxy()](#observerproxytarget-options)
223
+ - [Observer.unproxy()](#observerunproxytarget-options)
224
+ - [Observer.path()](#observerpathsegments)
225
+ - [Observer.batch()](#observerbatchtarget-callback-options)
226
+ - [Observer.map()](#observermapsource-target-options)
227
+ - [Observer.any()](#observerany)
228
+ - [Observer.subtree()](#observersubtree)
229
+ - [Other Methods](#other-methods)
230
+
231
+ ### `Observer.observe(target, callback, options?)`
232
+
233
+ Watch for changes on an object or array.
234
+ Returns an AbortController for lifecycle management.
235
+
236
+ **Basic Usage:**
91
237
  ```js
92
- // Parent -
93
- const abortController = Observer.observe( obj, ( mutations, flags ) => {
238
+ const obj = {};
239
+ const controller = Observer.observe(obj, (mutations) => {
240
+ mutations.forEach(mutation => {
241
+ console.log(`${mutation.type}: ${mutation.key} = ${mutation.value}`);
242
+ });
243
+ });
244
+
245
+ // Changes are delivered synchronously
246
+ Observer.set(obj, 'name', 'Bob');
247
+ Observer.set(obj, 'age', 30);
248
+
249
+ // Stop observing
250
+ controller.abort();
251
+ ```
94
252
 
95
- // Child
96
- Observer.observe( obj, inspect, { signal: flags.signal } ); // <<<---- AbortSignal-cascading
253
+ **Alternative Method Shapes:**
254
+ ```js
255
+ // Watch specific properties
256
+ Observer.observe(obj, ['name', 'email'], callback);
97
257
 
98
- // Child
99
- Observer.observe( obj, inspect, { signal: flags.signal } ); // <<<---- AbortSignal-cascading
258
+ // Watch a single property
259
+ Observer.observe(obj, 'name', callback);
100
260
 
101
- } );
261
+ // Watch all properties (default)
262
+ Observer.observe(obj, callback);
102
263
  ```
103
264
 
104
- └ *"Child" observers get automatically aborted at parent's "next turn", and at parent's own abortion!*
265
+ **Options:**
266
+ - `signal`: A custom AbortSignal instance that control lifecycle
267
+ - `diff`: Only fire for values that actually changed
268
+ - `recursions`: Controls recursion handling (`'inject'`, `'force-sync'`, `'force-async'`)
269
+ - `withPropertyDescriptors`: Include property descriptor information
105
270
 
106
- **-->** Use the `options.diff` parameter to ignore mutation events whose current value is same as previous value:
271
+ #### Abort Signals
272
+
273
+ Observer returns a standard [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) instance for managing observer lifecycle.
107
274
 
108
275
  ```js
109
- // Parent -
110
- const abortController = Observer.observe( obj, mutations => {
111
- console.log( m.type, m.value, m.oldValue );
112
- }, { diff: true } );
276
+ // Returns AbortController for lifecycle management
277
+ const controller = Observer.observe(obj, callback);
278
+ controller.abort(); // Stop observing
279
+
280
+ // Provide your own AbortSignal
281
+ const abortController = new AbortController();
282
+ Observer.observe(obj, callback, { signal: abortController.signal });
283
+ abortController.abort(); // Stop observing
113
284
  ```
114
285
 
286
+ Or, you can provide your own:
287
+
115
288
  ```js
116
- obj.property = 'Same value';
289
+ // Providing an AbortSignal
290
+ const abortController = new AbortController;
291
+ Observer.observe(obj, inspect, { signal: abortController.signal });
117
292
  ```
118
293
 
119
294
  ```js
120
- obj.property = 'Same value';
295
+ // Abort at any time
296
+ abortController.abort();
121
297
  ```
122
298
 
123
- *Observer is called only on the first update!*
124
-
125
- #### Concept: *Mutation APIs*
299
+ #### Lifecycle Signals
126
300
 
127
- 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:
301
+ 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.
302
+ Leverage this to simplify hierarchical observer patterns.
128
303
 
129
304
  ```js
130
- // A single "set" operation on an object
131
- Observer.set( obj, 'prop0', 'value0' );
132
- Observer.defineProperty( obj, 'prop1', { get: () => 'value1' } );
133
- Observer.deleteProperty( obj, 'prop2' );
305
+ // Parent observer with lifecycle management
306
+ const parentController = Observer.observe(obj, (mutations, flags) => {
307
+ // Child observers automatically abort when parent aborts
308
+ Observer.observe(obj, childCallback, { signal: flags.signal });
309
+
310
+ // Multiple child observers tied to parent lifecycle
311
+ Observer.observe(obj, anotherCallback, { signal: flags.signal });
312
+ });
313
+
314
+ // All child observers abort when parent aborts
315
+ parentController.abort();
134
316
  ```
135
317
 
318
+ #### Parity Table
319
+
320
+ | | Observer API | Object.observe() (Deprecated) |
321
+ |-------------|--------------|-------------------------------|
322
+ | **Signature** | `.observe(target, callback, options?)` | `.observe(target, callback, acceptList?)` |
323
+ | **Return Value** | `AbortController` (lifecycle management) | `undefined` (no lifecycle management) |
324
+ | **Additional Features** | AbortSignal integration, path watching, batch/atomic operations, synchronous event model, etc. | Basic object observation, asynchronous event model (deprecated) |
325
+
326
+ ### `Observer.intercept(target, operation, handler, options?)`
327
+
328
+ Intercept operations before they happen.
329
+ Can intercept individual operations or multiple operations at once.
330
+
331
+ **Single Operation Interception:**
332
+
333
+ Intercept individual operations to transform, validate, or block them before they execute.
334
+
136
335
  ```js
137
- // A single "set" operation on an array
138
- Observer.set( arr, 0, 'item0' ); // Array [ 'item0' ]
139
- Observer.deleteProperty( arr, 0 ); // Array [ <1 empty slot> ]
336
+ // Transform values before they're set
337
+ Observer.intercept(obj, 'set', (operation, previous, next) => {
338
+ if (operation.key === 'email') {
339
+ operation.value = operation.value.toLowerCase();
340
+ }
341
+ return next();
342
+ });
343
+
344
+ Observer.set(obj, 'email', 'JOHN@EXAMPLE.COM'); // Becomes 'john@example.com'
140
345
  ```
141
346
 
142
- <details><summary>Polyfill limitations</summary>
347
+ **Multiple Operations Interception:**
143
348
 
144
- In the polyfill, object observability doesn't work with literal operations. **Beware non-reactive operations**:
349
+ Intercept multiple operations simultaneously to create comprehensive behavior modifications.
145
350
 
146
351
  ```js
147
- // Literal object operators
148
- delete obj.prop0;
149
- obj.prop3 = 'value3';
352
+ const options = {};
353
+ Observer.intercept(obj, {
354
+ set: (operation, previous, next) => {
355
+ if (operation.key === 'email') {
356
+ operation.value = operation.value.toLowerCase();
357
+ }
358
+ return next();
359
+ },
360
+ get: (operation, previous, next) => {
361
+ if (operation.key === 'token') {
362
+ return next(fetchToken());
363
+ }
364
+ return next();
365
+ },
366
+ deleteProperty: (operation, previous, next) => {
367
+ if (operation.key === 'password') {
368
+ console.log('Password deletion blocked');
369
+ return false; // Block the operation
370
+ }
371
+ return next();
372
+ }
373
+ }, options);
150
374
  ```
151
375
 
376
+ **Traps Pipeline:**
377
+
378
+ 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.
379
+
152
380
  ```js
153
- // Array methods
154
- arr.push( 'item3' );
155
- arr.pop();
381
+ // First interceptor: Transform email to lowercase
382
+ Observer.intercept(obj, 'get', (operation, previous, next) => {
383
+ if (operation.key === 'email') {
384
+ const result = next();
385
+ return result ? result.toLowerCase() : result;
386
+ }
387
+ return next();
388
+ });
389
+
390
+ // Second interceptor: Add validation
391
+ Observer.intercept(obj, 'get', (operation, previous, next) => {
392
+ if (operation.key === 'email') {
393
+ const result = next();
394
+ if (result && !result.includes('@')) {
395
+ throw new Error('Invalid email format');
396
+ }
397
+ return result;
398
+ }
399
+ return next();
400
+ });
401
+
402
+ // Now when accessing email, both interceptors run in sequence:
403
+ // 1. First: transforms to lowercase
404
+ // 2. Second: validates format
405
+ // Result: 'JOHN@EXAMPLE.COM' → 'john@example.com' → validation passes
156
406
  ```
157
407
 
158
- </details>
408
+ #### Interceptable Operations
159
409
 
160
- **-->** Enable reactivity on *specific* properties with literal *object accessors* - using the `Observer.accessorize()` method:
410
+ - `set` - Property assignment
411
+ - `get` - Property access
412
+ - `has` - Property existence check
413
+ - `ownKeys` - Object key enumeration
414
+ - `deleteProperty` - Property deletion
415
+ - `defineProperty` - Property definition
416
+ - `getOwnPropertyDescriptor` - Property descriptor access
417
+
418
+ #### Parity Table
419
+
420
+ | | Observer API | Proxy Traps |
421
+ |-------------|--------------|-------------|
422
+ | **Signature** | `.intercept(target, operation, handler, options?)`<br>`.intercept(target, { [operation]: handler[, ...]}, options?)` | `new Proxy(target, { [operation]: handler[, ...] })` |
423
+ | **Return Value** | `undefined` (registration) | `Proxy` (wrapped object) |
424
+ | **Additional Features** | Traps pipeline, composable interceptors | Single trap per operation, no composability |
425
+
426
+ ---
427
+
428
+ ### `Observer.set(target, key, value, options?)`
429
+
430
+ Set properties _reactively_ using a programmatic mutation API.
431
+ Triggers observers and can be intercepted via `Observer.intercept()`.
432
+
433
+ **Basic Usage:**
161
434
 
162
435
  ```js
163
- // Accessorize all current enumerable properties
164
- Observer.accessorize( obj );
165
- // Accessorize specific properties (existing or new)
166
- Observer.accessorize( obj, [ 'prop0', 'prop1', 'prop2' ] );
436
+ Observer.set(obj, 'name', 'Alice');
437
+ Observer.set(arr, 0, 'first item');
438
+ ```
439
+
440
+ **Alternative Method Shapes:**
167
441
 
168
- // Make reactive UPDATES
169
- obj.prop0 = 'value0';
170
- obj.prop1 = 'value1';
171
- obj.prop2 = 'value2';
442
+ ```js
443
+ // Set multiple properties at once
444
+ Observer.set(obj, {
445
+ name: 'Alice',
446
+ age: 25,
447
+ email: 'alice@example.com'
448
+ });
449
+
450
+ // Set with receiver context
451
+ Observer.set(obj, 'name', 'Alice', receiver);
172
452
  ```
173
453
 
454
+ #### Usage Patterns
455
+
174
456
  ```js
175
- // Accessorize all current indexes
176
- Observer.accessorize( arr );
177
- // Accessorize specific indexes (existing or new)
178
- Observer.accessorize( arr, [ 0, 1, 2 ] );
457
+ // Reactive state updates
458
+ Observer.set(state, 'loading', true);
459
+ Observer.set(state, 'data', responseData);
179
460
 
180
- // Make reactive UPDATES
181
- arr[ 0 ] = 'item0';
182
- arr[ 1 ] = 'item1';
183
- arr[ 2 ] = 'item2';
461
+ // Array operations
462
+ Observer.set(items, 0, 'new item');
463
+ Observer.set(items, items.length, 'append item');
184
464
 
185
- // Bonus reactivity with array methods that re-index existing items
186
- arr.unshift( 'new-item0' );
187
- arr.shift();
465
+ // Nested property updates
466
+ Observer.set(obj, Observer.path('user', 'profile', 'name'), 'Alice');
188
467
  ```
189
468
 
190
- <details><summary>Polyfill limitations</summary>
469
+ #### Parity Table
470
+
471
+ | | Observer API | Reflect API |
472
+ |-------------|--------------|-------------|
473
+ | **Signature** | `.set(target, key, value, options?)` | `.set(target, key, value)` |
474
+ | **Return Value** | `boolean` (success) | `boolean` (success) |
475
+ | **Additional Features** | Triggers observers, interceptable | Standard property setting |
476
+
477
+ ### `Observer.get(target, key, options?)`
191
478
 
192
- In the polyfill, object observability doesn't work with literal operations. **Beware non-reactive operations**:
479
+ Get properties using a programmatic API.
480
+ Can be intercepted via `Observer.intercept()` to provide computed values or transformations.
481
+
482
+ **Basic Usage:**
193
483
 
194
484
  ```js
195
- // The delete operator and object properties that haven't been accessorized
196
- delete obj.prop0;
197
- obj.prop3 = 'value3';
485
+ const value = Observer.get(obj, 'name');
486
+ const nested = Observer.get(obj, 'user.profile.name');
198
487
  ```
199
488
 
489
+ **_Scenario_: Computed Properties:**
490
+
200
491
  ```js
201
- // Array methods that do not re-index existing items
202
- arr.push( 'item0' );
203
- arr.pop();
492
+ // Intercept to provide computed values
493
+ Observer.intercept(obj, 'get', (operation, previous, next) => {
494
+ if (operation.key === 'fullName') {
495
+ return `${obj.firstName} ${obj.lastName}`;
496
+ }
497
+ return next();
498
+ });
499
+
500
+ Observer.get(obj, 'fullName'); // "John Doe" (computed on-the-fly)
204
501
  ```
205
502
 
206
- </details>
503
+ #### Parity Table
207
504
 
208
- **-->** Enable reactivity on *arbitray* properties with *Proxies* - using the `Observer.proxy()` method:
505
+ | | Observer API | Reflect API |
506
+ |-------------|--------------|-------------|
507
+ | **Signature** | `.get(target, key, options?)` | `.get(target, key)` |
508
+ | **Return Value** | `any` (property value) | `any` (property value) |
509
+ | **Additional Features** | Interceptable for computed values | Standard property access |
209
510
 
210
- ```js
211
- // Obtain a reactive Proxy for an object
212
- const $obj = Observer.proxy( obj );
511
+ ### `Observer.has(target, key, options?)`
213
512
 
214
- // Make reactive operations
215
- $obj.prop1 = 'value1';
216
- $obj.prop4 = 'value4';
217
- $obj.prop8 = 'value8';
513
+ Check if a property exists on an object.
514
+ Can be intercepted via `Observer.intercept()` to hide or reveal properties dynamically.
218
515
 
219
- // With the delete operator
220
- delete $obj.prop0;
221
- ```
516
+ **Basic Usage:**
222
517
 
223
518
  ```js
224
- // Obtain a reactive Proxy for an array
225
- const $arr = Observer.proxy( arr );
519
+ Observer.has(obj, 'name'); // true/false
520
+ Observer.has(obj, 'user.profile.name'); // nested property check
521
+ ```
226
522
 
227
- // Make reactive operations
228
- $arr[ 0 ] = 'item0';
229
- $arr[ 1 ] = 'item1';
230
- $arr[ 2 ] = 'item2';
523
+ **_Scenario_: Property Hiding:**
231
524
 
232
- // With an instance method
233
- $arr.push( 'item3' );
525
+ ```js
526
+ // Intercept to hide sensitive properties
527
+ Observer.intercept(obj, 'has', (operation, previous, next) => {
528
+ if (operation.key === 'password') {
529
+ return false; // Hide password from property checks
530
+ }
531
+ return next();
532
+ });
533
+
534
+ Observer.has(obj, 'password'); // false (hidden from checks)
234
535
  ```
235
536
 
236
- *And no problem if you end up nesting the approaches.*
537
+ #### Parity Table
237
538
 
238
- ```js
239
- // 'value1'-->obj
240
- Observer.accessorize( obj, [ 'prop0', 'prop1', 'prop2', ] );
241
- obj.prop1 = 'value1';
539
+ | | Observer API | Reflect API |
540
+ |-------------|--------------|-------------|
541
+ | **Signature** | `.has(target, key, options?)` | `.has(target, key)` |
542
+ | **Return Value** | `boolean` (exists) | `boolean` (exists) |
543
+ | **Additional Features** | Interceptable for property hiding | Standard property existence check |
544
+
545
+ ### `Observer.ownKeys(target, options?)`
242
546
 
243
- // 'value1'-->$obj-->obj
244
- let $obj = Observer.proxy( obj );
245
- $obj.prop1 = 'value1';
547
+ Get all own property keys of an object.
548
+ Can be intercepted via `Observer.intercept()` to filter or transform the key list.
246
549
 
247
- // 'value1'-->set()-->$obj-->obj
248
- Observer.set( $obj, 'prop1', 'value1' );
550
+ **Basic Usage:**
551
+
552
+ ```js
553
+ Observer.ownKeys(obj); // ['name', 'email', 'age']
249
554
  ```
250
555
 
251
- **-->** "Restore" accessorized properties to their normal state by calling the `unaccessorize()` method:
556
+ **_Scenario_: Key Filtering:**
252
557
 
253
558
  ```js
254
- Observer.unaccessorize( obj, [ 'prop1', 'prop6', 'prop10' ] );
559
+ // Intercept to filter out sensitive keys
560
+ Observer.intercept(obj, 'ownKeys', (operation, previous, next) => {
561
+ const keys = next();
562
+ return keys.filter(key => key !== 'password');
563
+ });
564
+
565
+ Observer.ownKeys(obj); // ['name', 'email'] (password filtered out)
255
566
  ```
256
567
 
257
- **-->** "Reproduce" original objects from Proxies obtained via `Observer.proxy()` by calling the `unproxy()` method:
568
+ #### Parity Table
258
569
 
570
+ | | Observer API | Reflect API | Object API |
571
+ |-------------|--------------|-------------|-------------|
572
+ | **Signature** | `.ownKeys(target, options?)` | `.ownKeys(target)` | `.keys(obj)` |
573
+ | **Return Value** | `string[]` (keys) | `string[]` (keys) | `string[]` (keys) |
574
+ | **Additional Features** | Interceptable for key filtering | Standard key enumeration | Standard key enumeration |
575
+
576
+ ### `Observer.deleteProperty(target, key, options?)`
577
+
578
+ Delete properties _reactively_ using a programmatic mutation API.
579
+
580
+ **Basic Usage:**
259
581
  ```js
260
- obj = Observer.unproxy( $obj );
582
+ Observer.deleteProperty(obj, 'oldProp');
583
+ Observer.deleteProperty(arr, 0);
261
584
  ```
262
585
 
263
- #### Concept: *Paths*
586
+ #### Parity Table
587
+
588
+ | | Observer API | Reflect API |
589
+ |-------------|--------------|-------------|
590
+ | **Signature** | `.deleteProperty(target, key, options?)` | `.deleteProperty(target, key)` |
591
+ | **Return Value** | `boolean` (success) | `boolean` (success) |
592
+ | **Additional Features** | Triggers observers, interceptable | Standard property deletion |
264
593
 
265
- Observe "a property" at a path in an object tree:
594
+ ### `Observer.deleteProperties(target, keys, options?)`
595
+
596
+ Delete multiple properties at once.
266
597
 
267
598
  ```js
268
- const obj = {
269
- level1: {
270
- level2: 'level2-value',
271
- },
272
- };
599
+ Observer.deleteProperties(obj, ['oldProp1', 'oldProp2', 'tempProp']);
273
600
  ```
274
601
 
602
+ #### Parity Table
603
+
604
+ | | Observer API | No Direct Equivalent |
605
+ |-------------|--------------|---------------------|
606
+ | **Signature** | `.deleteProperties(target, keys, options?)` | No batch delete in standard APIs |
607
+ | **Return Value** | `boolean[]` (success array) | N/A |
608
+ | **Additional Features** | Triggers observers, interceptable | N/A |
609
+
610
+ ### `Observer.defineProperty(target, key, descriptor, options?)`
611
+
612
+ Define properties _reactively_ using the programmatic mutation API.
613
+
614
+ **Basic Usage:**
615
+
275
616
  ```js
276
- const path = Observer.path( 'level1', 'level2' );
277
- Observer.observe( obj, path, m => {
278
- console.log( m.type, m.path, m.value, m.isUpdate );
279
- } );
617
+ Observer.defineProperty(obj, 'computed', {
618
+ get: () => obj.value * 2
619
+ });
280
620
  ```
281
621
 
622
+ #### Parity Table
623
+
624
+ | | Observer API | Reflect API | Object API |
625
+ |-------------|--------------|-------------|-------------|
626
+ | **Signature** | `.defineProperty(target, key, descriptor, options?)` | `.defineProperty(target, key, descriptor)` | `.defineProperty(obj, key, descriptor)` |
627
+ | **Return Value** | `boolean` (success) | `boolean` (success) | `object` (modified object) |
628
+ | **Additional Features** | Triggers observers, interceptable | Standard property definition | Standard property definition |
629
+
630
+ ### `Observer.defineProperties(target, descriptors, options?)`
631
+
632
+ Define multiple properties at once.
633
+
282
634
  ```js
283
- Observer.set( obj.level1, 'level2', 'level2-new-value' );
635
+ Observer.defineProperties(obj, {
636
+ name: { value: 'Alice', writable: true },
637
+ email: { value: 'alice@example.com', writable: true },
638
+ age: { value: 25, writable: true }
639
+ });
284
640
  ```
285
641
 
286
- <details><summary>Console</summary>
642
+ #### Parity Table
287
643
 
288
- | type | path | value | isUpdate |
289
- | ---- | ---- | ----- | -------- |
290
- | `set` | [ `level1`, `level2`, ] | `level2-new-value` | `true` |
644
+ | | Observer API | Object API |
645
+ |-------------|--------------|-------------|
646
+ | **Signature** | `.defineProperties(target, descriptors, options?)` | `.defineProperties(obj, descriptors)` |
647
+ | **Return Value** | `boolean` (success) | `object` (modified object) |
648
+ | **Additional Features** | Triggers observers, interceptable | Standard property definition |
291
649
 
292
- </details>
650
+ ---
293
651
 
294
- *And the initial tree structure can be whatever*:
652
+ ### `Observer.accessorize(target, properties?, options?)`
653
+
654
+ Make properties reactive for direct assignment.
295
655
 
296
656
  ```js
297
- // A tree structure that is yet to be built
298
- const obj = {};
657
+ const obj = { age: null };
658
+
659
+ // Make all CURRENT properties reactive
660
+ Observer.accessorize(obj);
661
+
662
+ // Make specific properties reactive
663
+ Observer.accessorize(obj, ['name', 'email']);
664
+
665
+ // Now direct assignment works
666
+ obj.name = 'Alice';
667
+ obj.email = 'alice@example.com';
299
668
  ```
300
669
 
670
+ #### Parity Table
671
+
672
+ | | Observer API | No Direct Equivalent |
673
+ |-------------|--------------|---------------------|
674
+ | **Signature** | `.accessorize(target, properties?, options?)` | No direct equivalent in standard APIs |
675
+ | **Return Value** | `undefined` (modification) | N/A |
676
+ | **Additional Features** | Makes properties reactive for direct assignment | N/A |
677
+
678
+ ### `Observer.unaccessorize(target, properties?)`
679
+
680
+ Restore accessorized properties to their normal state.
681
+
301
682
  ```js
302
- const path = Observer.path( 'level1', 'level2', 'level3', 'level4' );
303
- Observer.observe( obj, path, m => {
304
- console.log( m.type, m.path, m.value, m.isUpdate );
305
- } );
683
+ // Restore specific properties
684
+ Observer.unaccessorize(obj, ['name', 'email'], options?);
685
+
686
+ // Restore all accessorized properties
687
+ Observer.unaccessorize(obj);
306
688
  ```
307
689
 
308
- *Now, any operation that changes what "the value" at the path resolves to - either by tree extension or tree truncation - will fire our listener*:
690
+ #### Parity Table
691
+
692
+ | | Observer API | No Direct Equivalent |
693
+ |-------------|--------------|---------------------|
694
+ | **Signature** | `.unaccessorize(target, properties?, options?)` | No direct equivalent in standard APIs |
695
+ | **Return Value** | `undefined` (modification) | N/A |
696
+ | **Additional Features** | Restores accessorized properties to normal state | N/A |
697
+
698
+ ---
699
+
700
+ ### `Observer.proxy(target, options?)`
701
+
702
+ Create a reactive proxy of any object to get automatic reactivity and interceptibility over on-the-fly operations.
703
+
704
+ **Basic Usage:**
309
705
 
310
706
  ```js
311
- Observer.set( obj, 'level1', { level2: {}, } );
707
+ const $obj = Observer.proxy(obj);
708
+
709
+ // All operations are reactive
710
+ $obj.name = 'Alice'; // Triggers observers
711
+ $obj.newProp = 'value'; // Triggers observers
712
+ delete $obj.oldProp; // Triggers observers
713
+
714
+ // Array methods are reactive
715
+ $arr.push('item1', 'item2'); // Triggers observers
716
+ $arr[0] = 'newValue'; // Triggers observers
312
717
  ```
313
718
 
314
- <details>
315
- <summary>Console</summary>
719
+ #### Nested Operations (Requires `chainable: true`)
316
720
 
317
- | type | path | value | isUpdate |
318
- | ---- | ---- | ----- | -------- |
319
- | `set` | [ `level1`, `level2`, `level3`, `level4`, ] | `undefined` | `false` |
721
+ 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.
320
722
 
321
- </details>
723
+ ```js
724
+ const $obj = Observer.proxy(obj, { chainable: true });
322
725
 
323
- *Meanwhile, this next one completes the tree, and the listener reports a value at its observed path*:
726
+ // Nested objects are automatically proxied when accessed
727
+ const $user = $obj.user; // Returns a proxied object
728
+ $user.name = 'Alice'; // Triggers observers
729
+ $user.profile.theme = 'dark'; // Triggers observers
730
+
731
+ // Array methods return proxied arrays
732
+ const $filtered = $obj.items.filter(x => x.active); // Returns proxied array
733
+ $filtered.push('newItem'); // Triggers observers
734
+
735
+ // Direct nested access also works
736
+ $obj.users[0].name = 'Bob'; // Triggers observers
737
+ $obj.data.splice(0, 1); // Triggers observers
738
+ ```
739
+
740
+ #### Membrane Mode
741
+
742
+ 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.
324
743
 
325
744
  ```js
326
- Observer.set( obj.level1, 'level2', { level3: { level4: 'level4-value', }, } );
745
+ // Create membrane for consistent proxy identity
746
+ const $obj1 = Observer.proxy(obj, { membrane: 'userData' });
747
+ const $obj2 = Observer.proxy(obj, { membrane: 'userData' });
748
+
749
+ // Same proxy instance returned
750
+ console.log($obj1 === $obj2); // true
751
+
752
+ // Root operations are reactive
753
+ $obj1.name = 'Alice'; // Triggers observers
754
+ $obj2.email = 'alice@example.com'; // Triggers observers (same proxy)
755
+
756
+ // When combined with chainable: true
757
+ const $obj3 = Observer.proxy(obj, { membrane: 'userData', chainable: true });
758
+ const $user1 = $obj3.user;
759
+ const $user2 = $obj3.user;
760
+
761
+ // Same nested proxy instance returned
762
+ console.log($user1 === $user2); // true
763
+ $user1.name = 'Alice'; // Triggers observers
327
764
  ```
328
765
 
329
- <details>
330
- <summary>Console</summary>
766
+ #### How Membranes Work
331
767
 
332
- | type | path | value | isUpdate |
333
- | ---- | ---- | ----- | -------- |
334
- | `set` | [ `level1`, `level2`, `level3`, `level4`, ] | `level4-value` | `false` |
768
+ - **Root Object Identity** - Same root object always returns the same proxy instance across multiple `.proxy()` calls
769
+ - **Membrane References** - Uses a reference system to ensure consistent proxy identity
770
+ - **Nested Object Identity** - When combined with `chainable: true`, ensures same nested objects return same proxy instances
771
+ - **Performance** - Only creates one proxy per object (root or nested)
772
+ - **Consistency** - Maintains referential equality for both root and nested objects
335
773
 
336
- </details>
774
+ #### Membrane vs Chainable Object Identity
775
+
776
+ ```js
777
+ const obj = { user: { name: 'Alice' }, items: ['item1'] };
778
+
779
+ // MEMBRANE: Same root object = same proxy instance
780
+ const $obj1 = Observer.proxy(obj, { membrane: 'test' });
781
+ const $obj2 = Observer.proxy(obj, { membrane: 'test' });
782
+ console.log($obj1 === $obj2); // true - same root proxy
783
+
784
+ // Nested objects are NOT automatically proxied (without chainable)
785
+ const user1 = $obj1.user; // Plain object, not proxied
786
+ const user2 = $obj2.user; // Plain object, not proxied
787
+ console.log(user1 === user2); // true - same plain object
788
+
789
+ // CHAINABLE: Auto-proxies nested objects when accessed
790
+ const $obj = Observer.proxy(obj, { chainable: true });
791
+ const $user1 = $obj.user; // Proxied object
792
+ const $user2 = $obj.user; // Different proxy instance
793
+ console.log($user1 !== $user2); // true - different proxy instances
794
+
795
+ // MEMBRANE + CHAINABLE: Consistent nested proxy identity
796
+ const $obj3 = Observer.proxy(obj, { membrane: 'test', chainable: true });
797
+ const $user3 = $obj3.user; // Proxied object
798
+ const $user4 = $obj3.user; // Same proxy instance
799
+ console.log($user3 === $user4); // true - same nested proxy
800
+ ```
801
+
802
+ #### Real-World Usage Patterns
337
803
 
338
- **-->** 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:
804
+ **_Scenario_: Form Handling:**
339
805
 
340
806
  ```js
341
- let context = m.context;
342
- console.log(context);
807
+ const $form = Observer.proxy(formData, { membrane: 'form' });
808
+ $form.name = 'John'; // Auto-save, validation
809
+ $form.email = 'john@example.com'; // Auto-save, validation
810
+ $form.tags.push('urgent'); // Auto-save, validation
343
811
  ```
344
812
 
345
- *And up again one level until the root event*:
813
+ **_Scenario_: State Management:**
346
814
 
347
815
  ```js
348
- let parentContext = context.context;
349
- console.log(parentContext);
816
+ const $state = Observer.proxy(appState, { chainable: true });
817
+ $state.user.isLoggedIn = true; // UI updates
818
+ $state.cart.items.push(product); // UI updates
819
+ $state.getUser().profile.theme = 'dark'; // UI updates (chainable)
350
820
  ```
351
821
 
352
- **-->** Observe trees that are built *asynchronously*! Where a promise is encountered along the path, further access is paused until promise resolves:
822
+ #### Formal Arguments
353
823
 
354
824
  ```js
355
- Observer.set( obj.level1, 'level2', Promise.resolve( { level3: { level4: 'level4-value', }, } ) );
825
+ // Basic proxy
826
+ const $obj = Observer.proxy(obj);
827
+
828
+ // Proxy with options
829
+ const $obj = Observer.proxy(obj, {
830
+ membrane: 'userData', // Auto-proxy nested objects
831
+ chainable: true // Auto-wrap returned objects
832
+ });
833
+
834
+ // Proxy with custom extension
835
+ const $obj = Observer.proxy(obj, {}, (traps) => {
836
+ // Extend proxy traps
837
+ traps.get = (target, key, receiver) => {
838
+ if (key === 'computed') {
839
+ return target.firstName + ' ' + target.lastName;
840
+ }
841
+ return traps.get(target, key, receiver);
842
+ };
843
+ return traps;
844
+ });
356
845
  ```
357
846
 
358
- #### Concept: *Batch Mutations*
847
+ #### Proxy Features (Summary)
359
848
 
360
- Make multiple mutations at a go, and they'll be correctly delivered as a batch to observers!
849
+ - **Literal syntax** - Use normal JavaScript operations
850
+ - **Array methods** - All array methods are reactive
851
+ - **Property access** - All property operations are reactive
852
+ - **Nested operations** - Works with deeply nested objects
853
+ - **Dynamic properties** - Supports computed property names
854
+ - **Method chaining** - Array methods can be chained
855
+ - **Membrane support** - Auto-proxy nested objects
856
+ - **Chainable operations** - Auto-wrap returned values
857
+ - **Custom traps** - Extend proxy behavior
858
+ - **Namespace isolation** - Separate observer namespaces
859
+
860
+ #### Parity Table
861
+
862
+ | | Observer API | Proxy API |
863
+ |-------------|--------------|-----------|
864
+ | **Signature** | `.proxy(target, options?)` | `new Proxy(target, handlers)` |
865
+ | **Return Value** | `Proxy` (reactive proxy) | `Proxy` (standard proxy) |
866
+ | **Additional Features** | Built-in reactivity, membrane, chainable | Manual trap implementation required |
867
+
868
+ ### `Observer.unproxy(target, options?)`
869
+
870
+ Get the original object from a proxy.
361
871
 
362
872
  ```js
363
- // Batch operations on an object
364
- Observer.set( obj, {
365
- prop0: 'value0',
366
- prop1: 'value1',
367
- prop2: 'value2',
368
- } );
369
- Observer.defineProperties( obj, {
370
- prop0: { value: 'value0' },
371
- prop1: { value: 'value1' },
372
- prop2: { get: () => 'value2' },
373
- } );
374
- Observer.deleteProperties( obj, [ 'prop0', 'prop1', 'prop2' ] );
873
+ const $obj = Observer.proxy(obj);
874
+ const original = Observer.unproxy($obj); // Returns original obj
375
875
  ```
376
876
 
877
+ #### Parity Table
878
+
879
+ | | Observer API | No Direct Equivalent |
880
+ |-------------|--------------|---------------------|
881
+ | **Signature** | `.unproxy(target)` | No direct equivalent in standard APIs |
882
+ | **Return Value** | `object` (original object) | N/A |
883
+ | **Additional Features** | Extracts original object from Observer proxy | N/A |
884
+
885
+ ---
886
+
887
+ ### `Observer.path(...segments)`
888
+
889
+ 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.
890
+
891
+ **Basic Usage:**
892
+
377
893
  ```js
378
- // Batch operations on an array
379
- Observer.set( arr, {
380
- '0': 'item0',
381
- '1': 'item1',
382
- '2': 'item2',
383
- } );
384
- Object.proxy( arr ).push( 'item3', 'item4', 'item5', );
385
- Object.proxy( arr ).unshift( 'new-item0' );
386
- Object.proxy( arr ).splice( 0 );
894
+ // Watch deep paths
895
+ const path = Observer.path('user', 'profile', 'settings');
896
+ Observer.observe(obj, path, (mutation) => {
897
+ console.log(`Deep change: ${mutation.path} = ${mutation.value}`);
898
+ });
387
899
  ```
388
900
 
389
- **-->** Use the `Observer.batch()` to batch multiple arbitrary mutations - whether related or not:
901
+ #### Usage Patterns
390
902
 
391
903
  ```js
392
- Observer.batch( arr, async () => {
393
- Observer.set( arr, 0, 'item0' ); // Array [ 'item0' ]
394
- await somePromise();
395
- Observer.set( arr, 2, 'item2' ); // Array [ 'item0', <1 empty slot>, 'item2' ]
396
- } );
904
+ // Form validation
905
+ const path = Observer.path('form', 'user', 'email');
906
+ Observer.observe(form, path, (mutation) => {
907
+ validateEmail(mutation.value);
908
+ });
909
+
910
+ // State management
911
+ const path = Observer.path('app', 'user', 'preferences', 'theme');
912
+ Observer.observe(state, path, (mutation) => {
913
+ updateTheme(mutation.value);
914
+ });
915
+
916
+ // Configuration watching
917
+ const path = Observer.path('config', 'api', 'endpoint');
918
+ Observer.observe(config, path, (mutation) => {
919
+ updateApiEndpoint(mutation.value);
920
+ });
397
921
  ```
398
922
 
399
- > Method calls on a proxied instance - e.g. `Object.proxy( arr ).splice( 0 )` - also follow this strategy.
923
+ #### Path Features (Summary)
400
924
 
401
- ### Method: `Observer.intercept()`
925
+ - Watches paths that are created dynamically
926
+ - Uses an array syntax to avoid conflicting with property names with dots
927
+ - Returns mutation context for audit trails
402
928
 
403
- 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.
929
+ ### `Observer.any()`
404
930
 
405
- *Below, we intercept all "set" operations for an HTTP URL then transform it to an HTTPS URL.*
931
+ 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.
932
+
933
+ **Basic Usage:**
406
934
 
407
935
  ```js
408
- const setTrap = ( operation, previous, next ) => {
409
- if ( operation.key === 'url' && operation.value.startsWith( 'http:' ) ) {
410
- operation.value = operation.value.replace( 'http:', 'https:' );
411
- }
412
- return next();
413
- };
414
- Observer.intercept( obj, 'set', setTrap );
936
+ // Watch any user at any index
937
+ const path = Observer.path('users', Observer.any(), 'name');
938
+ Observer.observe(obj, path, (mutation) => {
939
+ console.log(`User name changed: ${mutation.path} = ${mutation.value}`);
940
+ });
415
941
  ```
416
942
 
417
- *Now, only the first of the following will fly as-is.*
943
+ **Advanced Compositions:**
944
+
945
+ 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.
418
946
 
419
947
  ```js
420
- // Not transformed
421
- Observer.set( obj, 'url', 'https://webqit.io' );
948
+ // Multiple wildcards in sequence
949
+ const path = Observer.path('sections', Observer.any(), 'items', Observer.any(), 'name');
950
+ // Matches: sections[0].items[1].name, sections[2].items[0].name, etc.
951
+
952
+ // Wildcard at different levels
953
+ const path = Observer.path('app', 'users', Observer.any(), 'profile', 'settings', Observer.any());
954
+ // Matches: app.users[0].profile.settings[theme], app.users[1].profile.settings[language], etc.
422
955
 
423
- // Transformed
424
- Observer.set( obj, 'url', 'http://webqit.io' );
956
+ // Wildcard with specific properties
957
+ const path = Observer.path('data', Observer.any(), 'metadata', 'version');
958
+ // Matches: data[item1].metadata.version, data[item2].metadata.version, etc.
425
959
  ```
426
960
 
427
- *And below, we intercept all "get" operations for a certain value to trigger a network fetch behind the scenes.*
961
+ ### `Observer.subtree()`
962
+
963
+ 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.
964
+
965
+ **Basic Usage:**
428
966
 
429
967
  ```js
430
- const getTrap = ( operation, previous, next ) => {
431
- if ( operation.key === 'token' ) {
432
- return next( fetch( tokenUrl ) );
433
- }
434
- return next();
435
- };
436
- Observer.intercept( obj, 'get', getTrap );
968
+ // Watch all changes from this level down
969
+ Observer.observe(obj, Observer.subtree(), (mutation) => {
970
+ console.log(`Any change: ${mutation.path} = ${mutation.value}`);
971
+ });
437
972
  ```
438
973
 
439
- *And all of that can go into one "traps" object:*
974
+ **Advanced Compositions:**
440
975
 
441
976
  ```js
442
- Observer.intercept( obj, {
443
- get: getTrap,
444
- set: setTrap,
445
- deleteProperty: deletePropertyTrap,
446
- defineProperty: definePropertyTrap,
447
- ownKeys: ownKeysTrap,
448
- has: hasTrap,
449
- // etc
450
- } );
977
+ // Subtree after specific path
978
+ const path = Observer.path('app', 'users', Observer.subtree());
979
+ // Watches: app.users.name, app.users.profile.theme, app.users.settings.notifications, etc.
980
+
981
+ // Subtree with wildcards
982
+ const path = Observer.path('sections', Observer.any(), Observer.subtree());
983
+ // Watches: sections[0].title, sections[0].items[1].name, sections[1].config.theme, etc.
984
+
985
+ // Multiple subtrees
986
+ const path = Observer.path('app', 'users', Observer.any(), 'profile', Observer.subtree());
987
+ // Watches: app.users[0].profile.name, app.users[0].profile.settings.theme, etc.
988
+
989
+ // Subtree at root level
990
+ Observer.observe(obj, Observer.subtree(), (mutation) => {
991
+ // Watches EVERY change in the entire object tree
992
+ });
451
993
  ```
452
994
 
453
- ## Documentation
995
+ #### Real-World Usage Patterns
454
996
 
455
- 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.
997
+ ```js
998
+ // E-commerce: Watch any product in any category
999
+ const path = Observer.path('store', 'categories', Observer.any(), 'products', Observer.any(), Observer.subtree());
1000
+ // Triggers for: store.categories[electronics].products[laptop].price
1001
+ // store.categories[books].products[novel].title
1002
+ // store.categories[clothing].products[shirt].sizes[large]
1003
+ ```
456
1004
 
457
- ## The Polyfill
1005
+ ```js
1006
+ // Multi-tenant: Watch any user's data in any organization
1007
+ const path = Observer.path('orgs', Observer.any(), 'users', Observer.any(), Observer.subtree());
1008
+ // Triggers for: orgs[company1].users[alice].profile.name
1009
+ // orgs[company2].users[bob].settings.theme
1010
+ ```
458
1011
 
459
- 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).
1012
+ ```js
1013
+ // Content Management: Watch any page in any section
1014
+ const path = Observer.path('cms', 'sections', Observer.any(), 'pages', Observer.any(), Observer.subtree());
1015
+ // Triggers for: cms.sections[blog].pages[post1].content
1016
+ // cms.sections[news].pages[article].metadata.tags
1017
+ ```
460
1018
 
461
- <details><summary>Load from a CDN</summary>
1019
+ ---
462
1020
 
463
- ```html
464
- <script src="https://unpkg.com/@webqit/observer/dist/main.js"></script>
465
- ```
1021
+ ### `Observer.batch(target, callback, options?)`
466
1022
 
467
- > `4.4` kB min + gz | `13.9` KB min [↗](https://bundlephobia.com/package/@webqit/observer)
1023
+ 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.
1024
+
1025
+ **Basic Usage:**
468
1026
 
469
1027
  ```js
470
- // Obtain the APIs
471
- const Observer = window.webqit.Observer;
1028
+ // Batch multiple changes
1029
+ Observer.batch(obj, () => {
1030
+ Observer.set(obj, 'name', 'Alice');
1031
+ Observer.set(obj, 'email', 'alice@example.com');
1032
+ Observer.deleteProperty(obj, 'age');
1033
+ });
1034
+
1035
+ // All changes are delivered as a single batch to observers
472
1036
  ```
473
1037
 
474
- </details>
1038
+ ---
475
1039
 
476
- <details><summary>Install from NPM</summary>
1040
+ ### `Observer.map(source, target, options?)`
477
1041
 
478
- ```bash
479
- npm i @webqit/observer
1042
+ 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.
1043
+
1044
+ **Basic Usage:**
1045
+
1046
+ ```js
1047
+ const source = { name: 'Alice', age: 25 };
1048
+ const target = {};
1049
+
1050
+ // Create reactive mirror
1051
+ const controller = Observer.map(source, target);
1052
+
1053
+ // Changes in source automatically sync to target
1054
+ Observer.set(source, 'name', 'Bob');
1055
+ console.log(target.name); // 'Bob'
1056
+
1057
+ // Stop mirroring
1058
+ controller.abort();
480
1059
  ```
481
1060
 
1061
+ **Alternative Method Shapes:**
1062
+
482
1063
  ```js
483
- // Import
484
- import Observer from '@webqit/observer';;
1064
+ // Mirror with options
1065
+ Observer.map(source, target, {
1066
+ only: ['name', 'email'], // Only mirror specific properties
1067
+ except: ['password'], // Exclude specific properties
1068
+ spread: true, // Spread array elements
1069
+ onlyEnumerable: false // Include non-enumerable properties
1070
+ });
1071
+
1072
+ // Mirror with namespace
1073
+ Observer.map(source, target, { namespace: 'user' });
485
1074
  ```
486
1075
 
487
- </details>
1076
+ #### Usage Patterns
1077
+
1078
+ ```js
1079
+ // State synchronization
1080
+ const appState = { user: { name: 'Alice' } };
1081
+ const uiState = {};
1082
+ Observer.map(appState, uiState);
1083
+
1084
+ // Form data mirroring
1085
+ const formData = { name: '', email: '' };
1086
+ const validationState = {};
1087
+ Observer.map(formData, validationState);
1088
+
1089
+ // Array synchronization
1090
+ const sourceArray = [1, 2, 3];
1091
+ const targetArray = [];
1092
+ Observer.map(sourceArray, targetArray, { spread: true });
1093
+ ```
1094
+
1095
+ ---
488
1096
 
489
- ## Getting Involved
1097
+ ### Other Methods
490
1098
 
491
- All forms of contributions are welcome at this time. For example, implementation details are all up for discussion. And here are specific links:
1099
+ Mentioned here for completeness, Observer also provides these utility methods:
492
1100
 
493
- + [Project](https://github.com/webqit/observer)
494
- + [Documentation](https://github.com/webqit/observer/wiki)
495
- + [Discusions](https://github.com/webqit/observer/discussions)
496
- + [Issues](https://github.com/webqit/observer/issues)
1101
+ - **`Observer.apply(target, thisArg, args)`** - Apply functions reactively
1102
+ - **`Observer.construct(target, args)`** - Construct objects reactively
1103
+ - **`Observer.getOwnPropertyDescriptor(target, key)`** - Get property descriptors reactively
1104
+ - **`Observer.getPrototypeOf(target)`** - Get prototype reactively
1105
+ - **`Observer.setPrototypeOf(target, prototype)`** - Set prototype reactively
1106
+ - **`Observer.isExtensible(target)`** - Check extensibility reactively
1107
+ - **`Observer.preventExtensions(target)`** - Prevent extensions reactively
1108
+
1109
+ ## Extended Documentation
1110
+
1111
+ - [Timing and Batching](https://github.com/webqit/observer/wiki#timing-and-batching)
1112
+ - [Reflect API Supersets](https://github.com/webqit/observer/wiki#featuring-reflect-api-supersets)
1113
+
1114
+ ## Contributing
1115
+
1116
+ We welcome contributions! Here's how to get involved:
1117
+
1118
+ - 🐛 [Report Issues](https://github.com/webqit/observer/issues)
1119
+ - 💬 [Join Discussions](https://github.com/webqit/observer/discussions)
1120
+ - 📖 [Read Documentation](https://github.com/webqit/observer/wiki)
1121
+ - 🔧 [View Source](https://github.com/webqit/observer)
497
1122
 
498
1123
  ## License
499
1124
 
500
- MIT.
1125
+ MIT
1126
+
1127
+ [npm-version-src]: https://img.shields.io/npm/v/@webqit/observer?style=flat&colorA=18181B&colorB=F0DB4F
1128
+ [npm-version-href]: https://npmjs.com/package/@webqit/observer
1129
+ [npm-downloads-src]: https://img.shields.io/npm/dm/@webqit/observer?style=flat&colorA=18181B&colorB=F0DB4F
1130
+ [npm-downloads-href]: https://npmjs.com/package/@webqit/observer
1131
+ [bundle-src]: https://img.shields.io/bundlephobia/minzip/@webqit/observer?style=flat&colorA=18181B&colorB=F0DB4F
1132
+ [bundle-href]: https://bundlephobia.com/result?p=@webqit/observer
1133
+ [license-src]: https://img.shields.io/github/license/webqit/observer.svg?style=flat&colorA=18181B&colorB=F0DB4F
1134
+ [license-href]: https://github.com/webqit/observer/LICENSE