@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.
- package/README.md +918 -284
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,500 +1,1134 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Observer
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
+
The Observer API comes as one little API for all things _object observability_. (Only `~5.8KiB min|zip`)
|
|
8
11
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
+
<details><summary>Looking for Observer@1.x?</summary>
|
|
14
34
|
|
|
15
|
-
|
|
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
|
-
|
|
37
|
+
</details>
|
|
18
38
|
|
|
19
|
-
|
|
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
|
-
|
|
47
|
+
## Why Observer?
|
|
22
48
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
```js
|
|
54
|
+
const state = { count: 0, items: [] };
|
|
31
55
|
|
|
32
|
-
|
|
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
|
-
|
|
61
|
+
// No way to detect these changes
|
|
62
|
+
```
|
|
35
63
|
|
|
36
|
-
|
|
64
|
+
This limitation in the language has long created a **blindspot** — and a **weakness** — for reactive systems. Consequently:
|
|
37
65
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
+
### CDN
|
|
59
111
|
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
119
|
+
### Basic Usage
|
|
70
120
|
|
|
71
121
|
```js
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
141
|
+
### Working with Arrays
|
|
77
142
|
|
|
78
143
|
```js
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
Observer.observe(
|
|
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
|
-
//
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
const
|
|
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
|
-
|
|
96
|
-
|
|
253
|
+
**Alternative Method Shapes:**
|
|
254
|
+
```js
|
|
255
|
+
// Watch specific properties
|
|
256
|
+
Observer.observe(obj, ['name', 'email'], callback);
|
|
97
257
|
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
+
// Abort at any time
|
|
296
|
+
abortController.abort();
|
|
121
297
|
```
|
|
122
298
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
#### Concept: *Mutation APIs*
|
|
299
|
+
#### Lifecycle Signals
|
|
126
300
|
|
|
127
|
-
|
|
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
|
-
//
|
|
131
|
-
Observer.
|
|
132
|
-
|
|
133
|
-
Observer.
|
|
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
|
-
//
|
|
138
|
-
Observer.
|
|
139
|
-
|
|
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
|
-
|
|
347
|
+
**Multiple Operations Interception:**
|
|
143
348
|
|
|
144
|
-
|
|
349
|
+
Intercept multiple operations simultaneously to create comprehensive behavior modifications.
|
|
145
350
|
|
|
146
351
|
```js
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
408
|
+
#### Interceptable Operations
|
|
159
409
|
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
Observer.
|
|
165
|
-
|
|
166
|
-
|
|
436
|
+
Observer.set(obj, 'name', 'Alice');
|
|
437
|
+
Observer.set(arr, 0, 'first item');
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Alternative Method Shapes:**
|
|
167
441
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
obj
|
|
171
|
-
|
|
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
|
-
//
|
|
176
|
-
Observer.
|
|
177
|
-
|
|
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
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
//
|
|
186
|
-
|
|
187
|
-
arr.shift();
|
|
465
|
+
// Nested property updates
|
|
466
|
+
Observer.set(obj, Observer.path('user', 'profile', 'name'), 'Alice');
|
|
188
467
|
```
|
|
189
468
|
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
503
|
+
#### Parity Table
|
|
207
504
|
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
// Obtain a reactive Proxy for an object
|
|
212
|
-
const $obj = Observer.proxy( obj );
|
|
511
|
+
### `Observer.has(target, key, options?)`
|
|
213
512
|
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
220
|
-
delete $obj.prop0;
|
|
221
|
-
```
|
|
516
|
+
**Basic Usage:**
|
|
222
517
|
|
|
223
518
|
```js
|
|
224
|
-
|
|
225
|
-
|
|
519
|
+
Observer.has(obj, 'name'); // true/false
|
|
520
|
+
Observer.has(obj, 'user.profile.name'); // nested property check
|
|
521
|
+
```
|
|
226
522
|
|
|
227
|
-
|
|
228
|
-
$arr[ 0 ] = 'item0';
|
|
229
|
-
$arr[ 1 ] = 'item1';
|
|
230
|
-
$arr[ 2 ] = 'item2';
|
|
523
|
+
**_Scenario_: Property Hiding:**
|
|
231
524
|
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
537
|
+
#### Parity Table
|
|
237
538
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
248
|
-
|
|
550
|
+
**Basic Usage:**
|
|
551
|
+
|
|
552
|
+
```js
|
|
553
|
+
Observer.ownKeys(obj); // ['name', 'email', 'age']
|
|
249
554
|
```
|
|
250
555
|
|
|
251
|
-
|
|
556
|
+
**_Scenario_: Key Filtering:**
|
|
252
557
|
|
|
253
558
|
```js
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
582
|
+
Observer.deleteProperty(obj, 'oldProp');
|
|
583
|
+
Observer.deleteProperty(arr, 0);
|
|
261
584
|
```
|
|
262
585
|
|
|
263
|
-
####
|
|
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
|
-
|
|
594
|
+
### `Observer.deleteProperties(target, keys, options?)`
|
|
595
|
+
|
|
596
|
+
Delete multiple properties at once.
|
|
266
597
|
|
|
267
598
|
```js
|
|
268
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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.
|
|
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
|
-
|
|
642
|
+
#### Parity Table
|
|
287
643
|
|
|
288
|
-
|
|
|
289
|
-
|
|
290
|
-
|
|
|
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
|
-
|
|
650
|
+
---
|
|
293
651
|
|
|
294
|
-
|
|
652
|
+
### `Observer.accessorize(target, properties?, options?)`
|
|
653
|
+
|
|
654
|
+
Make properties reactive for direct assignment.
|
|
295
655
|
|
|
296
656
|
```js
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
303
|
-
Observer.
|
|
304
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
315
|
-
<summary>Console</summary>
|
|
719
|
+
#### Nested Operations (Requires `chainable: true`)
|
|
316
720
|
|
|
317
|
-
|
|
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
|
-
|
|
723
|
+
```js
|
|
724
|
+
const $obj = Observer.proxy(obj, { chainable: true });
|
|
322
725
|
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
<summary>Console</summary>
|
|
766
|
+
#### How Membranes Work
|
|
331
767
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
804
|
+
**_Scenario_: Form Handling:**
|
|
339
805
|
|
|
340
806
|
```js
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
813
|
+
**_Scenario_: State Management:**
|
|
346
814
|
|
|
347
815
|
```js
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
822
|
+
#### Formal Arguments
|
|
353
823
|
|
|
354
824
|
```js
|
|
355
|
-
|
|
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
|
-
####
|
|
847
|
+
#### Proxy Features (Summary)
|
|
359
848
|
|
|
360
|
-
|
|
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
|
-
|
|
364
|
-
Observer.
|
|
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
|
-
//
|
|
379
|
-
Observer.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
901
|
+
#### Usage Patterns
|
|
390
902
|
|
|
391
903
|
```js
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
923
|
+
#### Path Features (Summary)
|
|
400
924
|
|
|
401
|
-
|
|
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
|
-
|
|
929
|
+
### `Observer.any()`
|
|
404
930
|
|
|
405
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
421
|
-
Observer.
|
|
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
|
-
//
|
|
424
|
-
Observer.
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
974
|
+
**Advanced Compositions:**
|
|
440
975
|
|
|
441
976
|
```js
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
995
|
+
#### Real-World Usage Patterns
|
|
454
996
|
|
|
455
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1019
|
+
---
|
|
462
1020
|
|
|
463
|
-
|
|
464
|
-
<script src="https://unpkg.com/@webqit/observer/dist/main.js"></script>
|
|
465
|
-
```
|
|
1021
|
+
### `Observer.batch(target, callback, options?)`
|
|
466
1022
|
|
|
467
|
-
|
|
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
|
-
//
|
|
471
|
-
|
|
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
|
-
|
|
1038
|
+
---
|
|
475
1039
|
|
|
476
|
-
|
|
1040
|
+
### `Observer.map(source, target, options?)`
|
|
477
1041
|
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
//
|
|
484
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1097
|
+
### Other Methods
|
|
490
1098
|
|
|
491
|
-
|
|
1099
|
+
Mentioned here for completeness, Observer also provides these utility methods:
|
|
492
1100
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|