Skip to content

Commit 84abba0

Browse files
authored
Merge pull request #3492 from maartenbreddels/fix_change_trigger_after_exception
fix: backbone set patch in ill defined state after exceptions in handler
2 parents e6ba2b6 + 2cf4848 commit 84abba0

File tree

2 files changed

+80
-46
lines changed

2 files changed

+80
-46
lines changed

packages/base/src/backbone-patch.ts

Lines changed: 53 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -76,58 +76,65 @@ export function set(
7676
const changes = [];
7777
const changing = this._changing;
7878
this._changing = true;
79-
80-
if (!changing) {
81-
// EDIT: changed to use object spread instead of _.clone
82-
this._previousAttributes = { ...this.attributes };
83-
this.changed = {};
84-
}
85-
86-
const current = this.attributes;
87-
const changed = this.changed;
88-
const prev = this._previousAttributes;
89-
90-
// For each `set` attribute, update or delete the current value.
91-
for (const attr in attrs) {
92-
val = attrs[attr];
93-
// EDIT: the following two lines use our isEqual instead of _.isEqual
94-
if (!utils.isEqual(current[attr], val)) {
95-
changes.push(attr);
96-
}
97-
if (!utils.isEqual(prev[attr], val)) {
98-
changed[attr] = val;
99-
} else {
100-
delete changed[attr];
79+
try {
80+
if (!changing) {
81+
// EDIT: changed to use object spread instead of _.clone
82+
this._previousAttributes = { ...this.attributes };
83+
this.changed = {};
10184
}
102-
unset ? delete current[attr] : (current[attr] = val);
103-
}
10485

105-
// Update the `id`.
106-
this.id = this.get(this.idAttribute);
107-
108-
// Trigger all relevant attribute changes.
109-
if (!silent) {
110-
if (changes.length) {
111-
this._pending = options;
86+
const current = this.attributes;
87+
const changed = this.changed;
88+
const prev = this._previousAttributes;
89+
90+
// For each `set` attribute, update or delete the current value.
91+
for (const attr in attrs) {
92+
val = attrs[attr];
93+
// EDIT: the following two lines use our isEqual instead of _.isEqual
94+
if (!utils.isEqual(current[attr], val)) {
95+
changes.push(attr);
96+
}
97+
if (!utils.isEqual(prev[attr], val)) {
98+
changed[attr] = val;
99+
} else {
100+
delete changed[attr];
101+
}
102+
unset ? delete current[attr] : (current[attr] = val);
112103
}
113-
for (let i = 0; i < changes.length; i++) {
114-
this.trigger('change:' + changes[i], this, current[changes[i]], options);
104+
105+
// Update the `id`.
106+
this.id = this.get(this.idAttribute);
107+
108+
// Trigger all relevant attribute changes.
109+
if (!silent) {
110+
if (changes.length) {
111+
this._pending = options;
112+
}
113+
for (let i = 0; i < changes.length; i++) {
114+
this.trigger(
115+
'change:' + changes[i],
116+
this,
117+
current[changes[i]],
118+
options
119+
);
120+
}
115121
}
116-
}
117122

118-
// You might be wondering why there's a `while` loop here. Changes can
119-
// be recursively nested within `"change"` events.
120-
if (changing) {
121-
return this;
122-
}
123-
if (!silent) {
124-
while (this._pending) {
125-
options = this._pending;
126-
this._pending = false;
127-
this.trigger('change', this, options);
123+
// You might be wondering why there's a `while` loop here. Changes can
124+
// be recursively nested within `"change"` events.
125+
if (changing) {
126+
return this;
127+
}
128+
if (!silent) {
129+
while (this._pending) {
130+
options = this._pending;
131+
this._pending = false;
132+
this.trigger('change', this, options);
133+
}
128134
}
135+
} finally {
136+
this._pending = false;
137+
this._changing = false;
129138
}
130-
this._pending = false;
131-
this._changing = false;
132139
return this;
133140
}

packages/base/test/src/widget_test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,33 @@ describe('WidgetModel', function () {
534534
expect(changeA).to.be.calledBefore(change);
535535
expect(change).to.be.calledWith(this.widget);
536536
});
537+
it('triggers change events after exception', async function () {
538+
const changeA = sinon.spy(function changeA() {
539+
return;
540+
});
541+
const changeB = sinon.spy(function changeB() {
542+
throw 'error';
543+
});
544+
const change = sinon.spy(function change() {
545+
return;
546+
});
547+
this.widget.on('change:a', changeA);
548+
this.widget.on('change:b', changeB);
549+
this.widget.on('change', change);
550+
// first we trigger a set that causes an exception
551+
try {
552+
this.widget.set('b', 42);
553+
} catch {
554+
// empty
555+
}
556+
expect(change).to.not.be.called;
557+
// from now on this test is similar to 'triggers change events'
558+
this.widget.set('a', 100);
559+
expect(changeA).to.be.calledOnce;
560+
expect(changeA).to.be.calledWith(this.widget, 100);
561+
expect(changeA).to.be.calledBefore(change);
562+
expect(change).to.be.calledWith(this.widget);
563+
});
537564
it('handles multiple values to set', function () {
538565
expect(this.widget.get('a')).to.be.undefined;
539566
expect(this.widget.get('b')).to.be.undefined;

0 commit comments

Comments
 (0)