Skip to content

Commit

Permalink
Remove custom effects from within another effect without breaking.
Browse files Browse the repository at this point in the history
When an effect loop is running, we cannot simply modify (mutate!) the effects
array without breaking the for-loop. So we defer the mutating to a microtask.
  • Loading branch information
kaste committed Mar 30, 2016
1 parent d10636c commit 2a02678
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 14 deletions.
4 changes: 3 additions & 1 deletion src/lib/bind/accessors.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@

_effectEffects: function(property, value, effects, old, fromAbove) {
for (var i=0, l=effects.length, fx; (i<l) && (fx=effects[i]); i++) {
fx.fn.call(this, property, value, fx.effect, old, fromAbove);
if (!fx.disabled) {
fx.fn.call(this, property, value, fx.effect, old, fromAbove);
}
}
},

Expand Down
31 changes: 22 additions & 9 deletions src/standard/effectBuilder.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,33 @@
}

// Theoretically the effects must be sorted again, but `function`-effects
// are executed last anyway, so we can skip this for now.
// are executed last anyway, so we can skip this for now. NOTE that, if
// you add a custom effect inside another effect, t.i. the notification-
// loop is already running, the newly added effect will not be executed
// before the next notification we have to propagate. This is intended
// and a bonus point for adding all custom effects at the end.
return this._addPropertyEffect(property, 'function', fn);
},

removeCustomEffect: function(fx) {
var property = fx.trigger;
var effects = property && this._propertyEffects &&
this._propertyEffects[property];
if (effects) {
var index = effects.indexOf(fx);
if (index !== -1) {
effects.splice(index, 1);
// Since effects can be removed as a side-effect, t.i. within a running
// effects-loop, we must ensure not to change the effects- array
// immediately, otherwise the for-loop would break. On the other side
// removing an effect must be immediate too. So we use a flag here, and
// actually remove the effect in a microtask.
fx.disabled = true;

this.async(function() {
var property = fx.trigger;
var effects = property && this._propertyEffects &&
this._propertyEffects[property];
if (effects) {
var index = effects.indexOf(fx);
if (index !== -1) {
effects.splice(index, 1);
}
}
}
});
},

// prototyping
Expand Down
10 changes: 6 additions & 4 deletions src/standard/notify-path.html
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,12 @@
var fx$ = this._propertyEffects && this._propertyEffects[model];
if (fx$) {
for (var i=0, fx; (i<fx$.length) && (fx=fx$[i]); i++) {
// use memoized path functions
var fxFn = fx.pathFn;
if (fxFn) {
fxFn.call(this, path, value, fx.effect);
if (!fx.disabled) {
// use memoized path functions
var fxFn = fx.pathFn;
if (fxFn) {
fxFn.call(this, path, value, fx.effect);
}
}
}
}
Expand Down
41 changes: 41 additions & 0 deletions test/unit/bind.html
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,47 @@
assert.equal(called, 1);
});

test('Removing an effect as a side-effect does not break the running ' +
'effect loop', function() {
var el = document.createElement('x-custom-effect');

var called = 0;
var fx = el.addCustomEffect('foo', function() {
el.removeCustomEffect(fx);
});
el.addCustomEffect('foo', function() {
called += 1;
});

el.foo = 'bar';
assert.equal(called, 1);
});

test('Adding an effect as a side-effect does not invoke it immediately',
function() {
var el = document.createElement('x-custom-effect');

var called = 0;
var firstRun = true;
el.addCustomEffect('foo', function() {
if (firstRun) {
el.addCustomEffect('foo', function() {
called += 1;
});
firstRun = false;
}
});

el.foo = 'bar';
assert.equal(firstRun, false);
assert.equal(called, 0);

el.foo = 'quod';
assert.equal(called, 1);


});


});

Expand Down

0 comments on commit 2a02678

Please sign in to comment.