Skip to content

Commit d52991c

Browse files
committed
feat: pubsub hooks
1 parent d2f2cb6 commit d52991c

File tree

1 file changed

+197
-0
lines changed

1 file changed

+197
-0
lines changed

electron/renderer/hooks/pubsub.tsx

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { useEffect, useMemo } from 'react';
2+
import { create } from 'zustand';
3+
import { useShallow } from 'zustand/react/shallow';
4+
import { runInBackground } from '../lib/async/run-in-background.js';
5+
6+
export type PubSubSubscriber = (data?: any) => Promise<void> | void;
7+
8+
export type PubSubUnsubscribeCallback = () => void;
9+
10+
/**
11+
* This interface is designed to be simple.
12+
* The methods accept two arguments: an event (string) and a subscriber (function).
13+
* They deviate from the convention of named arguments in the interest
14+
* of simplicity and brevity.
15+
*/
16+
interface PubSub {
17+
/**
18+
* Subscribes to an event.
19+
* Returns a method that will unsubscribe from the event.
20+
* Or, you can explicitly call `unsubscribe(event, subscriber)`.
21+
*/
22+
subscribe: (
23+
event: string,
24+
subscriber: PubSubSubscriber
25+
) => PubSubUnsubscribeCallback;
26+
27+
/**
28+
* Unsubscribe from an event.
29+
*/
30+
unsubscribe: (event: string, subscriber: PubSubSubscriber) => void;
31+
32+
/**
33+
* Publish a message to all subscribers of the event.
34+
*/
35+
publish: (event: string, data?: any) => void;
36+
}
37+
38+
/**
39+
* Hook that subscribes to an event.
40+
* Automatically unsubscribes when the component unmounts.
41+
*
42+
* For more granular control, use `usePubSub()`.
43+
*/
44+
let counter = 0;
45+
export const useSubscribe = (
46+
event: string,
47+
subscriber: PubSubSubscriber
48+
): void => {
49+
counter += 1;
50+
console.log('useSubscribe', counter);
51+
52+
const subscribe = usePubSubStore((state) => state.subscribe);
53+
54+
useEffect(() => {
55+
return subscribe({ event, subscriber });
56+
}, [event, subscriber, subscribe]);
57+
};
58+
59+
/**
60+
* Hook that provides functions for
61+
* subscribing, unsubscribing, and publishing events.
62+
*
63+
* The `subscribe` function returns a function that unsubscribes from the event.
64+
* It is your responsibility to unsubscribe when the component unmounts.
65+
* For automatic unsubscription, use `useSubscribe` hook.
66+
*/
67+
export const usePubSub = (): PubSub => {
68+
const store = usePubSubStore(
69+
// Technically, our state reducer is returning a new object
70+
// each time although the properties are the same.
71+
// Use the `useShallow` operator to prevent unnecessary re-renders.
72+
useShallow((state) => {
73+
return {
74+
// We exclude other properties like `subscribers`
75+
// so that we don't re-render when they change.
76+
// Who is subscribed or not is now relevant to this API shape.
77+
subscribe: state.subscribe,
78+
unsubscribe: state.unsubscribe,
79+
publish: state.publish,
80+
};
81+
})
82+
);
83+
84+
const pubsub = useMemo(() => {
85+
return {
86+
subscribe: (event: string, subscriber: PubSubSubscriber) => {
87+
return store.subscribe({ event, subscriber });
88+
},
89+
unsubscribe: (event: string, subscriber: PubSubSubscriber) => {
90+
store.unsubscribe({ event, subscriber });
91+
},
92+
publish: (event: string, data?: any) => {
93+
store.publish({ event, data });
94+
},
95+
};
96+
}, [store]);
97+
98+
return pubsub;
99+
};
100+
101+
interface PubSubStoreData {
102+
/**
103+
* Map of event names to subscribers.
104+
*/
105+
subscribers: Record<string, Array<PubSubSubscriber>>;
106+
107+
/**
108+
* Subscribes to an event.
109+
* Returns a method that will unsubscribe from the event.
110+
* Or, you can explicitly call `unsubscribe(event, subscriber)`.
111+
*/
112+
subscribe: (options: {
113+
event: string;
114+
subscriber: PubSubSubscriber;
115+
}) => PubSubUnsubscribeCallback;
116+
117+
/**
118+
* Unsubscribe from an event.
119+
*/
120+
unsubscribe: (options: {
121+
event: string;
122+
subscriber: PubSubSubscriber;
123+
}) => void;
124+
125+
/**
126+
* Publish a message to all subscribers of the event.
127+
*/
128+
publish: (options: { event: string; data?: any }) => void;
129+
}
130+
131+
/**
132+
* An implementation of the PubSub pattern.
133+
*/
134+
const usePubSubStore = create<PubSubStoreData>((set, get) => ({
135+
subscribers: {},
136+
137+
subscribe: (options: { event: string; subscriber: PubSubSubscriber }) => {
138+
const { event, subscriber } = options;
139+
140+
set((state: PubSubStoreData) => {
141+
const subscribers = state.subscribers[event] ?? [];
142+
143+
const updatedSubscribers = [...subscribers, subscriber];
144+
145+
return {
146+
subscribers: {
147+
...state.subscribers,
148+
[event]: updatedSubscribers,
149+
},
150+
};
151+
});
152+
153+
const unsub: PubSubUnsubscribeCallback = () => {
154+
// Get the current state and unsubscribe without causing a re-render.
155+
// This also lets us reuse the same unsubscribe logic.
156+
get().unsubscribe({ event, subscriber });
157+
};
158+
159+
return unsub;
160+
},
161+
162+
unsubscribe: (options: { event: string; subscriber: PubSubSubscriber }) => {
163+
const { event, subscriber } = options;
164+
165+
set((state: PubSubStoreData) => {
166+
const subscribers = state.subscribers[event] ?? [];
167+
168+
const updatedSubscribers = subscribers.filter((sub) => {
169+
return sub !== subscriber;
170+
});
171+
172+
return {
173+
subscribers: {
174+
...state.subscribers,
175+
[event]: updatedSubscribers,
176+
},
177+
};
178+
});
179+
},
180+
181+
publish: (options: { event: string; data?: any }) => {
182+
const { event, data } = options;
183+
184+
const state = get();
185+
const subscribers = state.subscribers[event] ?? [];
186+
187+
// Optmistically run all subscribers simultaneously
188+
// so that a slow subscriber doesn't block the others.
189+
runInBackground(async () => {
190+
await Promise.allSettled(
191+
subscribers.map((subscriber) => {
192+
return subscriber(data);
193+
})
194+
);
195+
});
196+
},
197+
}));

0 commit comments

Comments
 (0)