This commit is contained in:
Shusui MOYATANI
2023-03-05 09:05:21 +09:00
parent 0327af6ba1
commit 30e6e894ed
13 changed files with 144 additions and 102 deletions

View File

@@ -16,40 +16,37 @@ export type UseBatchedEventProps<TaskArgs> = {
const useBatchedEvent = <TaskArgs>(propsProvider: () => UseBatchedEventProps<TaskArgs>) => { const useBatchedEvent = <TaskArgs>(propsProvider: () => UseBatchedEventProps<TaskArgs>) => {
const props = createMemo(propsProvider); const props = createMemo(propsProvider);
return useBatch<TaskArgs, NostrEvent>(() => { return useBatch<TaskArgs, NostrEvent>(() => ({
return { interval: props().interval,
interval: props().interval, executor: (tasks) => {
executor: (tasks) => { const { generateKey, mergeFilters, extractKey } = props();
const { generateKey, mergeFilters, extractKey } = props(); // TODO relayUrlsを考慮する
// TODO relayUrlsを考慮する const [config] = useConfig();
const [config] = useConfig();
const keyTaskMap = new Map<string | number, Task<TaskArgs, NostrEvent>>( const keyTaskMap = new Map<string | number, Task<TaskArgs, NostrEvent>>(
tasks.map((task) => [generateKey(task.args), task]), tasks.map((task) => [generateKey(task.args), task]),
); );
const filters = mergeFilters(tasks.map((task) => task.args)); const filters = mergeFilters(tasks.map((task) => task.args));
useSubscription(() => ({ useSubscription(() => ({
relayUrls: config().relayUrls, relayUrls: config().relayUrls,
filters, filters,
continuous: false, continuous: false,
onEvent: (event: NostrEvent) => { onEvent: (event: NostrEvent) => {
const key = extractKey(event); const key = extractKey(event);
if (key == null) return; if (key == null) return;
const task = keyTaskMap.get(key); const task = keyTaskMap.get(key);
// possibly, the new event received if (task == null) return;
if (task == null) return; task.resolve(event);
task.resolve(event); },
}, onEOSE: () => {
onEOSE: () => { tasks.forEach((task) => {
tasks.forEach((task) => { task.reject(new Error(`NotFound: ${JSON.stringify(filters)}`));
task.reject(new Error('NotFound')); });
}); },
}, }));
})); },
}, }));
};
});
}; };
export default useBatchedEvent; export default useBatchedEvent;

View File

@@ -35,64 +35,62 @@ const addEvent =
const useBatchedEvents = <TaskArgs>(propsProvider: () => UseBatchedEventsProps<TaskArgs>) => { const useBatchedEvents = <TaskArgs>(propsProvider: () => UseBatchedEventsProps<TaskArgs>) => {
const props = createMemo(propsProvider); const props = createMemo(propsProvider);
return useBatch<TaskArgs, Accessor<BatchedEvents>>(() => { return useBatch<TaskArgs, Accessor<BatchedEvents>>(() => ({
return { interval: props().interval,
interval: props().interval, executor: (tasks) => {
executor: (tasks) => { const { generateKey, mergeFilters, extractKey } = props();
const { generateKey, mergeFilters, extractKey } = props(); // TODO relayUrlsを考慮する
// TODO relayUrlsを考慮する const [config] = useConfig();
const [config] = useConfig();
const keyTaskMap = new Map<string | number, Task<TaskArgs, Accessor<BatchedEvents>>>( const keyTaskMap = new Map<string | number, Task<TaskArgs, Accessor<BatchedEvents>>>(
tasks.map((task) => [generateKey(task.args), task]), tasks.map((task) => [generateKey(task.args), task]),
); );
const filters = mergeFilters(tasks.map((task) => task.args)); const filters = mergeFilters(tasks.map((task) => task.args));
const keyEventSignalsMap = new Map<string | number, Signal<BatchedEvents>>(); const keyEventSignalsMap = new Map<string | number, Signal<BatchedEvents>>();
const getSignalForKey = (key: string | number): Signal<BatchedEvents> => { const getSignalForKey = (key: string | number): Signal<BatchedEvents> => {
const eventsSignal = const eventsSignal =
keyEventSignalsMap.get(key) ?? keyEventSignalsMap.get(key) ??
createSignal<BatchedEvents>({ createSignal<BatchedEvents>({
events: [], events: [],
completed: false, completed: false,
}); });
keyEventSignalsMap.set(key, eventsSignal); keyEventSignalsMap.set(key, eventsSignal);
return eventsSignal; return eventsSignal;
}; };
const didReceivedEventsForKey = (key: string | number): boolean => const didReceivedEventsForKey = (key: string | number): boolean =>
keyEventSignalsMap.has(key); keyEventSignalsMap.has(key);
useSubscription(() => ({ useSubscription(() => ({
relayUrls: config().relayUrls, relayUrls: config().relayUrls,
filters, filters,
continuous: false, continuous: false,
onEvent: (event: NostrEvent) => { onEvent: (event: NostrEvent) => {
const key = extractKey(event); const key = extractKey(event);
if (key == null) return; if (key == null) return;
const task = keyTaskMap.get(key); const task = keyTaskMap.get(key);
if (task == null) return; if (task == null) return;
const [events, setEvents] = getSignalForKey(key); const [events, setEvents] = getSignalForKey(key);
setEvents(addEvent(event)); setEvents(addEvent(event));
task.resolve(events); task.resolve(events);
}, },
onEOSE: () => { onEOSE: () => {
tasks.forEach((task) => { tasks.forEach((task) => {
const key = generateKey(task.args); const key = generateKey(task.args);
if (didReceivedEventsForKey(key)) { if (didReceivedEventsForKey(key)) {
const [, setEvents] = getSignalForKey(key); const [, setEvents] = getSignalForKey(key);
setEvents(completeBatchedEvents); setEvents(completeBatchedEvents);
} else { } else {
task.resolve(emptyBatchedEvents); task.resolve(emptyBatchedEvents);
} }
}); });
}, },
})); }));
}, },
}; }));
});
}; };
export default useBatchedEvents; export default useBatchedEvents;

View File

@@ -49,20 +49,20 @@ const getEvents = async ({
* This is useful when you want to fetch some data which change occasionally: * This is useful when you want to fetch some data which change occasionally:
* profile or following list, reactions, and something like that. * profile or following list, reactions, and something like that.
*/ */
const useCachedEvents = (propsProvider: () => UseSubscriptionProps) => { const useCachedEvents = (propsProvider: () => UseSubscriptionProps | null) => {
const pool = usePool(); const pool = usePool();
return createQuery( return createQuery(
() => { () => {
const { relayUrls, filters, continuous, options } = propsProvider(); const currentProps = propsProvider();
return ['useCachedEvents', relayUrls, filters, continuous, options] as const; return ['useCachedEvents', currentProps] as const;
}, },
({ queryKey, signal }) => { ({ queryKey, signal }) => {
const [, relayUrls, filters, continuous, options] = queryKey; const [, currentProps] = queryKey;
return getEvents({ pool: pool(), relayUrls, filters, options, continuous, signal }); if (currentProps == null) return [];
return getEvents({ pool: pool(), signal, ...currentProps });
}, },
{ {
// 5 minutes
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
cacheTime: 15 * 60 * 1000, cacheTime: 15 * 60 * 1000,
}, },

View File

@@ -39,6 +39,7 @@ const useDeprecatedReposts = (
() => queryKey(), () => queryKey(),
({ queryKey: currentQueryKey, signal }) => { ({ queryKey: currentQueryKey, signal }) => {
const [, currentProps] = currentQueryKey; const [, currentProps] = currentQueryKey;
if (currentProps == null) return () => ({ events: [], completed: false });
return timeout( return timeout(
15000, 15000,
`useDeprecatedReposts: ${currentProps.eventId}`, `useDeprecatedReposts: ${currentProps.eventId}`,

View File

@@ -13,7 +13,7 @@ export type UseEventProps = {
export type UseEvent = { export type UseEvent = {
event: Accessor<NostrEvent | undefined>; event: Accessor<NostrEvent | undefined>;
query: CreateQueryResult<NostrEvent>; query: CreateQueryResult<NostrEvent | undefined>;
}; };
const { exec } = useBatchedEvent<UseEventProps>(() => ({ const { exec } = useBatchedEvent<UseEventProps>(() => ({
@@ -25,12 +25,13 @@ const { exec } = useBatchedEvent<UseEventProps>(() => ({
extractKey: (event: NostrEvent) => event.id, extractKey: (event: NostrEvent) => event.id,
})); }));
const useEvent = (propsProvider: () => UseEventProps): UseEvent => { const useEvent = (propsProvider: () => UseEventProps | null): UseEvent => {
const props = createMemo(propsProvider); const props = createMemo(propsProvider);
const query = createQuery( const query = createQuery(
() => ['useEvent', props()] as const, () => ['useEvent', props()] as const,
({ queryKey, signal }) => { ({ queryKey, signal }) => {
const [, currentProps] = queryKey; const [, currentProps] = queryKey;
if (currentProps == null) return undefined;
return timeout(15000, `useEvent: ${currentProps.eventId}`)(exec(currentProps, signal)); return timeout(15000, `useEvent: ${currentProps.eventId}`)(exec(currentProps, signal));
}, },
{ {

View File

@@ -1,3 +1,4 @@
import { createMemo } from 'solid-js';
import useCachedEvents from '@/clients/useCachedEvents'; import useCachedEvents from '@/clients/useCachedEvents';
type UseFollowingsProps = { type UseFollowingsProps = {
@@ -12,8 +13,11 @@ type Following = {
}; };
const useFollowings = (propsProvider: () => UseFollowingsProps) => { const useFollowings = (propsProvider: () => UseFollowingsProps) => {
const props = createMemo(propsProvider);
const query = useCachedEvents(() => { const query = useCachedEvents(() => {
const { relayUrls, pubkey } = propsProvider(); const currentProps = props();
if (currentProps == null) return null;
const { relayUrls, pubkey } = currentProps;
return { return {
relayUrls, relayUrls,
filters: [ filters: [

View File

@@ -43,12 +43,14 @@ const { exec } = useBatchedEvent<UseProfileProps>(() => ({
extractKey: (event: NostrEvent): string => event.pubkey, extractKey: (event: NostrEvent): string => event.pubkey,
})); }));
const useProfile = (propsProvider: () => UseProfileProps): UseProfile => { const useProfile = (propsProvider: () => UseProfileProps | null): UseProfile => {
const props = createMemo(propsProvider); const props = createMemo(propsProvider);
const query = createQuery( const query = createQuery(
() => ['useProfile', props()] as const, () => ['useProfile', props()] as const,
({ queryKey, signal }) => { ({ queryKey, signal }) => {
const [, currentProps] = queryKey; const [, currentProps] = queryKey;
if (currentProps == null) return null;
// TODO timeoutと同時にsignalでキャンセルするようにしたい // TODO timeoutと同時にsignalでキャンセルするようにしたい
return timeout(15000, `useProfile: ${currentProps.pubkey}`)(exec(currentProps, signal)); return timeout(15000, `useProfile: ${currentProps.pubkey}`)(exec(currentProps, signal));
}, },

View File

@@ -6,13 +6,28 @@ const [pubkey, setPubkey] = createSignal<string | undefined>(undefined);
const usePubkey = (): Accessor<string | undefined> => { const usePubkey = (): Accessor<string | undefined> => {
onMount(() => { onMount(() => {
if (window.nostr != null && pubkey() == null && !asking) { let count = 0;
asking = true; const intervalId = setInterval(() => {
window.nostr if (count >= 5) {
.getPublicKey() clearInterval(intervalId);
.then((key) => setPubkey(key)) if (pubkey() == null && !asking) {
.catch((err) => console.error(`failed to obtain public key: ${err}`)); if (window.nostr == null) {
} throw new Error('Failed to obtain public key: Timeout. window.nostr is not defined.');
}
throw new Error('Failed to obtain public key: Timeout');
}
return;
}
if (window.nostr != null && pubkey() == null && !asking) {
asking = true;
window.nostr
.getPublicKey()
.then((key) => setPubkey(key))
.catch((err) => console.error(`failed to obtain public key: ${err}`));
}
count += 1;
}, 1000);
}); });
return pubkey; return pubkey;

View File

@@ -29,7 +29,7 @@ const { exec } = useBatchedEvents<UseReactionsProps>(() => ({
}, },
})); }));
const useReactions = (propsProvider: () => UseReactionsProps): UseReactions => { const useReactions = (propsProvider: () => UseReactionsProps | null): UseReactions => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const props = createMemo(propsProvider); const props = createMemo(propsProvider);
const queryKey = createMemo(() => ['useReactions', props()] as const); const queryKey = createMemo(() => ['useReactions', props()] as const);
@@ -38,6 +38,7 @@ const useReactions = (propsProvider: () => UseReactionsProps): UseReactions => {
() => queryKey(), () => queryKey(),
({ queryKey: currentQueryKey, signal }) => { ({ queryKey: currentQueryKey, signal }) => {
const [, currentProps] = currentQueryKey; const [, currentProps] = currentQueryKey;
if (currentProps == null) return () => ({ events: [], completed: false });
return timeout(15000, `useReactions: ${currentProps.eventId}`)(exec(currentProps, signal)); return timeout(15000, `useReactions: ${currentProps.eventId}`)(exec(currentProps, signal));
}, },
{ {

0
src/components/Hello.tsx Normal file
View File

View File

@@ -13,6 +13,7 @@ import useSubscription from '@/clients/useSubscription';
import useFollowings from '@/clients/useFollowings'; import useFollowings from '@/clients/useFollowings';
import usePubkey from '@/clients/usePubkey'; import usePubkey from '@/clients/usePubkey';
import useShortcutKeys from '@/hooks/useShortcutKeys'; import useShortcutKeys from '@/hooks/useShortcutKeys';
import ensureNonNull from '@/hooks/ensureNonNull';
useShortcutKeys({ useShortcutKeys({
onShortcut: (s) => console.log(s), onShortcut: (s) => console.log(s),
@@ -106,6 +107,7 @@ const Home: Component = () => {
}); });
}; };
const japaneseRegex = /[あ-ん]/;
return ( return (
<div class="flex h-screen w-screen flex-row overflow-hidden"> <div class="flex h-screen w-screen flex-row overflow-hidden">
<SideBar postForm={() => <NotePostForm onPost={handlePost} />} /> <SideBar postForm={() => <NotePostForm onPost={handlePost} />} />

View File

@@ -0,0 +1,14 @@
export type TupleNonNull<T extends readonly any[]> = {
[P in keyof T]: NonNullable<T[P]>;
};
const ensureNonNull =
<T extends readonly any[], R>(tuple: T) =>
(f: (tupleNonNull: TupleNonNull<T>) => R): R | null => {
if (tuple.some((e) => e == null)) {
return null;
}
return f(tuple as TupleNonNull<T>);
};
export default ensureNonNull;

7
src/utils/sleep.ts Normal file
View File

@@ -0,0 +1,7 @@
const sleep = (ms: number): Promise<void> => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
export default sleep;