Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

Commit 548b0d4

Browse files
EisenbergEffectcaitp
authored andcommitted
feat(observer): enable a general mechanism for plugging in custom field observers
We need a mechanism for handling different types of object/field observation. The first prototype was geared towards handling certain types of bindings to HTML elements. This is the improved implementation which can handle that scenario as well as O.o and any custom change notification system. Inteternally, explicit observer related modes for the dirty checker are use so that we can noop when no event is raised. Interaction tests included for ObserverSelector and Observer interaction.
1 parent 0828f50 commit 548b0d4

File tree

2 files changed

+219
-73
lines changed

2 files changed

+219
-73
lines changed

src/dirty_checking.js

Lines changed: 40 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ var _MODE_GETTER_ = 3;
2222
var _MODE_MAP_FIELD_ = 4;
2323
var _MODE_ITERABLE_ = 5;
2424
var _MODE_MAP_ = 6;
25-
var _MODE_MAP_FIELD_NOTIFY_ONLY_= 10;
25+
var _NOT_NOTIFIED_= 10;
26+
var _NOTIFIED_= 11;
27+
2628
export class GetterCache {
2729
constructor(map) {
2830
this._map = map;
@@ -31,6 +33,7 @@ export class GetterCache {
3133
return this._map[field] || null;
3234
}
3335
}
36+
3437
export class DirtyCheckingChangeDetectorGroup extends ChangeDetector {
3538
constructor(parent, cache) {
3639
this._parent = parent;
@@ -186,11 +189,16 @@ export class DirtyCheckingChangeDetectorGroup extends ChangeDetector {
186189
return lines.join('\n');
187190
}
188191
}
192+
189193
export class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup {
190-
constructor(cache, notifier = new ChangeNotifier()) {
194+
constructor(cache, observerSelector=null) {
191195
super(null, cache);
192196
this._fakeHead = DirtyCheckingRecord.marker();
193-
this._notifier = notifier;
197+
this._observerSelector = observerSelector || { getObserver(){ return null; } };
198+
}
199+
200+
getObserver(obj, field){
201+
return this._observerSelector.getObserver(obj, field);
194202
}
195203

196204
_assertRecordsOk() {
@@ -278,47 +286,6 @@ class ChangeIterator {
278286
}
279287
}
280288

281-
// TODO: Add unit tests for this!
282-
export class ChangeNotifier {
283-
constructor() {
284-
this.objectRecords = new WeakMap();
285-
}
286-
notify(object) {
287-
var records = this.objectRecords.get(object) || [];
288-
records.forEach((record) => {
289-
// TODO: Is this the correct way of manually dirty checking
290-
// some records??
291-
var watch = record._handler._watchHead;
292-
if (watch && record.check(true)) {
293-
watch._dirty = true;
294-
watch.invoke();
295-
}
296-
});
297-
}
298-
isNotifyOnly(object, fieldName) {
299-
return false;
300-
}
301-
addWatch(object, fieldName, record) {
302-
var records = this.objectRecords.get(object);
303-
if (!records) {
304-
records = [];
305-
this.objectRecords.set(object, records);
306-
}
307-
records.push(record);
308-
return records.length;
309-
}
310-
removeWatch(object, fieldName, record) {
311-
var records = this.objectRecords.get(object);
312-
if (records) {
313-
var index = records.indexOf(record);
314-
if (index !== -1) {
315-
records.splice(index, 1);
316-
}
317-
}
318-
return records ? records.length : 0;
319-
}
320-
}
321-
322289
class DirtyCheckingRecord extends ChangeRecord {
323290
constructor(group, object, fieldName, getter, handler) {
324291
this._group = group;
@@ -351,21 +318,22 @@ class DirtyCheckingRecord extends ChangeRecord {
351318
return this._object;
352319
}
353320
_clearObject() {
354-
var notifier = this._group && this._group._root._notifier;
355-
if (notifier && this._object && (
356-
this._mode === _MODE_MAP_FIELD_NOTIFY_ONLY_ || this._mode === _MODE_MAP_FIELD_)
357-
) {
358-
notifier.removeWatch(this._object, this._field, this);
321+
if(this._observer){
322+
this._observer.close();
323+
this._observer = null;
359324
}
325+
360326
this._object = null;
361327
}
362328
set object(obj) {
363329
this._clearObject(obj);
364330
this._object = obj;
331+
365332
if (obj === null) {
366333
this._mode = _MODE_IDENTITY_;
367334
return;
368335
}
336+
369337
if (this.field === null) {
370338
// _instanceMirror = null; --- Again, do we need reflection?
371339
if (typeof obj === "object") {
@@ -385,28 +353,32 @@ class DirtyCheckingRecord extends ChangeRecord {
385353
}
386354
return;
387355
}
388-
if (typeof obj === "object") {
389-
var notifier = this._group && this._group._root._notifier;
390-
if (notifier && notifier.isNotifyOnly(obj, this._field)) {
391-
this._mode = _MODE_MAP_FIELD_NOTIFY_ONLY_;
392-
} else {
393-
this._mode = _MODE_MAP_FIELD_;
394-
}
395-
notifier.addWatch(obj, this._field, this);
396-
// _instanceMirror = null; --- Reflection needed?
397-
} else if (this._getter !== null) {
356+
357+
this._observer = this._group && this._group._root.getObserver(obj, this._field);
358+
359+
if(this._observer){
360+
this._mode = _NOTIFIED_;
361+
this.newValue = this._observer.open((value) =>{
362+
this.newValue = value;
363+
this._mode = _NOTIFIED_;
364+
});
365+
}else if(this._getter !== null){
398366
this._mode = _MODE_GETTER_;
399-
// _instanceMirror = null; --- Reflection needed?
400-
} else {
401-
this._mode = _MODE_REFLECT_;
402-
// _instanceMirror = reflect(obj); --- I'm really not sure about this!
367+
}else{
368+
this._mode = _MODE_MAP_FIELD_;
403369
}
404370
}
405-
check(inNotify) {
371+
check() {
406372
// assert(_mode != null); --- Traceur v0.0.24 missing assert()
407373
var current;
408374
switch (this._mode) {
409-
case _MODE_MARKER_: return false;
375+
case _NOT_NOTIFIED_:
376+
case _MODE_MARKER_:
377+
return false;
378+
case _NOTIFIED_:
379+
current = this.newValue;
380+
this._mode = _NOT_NOTIFIED_;
381+
break;
410382
case _MODE_REFLECT_:
411383
// TODO:
412384
// I'm not sure how much support for Reflection is available in Traceur
@@ -417,14 +389,7 @@ class DirtyCheckingRecord extends ChangeRecord {
417389
break;
418390
case _MODE_GETTER_:
419391
current = this._getter(this.object);
420-
break;
421-
case _MODE_MAP_FIELD_NOTIFY_ONLY_:
422-
if (inNotify) {
423-
current = this.object[this.field];
424-
} else {
425-
return false;
426-
}
427-
break;
392+
break;
428393
case _MODE_MAP_FIELD_:
429394
if (!this.object) return undefined;
430395
current = this.object[this.field];
@@ -439,6 +404,7 @@ class DirtyCheckingRecord extends ChangeRecord {
439404
throw "UNREACHABLE";
440405
// assert(false); --- Traceur 0.0.24 missing assert()
441406
}
407+
442408
var last = this.currentValue;
443409
if (last !== current) {
444410
// TODO:
@@ -458,6 +424,7 @@ class DirtyCheckingRecord extends ChangeRecord {
458424
return true;
459425
}
460426
}
427+
461428
return false;
462429
}
463430
remove() {

test/observer.spec.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import {
2+
GetterCache,
3+
DirtyCheckingChangeDetector,
4+
DirtyCheckingChangeDetectorGroup
5+
} from '../src/dirty_checking';
6+
7+
describe('observer', function() {
8+
var getterCache, detector, setup, setupUser, selector;
9+
10+
beforeEach(function() {
11+
setup = function(observer) {
12+
getterCache = new GetterCache({
13+
'name': function(o) { return o.name; }
14+
});
15+
16+
selector = new ExplicitObserverSelector(observer);
17+
detector = new DirtyCheckingChangeDetector(getterCache, selector);
18+
}
19+
});
20+
21+
describe('selector', function() {
22+
it('should receive object instance for use in selection process', function() {
23+
var observer = new ExplicitObserver(),
24+
user = new _User('Rob');
25+
26+
setup(observer);
27+
detector.watch(user, 'name', null);
28+
29+
expect(selector.lastObj).toBe(user);
30+
});
31+
32+
it('should recieve field name for use in selection process', function() {
33+
var observer = new ExplicitObserver(),
34+
user = new _User('Rob');
35+
36+
setup(observer);
37+
detector.watch(user, 'name', null);
38+
39+
expect(selector.lastField).toBe('name');
40+
});
41+
42+
it('should return an observer if obj/field observation is possible', function() {
43+
var observer = new ExplicitObserver(),
44+
user = new _User('Rob');
45+
46+
setup(observer);
47+
detector.watch(user, 'name', null);
48+
49+
expect(selector.observersReturned).toBe(1);
50+
});
51+
52+
it('should return null if obj/field observation is not possible', function() {
53+
var user = new _User('Rob');
54+
55+
setup(null);
56+
detector.watch(user, 'name', null);
57+
58+
expect(selector.observersReturned).toBe(1);
59+
});
60+
});
61+
62+
describe('instances', function() {
63+
it('are opened by the dirty checking mechanism', function() {
64+
var observer = new ExplicitObserver(),
65+
user = new _User('Rob');
66+
67+
setup(observer);
68+
detector.watch(user, 'name', null);
69+
70+
expect(observer.openCalls).toBe(1);
71+
});
72+
73+
it('are provided with a callback by the dirty checking mechanism', function() {
74+
var observer = new ExplicitObserver(),
75+
user = new _User('Rob');
76+
77+
setup(observer);
78+
detector.watch(user, 'name', null);
79+
80+
expect(observer.callback).not.toBe(null);
81+
});
82+
83+
it('immediately notify the dirty checker when opened', function() {
84+
var observer = new ExplicitObserver(),
85+
user = new _User('Rob');
86+
87+
setup(observer);
88+
detector.watch(user, 'name', null);
89+
90+
var changes = detector.collectChanges();
91+
expect(changes.iterate()).toBe(true);
92+
});
93+
94+
it('notify the dirty checker when changes occur in the observed object', function() {
95+
var observer = new ExplicitObserver(),
96+
user = new _User('Rob');
97+
98+
setup(observer);
99+
detector.watch(user, 'name', null);
100+
101+
var changes = detector.collectChanges();
102+
expect(changes.iterate()).toBe(true);
103+
104+
changes = detector.collectChanges();
105+
expect(changes.iterate()).toBe(false);
106+
107+
observer.notify('Eisenberg');
108+
109+
changes = detector.collectChanges();
110+
expect(changes.iterate()).toBe(true);
111+
});
112+
113+
//TODO: internally, it appears that remove calls are not being made...
114+
xit('closes observers when dirty checking records are removed', function() {
115+
var observer = new ExplicitObserver(),
116+
user = new _User('Rob'),
117+
group;
118+
119+
setup(observer);
120+
group = detector.newGroup();
121+
group.watch(user, 'name', null);
122+
123+
expect(observer.closeCalls).toBe(0);
124+
group.remove();
125+
expect(observer.closeCalls).toBe(1);
126+
});
127+
});
128+
});
129+
130+
class _User {
131+
constructor(name) {
132+
this.name = name;
133+
}
134+
}
135+
136+
class ExplicitObserverSelector{
137+
constructor(observer){
138+
this.observer = observer;
139+
this.observersReturned = 0;
140+
}
141+
142+
getObserver(obj, field){
143+
var observer;
144+
145+
this.lastObj = obj;
146+
this.lastField = field;
147+
148+
if(this.observer){
149+
this.observer.obj = obj;
150+
this.observer.field = field;
151+
}
152+
153+
this.observersReturned++;
154+
return this.observer;
155+
}
156+
}
157+
158+
class ExplicitObserver{
159+
constructor(){
160+
this.openCalls = 0;
161+
this.closeCalls = 0;
162+
}
163+
164+
notify(value){
165+
this.obj[this.field] = value;
166+
this.callback(value);
167+
}
168+
169+
open(callback){
170+
this.openCalls++;
171+
this.callback = callback;
172+
return this.obj[this.field];
173+
}
174+
175+
close(){
176+
this.closeCalls++;
177+
this.callback = null;
178+
}
179+
}

0 commit comments

Comments
 (0)