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

Limitation: event.preventDefault() #2

Open
andywer opened this issue Mar 5, 2018 · 6 comments
Open

Limitation: event.preventDefault() #2

andywer opened this issue Mar 5, 2018 · 6 comments

Comments

@andywer
Copy link

andywer commented Mar 5, 2018

Hi there!

I thought about outsourcing rendering of React components into web workers a lot last year, and there was one issue I couldn't really solve. It might help you to know about this limitation when accessing the DOM from within web workers :)

event.preventDefault()

If the click handler resides in the web worker, then it will be invoked asynchronously. Unfortunately, due to its very nature, event.preventDefault() and event.stopPropagation() have to be called synchronously in order to work.

event.stopPropagation() might be worked around by using event delegation, but I don't see a good solution for event.preventDefault(). You could only highlight the DOM node / the event listener to always prevent default behavior, so the call happens immediately in the main thread. But a conditional preventDefault() is then not possible.

Hope you can make some use of that!

Btw, keep me posted if you have a good idea how to approach this issue 😊

@AshleyScirra
Copy link
Owner

You're right, it's a fundamental limitation in the browser APIs: those methods must be called synchronously so if most of your logic is in a worker you can't respond in time.

I guess you'd have to process some logic on the DOM to be able to respond to those events synchronously. That increases the complexity of the code though...

@rchipka
Copy link

rchipka commented Dec 3, 2019

We could use something like sleep.wasm to pause execution of the source thread until we return within the target.

While this would solve the synchronization issue, we would effectively cause single-threaded execution while any remote access is taking place.

This could still be considered a reasonable trade-off, since it would allow a developer to write multi-threaded code quickly without worrying about implementing a communication layer between threads.

A developer would be able to prototype quickly and worry about performance optimization after the code has actually been written, rather than thinking about performance (i.e. implementing threads + communication layer between them) ahead of time.

When the code is written and actual problem is solved, the developer can then think about optimization by observing bottlenecks caused by heavy remote access.

At this point, the developer could wrap any sync-dependent code in a via.js API call, which would instantly turn that code-path into fully isolated, asynchronous, and thread-safe code.

For example, if I'm assigning a remote reference to a variable in a WebWorker and the code immediately following depends on that remote reference, we must stop execution until we receive the remote reference.

But, if there were something like Via.get(parentRef, property, callback), a developer could use that to easily translate sync-dependent, thread-stopping bottleneck lines of code into fully asynchronous thread-safe code with minimum effort.

Again, we're trading-off full performance out of the gate for the ability to implement faster, architect less, and optimize later.


I'd like to see a solution that embraces this trade-off in order to provide a "networked javascript scope" that can seamlessly bridge isolated runtimes without the need to implement various communication layers between them.

In particular, I'd like to see a peer-to-peer solution, where objects and their properties can be completely scattered across several runtimes and yet code in any individual runtime can access those objects as if they exist locally.

I think a completely cross-runtime (Node.js, Electron, browser, etc.) solution could be achieved with the combination of ES6 Proxies, WeakRefs, HumbleNet, and WebAssembly.

If anyone else has started on something like this, feel free to reach out to me via email - I'd love to help.

@AshleyScirra
Copy link
Owner

I don't think that solves this problem. If it wakes up on a timer, it returns from the event handler and the problem still happens. If it spins the CPU, it will block any messages arriving from the service worker. To receive a message it fundamentally must return from the event handler, therefore it is impossible to synchronously call preventDefault().

@rchipka
Copy link

rchipka commented Dec 4, 2019

@AshleyScirra good point, we can't implement a polling mechanism in javascript by calling a WebAssembly-implemented sleep function since we can't receive the event in javascript until we stop polling for it.

This means that we'd would have to move the communication layer into the WebAssembly code... which is complicated. We'd have to switch to network requests instead of the more convenient postMessage.

There are several emscripten functions that would allow us to create a network request, stop javascript execution, poll for a response, and resume execution all within WebAssembly.

But... this would seem to go beyond the scope of Via.js since we'd be implementing our own built-in network communication layer.

Not to mention, the network communication facilities provided by emscripten are fairly unsophisticated, and while we could use something like HumbleNet to create a very capable solution, it would very much expand the scope of this project.

That said, I don't think a completely separate solution needs to exist... a WebAssembly-based solution could be built that handles the synchronous p2p communication layer, while Via.js could handle the Proxy and WeakRef side of things.


TLDR;

While making this work within the scope of Via.js seems unlikely (and this issue could probably be closed), I think a solution to this problem can be built separately, on top of Via.js.

@AshleyScirra
Copy link
Owner

Fundamentally, this is nothing to do with WebAssembly. The ultimate problem is it needs to synchronously determine on a worker whether to call preventDefault(), before returning from the event handler function.

Network requests are typically async so will have the same problem as a timer.

I think the only way this could actually be done is using SharedArrayBuffer to implement a cross-thread communication bridge which can be waited on synchronously. However I think that is really overengineering the problem and will introduce a bunch of security restrictions due to the fall-out from Spectre. And I'd agree this level of solution is out-of-scope for a library like Via.js.

What I think would be much better is a way to have something like waitUntil() for events, allowing you to postpone a call to preventDefault(). There is precedence for this in the Windows JS APIs which use deferrals in a similar manner, e.g.:

Windows.ApplicationModel.DataTransfer.DataTransferManager.getForCurrentView().addEventListener("datarequested", async e =>
{
	const request = e.request;
	const deferral = request.getDeferral();

	await prepareDataAsync();
					
	deferral.complete();
});

A similar pattern for JS events where the browser waits until the complete() call before handling the default action should solve this for Via.js.

@schontz
Copy link

schontz commented Apr 26, 2024

I think this could be supported with a decorator so long as there is no conditional logic to running preventDefault. For example:

form.addEventListener('submit', (e) =>{}, undefined, { preventDefault: true })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants