-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
feat(client): add callback/event mechanism between TypeScript and Go #2330
Conversation
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 seems you still need to update the PR to decouple the callback from the event handling. Let me know when I should take a look
f617f0b
to
3ced93e
Compare
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.
Nice. Just one small nit.
client/electron/vpn_service.ts
Outdated
* Subscribes to all VPN connection related events. | ||
* This function ensures that the subscription only happens once. | ||
*/ | ||
async function subscribeVPNConnEvents(): Promise<void> { |
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.
Pass the VpnStatusCallback here and delete onVpnStatusChanged.
This eliminates the global state and makes the lifecycle/object relationship clearer. Easier to reason about.
Let the app worry about calling it once.
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 InitVpnService
function to index.ts
, so it have a chance to subscribe to the event only once when the electron app is ready.
client/electron/vpn_service.ts
Outdated
const VPNConnDisconnecting: VPNConnStatus = 'Disconnecting'; | ||
const VPNConnDisconnected: VPNConnStatus = 'Disconnected'; | ||
|
||
interface VPNConn { |
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 not a VPN connection: you can't read/write to it.
Perhaps call it VPNStatus
?
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.
VPNConnectionStatus
or VPNConnectionState
sounds good to me.
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.
Renamed to VPNConnectionState
to avoid confusion with VPNConnStatus
.
client/electron/go_plugin.ts
Outdated
* Koffi requires us to register all persistent callbacks; we track the registrations here. | ||
* @see https://koffi.dev/callbacks#registered-callbacks | ||
*/ | ||
const koffiCallbacks = new Map<string, koffi.IKoffiRegisteredCallback>(); |
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 change for this PR since it's already implemented, but there's an alternative design where the callback mapping you do in the Go code is done in the Typescript code instead. That's what Cordova does. This would circumvent the restrictions from koffi, since all you need in that case is a single callback.
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.
Actually Koffi's limit is pretty big. We might not event need that much of callbacks in the entire system.
A maximum of 8192 callbacks can exist at the same time.
client/go/outline/event/event.go
Outdated
// RemoveListener removes a [callback.Callback] from the specified [EventName]. | ||
// | ||
// Calling this function is safe even if the event has not been registered. | ||
func RemoveListener(evt EventName, cb callback.Token) { |
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 worry about global events. It hurts composability and reusability, as it's impossible to track who is firing what events. Different components may conflict with the same events (e.g. "OnError").
We need a design that can compartmentalize events.
For example, you could have functions specific for the VPN events (AddVPNAPIListener
). Or have a way to create independent "channels" (AddListener(channel, event, callback)
). The channel would have a unique id.
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 also talked about having a SetVPNListener
, which is easier to implement and reason about.
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 would not worry too much about "tracking who is firing the event" because the trigger object can (and must) include itself in the event data. (Also OnError
is a bad event, events should be concrete, like OnVPNConnectionConnectError
, OnVPNConnectionDisconnectError
, etc., the convention is On<ObjectType><EventName>
, so actually no global events here).
The firing object should call the event handler with at least the following fields: { "this": "<object-id>", "data": "..." }
.
The event handler can then distinguish the trigger object by looking at the event details and then distribute it accordingly in TypeScript. That's also why I included the VPNConn.ID
in the event, it's not just an ID from the app code, it's also the ID which can uniquely identify the VPN connection object in Go.
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 it's ok, but the code safety and correctness relies on adhering to a strict structure that is not enforced by the compiler. A better approach would be to ensure we can create independent channels.
At a minimal create the object that represents the channel to stop using globals. This way we can have other methods that use their own channels.
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.
Same question as the one in callback.go
, do you mean a EventManager
and a event.DefaultManager()
method? But then will an EventManager
also accepts a CallbackManager
? This would add more unnecessary complexity to the code.
client/electron/go_plugin.ts
Outdated
} | ||
|
||
/** Interface containing the exported native CGo functions. */ | ||
interface CgoFunctions { |
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 encapsulation here is a bit off. It can be more object-oriented and remove globals.
Perhaps call this CallbackMap with public New and Delete methods.
Make the fields private.
Move koffiCallbacks into here.
The logic in ensureCgo can be in the constructor or factory function. Then there's no need to call ensureCgo in the methods.
Perhaps rename ensureCgo to getCallbackMap()
This makes the code more encapsulated, safer and with less globals.
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 involves multiple components, I would prefer getBackendLib()
, and returns an object with a methodChannel
and a callbackManager
object.
client/electron/index.ts
Outdated
@@ -471,6 +477,8 @@ function main() { | |||
// TODO(fortuna): Start the app with the window hidden on auto-start? | |||
setupWindow(); | |||
|
|||
await initVpnService(); |
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.
You can be more explicit. Like monitorVpnEvents
. Or you could just inline the function here.
"init" doesn't say much, and it feels like you are doing much more than you actually are. It's also misleading, since some VPN initialization happen somewhere else
|
||
var ( | ||
mu sync.RWMutex | ||
callbacks = make(map[Token]Callback) |
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.
Same comment here. Create a callback map object, rather than using globals.
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.
Do you mean introduce a CallbackManager
and then have a default singleton method such as callback.DefaultManager()
?
client/go/outline/event/event.go
Outdated
|
||
var ( | ||
mu sync.RWMutex | ||
listeners = make(map[EventName][]callback.Token) |
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.
Again, please avoid globals. Create an object to hold the fields and methods to manage the listeners.
client/go/outline/event/event.go
Outdated
// RemoveListener removes a [callback.Callback] from the specified [EventName]. | ||
// | ||
// Calling this function is safe even if the event has not been registered. | ||
func RemoveListener(evt EventName, cb callback.Token) { |
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 it's ok, but the code safety and correctness relies on adhering to a strict structure that is not enforced by the compiler. A better approach would be to ensure we can create independent channels.
At a minimal create the object that represents the channel to stop using globals. This way we can have other methods that use their own channels.
This PR introduces a
callback
mechanism that allows TypeScript code to subscribe to events triggered in the Go code.To demonstrate the functionality, this PR implements a VPN connection state change event. The TypeScript code can now subscribe to this event and receive updates on the VPN connection state.
This mechanism can be leveraged for both electron and the Cordova code.
Tested Outline-Client.deb on Ubuntu 22