-
-
Notifications
You must be signed in to change notification settings - Fork 246
feat: Add Messenger parent
parameter
#6142
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
Changes from all commits
f263e17
60681f4
5fe0752
dfa154b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -181,9 +181,24 @@ export class Messenger< | |
Namespace extends string, | ||
Action extends ActionConstraint, | ||
Event extends EventConstraint, | ||
Parent extends Messenger< | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a new addition from what was described in the ADR. As you can see in the tests, it does make using this a bit more verbose, but it's not too bad IMO. This was the only way I could find to validate that the parent had all necessary actions/events (this was validated in the constructor params, but the method of validation relied on there being a generic type param to validate against). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense given how we've designed |
||
string, | ||
ActionConstraint, | ||
EventConstraint, | ||
// Use `any` to avoid preventing a parent from having a parent. `any` is harmless in a type | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does work, but wouldn't leaving this type parameter out also work? Then the default would be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was hesitant to do this because there is nothing really stopping someone from writing a messenger that is both a parent and has a parent. It might even be a reasonably good design. For example, if the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I missed the double negative. "Avoid preventing". Got it :) Yes I agree we should make it possible to essentially create a delegation chain. |
||
// constraint anyway, it's the one totally safe place to use it. | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
any | ||
> = never, | ||
> { | ||
readonly #namespace: Namespace; | ||
|
||
/** | ||
* The parent messenger. All actions/events under this namespace are automatically delegated to | ||
* the parent messenger. | ||
*/ | ||
readonly #parent?: DelegatedMessenger; | ||
|
||
readonly #actions = new Map<Action['type'], Action['handler']>(); | ||
|
||
readonly #events = new Map<Event['type'], EventSubscriptionMap<Event>>(); | ||
|
@@ -225,11 +240,26 @@ export class Messenger< | |
/** | ||
* Construct a messenger. | ||
* | ||
* If a parent messenger is given, all actions and events under this messenger's namespace will | ||
* be delegated to the parent automatically. | ||
* | ||
* @param args - Constructor arguments | ||
* @param args.namespace - The messenger namespace. | ||
* @param args.parent - The parent messenger. | ||
*/ | ||
constructor({ namespace }: { namespace: Namespace }) { | ||
constructor({ | ||
namespace, | ||
parent, | ||
}: { | ||
namespace: Namespace; | ||
parent?: Action['type'] extends MessengerActions<Parent>['type'] | ||
? Event['type'] extends MessengerEvents<Parent>['type'] | ||
? Parent | ||
: never | ||
: never; | ||
}) { | ||
this.#namespace = namespace; | ||
this.#parent = parent; | ||
} | ||
|
||
/** | ||
|
@@ -257,6 +287,11 @@ export class Messenger< | |
); | ||
} | ||
this.#registerActionHandler(actionType, handler); | ||
if (this.#parent) { | ||
// @ts-expect-error The parent type isn't constructed in a way that proves it supports this | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like there ought to be a way to align the types so this isn't necessary, but I don't know how. I tried changing the type of I do agree this seems type-safe though given the check we have in the constructor. |
||
// action, but this is OK because it's validated in the constructor. | ||
this.delegate({ actions: [actionType], messenger: this.#parent }); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Parent Delegation Fails: Non-Transitive & Duplicate ErrorsThe new automatic parent delegation feature introduces two bugs:
Additional Locations (1)There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is fine; it's OK for there to be an error if someone manually delegates to a parent. Just don't do that. The transitive delegation issue is interesting though. I hadn't thought too much about that. We could disallow it for simplicity, but it may come in handy in some cases so I'll take a closer look. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait no, delegation to parents only applies to capabilities under the current namespace. Nothing delegated to a messenger would be under its namespace. Therefore delegation to a parent is inherently not transitive. It's intentional and OK for it not to be transitive. |
||
} | ||
|
||
#registerActionHandler<ActionType extends Action['type']>( | ||
|
@@ -400,6 +435,14 @@ export class Messenger< | |
}:'`, | ||
); | ||
} | ||
if ( | ||
this.#parent && | ||
!this.#subscriptionDelegationTargets.get(eventType)?.has(this.#parent) | ||
) { | ||
// @ts-expect-error The parent type isn't constructed in a way that proves it supports this | ||
// event, but this is OK because it's validated in the constructor. | ||
this.delegate({ events: [eventType], messenger: this.#parent }); | ||
} | ||
this.#registerInitialEventPayload({ eventType, getPayload }); | ||
} | ||
|
||
|
@@ -449,6 +492,14 @@ export class Messenger< | |
`Only allowed publishing events prefixed by '${this.#namespace}:'`, | ||
); | ||
} | ||
if ( | ||
this.#parent && | ||
!this.#subscriptionDelegationTargets.get(eventType)?.has(this.#parent) | ||
) { | ||
// @ts-expect-error The parent type isn't constructed in a way that proves it supports this | ||
// event, but this is OK because it's validated in the constructor. | ||
this.delegate({ events: [eventType], messenger: this.#parent }); | ||
} | ||
this.#publish(eventType, ...payload); | ||
} | ||
|
||
|
@@ -805,6 +856,9 @@ export class Messenger< | |
events?: DelegatedEvents; | ||
messenger: Delegatee; | ||
}) { | ||
if (messenger === this.#parent) { | ||
throw new Error('Cannot revoke from parent'); | ||
} | ||
for (const actionType of actions || []) { | ||
const delegationTargets = this.#actionDelegationTargets.get(actionType); | ||
if (!delegationTargets || !delegationTargets.has(messenger)) { | ||
|
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.
Side note, but should
Action
andEvent
default tonever
so that if you don't supply any type parameters toMessenger
, allowing TypeScript to infer them, you don't end up with a messenger that recognizes all actions and events?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.
Good idea. I will try this separately
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.
Done here: #6311