Skip to content

Commit 5fc8f2d

Browse files
committed
WIP
1 parent 2d564bf commit 5fc8f2d

File tree

1 file changed

+161
-64
lines changed

1 file changed

+161
-64
lines changed

packages/base-controller/src/Messenger.ts

Lines changed: 161 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export type NamespacedName<Namespace extends string = string> =
8989
type DelegatedMessenger<
9090
Action extends ActionConstraint,
9191
Event extends EventConstraint,
92-
Namespace extends string,
92+
Namespace extends string = string,
9393
> = Pick<
9494
Messenger<Action, Event, Namespace>,
9595
| 'publishDelegated'
@@ -121,15 +121,21 @@ export class Messenger<
121121

122122
readonly #events = new Map<Event['type'], EventSubscriptionMap<Event>>();
123123

124-
readonly #delegatedEventSubscriptions = new Map<
124+
/**
125+
* The set of messengers we've delegated events to, by event type.
126+
*/
127+
readonly #subscriptionDelegationTargets = new Map<
125128
Event['type'],
126129
Map<
127130
DelegatedMessenger<Action, Event>,
128131
ExtractEventHandler<Event, Event['type']>
129132
>
130133
>();
131134

132-
readonly #delegatedActionHandlers = new Map<
135+
/**
136+
* The set of messengers we've delegated actions to, by action type.
137+
*/
138+
readonly #actionDelegationTargets = new Map<
133139
Action['type'],
134140
Set<DelegatedMessenger<Action, Event>>
135141
>();
@@ -172,7 +178,6 @@ export class Messenger<
172178
* @throws Will throw when a handler has been registered for this action type already.
173179
* @template ActionType - A type union of Action type strings.
174180
*/
175-
176181
registerActionHandler<
177182
ActionType extends Action['type'] & NamespacedName<Namespace>,
178183
>(actionType: ActionType, handler: ActionHandler<Action, ActionType>) {
@@ -260,14 +265,14 @@ export class Messenger<
260265
actionType: ActionType,
261266
) {
262267
this.#actions.delete(actionType);
263-
const delegatedMessengers = this.#delegatedActionHandlers.get(actionType);
264-
if (!delegatedMessengers) {
268+
const delegationTargets = this.#actionDelegationTargets.get(actionType);
269+
if (!delegationTargets) {
265270
return;
266271
}
267-
for (const messenger of delegatedMessengers) {
272+
for (const messenger of delegationTargets) {
268273
messenger.unregisterDelegatedActionHandler(actionType);
269274
}
270-
this.#delegatedActionHandlers.delete(actionType);
275+
this.#actionDelegationTargets.delete(actionType);
271276
}
272277

273278
/**
@@ -317,7 +322,49 @@ export class Messenger<
317322
* @param args.eventType - The event type to register a payload for.
318323
* @param args.getPayload - A function for retrieving the event payload.
319324
*/
320-
registerInitialEventPayload<EventType extends Event['type']>({
325+
registerInitialEventPayload<
326+
EventType extends Event['type'] & NamespacedName<Namespace>,
327+
>({
328+
eventType,
329+
getPayload,
330+
}: {
331+
eventType: EventType;
332+
getPayload: () => ExtractEventPayload<Event, EventType>;
333+
}) {
334+
/* istanbul ignore if */ // Branch unreachable with valid types
335+
if (!this.#isInCurrentNamespace(eventType)) {
336+
throw new Error(
337+
`Only allowed registering initial payloads for events prefixed by '${
338+
this.#namespace
339+
}:'`,
340+
);
341+
}
342+
this.#registerInitialEventPayload({ eventType, getPayload });
343+
}
344+
345+
/**
346+
* Register a function for getting the initial payload for a delegated event.
347+
*
348+
* This is used for events that represent a state change, where the payload is the state.
349+
* Registering a function for getting the payload allows event selectors to have a point of
350+
* comparison the first time state changes.
351+
*
352+
* @deprecated Do not call this directly, instead use the `delegate` method.
353+
* @param args - The arguments to this function
354+
* @param args.eventType - The event type to register a payload for.
355+
* @param args.getPayload - A function for retrieving the event payload.
356+
*/
357+
registerDelegatedInitialEventPayload<EventType extends Event['type']>({
358+
eventType,
359+
getPayload,
360+
}: {
361+
eventType: EventType;
362+
getPayload: () => ExtractEventPayload<Event, EventType>;
363+
}) {
364+
this.#registerInitialEventPayload({ eventType, getPayload });
365+
}
366+
367+
#registerInitialEventPayload<EventType extends Event['type']>({
321368
eventType,
322369
getPayload,
323370
}: {
@@ -341,6 +388,40 @@ export class Messenger<
341388
* @template EventType - A type union of Event type strings.
342389
*/
343390
publish<EventType extends Event['type']>(
391+
eventType: EventType & NamespacedName<Namespace>,
392+
...payload: ExtractEventPayload<Event, EventType>
393+
) {
394+
/* istanbul ignore if */ // Branch unreachable with valid types
395+
if (!this.#isInCurrentNamespace(eventType)) {
396+
throw new Error(
397+
`Only allowed publishing events prefixed by '${this.#namespace}:'`,
398+
);
399+
}
400+
this.#publish(eventType, ...payload);
401+
}
402+
403+
/**
404+
* Publish a delegated event.
405+
*
406+
* Publishes the given payload to all subscribers of the given event type.
407+
*
408+
* Note that this method should never throw directly. Any errors from
409+
* subscribers are captured and re-thrown in a timeout handler.
410+
*
411+
* @deprecated Do not call this directly, instead use the `delegate` method.
412+
* @param eventType - The event type. This is a unique identifier for this event.
413+
* @param payload - The event payload. The type of the parameters for each event handler must
414+
* match the type of this payload.
415+
* @template EventType - A type union of Event type strings.
416+
*/
417+
publishDelegated<EventType extends Event['type']>(
418+
eventType: EventType,
419+
...payload: ExtractEventPayload<Event, EventType>
420+
) {
421+
this.#publish(eventType, ...payload);
422+
}
423+
424+
#publish<EventType extends Event['type']>(
344425
eventType: EventType,
345426
...payload: ExtractEventPayload<Event, EventType>
346427
) {
@@ -483,63 +564,79 @@ export class Messenger<
483564
}
484565

485566
/**
486-
* Get a restricted messenger
487-
*
488-
* Returns a wrapper around the messenger instance that restricts access to actions and events.
489-
* The provided allowlists grant the ability to call the listed actions and subscribe to the
490-
* listed events. The "name" provided grants ownership of any actions and events under that
491-
* namespace. Ownership allows registering actions and publishing events, as well as
492-
* unregistering actions and clearing event subscriptions.
493-
*
494-
* @param options - Messenger options.
495-
* @param options.name - The name of the thing this messenger will be handed to (e.g. the
496-
* controller name). This grants "ownership" of actions and events under this namespace to the
497-
* restricted messenger returned.
498-
* @param options.allowedActions - The list of actions that this restricted messenger should be
499-
* allowed to call.
500-
* @param options.allowedEvents - The list of events that this restricted messenger should be
501-
* allowed to subscribe to.
502-
* @template Namespace - The namespace for this messenger. Typically this is the name of the
503-
* module that this messenger has been created for. The authority to publish events and register
504-
* actions under this namespace is granted to this restricted messenger instance.
505-
* @template AllowedAction - A type union of the 'type' string for any allowed actions.
506-
* This must not include internal actions that are in the messenger's namespace.
507-
* @template AllowedEvent - A type union of the 'type' string for any allowed events.
508-
* This must not include internal events that are in the messenger's namespace.
509-
* @returns The restricted messenger.
567+
* Delegate actions and/or events to another messenger.
568+
*
569+
* @param args - Arguments.
570+
* @param args.actions - The action types to delegate.
571+
* @param args.events - The event types to delegate.
572+
* @param args.messenger - The messenger to delegate to.
510573
*/
511-
getRestricted<
512-
Namespace extends string,
513-
AllowedAction extends NotNamespacedBy<Namespace, Action['type']> = never,
514-
AllowedEvent extends NotNamespacedBy<Namespace, Event['type']> = never,
515-
>({
516-
name,
517-
allowedActions,
518-
allowedEvents,
574+
delegate<DelegatedAction extends Action, DelegatedEvent extends Event>({
575+
actions = [],
576+
events = [],
577+
messenger,
519578
}: {
520-
name: Namespace;
521-
allowedActions: NotNamespacedBy<
522-
Namespace,
523-
Extract<Action['type'], AllowedAction>
524-
>[];
525-
allowedEvents: NotNamespacedBy<
526-
Namespace,
527-
Extract<Event['type'], AllowedEvent>
528-
>[];
529-
}): RestrictedMessenger<
530-
Namespace,
531-
| NarrowToNamespace<Action, Namespace>
532-
| NarrowToAllowed<Action, AllowedAction>,
533-
NarrowToNamespace<Event, Namespace> | NarrowToAllowed<Event, AllowedEvent>,
534-
AllowedAction,
535-
AllowedEvent
536-
> {
537-
return new RestrictedMessenger({
538-
messenger: this,
539-
name,
540-
allowedActions,
541-
allowedEvents,
542-
});
579+
actions?: DelegatedAction['type'][];
580+
events?: DelegatedEvent['type'][];
581+
messenger: DelegatedMessenger<DelegatedAction, DelegatedEvent>;
582+
}) {
583+
for (const actionType of actions) {
584+
const delegatedActionHandler = (
585+
...args: ExtractActionParameters<DelegatedAction, typeof actionType>
586+
) => {
587+
// TODO: Explain cast
588+
const actionHandler = this.#actions.get(actionType) as
589+
| ActionHandler<DelegatedAction, typeof actionType>
590+
| undefined;
591+
if (!actionHandler) {
592+
throw new Error(
593+
`Cannot call '${actionType}', action not registered.`,
594+
);
595+
}
596+
return actionHandler(...args);
597+
};
598+
let delegationTargets = this.#actionDelegationTargets.get(actionType);
599+
if (!delegationTargets) {
600+
delegationTargets = new Set<DelegatedMessenger<Action, Event>>();
601+
this.#actionDelegationTargets.set(actionType, delegationTargets);
602+
}
603+
delegationTargets.add(messenger);
604+
605+
messenger.registerDelegatedActionHandler(
606+
actionType,
607+
delegatedActionHandler,
608+
);
609+
}
610+
for (const eventType of events) {
611+
const untypedSubscriber = (
612+
...payload: ExtractEventPayload<DelegatedEvent, typeof eventType>
613+
) => {
614+
messenger.publishDelegated(eventType, ...payload);
615+
};
616+
// TODO: Explain cast
617+
const subscriber = untypedSubscriber as ExtractEventHandler<
618+
DelegatedEvent,
619+
typeof eventType
620+
>;
621+
let delegatedEventSubscriptions =
622+
this.#delegatedEventSubscriptions.get(eventType);
623+
if (!delegatedEventSubscriptions) {
624+
delegatedEventSubscriptions = new Map();
625+
this.#delegatedEventSubscriptions.set(
626+
eventType,
627+
delegatedEventSubscriptions,
628+
);
629+
}
630+
delegatedEventSubscriptions.set(messenger, subscriber);
631+
const getPayload = this.#initialEventPayloadGetters.get(eventType);
632+
if (getPayload) {
633+
messenger.registerDelegatedInitialEventPayload({
634+
eventType,
635+
getPayload,
636+
});
637+
}
638+
this.subscribe(eventType, subscriber);
639+
}
543640
}
544641

545642
/**

0 commit comments

Comments
 (0)