Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ [Observable] Make observable safe to remove handler while firing. #37887

Merged
merged 22 commits into from
Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6aabe91
Added tasts
mszylkowski Dec 21, 2021
dc1fa87
Undo
mszylkowski Dec 21, 2021
d707e5b
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Dec 28, 2021
0e3df75
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Dec 28, 2021
a3f7bd0
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Jan 4, 2022
8c5119c
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Jan 6, 2022
77f8435
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Jan 10, 2022
16e712e
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Jan 19, 2022
8088eb7
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Jan 25, 2022
3858b2a
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Jan 28, 2022
e12d2f6
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Jan 31, 2022
2a29105
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Feb 1, 2022
a5b781f
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Feb 10, 2022
b8006ee
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Mar 8, 2022
b570e86
Merge branch 'main' of github.com:ampproject/amphtml
mszylkowski Mar 10, 2022
974e219
Make observable work with removing items while firing
mszylkowski Mar 17, 2022
e0a87a1
Using set for removing
mszylkowski Mar 17, 2022
5dc4e93
Remove previous code
mszylkowski Mar 17, 2022
c5d9d63
Added iterating depth
mszylkowski Mar 17, 2022
e844875
Added test to check iterations of handler removal
mszylkowski Mar 17, 2022
09809c6
Simplified impl and cleaned unused tests
mszylkowski Mar 17, 2022
be19f25
Added comment
mszylkowski Mar 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/core/data-structures/observable.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {removeItem} from '#core/types/array';
import {remove, removeItem} from '#core/types/array';

/**
* This class helps to manage observers. Observers can be added, removed or
Expand All @@ -12,6 +12,12 @@ export class Observable {
constructor() {
/** @type {?Array<function(TYPE=):void>} */
this.handlers_ = null;

/** @type {!Set<function(TYPE=):void>} */
this.handlersToRemove_ = new Set();

/** @type {number} */
this.iteratingDepth_ = 0;
}

/**
Expand All @@ -31,13 +37,18 @@ export class Observable {

/**
* Removes the observer from this instance.
* Can be called in a handler fired.
* @param {function(TYPE=):void} handler Observer's instance.
*/
remove(handler) {
if (!this.handlers_) {
return;
}
removeItem(this.handlers_, handler);
if (this.iteratingDepth_ > 0) {
this.handlersToRemove_.add(handler);
} else {
removeItem(this.handlers_, handler);
}
}

/**
Expand All @@ -58,9 +69,15 @@ export class Observable {
if (!this.handlers_) {
return;
}
this.iteratingDepth_++;
for (const handler of this.handlers_) {
handler(opt_event);
}
this.iteratingDepth_--;
if (this.handlersToRemove_.size && this.iteratingDepth_ == 0) {
remove(this.handlers_, (handler) => this.handlersToRemove_.has(handler));
this.handlersToRemove_.clear();
}
}

/**
Expand Down
44 changes: 44 additions & 0 deletions test/unit/core/data-structures/test-observable.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,48 @@ describes.sandboxed('data structures - Observable', {}, () => {
expect(observer1Called).to.equal(1);
expect(observer2Called).to.equal(2);
});

it('remove while firing', () => {
let observer1Called = 0;
const observer1 = () => {
observer1Called++;
};
observable.add(observer1);
const remove = observable.add(() => {
remove();
});
observable.add(observer1);

observable.fire();

expect(observer1Called).to.equal(2);
});

it('remove while firing on multiple depths', () => {
// First depth of iteration will remove the handler, should not stop second depth.
let observer1Called = 0;
let iterations = 0;
const observer1 = () => {
observer1Called++;
};
const secondRemove = observable.add(observer1);
observable.add(() => {
iterations++;
if (iterations == 1) {
secondRemove();
observable.fire();
}
});

// At this point handlers are: [increaseByOne, removePreviousHandler]
// When firing the first time, handler should not be removed until the end.
observable.fire();
expect(observer1Called).to.equal(2);
expect(iterations).to.equal(2);

// When firing again, handler should have been removed.
observable.fire();
expect(observer1Called).to.equal(2);
expect(iterations).to.equal(3);
});
});