-
Notifications
You must be signed in to change notification settings - Fork 1
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
Events Refactoring (integrating js-events
)
#53
Conversation
If the If When One issue is that when stopping the Well atm, it's a no, because due to However as a general pattern, we shouldn't depend on the async-init in this situation. There could be other situations where an event is emitted from another object that we need to handle, which causes to execute a pull-method which triggers another event of the same type, thus creating an infinite loop. Therefore, rather than simply relying on async-init to cancel the infiniteness (not only is this relying on implicit behaviour, but it's not even guaranteed in the future). We need something else as a general pattern to prevent such things. Here are some possibilities.
Note that solution 2 means rather than just calling Is this actually any better than solution 1? Or just relying on I think it is better than solution 1 as it centralises logic. It's better than simply relying on The presence of infinite loops here is primarily due to this 1 reason: the inability to easily distinguish if an event happens due to something I'm already doing. The only good solution is to rely on status conditionals representing the state of doing that operation. |
I think it ties into the idempotent nature of the stop method. In this case it's handled by the decorator. but as a general rule, for things like this, it can only really stopped or destroyed once. So the stop or destroy event can only be emitted once. In the case of stop, it's emitted only once per state change since it could be started again. In that case it still prevents the cycle. This might generalise as, any event that is the result of a state change, can only emit once per state change? we can refer to them as state events or... Once-lers? In any case, how does that sound for a general rule? |
Now on the topic of encapsulated events. Where Firstly there will be "relevant" and "irrelevant" events to The "relevant" events are events that The "irrelevant" events are events that In both cases, there's a question about whether such events should be bubbled upwards outside of
In terms of 1. even if we could argue that certain events shouldn't be relevant. Dispatching such an event is still useful for "observability". Where it can be useful to debug the internal state of the system, such as for tracing. The question is whether such tracing should make use of these events, or if a separate system should be used for such a thing. It also important to note that events could represent "pre-" or "post-" op events. However we defaulted to In terms of 2., dispatching an event chain can help maintain a trace of these concurrent operations, similar to maintaining a trace of exceptions that was thrown, caught and rethrown. Exceptions are a bit easier since you can rethrow the same exception. I think that there are 2 equivalences:
Based on this https://gist.github.com/CMCDragonkai/08266b1463158f4156f66d4bf077add6 for exceptions, I think the same guidelines apply here.
I think both would be applicable depending on the situation. In the case of |
I think using In situations where you have a common event handler, then |
So it's possible to do this simultaneously:
Doing this would then enable using This means "relevant" event handlers should not re-dispatch their events, but should just handle them with respect to |
I'm using this as scaffolding for the event with clone method for now. It will be replaced with your implementation. type OptDetail<T> = T extends undefined ? {} : { detail: T };
class TempBaseEvent<T = undefined> extends Event {
public readonly detail: T
constructor(type: string, protected options?: EventInit & OptDetail<T>) {
super(type, options);
// @ts-ignore: Detail should be undefined here if it doesn't exist
this.detail = options?.detail;
}
public clone(): this {
// @ts-ignore: cheeky self prototype reference
return new this.constructor(this.options);
};
} |
As per MatrixAI/js-events#2, it should be easy to find and replace all uses to just
And that's enough to create the relevant event. When performing a redispatch you will need to do Node 20 is now LTS so it should work... |
Some notes on redispatch: InjectionDon't redispatch relevant and irrelevant events. Only handle relevant events. EncapsulationRedispatch all events including relevant and irrelevant events On Redispatch
Do 2. whenever there's an "equivalent" event on the domain model. Do 1. when there is not. When doing 2, the chain event structure should be always have a base type. Consider this:
Notice that I'm wrapping
This is not true for |
Let's see how the above structure plays out in code before reviewing. |
So one thing I've had to do is to introduce Furthermore my attempt at specialising the
The above does not compile unfortunately. Even though it would be nice since we can continue using the same pattern like I used one before called
|
Ok the change of nameclashes is quite low. So I'm just going to use |
The 2 examples of this is actually in the Can follow that as a guideline on what to do @amydevs @tegefaulkes. |
So this has changed a bit. On injection, we may redispatch the relevant event, if the handling of the relevant event, is itself an event on the object receiving the event. |
@tegefaulkes brought up a situation like this: If an object has a pull-method that could result in an error, and the object itself is evented, does that mean:
I think all 3 are possibly valid depending on the situation. For 1., this makes sense if the pull-method doesn't actually do any state changes on the object. Thus the exception is only relevant to the caller. For 2., this makes sense if you can say that the method is entirely internal to the library or module context. Like for example For 3., this makes sense if the method performs a state change, and it's not an "internal" method. It is expected that things outside your library may call it. In this case, you have to document that it will dispatch and throw an exception. In which case, the user of the object has to decide what to do, whether it ignores one and handles the other. |
So for Note that in |
As it's currently implemented, the
In cases 1, 3 and 4 it makes sense since we're responding to the the underlying quiche connection's state change. 2 is technically an internal error separate from quiche but it's a transport failure. This is the only one I can see emitting an error event that would trigger stop externally. I really don't like this since it's clean up logic critical to the function of Also, most errors coming out of quiche are obtained with |
@tegefaulkes you will need to update the |
In your 2, you would follow 2. |
In this case, these errors are not errors of the |
So based on discussion, The QUICSocket.send will not throw, but if it fails it will dispatch an error event. This event should ultimately cause a Switching to this new pattern also means the The Likewise for the |
When separating the functionality between encapsulated dependencies and injected dependencies, we have to basically have a boolean that branches out the functionality. However this results in alot of boilerplate. Requiring a separate Only a few dependencies make use of this distinction during runtime. Most of the time, this is actually only used for testing & mocking. Consider for example in the 3 situations of EFS, QUIC. In EFS, the Consider the case of The only way to make such a thing manageable would be to introduce parameter decorators on the constructor that automatically produces properties. However I don't want to do that right now, it might introduce too much complexity. Instead let's do something different.
This doesn't have to be a wholesale change immediately... we can just keep an issue in PK and slowly apply this across the ecosystem. In some cases, optional injection can still be kept since there's barely any lifecycle related activities, or the relevant objects in question are not evented objects. |
If dispatching Perhaps instead we would say |
…ld be in the test utilities
[ci skip]
[ci skip]
…early cancellation or abortion
[ci skip]
[ci skip]
[ci skip]
[ci skip]
[ci skip]
b81d63f
to
87ca776
Compare
87ca776
to
a24b278
Compare
…tus is lazily changed
I've reworked all the commits, the last commit still wip. But this MR is ready. We can merge now. Also confirmed that a lazy timer indeed only changes the status lazily. So it does need to be deleted. Only non-lazy timer will synchronously change the status. |
Description
https://github.com/MatrixAI/js-events provides a
Evented
decorator andEventAny
that allows one to depend on all relevant of a given object.This results in:
EventTarget
, instead we use a mixin.EventTarget
anywayaddEventListener(callback)
without any type key string.Event...
basically starting with anEvent
prefix similar toError...
prefix for errors.EventClass.name
static property, rather than having to make up event namesUsing events can be quite tricky, but we have come up with a general pattern.
EventXError
- this is the general domain error event, all relevant exceptions will be part of thedetail
type.EventXClose
- this is a "close" event, the exact meaning depends on the construct. For exampleQUICStream
has 2 separate close events for each side of the stream. OnQUICConnection
the close event handle has many duties because closing is an asynchronous concurrent process that involves draining whatever is still happening. Thedetail
property will contain a subset of what is in thedetail
property of the error. This is because the error handler eats up some errors.handleEventXError
andhandleEventXClose
.ErrorXInternal
representing unrecoverable exceptions. This is expected to fail the program. Users can see it by assigning an uncaught exception handler. No close event is dispatched in this case.stop
ordestroy
. But it does this intelligently, and just beforehand, it will resolve some promise indicating that closing is finished, that promise can be what thestop
ordestroy
is waiting on.stop
ordestroy
, this flow is separate from error -> close, instead thestop
ordestroy
may proceed to dispatch close or error (even in case of graceful errors depending on the situation).QUICConnection
, closing always involves an error even if the error code is0
that's why graceful close is still an error event. But this is not necessarily true in other cases.EventAll
andhandleEventX
handlers. Injected domains should not have all their events completely captured and redispatched.ErrorXInternal
.Issues Fixed
Tasks
Event...
prefixed class names.Use- the@Evented()
as class decorator for all things that useextends EventTarget
js-async-init
1.9.x is now integrated withjs-events
Use- Can useaddEventListener(callback)
instead of hardcoding all the relevant event namesEventDefault.name
instead.events.ts
isn't properly setting up the event class hierarchy, they should be extending the abstract classes for the grouped events.- Native multithreading for performance - need to bench this against socket buffer size increase
- Connection Migration - should support it for the client, but not sure for serer
- Server name check should support IP checks if the server name is just the IP - requires upstream fix - Events Refactoring (integrating
js-events
) #53 (comment)0.18.0
(we are currently 2 versions behind)- Note this requires an update to
boring
to v3. There are some notes on v3 fixing the windows build problem. Failure when compiling boring-sys v2.1.0 on Windows cloudflare/boring#121 (comment) (So it's possible that0.18.0
will be the build where windows succeeds!!)toCanonicalIp
totoCanonicalIP
generateCertificate
with PK'sx509
module where we actually setup the proper subject and issuer common names along with SAN for testinglocalhost
DNS and127.0.0.1
and::1
IP.EventError
which by default results in an uncaught exception that represents a fail fast software error.-
CryptoError
enum. Note that this is unique to QUIC, where TLS 1.3 alert codes is offset by0x0100
. Node's https doesn't really expose the alert codes, it just abstracts over it.Prototyping
test.ts
for random tests.Final checklist