@brinzl/observer 0.1.0
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/AGENTS.md +172 -0
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/index.html +10 -0
- package/index.js +2 -0
- package/observer.js +187 -0
- package/observer.min.js +1 -0
- package/package.json +20 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Observer.js
|
|
2
|
+
|
|
3
|
+
A lightweight, zero-dependency reactive object observation library built for learning purposes.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
This library provides a way to observe changes to JavaScript objects and arrays using ES6 Proxies. When properties are modified, subscribers are notified with detailed change records.
|
|
8
|
+
|
|
9
|
+
## API Design
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
// Create an observable from a plain object
|
|
13
|
+
const observable = Observer.create({ name: 'Alice', age: 30 });
|
|
14
|
+
|
|
15
|
+
// Subscribe to all changes
|
|
16
|
+
const unsubscribe = Observer.observe(observable, (changes) => {
|
|
17
|
+
changes.forEach(change => {
|
|
18
|
+
console.log(change.type); // 'set' | 'delete' | 'insert' | 'batch'
|
|
19
|
+
console.log(change.path); // ['user', 'name'] - path to the changed prop
|
|
20
|
+
console.log(change.oldValue);
|
|
21
|
+
console.log(change.newValue);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Mutations trigger the callback automatically
|
|
26
|
+
observable.name = 'Bob'; // fires the subscriber
|
|
27
|
+
observable.address = { city: 'NY' }; // nested objects also become observable
|
|
28
|
+
|
|
29
|
+
// Unsubscribe when done
|
|
30
|
+
unsubscribe();
|
|
31
|
+
|
|
32
|
+
// Or destroy entirely
|
|
33
|
+
Observer.destroy(observable);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
40
|
+
│ Observer (Public API) │
|
|
41
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
|
|
42
|
+
│ │ create() │ │ observe() │ │ destroy() │ │
|
|
43
|
+
│ └──────┬──────┘ └──────┬──────┘ └────────────┬────────────┘ │
|
|
44
|
+
└─────────┼────────────────┼──────────────────────┼───────────────┘
|
|
45
|
+
│ │ │
|
|
46
|
+
▼ ▼ ▼
|
|
47
|
+
┌─────────────────┐ ┌───────────────────┐ ┌──────────────────┐
|
|
48
|
+
│ObservableFactory│ │ SubscriberRegistry│ │ ProxyRegistry │
|
|
49
|
+
│ │ │ │ │ (WeakMap-based) │
|
|
50
|
+
│ - createProxy() │ │ - subscribe() │ │ │
|
|
51
|
+
│ - wrapValue() │ │ - unsubscribe() │ │ - stores metadata│
|
|
52
|
+
│ │ │ - notify() │ │ - revoke fns │
|
|
53
|
+
└────────┬────────┘ └─────────┬─────────┘ └──────────────────┘
|
|
54
|
+
│ │
|
|
55
|
+
▼ │
|
|
56
|
+
┌─────────────────────────┐ │
|
|
57
|
+
│ ProxyHandler │ │
|
|
58
|
+
│ │ │
|
|
59
|
+
│ - get trap (lazy wrap) │ │
|
|
60
|
+
│ - set trap ─────────────┼────┼──────┐
|
|
61
|
+
│ - deleteProperty trap ──┼────┼──────┤
|
|
62
|
+
│ - array method patches │ │ │
|
|
63
|
+
└─────────────────────────┘ │ ▼
|
|
64
|
+
│ ┌───────────────┐
|
|
65
|
+
│ │ ChangeQueue │
|
|
66
|
+
│ │ │
|
|
67
|
+
│ │ - enqueue() │
|
|
68
|
+
│ │ - flush() ───┼──► notify subscribers
|
|
69
|
+
│ └───────────────┘
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## File Structure
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
observer.js/
|
|
76
|
+
├── src/
|
|
77
|
+
│ ├── index.js # Public API (Observer.create/observe/destroy)
|
|
78
|
+
│ ├── symbols.js # Shared symbols
|
|
79
|
+
│ ├── registry.js # ProxyRegistry (WeakMap wrapper)
|
|
80
|
+
│ ├── factory.js # ObservableFactory (createProxy logic)
|
|
81
|
+
│ ├── queue.js # ChangeQueue (microtask batching)
|
|
82
|
+
│ ├── subscribers.js # SubscriberRegistry
|
|
83
|
+
│ └── array-methods.js # Array mutator wrappers
|
|
84
|
+
├── test.js # Manual testing script
|
|
85
|
+
├── package.json
|
|
86
|
+
└── AGENTS.md
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Design Decisions
|
|
90
|
+
|
|
91
|
+
### Core Principles
|
|
92
|
+
|
|
93
|
+
1. **WeakMap for metadata** - Storing observer state (subscribers, original target, revoke functions) in a WeakMap keyed by the proxy means observables are garbage collected naturally when nothing else holds a reference.
|
|
94
|
+
|
|
95
|
+
2. **Lazy recursive proxying** - Don't walk the entire object tree up front. When a nested object is accessed via a get trap, wrap it in a proxy at that point. This keeps `create()` fast regardless of object size.
|
|
96
|
+
|
|
97
|
+
3. **Double-wrap prevention** - Use a sentinel symbol (`IS_OBSERVABLE`) so that if someone passes an already-proxied object into `create()`, it returns as-is rather than double-wrapping.
|
|
98
|
+
|
|
99
|
+
4. **Revocable proxies** - Use `Proxy.revocable()` instead of `new Proxy()` so that `destroy()` can fully invalidate the proxy and prevent further mutations.
|
|
100
|
+
|
|
101
|
+
5. **Microtask batching** - Changes are batched per microtask (using `queueMicrotask`) rather than firing synchronously on every mutation. This avoids flooding subscribers when multiple properties change at once.
|
|
102
|
+
|
|
103
|
+
6. **Path tracking** - Each change record includes the path (e.g. `['address', 'city']`) from the root observable to the changed property.
|
|
104
|
+
|
|
105
|
+
### Scope Decisions
|
|
106
|
+
|
|
107
|
+
- **Objects & Arrays only** - No Map/Set support (can be added later)
|
|
108
|
+
- **Path-filtered subscriptions** - Planned as a later enhancement
|
|
109
|
+
- **Circular references** - Not currently handled (documented as unsupported)
|
|
110
|
+
|
|
111
|
+
## Implementation Phases
|
|
112
|
+
|
|
113
|
+
### Phase 1: Core Proxy Wrapper (No Reactivity)
|
|
114
|
+
- Create proxies that wrap objects/arrays
|
|
115
|
+
- Track them in a WeakMap registry
|
|
116
|
+
- Implement lazy nested proxy creation
|
|
117
|
+
- Expose internal symbols for debugging
|
|
118
|
+
|
|
119
|
+
### Phase 2: Change Detection (Set/Delete Traps)
|
|
120
|
+
- Detect mutations via `set` and `deleteProperty` traps
|
|
121
|
+
- Create change records with type, path, oldValue, newValue
|
|
122
|
+
- Wrap newly assigned objects in proxies
|
|
123
|
+
|
|
124
|
+
### Phase 3: Change Queue & Microtask Batching
|
|
125
|
+
- Accumulate changes within current microtask
|
|
126
|
+
- Flush batched changes to subscribers
|
|
127
|
+
- Group changes by root observable
|
|
128
|
+
|
|
129
|
+
### Phase 4: Subscriber Registry
|
|
130
|
+
- Implement `Observer.observe()` and unsubscribe
|
|
131
|
+
- Notify subscribers with batched changes
|
|
132
|
+
- Handle subscriber errors gracefully
|
|
133
|
+
|
|
134
|
+
### Phase 5: Array Method Interception
|
|
135
|
+
- Intercept mutating methods: push, pop, shift, unshift, splice, sort, reverse
|
|
136
|
+
- Emit meaningful `insert`/`delete` change types
|
|
137
|
+
- Wrap inserted objects in proxies
|
|
138
|
+
|
|
139
|
+
### Phase 6: Destroy & Cleanup
|
|
140
|
+
- Implement `Observer.destroy()`
|
|
141
|
+
- Revoke proxies to prevent further use
|
|
142
|
+
- Clear subscribers and registry entries
|
|
143
|
+
|
|
144
|
+
## Internal Symbols
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
SYMBOLS = {
|
|
148
|
+
IS_OBSERVABLE: Symbol('observer.isObservable'), // Detect wrapped proxies
|
|
149
|
+
RAW: Symbol('observer.raw'), // Access original target
|
|
150
|
+
ROOT: Symbol('observer.root'), // Get root observable
|
|
151
|
+
PATH: Symbol('observer.path') // Get path from root
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Change Record Format
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
{
|
|
159
|
+
type: 'set' | 'delete' | 'insert',
|
|
160
|
+
path: string[], // e.g. ['user', 'profile', 'name']
|
|
161
|
+
oldValue: any,
|
|
162
|
+
newValue: any
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Testing
|
|
167
|
+
|
|
168
|
+
Manual testing via `node test.js`. Run tests after each phase to verify implementation.
|
|
169
|
+
|
|
170
|
+
## Module System
|
|
171
|
+
|
|
172
|
+
ES Modules (import/export) with `"type": "module"` in package.json.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brinsil Elias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# observer.js
|
|
2
|
+
|
|
3
|
+
A tiny, zero-dependency reactive object observation library using ES6 Proxies.
|
|
4
|
+
Observe changes to JavaScript objects and arrays with automatic nested tracking and microtask batching.
|
|
5
|
+
|
|
6
|
+
## Why?
|
|
7
|
+
|
|
8
|
+
While exploring how Vue.js handle reactivity, and I came across observer pattern that powers their reactive systems. This library was built as a project to understand the fundamentals of reactive programming from first principles.
|
|
9
|
+
|
|
10
|
+
Inspired by libraries like [object-observer](https://github.com/gullerya/object-observer), [icaro](https://github.com/GianlucaGuarini/icaro), and [micro-observer](https://github.com/nicedoc/micro-observer).
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Observe deeply nested objects and arrays
|
|
15
|
+
- Automatic change batching via microtasks
|
|
16
|
+
- Path tracking for precise change locations
|
|
17
|
+
- Clean teardown with proxy revocation
|
|
18
|
+
- Zero dependencies, ~2KB minified
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
npm install @brinzl/observer
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Basic
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
import { Observer } from '@brinzl/observer';
|
|
30
|
+
|
|
31
|
+
// Create an observable object.
|
|
32
|
+
const obs = Observer.create({ name: 'Alice', age: 30 });
|
|
33
|
+
|
|
34
|
+
// Subscribe to changes.
|
|
35
|
+
const unsubscribe = Observer.observe(obs, (changes) => {
|
|
36
|
+
changes.forEach(change => {
|
|
37
|
+
console.log(change.type); // 'set' | 'delete'
|
|
38
|
+
console.log(change.path); // ['name']
|
|
39
|
+
console.log(change.oldValue); // 'Alice'
|
|
40
|
+
console.log(change.newValue); // 'Bob'
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Mutations trigger the callback.
|
|
45
|
+
obs.name = 'Bob';
|
|
46
|
+
obs.age = 31;
|
|
47
|
+
|
|
48
|
+
// Unsubscribe when done.
|
|
49
|
+
unsubscribe();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Nested Objects
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
const obs = Observer.create({
|
|
56
|
+
user: {
|
|
57
|
+
profile: { name: 'Alice' }
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
Observer.observe(obs, (changes) => {
|
|
62
|
+
console.log(changes);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Nested objects are automatically observed.
|
|
66
|
+
obs.user.profile.name = 'Bob';
|
|
67
|
+
// Change: { type: 'set', path: ['user', 'profile', 'name'], oldValue: 'Alice', newValue: 'Bob' }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Arrays
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
const obs = Observer.create({ items: [1, 2, 3] });
|
|
74
|
+
|
|
75
|
+
Observer.observe(obs, (changes) => {
|
|
76
|
+
console.log(changes);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
obs.items.push(4);
|
|
80
|
+
obs.items[0] = 10;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Cleanup
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
const obs = Observer.create({ data: 'test' });
|
|
87
|
+
|
|
88
|
+
// Completely destroy the observable.
|
|
89
|
+
Observer.destroy(obs);
|
|
90
|
+
|
|
91
|
+
// Further access throws an error.
|
|
92
|
+
obs.data; // TypeError: Cannot perform 'get' on a proxy that has been revoked
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Accessing Internals
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
const { IS_OBSERVABLE, RAW, ROOT, PATH } = Observer.symbols;
|
|
99
|
+
|
|
100
|
+
const obs = Observer.create({ nested: { value: 1 } });
|
|
101
|
+
|
|
102
|
+
obs[IS_OBSERVABLE]; // true
|
|
103
|
+
obs[RAW]; // { nested: { value: 1 } } (original object)
|
|
104
|
+
obs.nested[PATH]; // ['nested']
|
|
105
|
+
obs.nested[ROOT] === obs; // true
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## API
|
|
109
|
+
|
|
110
|
+
| Method | Description |
|
|
111
|
+
|--------|-------------|
|
|
112
|
+
| `Observer.create(target)` | Creates an observable from a plain object or array |
|
|
113
|
+
| `Observer.observe(observable, callback)` | Subscribes to changes, returns an unsubscribe function |
|
|
114
|
+
| `Observer.destroy(observable)` | Revokes the proxy and clears all subscribers |
|
|
115
|
+
| `Observer.symbols` | Internal symbols for accessing raw target, path, and root |
|
|
116
|
+
|
|
117
|
+
## Change Record
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
{
|
|
121
|
+
type: 'set' | 'delete',
|
|
122
|
+
path: string[], // e.g. ['user', 'profile', 'name']
|
|
123
|
+
oldValue: any,
|
|
124
|
+
newValue: any
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
package/index.html
ADDED
package/index.js
ADDED
package/observer.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const symbols = {
|
|
2
|
+
IS_OBSERVABLE: Symbol('observer.isObservable'),
|
|
3
|
+
RAW: Symbol('observer.raw'),
|
|
4
|
+
ROOT: Symbol('observer.root'),
|
|
5
|
+
PATH: Symbol('observer.path'),
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let queue = []
|
|
9
|
+
let scheduled = false
|
|
10
|
+
|
|
11
|
+
const registry = new WeakMap()
|
|
12
|
+
const targetToProxy = new WeakMap()
|
|
13
|
+
|
|
14
|
+
const subscribers = new Map()
|
|
15
|
+
|
|
16
|
+
const register = (proxy, metadata) => {
|
|
17
|
+
targetToProxy.set(metadata.target, proxy)
|
|
18
|
+
return registry.set(proxy, metadata)
|
|
19
|
+
}
|
|
20
|
+
const getMetadata = (proxy) => {
|
|
21
|
+
return registry.get(proxy)
|
|
22
|
+
}
|
|
23
|
+
const unregister = (proxy) => {
|
|
24
|
+
const metadata = registry.get(proxy)
|
|
25
|
+
if (metadata) {
|
|
26
|
+
targetToProxy.delete(metadata.target)
|
|
27
|
+
}
|
|
28
|
+
return registry.delete(proxy)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const notifySubscribers = (root, changes) => {
|
|
32
|
+
const callbacks = subscribers.get(root)
|
|
33
|
+
if (!callbacks || callbacks.size === 0) return
|
|
34
|
+
|
|
35
|
+
for (const callback of callbacks) {
|
|
36
|
+
try {
|
|
37
|
+
callback(changes)
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Subscriber error:', error)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const flush = () => {
|
|
45
|
+
const batch = queue
|
|
46
|
+
const changesByRoot = new Map()
|
|
47
|
+
|
|
48
|
+
queue = []
|
|
49
|
+
scheduled = false
|
|
50
|
+
|
|
51
|
+
for (let change of batch) {
|
|
52
|
+
if (!changesByRoot.has(change.root)) {
|
|
53
|
+
changesByRoot.set(change.root, [])
|
|
54
|
+
}
|
|
55
|
+
changesByRoot.get(change.root).push({
|
|
56
|
+
type: change.type,
|
|
57
|
+
path: change.path,
|
|
58
|
+
oldValue: change.oldValue,
|
|
59
|
+
newValue: change.newValue
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
for (let [root, changes] of changesByRoot) {
|
|
63
|
+
notifySubscribers(root, changes)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const enqueue = (change, root) => {
|
|
68
|
+
queue.push({ ...change, root })
|
|
69
|
+
if (!scheduled) {
|
|
70
|
+
scheduled = true
|
|
71
|
+
queueMicrotask(flush)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const handler = {
|
|
76
|
+
get: (target, property, receiver) => {
|
|
77
|
+
if (property === symbols.IS_OBSERVABLE) return true
|
|
78
|
+
if (property === symbols.RAW) return target
|
|
79
|
+
if (property === symbols.ROOT) return getMetadata(receiver).root
|
|
80
|
+
if (property === symbols.PATH) return getMetadata(receiver).path
|
|
81
|
+
|
|
82
|
+
const value = Reflect.get(target, property, receiver)
|
|
83
|
+
if (value === null || typeof value !== 'object') {
|
|
84
|
+
return value
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const metadata = getMetadata(receiver)
|
|
88
|
+
const wrappedValue = createProxy(value, metadata.root, [...metadata.path, property])
|
|
89
|
+
return wrappedValue
|
|
90
|
+
},
|
|
91
|
+
set: (target, property, value, receiver) => {
|
|
92
|
+
const oldValue = target[property]
|
|
93
|
+
const metadata = getMetadata(receiver)
|
|
94
|
+
|
|
95
|
+
if (value !== null && typeof value === 'object') {
|
|
96
|
+
value = createProxy(value, metadata.root, [...metadata.path, property])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const success = Reflect.set(target, property, value, receiver)
|
|
100
|
+
if (success && oldValue !== value) {
|
|
101
|
+
enqueue({
|
|
102
|
+
type: 'set',
|
|
103
|
+
path: [...metadata.path, property],
|
|
104
|
+
oldValue,
|
|
105
|
+
newValue: value
|
|
106
|
+
}, metadata.root)
|
|
107
|
+
}
|
|
108
|
+
return success
|
|
109
|
+
},
|
|
110
|
+
deleteProperty: (target, property) => {
|
|
111
|
+
if (!(property in target)) return true
|
|
112
|
+
const oldValue = target[property]
|
|
113
|
+
const proxy = targetToProxy.get(target)
|
|
114
|
+
const metadata = getMetadata(proxy)
|
|
115
|
+
|
|
116
|
+
const success = Reflect.deleteProperty(target, property)
|
|
117
|
+
if (success) {
|
|
118
|
+
enqueue({
|
|
119
|
+
type: 'delete',
|
|
120
|
+
path: [...metadata.path, property],
|
|
121
|
+
oldValue,
|
|
122
|
+
newValue: undefined
|
|
123
|
+
}, metadata.root)
|
|
124
|
+
}
|
|
125
|
+
return success
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const createProxy = (target, root = null, path = []) => {
|
|
130
|
+
if (target === null || typeof target !== 'object') return target
|
|
131
|
+
if (target[symbols.IS_OBSERVABLE] === true) return target
|
|
132
|
+
|
|
133
|
+
const { proxy, revoke } = Proxy.revocable(target, handler)
|
|
134
|
+
const metadata = {
|
|
135
|
+
target,
|
|
136
|
+
revoke,
|
|
137
|
+
root: root ?? proxy,
|
|
138
|
+
path
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
register(proxy, metadata)
|
|
142
|
+
return proxy
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const subscribe = (root, callback) => {
|
|
146
|
+
if (!subscribers.has(root)) subscribers.set(root, new Set())
|
|
147
|
+
subscribers.get(root).add(callback)
|
|
148
|
+
|
|
149
|
+
return () => {
|
|
150
|
+
const set = subscribers.get(root)
|
|
151
|
+
if (set) {
|
|
152
|
+
set.delete(callback)
|
|
153
|
+
if (set.size === 0) subscribers.delete(root)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const Observer = {
|
|
159
|
+
create: (target) => {
|
|
160
|
+
if (target === null || typeof target !== 'object') {
|
|
161
|
+
throw new TypeError('Observer.create requires an Object or an Array')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return createProxy(target)
|
|
165
|
+
},
|
|
166
|
+
observe: (observable, callback) => {
|
|
167
|
+
if (!observable[symbols.IS_OBSERVABLE]) {
|
|
168
|
+
throw new TypeError('Observer.observe must have an observable')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (typeof callback !== 'function') {
|
|
172
|
+
throw new TypeError('Observer.observe must have a callback function')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return subscribe(observable, callback)
|
|
176
|
+
},
|
|
177
|
+
destroy: (observable) => {
|
|
178
|
+
const metadata = getMetadata(observable)
|
|
179
|
+
if (!metadata) return
|
|
180
|
+
subscribers.delete(observable)
|
|
181
|
+
unregister(observable)
|
|
182
|
+
metadata.revoke()
|
|
183
|
+
},
|
|
184
|
+
symbols
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
package/observer.min.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var a={IS_OBSERVABLE:Symbol("observer.isObservable"),RAW:Symbol("observer.raw"),ROOT:Symbol("observer.root"),PATH:Symbol("observer.path")},f=[],i=!1,l=new WeakMap,d=new WeakMap,c=new Map,y=(e,t)=>{return d.set(t.target,e),l.set(e,t)},u=(e)=>{return l.get(e)},S=(e)=>{let t=l.get(e);if(t)d.delete(t.target);return l.delete(e)},O=(e,t)=>{let o=c.get(e);if(!o||o.size===0)return;for(let r of o)try{r(t)}catch(s){console.error("Subscriber error:",s)}},V=()=>{let e=f,t=new Map;f=[],i=!1;for(let o of e){if(!t.has(o.root))t.set(o.root,[]);t.get(o.root).push({type:o.type,path:o.path,oldValue:o.oldValue,newValue:o.newValue})}for(let[o,r]of t)O(o,r)},w=(e,t)=>{if(f.push({...e,root:t}),!i)i=!0,queueMicrotask(V)},m={get:(e,t,o)=>{if(t===a.IS_OBSERVABLE)return!0;if(t===a.RAW)return e;if(t===a.ROOT)return u(o).root;if(t===a.PATH)return u(o).path;let r=Reflect.get(e,t,o);if(r===null||typeof r!=="object")return r;let s=u(o);return b(r,s.root,[...s.path,t])},set:(e,t,o,r)=>{let s=e[t],n=u(r);if(o!==null&&typeof o==="object")o=b(o,n.root,[...n.path,t]);let h=Reflect.set(e,t,o,r);if(h&&s!==o)w({type:"set",path:[...n.path,t],oldValue:s,newValue:o},n.root);return h},deleteProperty:(e,t)=>{if(!(t in e))return!0;let o=e[t],r=d.get(e),s=u(r),n=Reflect.deleteProperty(e,t);if(n)w({type:"delete",path:[...s.path,t],oldValue:o,newValue:void 0},s.root);return n}},b=(e,t=null,o=[])=>{if(e===null||typeof e!=="object")return e;if(e[a.IS_OBSERVABLE]===!0)return e;let{proxy:r,revoke:s}=Proxy.revocable(e,m);return y(r,{target:e,revoke:s,root:t??r,path:o}),r},p=(e,t)=>{if(!c.has(e))c.set(e,new Set);return c.get(e).add(t),()=>{let o=c.get(e);if(o){if(o.delete(t),o.size===0)c.delete(e)}}},R={create:(e)=>{if(e===null||typeof e!=="object")throw TypeError("Observer.create requires an Object or an Array");return b(e)},observe:(e,t)=>{if(!e[a.IS_OBSERVABLE])throw TypeError("Observer.observe must have an observable");if(typeof t!=="function")throw TypeError("Observer.observe must have a callback function");return p(e,t)},destroy:(e)=>{let t=u(e);if(!t)return;c.delete(e),S(e),t.revoke()},symbols:a};export{R as Observer};
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brinzl/observer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight, zero-dependency reactive object observation library.",
|
|
5
|
+
"main": "observer.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [],
|
|
11
|
+
"author": "Brinsil Elias",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/brinzl/observer.js/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/brinzl/observer.js#readme",
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
}
|
|
20
|
+
}
|