@@ -16,15 +16,133 @@ See the License for the specific language governing permissions and
1616limitations under the License.
1717*/
1818
19- import { Dispatcher } from "flux" ;
20-
2119import { Action } from "./actions" ;
2220import { ActionPayload , AsyncActionPayload } from "./payloads" ;
2321
22+ type DispatchToken = string ;
23+
24+ function invariant ( cond : any , error : string ) : void {
25+ if ( ! cond ) throw new Error ( error ) ;
26+ }
27+
2428/**
2529 * A dispatcher for ActionPayloads (the default within the SDK).
30+ * Based on the old Flux dispatcher https://github.com/facebook/flux/blob/main/src/Dispatcher.js
2631 */
27- export class MatrixDispatcher extends Dispatcher < ActionPayload > {
32+ export class MatrixDispatcher {
33+ private readonly callbacks = new Map < DispatchToken , ( payload : ActionPayload ) => void > ( ) ;
34+ private readonly isHandled = new Map < DispatchToken , boolean > ( ) ;
35+ private readonly isPending = new Map < DispatchToken , boolean > ( ) ;
36+ private pendingPayload ?: ActionPayload ;
37+ private lastId = 1 ;
38+
39+ /**
40+ * Registers a callback to be invoked with every dispatched payload. Returns
41+ * a token that can be used with `waitFor()`.
42+ */
43+ public register ( callback : ( payload : ActionPayload ) => void ) : DispatchToken {
44+ const id = "ID_" + this . lastId ++ ;
45+ this . callbacks . set ( id , callback ) ;
46+ if ( this . isDispatching ( ) ) {
47+ // If there is a dispatch happening right now then the newly registered callback should be skipped
48+ this . isPending . set ( id , true ) ;
49+ this . isHandled . set ( id , true ) ;
50+ }
51+ return id ;
52+ }
53+
54+ /**
55+ * Removes a callback based on its token.
56+ */
57+ public unregister ( id : DispatchToken ) : void {
58+ invariant ( this . callbacks . has ( id ) , `Dispatcher.unregister(...): '${ id } ' does not map to a registered callback.` ) ;
59+ this . callbacks . delete ( id ) ;
60+ }
61+
62+ /**
63+ * Waits for the callbacks specified to be invoked before continuing execution
64+ * of the current callback. This method should only be used by a callback in
65+ * response to a dispatched payload.
66+ */
67+ public waitFor ( ids : DispatchToken [ ] ) : void {
68+ invariant ( this . isDispatching ( ) , "Dispatcher.waitFor(...): Must be invoked while dispatching." ) ;
69+ for ( const id of ids ) {
70+ if ( this . isPending . get ( id ) ) {
71+ invariant (
72+ this . isHandled . get ( id ) ,
73+ `Dispatcher.waitFor(...): Circular dependency detected while waiting for '${ id } '.` ,
74+ ) ;
75+ continue ;
76+ }
77+ invariant (
78+ this . callbacks . get ( id ) ,
79+ `Dispatcher.waitFor(...): '${ id } ' does not map to a registered callback.` ,
80+ ) ;
81+ this . invokeCallback ( id ) ;
82+ }
83+ }
84+
85+ /**
86+ * Dispatches a payload to all registered callbacks.
87+ */
88+ // eslint-disable-next-line @typescript-eslint/naming-convention
89+ private _dispatch = ( payload : ActionPayload ) : void => {
90+ invariant ( ! this . isDispatching ( ) , "Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch." ) ;
91+ this . startDispatching ( payload ) ;
92+ try {
93+ for ( const [ id ] of this . callbacks ) {
94+ if ( this . isPending . get ( id ) ) {
95+ continue ;
96+ }
97+ this . invokeCallback ( id ) ;
98+ }
99+ } finally {
100+ this . stopDispatching ( ) ;
101+ }
102+ } ;
103+
104+ /**
105+ * Is this Dispatcher currently dispatching.
106+ */
107+ public isDispatching ( ) : boolean {
108+ return ! ! this . pendingPayload ;
109+ }
110+
111+ /**
112+ * Call the callback stored with the given id. Also do some internal
113+ * bookkeeping.
114+ *
115+ * Must only be called with an id which has a callback and pendingPayload set
116+ * @internal
117+ */
118+ private invokeCallback ( id : DispatchToken ) : void {
119+ this . isPending . set ( id , true ) ;
120+ this . callbacks . get ( id ) ! ( this . pendingPayload ! ) ;
121+ this . isHandled . set ( id , true ) ;
122+ }
123+
124+ /**
125+ * Set up bookkeeping needed when dispatching.
126+ *
127+ * @internal
128+ */
129+ private startDispatching ( payload : ActionPayload ) : void {
130+ for ( const [ id ] of this . callbacks ) {
131+ this . isPending . set ( id , false ) ;
132+ this . isHandled . set ( id , false ) ;
133+ }
134+ this . pendingPayload = payload ;
135+ }
136+
137+ /**
138+ * Clear bookkeeping used for dispatching.
139+ *
140+ * @internal
141+ */
142+ private stopDispatching ( ) : void {
143+ this . pendingPayload = undefined ;
144+ }
145+
28146 /**
29147 * Dispatches an event on the dispatcher's event bus.
30148 * @param {ActionPayload } payload Required. The payload to dispatch.
@@ -42,14 +160,14 @@ export class MatrixDispatcher extends Dispatcher<ActionPayload> {
42160 }
43161
44162 if ( sync ) {
45- super . dispatch ( payload ) ;
163+ this . _dispatch ( payload ) ;
46164 } else {
47165 // Unless the caller explicitly asked for us to dispatch synchronously,
48166 // we always set a timeout to do this: The flux dispatcher complains
49167 // if you dispatch from within a dispatch, so rather than action
50168 // handlers having to worry about not calling anything that might
51169 // then dispatch, we just do dispatches asynchronously.
52- window . setTimeout ( super . dispatch . bind ( this , payload ) , 0 ) ;
170+ window . setTimeout ( this . _dispatch , 0 , payload ) ;
53171 }
54172 }
55173
0 commit comments