Skip to content

Commit bee4759

Browse files
authored
Remove flux dependency (matrix-org#10313)
1 parent 2631b63 commit bee4759

File tree

10 files changed

+231
-109
lines changed

10 files changed

+231
-109
lines changed

package.json

-2
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@
8080
"escape-html": "^1.0.3",
8181
"file-saver": "^2.0.5",
8282
"filesize": "10.0.6",
83-
"flux": "4.0.3",
8483
"focus-visible": "^5.2.0",
8584
"gfm.css": "^1.1.2",
8685
"glob-to-regexp": "^0.4.1",
@@ -153,7 +152,6 @@
153152
"@types/diff-match-patch": "^1.0.32",
154153
"@types/escape-html": "^1.0.1",
155154
"@types/file-saver": "^2.0.3",
156-
"@types/flux": "^3.1.9",
157155
"@types/fs-extra": "^11.0.0",
158156
"@types/geojson": "^7946.0.8",
159157
"@types/glob-to-regexp": "^0.4.1",

src/components/views/rooms/RoomSublist.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ limitations under the License.
1818

1919
import { Room } from "matrix-js-sdk/src/models/room";
2020
import classNames from "classnames";
21-
import { Dispatcher } from "flux";
2221
import { Enable, Resizable } from "re-resizable";
2322
import { Direction } from "re-resizable/lib/resizer";
2423
import * as React from "react";
@@ -28,7 +27,7 @@ import { polyfillTouchEvent } from "../../../@types/polyfill";
2827
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
2928
import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
3029
import { Action } from "../../../dispatcher/actions";
31-
import defaultDispatcher from "../../../dispatcher/dispatcher";
30+
import defaultDispatcher, { MatrixDispatcher } from "../../../dispatcher/dispatcher";
3231
import { ActionPayload } from "../../../dispatcher/payloads";
3332
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
3433
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
@@ -68,7 +67,7 @@ polyfillTouchEvent();
6867

6968
export interface IAuxButtonProps {
7069
tabIndex: number;
71-
dispatcher?: Dispatcher<ActionPayload>;
70+
dispatcher?: MatrixDispatcher;
7271
}
7372

7473
interface IProps {

src/dispatcher/dispatcher.ts

+123-5
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,133 @@ See the License for the specific language governing permissions and
1616
limitations under the License.
1717
*/
1818

19-
import { Dispatcher } from "flux";
20-
2119
import { Action } from "./actions";
2220
import { 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

src/hooks/useDispatcher.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,12 @@ limitations under the License.
1515
*/
1616

1717
import { useEffect, useRef } from "react";
18-
import { Dispatcher } from "flux";
1918

2019
import { ActionPayload } from "../dispatcher/payloads";
20+
import { MatrixDispatcher } from "../dispatcher/dispatcher";
2121

22-
// Hook to simplify listening to flux dispatches
23-
export const useDispatcher = (
24-
dispatcher: Dispatcher<ActionPayload>,
25-
handler: (payload: ActionPayload) => void,
26-
): void => {
22+
// Hook to simplify listening to event dispatches
23+
export const useDispatcher = (dispatcher: MatrixDispatcher, handler: (payload: ActionPayload) => void): void => {
2724
// Create a ref that stores handler
2825
const savedHandler = useRef((payload: ActionPayload) => {});
2926

src/stores/AsyncStore.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ limitations under the License.
1616

1717
import { EventEmitter } from "events";
1818
import AwaitLock from "await-lock";
19-
import { Dispatcher } from "flux";
2019

2120
import { ActionPayload } from "../dispatcher/payloads";
21+
import { MatrixDispatcher } from "../dispatcher/dispatcher";
2222

2323
/**
2424
* The event/channel to listen for in an AsyncStore.
@@ -52,7 +52,7 @@ export abstract class AsyncStore<T extends Object> extends EventEmitter {
5252
* @param {Dispatcher<ActionPayload>} dispatcher The dispatcher to rely upon.
5353
* @param {T} initialState The initial state for the store.
5454
*/
55-
protected constructor(private dispatcher: Dispatcher<ActionPayload>, initialState: T = <T>{}) {
55+
protected constructor(private dispatcher: MatrixDispatcher, initialState: T = <T>{}) {
5656
super();
5757

5858
this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this));

src/stores/AsyncStoreWithClient.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ limitations under the License.
1515
*/
1616

1717
import { MatrixClient } from "matrix-js-sdk/src/client";
18-
import { Dispatcher } from "flux";
1918

2019
import { AsyncStore } from "./AsyncStore";
2120
import { ActionPayload } from "../dispatcher/payloads";
2221
import { ReadyWatchingStore } from "./ReadyWatchingStore";
22+
import { MatrixDispatcher } from "../dispatcher/dispatcher";
2323

2424
export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<T> {
2525
protected readyStore: ReadyWatchingStore;
2626

27-
protected constructor(dispatcher: Dispatcher<ActionPayload>, initialState: T = <T>{}) {
27+
protected constructor(dispatcher: MatrixDispatcher, initialState: T = <T>{}) {
2828
super(dispatcher, initialState);
2929

3030
// Create an anonymous class to avoid code duplication

src/stores/LifecycleStore.ts

+8-21
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { Store } from "flux/utils";
18-
1917
import { Action } from "../dispatcher/actions";
2018
import dis from "../dispatcher/dispatcher";
2119
import { ActionPayload } from "../dispatcher/payloads";
2220
import { DoAfterSyncPreparedPayload } from "../dispatcher/payloads/DoAfterSyncPreparedPayload";
21+
import { AsyncStore } from "./AsyncStore";
2322

2423
interface IState {
2524
deferredAction: ActionPayload | null;
@@ -30,32 +29,24 @@ const INITIAL_STATE: IState = {
3029
};
3130

3231
/**
33-
* A class for storing application state to do with authentication. This is a simple flux
32+
* A class for storing application state to do with authentication. This is a simple
3433
* store that listens for actions and updates its state accordingly, informing any
3534
* listeners (views) of state changes.
3635
*/
37-
class LifecycleStore extends Store<ActionPayload> {
38-
private state: IState = INITIAL_STATE;
39-
36+
class LifecycleStore extends AsyncStore<IState> {
4037
public constructor() {
41-
super(dis);
42-
}
43-
44-
private setState(newState: Partial<IState>): void {
45-
this.state = Object.assign(this.state, newState);
46-
this.__emitChange();
38+
super(dis, INITIAL_STATE);
4739
}
4840

49-
// eslint-disable-next-line @typescript-eslint/naming-convention
50-
protected __onDispatch(payload: ActionPayload | DoAfterSyncPreparedPayload<ActionPayload>): void {
41+
protected onDispatch(payload: ActionPayload | DoAfterSyncPreparedPayload<ActionPayload>): void {
5142
switch (payload.action) {
5243
case Action.DoAfterSyncPrepared:
53-
this.setState({
44+
this.updateState({
5445
deferredAction: payload.deferred_action,
5546
});
5647
break;
5748
case "cancel_after_sync_prepared":
58-
this.setState({
49+
this.updateState({
5950
deferredAction: null,
6051
});
6152
break;
@@ -65,7 +56,7 @@ class LifecycleStore extends Store<ActionPayload> {
6556
}
6657
if (!this.state.deferredAction) break;
6758
const deferredAction = Object.assign({}, this.state.deferredAction);
68-
this.setState({
59+
this.updateState({
6960
deferredAction: null,
7061
});
7162
dis.dispatch(deferredAction);
@@ -77,10 +68,6 @@ class LifecycleStore extends Store<ActionPayload> {
7768
break;
7869
}
7970
}
80-
81-
private reset(): void {
82-
this.state = Object.assign({}, INITIAL_STATE);
83-
}
8471
}
8572

8673
let singletonLifecycleStore: LifecycleStore | null = null;

src/stores/ReadyWatchingStore.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,19 @@
1616

1717
import { MatrixClient } from "matrix-js-sdk/src/client";
1818
import { SyncState } from "matrix-js-sdk/src/sync";
19-
import { Dispatcher } from "flux";
2019
import { EventEmitter } from "events";
2120

2221
import { MatrixClientPeg } from "../MatrixClientPeg";
2322
import { ActionPayload } from "../dispatcher/payloads";
2423
import { IDestroyable } from "../utils/IDestroyable";
2524
import { Action } from "../dispatcher/actions";
25+
import { MatrixDispatcher } from "../dispatcher/dispatcher";
2626

2727
export abstract class ReadyWatchingStore extends EventEmitter implements IDestroyable {
2828
protected matrixClient: MatrixClient | null = null;
2929
private dispatcherRef: string | null = null;
3030

31-
public constructor(protected readonly dispatcher: Dispatcher<ActionPayload>) {
31+
public constructor(protected readonly dispatcher: MatrixDispatcher) {
3232
super();
3333
}
3434

0 commit comments

Comments
 (0)