mirror of
https://github.com/ElectronNET/Electron.NET.git
synced 2026-02-03 21:25:13 +00:00
ApiEventManager Rework #1008
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @softworkz on GitHub (Nov 9, 2025).
Figured out the problem
@softworkz could you advise whether it requires any locks and if so where.
Originally posted by @agracio in https://github.com/ElectronNET/Electron.NET/issues/913#issuecomment-3508416476
@softworkz commented on GitHub (Nov 9, 2025):
@agracio - This was much harder than I had thought and I had to try it out myself to be sure.
Below is a new version of the ApiEventManager which should be solid, elegant, easy-to-understand and low-code.
Notes
It's not worth to introduce common handling for those 4 cases
@softworkz commented on GitHub (Nov 9, 2025):
PS: I created this new issue because the PR isn't really a good place for discussing this. 😉
@agracio commented on GitHub (Nov 9, 2025):
I have done the following: https://github.com/agracio/Electron.NET/blob/develop/src/ElectronNET.API/API/ApiBase.cs
Scroll to line 126.
The only thing that is missing is locks in my opinion.
I will be happy to do that but are you sure Socket can handle correct serialization/deserialization? The only reason I have converters is because that is how it is used in original code.
Example
@softworkz commented on GitHub (Nov 9, 2025):
This is great, I would have moved it there as well and using CallerMemberName is cool on top.
And that is the problem. I've been sitting on this for 2.5h - almost since you had asked about locks.
Since you have already gone in the same direction, you just need to copy the EventContainer class into ApiBase and the 4 method bodies for Add/RemoveEvent.
You can try to figure out why I'm saying it's better, or I can explain tomorrow (I need a break now...)
The original code used the old SocketIO which couldn't properly handle it. For ElectronNET.Core, I had updated all libraries (but not the code).
I had tried without converters and all tests have still succeeded. I mean, you can also start by just ignoring the converters and see how it goes on your side.
Do you have something like ReSharper? Not that you start doing those removals by hand... 😆 ...it's a matter of 5 seconds.
@softworkz commented on GitHub (Nov 9, 2025):
I want to stress that I like the changes you made. If you hadn't made me think about locking, I would have said that it's very good..
@agracio commented on GitHub (Nov 9, 2025):
Of course 😄
I mean it is Jetbrains Rider it could not be any other way.
@FlorianRappl commented on GitHub (Nov 9, 2025):
Honestly, I think I can solve that problem by shifting it.
Let me explain: The main problem with the .NET code is that multi threading has always been a bit of pain. That's why many modern languages try to circumvent the issue by essentially only allowing code / variables to be touched by the owning thread (fiber, coroutine, whatever you want to call it - for sake of argument lets treat them as equal here). JavaScript for instance is single-threaded, with Node.js using a Reactor pattern to let all native code flow into that single thread.
That all being written - what if we just let the IPC counter part handle all that? Right now what we do:
What I propose is to leave that in Node.js:
Result? Node.js handles the concurrent part, which it will do very well. In .NET we only have a simple logic to reason with.
Any thoughts on that one @softworkz @agracio ?
Remark: Improving the
main.js/ Node.js process can also be done to avoid having Electron processes floating around. Once the parent process ID is removed / no longer active the process could be shut down. This could maybe be also achieved with a periodic keep alive from the WebSocket channel. If the keep alive is missing, we'll close the process.@agracio commented on GitHub (Nov 9, 2025):
I was thinking that it can be done the same way but keep wondering if there is a more elegant solution. To be honest probably not.
In a few hours of writing code and running tests had about 15 hanging electron.exe processes.
As for improving IPC - no thoughts about it yet. Refactoring code tends to create a very narrow field of view for me until it is finished. In other words right now I don't see the forest for the trees.
Will try to get a better overall view some time later.
@softworkz commented on GitHub (Nov 9, 2025):
You probably both missed my post https://github.com/ElectronNET/Electron.NET/pull/908#issuecomment-3506758001 - but admittedly, it's not a good idea to spin long discussions in PRs. So here's sure better. Repost:
Well, those are the collaterals, but the source of all evil is something quite different:
It's the use of SocketIO named events, and trying to squeeze the behavior that we would actually need into the limitations and constraints of SocketIO events.
That is the major design - let's say 'flaw' - that is leading to all the problems that we (and the fork) have to fight against.
The current implementation tries to establish short-lived SocketIO event channels "per invocation":
That's only a band-aid, but not really a good solution, because when the subsequent invocations could be made in parallel, they might not receive the same return value than you get from the still pending invocation. This can be important when you need to synchronize window sizes and/or positions.
In my case for example, I had to resort to using the native X-Window (x11Lib) API via P/Invoke in order to achieve a fast and reliable synchroniztation of windows on Linux (the were other reasons for this as well, though)
The Ideal Way
I have a bit of a problem talking about this, because I don't want to make any promises that I won't be able to fulfill, but as a matter of fact, I have done an implementation for bidirectional JS(browser) <=> CSharp projection which brings JS objects to C# and C# objects to JS. It supports method/function calling (async + sync from JS => C#), property retrieval and event registration (in both directions). It works cross-browser and "cross-ui-framework" (UWP/Xbox, WinUI w. WebView2, Electron, Android, iOS, Tizen/untested), battle-tested (approaching 1M installations) and high-performant (it can sync the movement of a CSS transition to a hWND native window on Windows).
Communication is message-based and all goes through a single channel. That would mean - in regards to SocketIO - there would be a single
On('message')at app start and a singleOff('message')on shutdown. Messages have a serial number, which means that - no matter how many invocations are active in parallel - each invocation will always get its own response, without any collisions or race conditions.On the JS side, all invocations are be made directly using the JS function prototypes,, there's no need to create any proxy classes like al the TS/JS code under
ElectronNET.Host/api- only some edge cases would need special/explicit handling.At the C# side, the JS objects are represented by interfaces (which are autogenerated). At runtime, objects are materialized at runtime via System.Reflection.DispatchProxy.
So - that's a recipe that works and that I would use when building ElectronNET from scratch.
I don't think that I'll have time to adapt this for ElectronNET - but the messaging part alone could be implemented with a lot less effort. The TypeScript code rewriting could probably be tasked to an AI agent once the required architecture change is implemented.
@agracio commented on GitHub (Nov 9, 2025):
Would it be able to handle enums?
Asking to make sure I can make changes.
EDIT: I will be adding tests alongside the changes so should be able to check soon.
@softworkz commented on GitHub (Nov 9, 2025):
Like this? No (we have differences in casing and sometimes even more different names where the mapping is done via certain attributes on enum members.
Enum.Parse()(better use the generic version) can do case-insensitive comparisons, but that's it then. Only a properly configured json serializer can do this.But this should work:
Socket.On(....
@agracio commented on GitHub (Nov 9, 2025):
works perfectly and is included in test 😉
@agracio commented on GitHub (Nov 10, 2025):
ApiEventManager rework is taking longer than expected as I am also refactoring tasks to use new
ApiBase.There are some obvious mistakes that I have to correct as well as just plain renaming of current event names. And of course having to add tests to any new tasks.
@agracio commented on GitHub (Nov 10, 2025):
Well apparently there was a major PR merged into
developwhile I was working and now pretty much everything is conflicting so no ETA for now. Was just about to create PR.Will go bang my head against some walls and then maybe much later today or tomorrow will have something.
@FlorianRappl commented on GitHub (Nov 10, 2025):
There is no need for you doing all the work. The PR has been announced for a while - sorry that you are going through that.
If you need help just make the PR, open the branch for changes and the conflicts can be resolved collaboratively.
Other question is if we should merge #920 already. It will for sure cause more conflicts, but the question is if its more efficient to do that in one sweep or use two takes (I tend to think that having conflicts once is more efficient than dealing with them twice).
@agracio commented on GitHub (Nov 10, 2025):
@softworkz should take a close look at https://github.com/ElectronNET/Electron.NET/pull/920 since it makes alot of changes to tasks. I will pause any work until all PRs are merged there is no point to do any changes right now.
I have modified quiet a few files to use ApiBase GetProperty instead of calling BridgeConnector so that is another big source of conflicts.
@softworkz commented on GitHub (Nov 10, 2025):
#920 is fine. You do not need to pause. I have rebased your branch on top of #920 and it had (simple) conflicts in two files only (Screen.cs and another). Easy to resolve. The result is here: https://github.com/softworkz/ElectronNET/tree/agracio_event_mgr_rebased
When you get conflicts on rebasing (after 920 merged), you can just take the whole file content from that branch to resolve.
@softworkz commented on GitHub (Nov 10, 2025):
Agree, I'll also happily help reolving - these are all quite trivial conflicts. It's just a matter of tooling and having some routine in working with Git in those cases.
@agracio commented on GitHub (Nov 11, 2025):
When using
BridgeConnector.Socket.Emitis there a difference between two calls in the code below, will anOnmethod treat them as different calls? Unfortunately I am not familiar with sockets that well.@softworkz commented on GitHub (Nov 11, 2025):
It has nothing to do with Sockets. The parameters are just serialized to an array (JSON).
So, for the latter, the remote side will see
[ 0 ]and for the former[ 0, null ]. So whether it matters or not, depends on how it's handled.@agracio commented on GitHub (Nov 11, 2025):
Thanks for quick reply, just tested in Electron.NET (we have tests now!!). The answer is that it is treated as same message.
This means that I can use this code in
ApiBaseand add more nullable args if needed without having to jump through multiple ifs.This also mean that you don't need to have
CallMethod0,CallMethod1,CallMethod2,CallMethod3@softworkz commented on GitHub (Nov 11, 2025):
Oh, no - that it doesn't mean...
@agracio commented on GitHub (Nov 11, 2025):
If the above code works with a nullable argument then this type of method should also produce the same results
So far tests agree.
@softworkz commented on GitHub (Nov 11, 2025):
I said:
See this example:
@agracio commented on GitHub (Nov 11, 2025):
Very good example, nullable method will not work if input to a same event can be optional. But hopefully a good 85% of cases can be simplified.
There are also tasks that use cancellation token and are not currently handled by ApiBase.
@agracio commented on GitHub (Nov 11, 2025):
Btw the task you highlighted is on the tasks that use new Guid in addition to Id, there are other tasks that use only new Guid without Id but tbh since it is always a new Guid I feel that Id should not even be there. Do you have any clues as to why that is.
@softworkz commented on GitHub (Nov 11, 2025):
Please leave that as is. The four methods are just 50 lines of code and not a good subject for "simplification". Especially in this case: it would not be clear to other developers in the future why this or that is used and then they might think that they can "simplify" it even more by applying the same pattern to those cases where it matters - not knowing that it will break things.
Also - after thinking about it, I think it's generally odd to always provide 3 parameters regardless of how many params are actually required and expected. That's not an improvement.
@agracio commented on GitHub (Nov 11, 2025):
CallMethodis actually a small example,GetPropertyis a bigger one and I have already added a nullable object to it but might need to add a second one, will handle it with ifs instead.@softworkz commented on GitHub (Nov 11, 2025):
The Id is needed to retrieve the window at the electron side and the guid is used for the event name.
@softworkz commented on GitHub (Nov 11, 2025):
Why?
@agracio commented on GitHub (Nov 11, 2025):
Example Screen.cs
ApiBase
@softworkz commented on GitHub (Nov 11, 2025):
Ah, I see. From a strict point of view, these are fuctions, not properties.. 😉
@softworkz commented on GitHub (Nov 11, 2025):
We might need to rename it later, but sure, why not. When an existing pattern can be applied, that's often better than creating many separate ones.
@agracio commented on GitHub (Nov 11, 2025):
I am not the one who named it
GetProperty- you initially used it in properties as well as methods but later reverted property usage due to incompatible event naming. But I will fix those as well.@softworkz commented on GitHub (Nov 11, 2025):
I used it in methods? I thought I used CallMethodX for methods, no? But honestly I don't remember all cases, I'm doing so many things in parallel...
More importantly
We'll also need to make the dictionary static. Currently there's no locking when the same call is made on different objects of the same type. I just didn't come to a good idea (without additional dictionary key prefixing). But now I have - and it's a very simple change - you can do it since you're at it anyway:
@softworkz commented on GitHub (Nov 11, 2025):
I think setting it in the constructor is a bit better as it avoids the first level lookups:
Whatever you like..
@agracio commented on GitHub (Nov 11, 2025):
Yes that was part of your refactoring, we discussed different endings for 'completed' at the time.
I will make code changes.
@softworkz commented on GitHub (Nov 11, 2025):
Perfect - then we should be even with the fork at minimum - probably better...
(I mean regarding this specific apart - otherwise we're anyway)
@agracio commented on GitHub (Nov 11, 2025):
Could you explain the importance of Guids in the methods I posted above, why not just use Id?
Another desirable addition would be inclusion of cancelation tokens for tasks that use them but thats a much bigger addition.
@softworkz commented on GitHub (Nov 11, 2025):
Gregor has done this in
412f628422in 2019.I can only speculate that it was done to mitigate the concurrency issues that we are still fighting against and that these might have been very apparent in this case because it takes a long time to return.
@softworkz commented on GitHub (Nov 11, 2025):
Or maybe because the Id is actually a window id and not really identifying a session.
@agracio commented on GitHub (Nov 12, 2025):
Ready for review: https://github.com/ElectronNET/Electron.NET/pull/924
@FlorianRappl commented on GitHub (Nov 13, 2025):
Merged - let's test this and publish the 0.1.0 tomorrow.
My proposal is from this on to pretty much release whenever we have something new (i.e., do fixes such as a 0.1.1, or add new features, such as a 0.2.0) quite quickly.
I'd go for a 1.0.0 release of this as early as ~mid of Jan. 2026 (unless we find some critical things). This should be sufficient time to get some user input and have enough experience to call it stable.