Skip to content

Commit 950eca5

Browse files
committed
event rewrite
1 parent bc26331 commit 950eca5

8 files changed

Lines changed: 69 additions & 43 deletions

File tree

frontend/src/ts/components/layout/header/AccountXpBar.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { XpBreakdown } from "@monkeytype/schemas/results";
22
import { isSafeNumber } from "@monkeytype/util/numbers";
33
import {
4-
createMemo,
54
createSignal,
65
For,
76
JSXElement,
@@ -14,7 +13,7 @@ import { createSignalWithSetters } from "../../../hooks/createSignalWithSetters"
1413
import { createEffectOn } from "../../../hooks/effects";
1514
import { getFocus } from "../../../states/core";
1615
import {
17-
getSkipBreakdownEvent,
16+
skipBreakdownEvent,
1817
getXpBarData,
1918
setAnimatedLevel,
2019
} from "../../../states/header";
@@ -38,11 +37,11 @@ export function AccountXpBar(): JSXElement {
3837
const [getBarAnimationDuration, setBarAnimationDuration] = createSignal(0);
3938
const [getBarAnimationEase, setBarAnimationEase] = createSignal("out(5)");
4039

41-
const [getAnimationEvent, fireAnimationEvent] = createEvent();
40+
const animationEvent = createEvent();
4241
const [getTotal, { setTotal }] = createSignalWithSetters(0)({
4342
setTotal: (set, value: number) => {
4443
set(value);
45-
fireAnimationEvent();
44+
animationEvent.dispatch();
4645
},
4746
});
4847

@@ -54,23 +53,29 @@ export function AccountXpBar(): JSXElement {
5453
let skipped = false;
5554
let runId = 0;
5655

57-
const flashAnimation = createMemo(() => {
58-
getAnimationEvent(); // trigger on every total update, even if value unchanged
56+
const [flashAnimation, setFlashAnimation] = createSignal({
57+
scale: [1, 1],
58+
rotate: [0, 0],
59+
duration: 2000,
60+
ease: "out(5)",
61+
});
62+
63+
animationEvent.useListener(() => {
5964
const rand = (Math.random() * 2 - 1) / 4;
6065
const rand2 = (Math.random() + 1) / 2;
61-
return {
66+
setFlashAnimation({
6267
scale: [1 + 0.5 * rand2, 1],
6368
rotate: [10 * rand, 0],
6469
duration: 2000,
6570
ease: "out(5)",
66-
};
71+
});
6772
});
6873

6974
const addItem = (label: string, amount: number | string): void => {
7075
setBreakdownItems((items) => [...items, { label, amount }]);
7176
};
7277

73-
createEffectOn(getSkipBreakdownEvent, async () => {
78+
skipBreakdownEvent.useListener(async () => {
7479
if (skipped || !canSkip) return;
7580

7681
const myId = runId; // capture before first await

frontend/src/ts/components/layout/header/Logo.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { JSXElement } from "solid-js";
22

33
import {
4-
dispatchRestartTest,
4+
restartTestEvent,
55
getActivePage,
66
getFocus,
77
} from "../../../states/core";
@@ -21,7 +21,7 @@ export function Logo(): JSXElement {
2121
}}
2222
data-ui-element="logo"
2323
onClick={() => {
24-
if (getActivePage() === "test") dispatchRestartTest();
24+
if (getActivePage() === "test") restartTestEvent.dispatch();
2525
}}
2626
>
2727
<svg

frontend/src/ts/components/layout/header/Nav.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createMemo, JSXElement, Show } from "solid-js";
44
import { createEffectOn } from "../../../hooks/effects";
55
import { getServerConfigurationQueryOptions } from "../../../queries/server-configuration";
66
import {
7-
dispatchRestartTest,
7+
restartTestEvent,
88
getActivePage,
99
getFocus,
1010
} from "../../../states/core";
@@ -80,7 +80,7 @@ export function Nav(): JSXElement {
8080
"data-nav-item": "test",
8181
}}
8282
onClick={() => {
83-
if (getActivePage() === "test") dispatchRestartTest();
83+
if (getActivePage() === "test") restartTestEvent.dispatch();
8484
}}
8585
/>
8686
<Button
Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,56 @@
1-
import { Accessor, createSignal } from "solid-js";
1+
import { onCleanup } from "solid-js";
2+
3+
type Listener<T> = (value: T) => void;
4+
5+
type EventBus<T> = {
6+
dispatch: (...args: T extends undefined ? [] : [value: T]) => void;
7+
subscribe: (fn: Listener<T>) => () => void;
8+
useListener: (fn: Listener<T>) => void;
9+
};
210

311
/**
4-
* Creates a reactive event primitive.
12+
* Creates a pub-sub event bus.
513
*
6-
* Returns a tuple of:
7-
* - An accessor whose value increments each time the event is dispatched.
8-
* Reactive consumers (effects, memos, etc.) re-run whenever the event fires.
9-
* - A dispatch function that triggers the event.
10-
*
11-
* @returns `[accessor, dispatch]`
14+
* - `dispatch(value)` notifies all listeners. No args needed when `T` is `undefined`.
15+
* - `subscribe(fn)` registers a listener, returns an unsubscribe function.
16+
* - `useListener(fn)` registers a listener that auto-unsubscribes via Solid's `onCleanup`.
1217
*
1318
* @example
1419
* ```ts
15-
* const [onSave, dispatchSave] = createEvent();
16-
*
17-
* createEffect(() => {
18-
* onSave(); // re-runs every time dispatchSave() is called
19-
* console.log("saved!");
20-
* });
20+
* const clickEvent = createEvent2<{ x: number; y: number }>();
21+
* clickEvent.useListener(({ x, y }) => console.log(x, y));
22+
* clickEvent.dispatch({ x: 10, y: 20 });
2123
*
22-
* dispatchSave(); // triggers the effect
24+
* const resetEvent = createEvent2();
25+
* resetEvent.useListener(() => console.log("reset"));
26+
* resetEvent.dispatch();
2327
* ```
2428
*/
2529

26-
export function createEvent(): [Accessor<number>, () => void] {
27-
const [get, set] = createSignal(0);
28-
return [
29-
get,
30-
() => {
31-
set((v) => v + 1);
32-
},
33-
];
30+
export function createEvent<T = undefined>(): EventBus<T> {
31+
const listeners = new Set<Listener<T>>();
32+
33+
function dispatch(...args: T extends undefined ? [] : [value: T]): void {
34+
const value = args[0] as T;
35+
for (const fn of listeners) {
36+
try {
37+
fn(value);
38+
} catch (e) {
39+
console.error(e);
40+
}
41+
}
42+
}
43+
44+
function subscribe(fn: Listener<T>): () => void {
45+
listeners.add(fn);
46+
return () => listeners.delete(fn);
47+
}
48+
49+
// Auto-unsubscribes when the Solid owner scope is disposed
50+
function useListener(fn: Listener<T>): void {
51+
listeners.add(fn);
52+
onCleanup(() => listeners.delete(fn));
53+
}
54+
55+
return { dispatch, subscribe, useListener };
3456
}

frontend/src/ts/states/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ export const [getSelectedProfileName, setSelectedProfileName] = createSignal<
3838
string | undefined
3939
>(undefined);
4040

41-
export const [onRestartTest, dispatchRestartTest] = createEvent();
41+
export const restartTestEvent = createEvent();

frontend/src/ts/states/header.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ export const [getXpBarData, setXpBarData] = createSignal<XpBarData | null>(
1515
null,
1616
);
1717

18-
export const [getSkipBreakdownEvent, skipBreakdown] = createEvent();
18+
export const skipBreakdownEvent = createEvent();

frontend/src/ts/test/test-logic.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ import * as TodayTracker from "./today-tracker";
2626
import * as ChallengeContoller from "../controllers/challenge-controller";
2727
import * as QuoteRateModal from "../modals/quote-rate";
2828
import * as Result from "./result";
29-
import { createEffect, on } from "solid-js";
30-
import { getActivePage, onRestartTest } from "../states/core";
29+
import { getActivePage, restartTestEvent } from "../states/core";
3130
import * as TestInput from "./test-input";
3231
import * as TestWords from "./test-words";
3332
import * as WordsGenerator from "./words-generator";
@@ -1433,7 +1432,7 @@ qs(".pageTest")?.onChild("click", "#restartTestButtonWithSameWordset", () => {
14331432
});
14341433
});
14351434

1436-
createEffect(on(onRestartTest, () => restart(), { defer: true }));
1435+
restartTestEvent.subscribe(() => restart());
14371436

14381437
qs(".pageTest")?.onChild("click", "#testConfig .mode .textButton", (e) => {
14391438
if (TestState.testRestarting) return;

frontend/src/ts/test/test-ui.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ import {
6868
qsr,
6969
} from "../utils/dom";
7070
import { getTheme } from "../states/theme";
71-
import { skipBreakdown } from "../states/header";
71+
import { skipBreakdownEvent } from "../states/header";
7272

7373
export const updateHintsPositionDebounced = Misc.debounceUntilResolved(
7474
updateHintsPosition,
@@ -1918,7 +1918,7 @@ export function onTestRestart(source: "testPage" | "resultPage"): void {
19181918
if (Config.randomTheme !== "off") {
19191919
void ThemeController.randomizeTheme();
19201920
}
1921-
skipBreakdown();
1921+
skipBreakdownEvent.dispatch();
19221922
}
19231923

19241924
currentTestLine = 0;

0 commit comments

Comments
 (0)