-
Notifications
You must be signed in to change notification settings - Fork 25
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
#4258: Remove trigger listeners after invalidation #7675
Conversation
id: this.id, | ||
instanceNonce: this.instanceNonce, | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test for this file is failing on watch
because:
- before: the events would be removed in the
uninstall
function - now: the events are removed in
cancelObservers
(via abortcontroller)
The difference is that cancelObservers
is being called by runModComponents
. Is that supposed to happen?
pixiebrix-extension/src/starterBricks/triggerExtension.test.ts
Lines 242 to 249 in 0ffbc92
await extensionPoint.install(); | |
await extensionPoint.runModComponents({ reason: RunReason.MANUAL }); | |
expect(rootReader.readCount).toBe(0); | |
screen.getByRole("button").click(); | |
await tick(); | |
expect(rootReader.readCount).toBe(1); |
Are the tests calling the wrong function? Why is runModComponents
calling cancelObservers
even on watch
?
pixiebrix-extension/src/starterBricks/triggerExtension.ts
Lines 799 to 800 in 0ffbc92
async runModComponents(): Promise<void> { | |
this.cancelObservers(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @twschiller because both snippets were written by you, you might have more context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the trigger extension point might need 2 layers of abort controllers?
- Overall abort controller
- Abort controller for the mutation observer/intersection observer(s) added in the appear trigger and "watch". (Or maybe just the "watch" mutation observers
Why is runModComponents calling cancelObservers
On SPA navigation, lifecycle calls runModComponents. The cancelObservers call aborts the observer/intersection observer so they can be re-added
I think it might be valid just to keep those registered on SPA navigation if they're already registered. But you'd definitely need logic to avoid re-adding the mutation/intersection observer. I don't remember exactly, but I probably just took the route of removing/re-adding vs. keeping track of state because
Also, the removing/re-adding would be resilient to changes in the trigger selector. (Although the trigger is readonly for trigger instances. If the trigger changes via the Page Editor IIRC the old extension point gets uninstalled)
Corner cases to consider:
- What happens on SPA navigation to a URL that's not targeted by the trigger - if this is not already handled, it should be handled by lifecycle.ts calling uninstall:
if (!(await extensionPoint.isAvailable())) { - What happens on Page Editor edit - IIRC, this already works because uninstall is called on the extension point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll do with the dual controller to replicate the previous behavior. I wouldn't want to change the logic further in this PR.
$elements.on(domEventName, this.eventHandler); | ||
for (const element of $elements) { | ||
element.addEventListener(domEventName, this.eventHandler, { | ||
signal: this.abortController.signal, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The signal
here is what's failing the tests. If I keep the events until uninstall
is called, it doesn't fail, but then what is cancelObserver
for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it doesn't fail, but then what is cancelObserver for?
It's specifically for the mutation/intersection observers: #7675 (comment)
@@ -768,17 +746,22 @@ export abstract class TriggerStarterBrickABC extends StarterBrickABC<TriggerConf | |||
$elements.off("mouseenter.hoverIntent"); | |||
$elements.off("mouseleave.hoverIntent"); | |||
$elements.hoverIntent({ | |||
over: this.eventHandler, | |||
over: async ({ originalEvent }) => this.eventHandler(originalEvent), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous piece of code used event.originalEvent
everywhere. This simplifies it by calling it once.
initializationInterval = undefined; | ||
} | ||
clearInterval(initializationInterval); | ||
initializationInterval = undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to do a null-check, as long as the type isn't null
(which isn't)
this.addEventListener(this.coreEvent, this.getNativeListener(callback)); | ||
add( | ||
callback: SimpleEventListener<Detail>, | ||
options?: AddEventListenerOptions, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added support for {signal}
etc
@@ -35,30 +36,17 @@ const MAX_MANAGED_STORAGE_WAIT_MILLIS = 4500; | |||
/** | |||
* Interval for checking managed storage initialization that takes longer than MAX_MANAGED_STORAGE_WAIT_MILLIS seconds. | |||
*/ | |||
let initializationInterval: Nullishable<ReturnType<typeof setTimeout>>; | |||
let initializationInterval: ReturnType<typeof setTimeout> | undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd recommend calling out that it should be undefined
instead of Nullishable
because of clearInterval has different behavior on null vs. undefined: https://github.com/pixiebrix/pixiebrix-extension/pull/7675/files#r1495773879
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can do it, but:
- we shouldn't really prefer Nullishable over the single types (
X | null
ORX | undefined
), but only if the type is necessarilyX | null | undefined
- it's not that
clearInterval
has a different behavior, but the types don't allownull
, so the change wouldn't pass the types checker
So as long as we don't go around replacing | undefined
with Nullishable
, this isn't an issue (and even then, tsc
will immediately complain)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we shouldn't really prefer Nullishable over the single types (X | null OR X | undefined), but only if the type is necessarily X | null | undefined
@grahamlangford to avoid future bike-shedding I recommend we decide an approach and enforce via eslint
I agree with @fregante that's unnecessarily expanding types isn't valuable. The counter principle for our own code is that we by and large shouldn't be writing code where the distinction between null
vs. undefined
matters
} | ||
} | ||
// Used only for testing | ||
let controller = new AbortController(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does the abort controller get used? I'm not seeing it get passed to the event target or interval?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's being used in INTERNAL_reset
? Federico's comment mentions its only used for testing. Or maybe you reviewed this before he added that comment?
@@ -342,10 +330,9 @@ export abstract class SidebarStarterBrickABC extends StarterBrickABC<SidebarConf | |||
|
|||
/** | |||
* Shared event handler for DOM event triggers. | |||
* It's bound to this instance so that it can be removed when the extension is uninstalled. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this what you mean in our newest terminology?
* It's bound to this instance so that it can be removed when the extension is uninstalled. | |
* It's bound to this instance so that it can be removed when the mod is deactivated. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe? The file is named -Extension
and the class has a uninstall
method, so we should probably match those (one way or another)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Part of the continuing naming/domain modeling work is to move from installed/uninstalled -> activated/deactivated.
We haven't really discussed changing the class methods though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll update the comment as Misha suggested for now because that's more likely to be left behind when the methods change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ready
|
||
// Capture state at the moment of the action | ||
const state = listenerApi.getState(); | ||
|
||
abortController = new AbortController(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that we have this pattern of creating, aborting, and recreating abort controllers. I thought it could be wrapped up and clarified in a single pattern:
https://github.com/fregante/abort-utils/blob/main/source/repeatable-abort-controller.md
Thoughts on naming welcome.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on naming welcome.
I think the naming is good enough. I'm not sure how generally applicable it will be, but worth refactoring
@@ -255,7 +255,6 @@ export abstract class MenuItemStarterBrickABC extends StarterBrickABC<MenuItemSt | |||
super(metadata, logger); | |||
this.menus = new Map<UUID, HTMLElement>(); | |||
this.removed = new Set<UUID>(); | |||
this.cancelController = new AbortController(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way we should move all of these initializations to the class itself. This is now possible:
class {
map = new Map()
}
instead of:
class {
map: Map;
constructor() {
this.map = new Map();
}
}
*/ | ||
private abortController = new AbortController(); | ||
private readonly cancelHandlers = new RepeatableAbortController(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the dual-controller setup I mentioned, which fixes the tests
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7675 +/- ##
==========================================
+ Coverage 72.54% 72.57% +0.02%
==========================================
Files 1259 1259
Lines 39264 39244 -20
Branches 7326 7321 -5
==========================================
- Hits 28484 28480 -4
+ Misses 10780 10764 -16 ☔ View full report in Codecov by Sentry. |
No loom links were found in the first post. Please add one there if you'd like to it to appear on Slack. Do not edit this comment manually. |
What does this PR do?
Demo
wip, there's an existing behavior issue to solve first
Checklist