From b1aa63d6a3ef540ae20a0de6e1f2504bdd5d0d71 Mon Sep 17 00:00:00 2001 From: Shusui MOYATANI Date: Wed, 1 Mar 2023 19:36:41 +0900 Subject: [PATCH] update --- src/clients/useBatch.ts | 85 ++++++++++++++++++++++++ src/clients/useCachedEvents.ts | 12 ++-- src/clients/useEvent.ts | 72 ++++++++++++++------ src/clients/useProfile.ts | 84 ++++++++++++++++------- src/clients/useSubscription.ts | 14 +++- src/components/DeprecatedRepost.tsx | 5 +- src/components/TextNote.tsx | 11 ++- src/components/notification/Reaction.tsx | 2 +- src/hooks/useDatePulser.ts | 2 +- src/pages/Home.tsx | 28 ++++---- 10 files changed, 247 insertions(+), 68 deletions(-) create mode 100644 src/clients/useBatch.ts diff --git a/src/clients/useBatch.ts b/src/clients/useBatch.ts new file mode 100644 index 0000000..d5b02f7 --- /dev/null +++ b/src/clients/useBatch.ts @@ -0,0 +1,85 @@ +import { createSignal, createEffect, onCleanup } from 'solid-js'; + +export type Task = { + id: number; + args: TaskArgs; + resolve: (result: TaskResult) => void; + reject: (error: any) => void; +}; + +export type UseBatchProps = { + executor: (task: Task[]) => void; + interval?: number; + // batchSize: number; +}; + +export type PromiseWithCallbacks = { + promise: Promise; + resolve: (e: T) => void; + reject: (e: any) => void; +}; + +const promiseWithCallbacks = (): PromiseWithCallbacks => { + let resolve: ((e: T) => void) | undefined; + let reject: ((e: any) => void) | undefined; + + const promise = new Promise((resolveFn, rejectFn) => { + resolve = resolveFn; + reject = rejectFn; + }); + + if (resolve == null || reject == null) { + throw new Error('PromiseWithCallbacks failed to extract callbacks'); + } + + return { promise, resolve, reject }; +}; + +const useBatch = ( + propsProvider: () => UseBatchProps, +) => { + const [seqId, setSeqId] = createSignal(0); + const [taskQueue, setTaskQueue] = createSignal[]>([]); + + createEffect(() => { + const { executor, interval = 1000 } = propsProvider(); + let timeoutId: ReturnType | undefined; + + if (timeoutId == null && taskQueue().length > 0) { + timeoutId = setTimeout(() => { + const currentTaskQueue = taskQueue(); + if (currentTaskQueue.length > 0) { + setTaskQueue([]); + executor(currentTaskQueue); + } + timeoutId = undefined; + }, interval); + } + }); + + const nextId = (): number => { + const id = seqId(); + setSeqId((currentId) => currentId + 1); + return id; + }; + + // enqueue task and wait response + const exec = async (args: TaskArgs, signal?: AbortSignal): Promise => { + const { promise, resolve, reject } = promiseWithCallbacks(); + const id = nextId(); + const newTask: Task = { id, args, resolve, reject }; + + signal?.addEventListener('abort', () => { + reject(new Error('AbortError')); + setTaskQueue((currentTaskQueue) => currentTaskQueue.filter((task) => task.id !== newTask.id)); + }); + + setTaskQueue((currentTaskQueue) => [...currentTaskQueue, newTask]); + + return promise; + }; + + return { exec }; +}; + +export default useBatch; diff --git a/src/clients/useCachedEvents.ts b/src/clients/useCachedEvents.ts index 8257471..32bec2c 100644 --- a/src/clients/useCachedEvents.ts +++ b/src/clients/useCachedEvents.ts @@ -1,5 +1,5 @@ import { createQuery } from '@tanstack/solid-query'; -import { UseSubscriptionProps } from '@/clients/useSubscription'; +import { type UseSubscriptionProps } from '@/clients/useSubscription'; import type { Event as NostrEvent } from 'nostr-tools/event'; import type { Filter } from 'nostr-tools/filter'; import type { SimplePool } from 'nostr-tools/pool'; @@ -10,6 +10,8 @@ type GetEventsArgs = { pool: SimplePool; relayUrls: string[]; filters: Filter[]; + // TODO 継続的に取得する場合、Promiseでは無理なので、無理やりキャッシュにストアする仕組みを使う + continuous?: boolean; options?: SubscriptionOptions; signal?: AbortSignal; }; @@ -52,12 +54,12 @@ const useCachedEvents = (propsProvider: () => UseSubscriptionProps) => { return createQuery( () => { - const { relayUrls, filters, options } = propsProvider(); - return ['useCachedEvents', relayUrls, filters, options] as const; + const { relayUrls, filters, continuous, options } = propsProvider(); + return ['useCachedEvents', relayUrls, filters, continuous, options] as const; }, ({ queryKey, signal }) => { - const [, relayUrls, filters, options] = queryKey; - return getEvents({ pool: pool(), relayUrls, filters, options, signal }); + const [, relayUrls, filters, continuous, options] = queryKey; + return getEvents({ pool: pool(), relayUrls, filters, options, continuous, signal }); }, { // 5 minutes diff --git a/src/clients/useEvent.ts b/src/clients/useEvent.ts index 1357930..6b8ddff 100644 --- a/src/clients/useEvent.ts +++ b/src/clients/useEvent.ts @@ -1,8 +1,10 @@ -import { type Accessor } from 'solid-js'; +import { createMemo, type Accessor } from 'solid-js'; import { type Event as NostrEvent } from 'nostr-tools/event'; -import { type CreateQueryResult } from '@tanstack/solid-query'; +import { createQuery, type CreateQueryResult } from '@tanstack/solid-query'; -import useCachedEvents from '@/clients/useCachedEvents'; +import useConfig from '@/clients/useConfig'; +import useBatch, { type Task } from '@/clients/useBatch'; +import useSubscription from '@/clients/useSubscription'; export type UseEventProps = { relayUrls: string[]; @@ -11,25 +13,57 @@ export type UseEventProps = { export type UseEvent = { event: Accessor; - query: CreateQueryResult; + query: CreateQueryResult; }; -const useEvent = (propsProvider: () => UseEventProps): UseEvent => { - const query = useCachedEvents(() => { - const { relayUrls, eventId } = propsProvider(); - return { - relayUrls, - filters: [ - { - ids: [eventId], - kinds: [1], - limit: 1, - }, - ], - }; - }); +const { exec } = useBatch(() => { + return { + executor: (tasks) => { + // TODO relayUrlsを考慮する + const [config] = useConfig(); + const eventIdTaskMap = new Map>( + tasks.map((task) => [task.args.eventId, task]), + ); + const eventIds = Array.from(eventIdTaskMap.keys()); - const event = () => query.data?.[0]; + useSubscription(() => ({ + relayUrls: config().relayUrls, + filters: [ + { + ids: eventIds, + kinds: [1], + }, + ], + continuous: false, + onEvent: (event: NostrEvent) => { + if (event.id == null) return; + const task = eventIdTaskMap.get(event.id); + // possibly, the new event received + if (task == null) return; + task.resolve(event); + }, + })); + }, + }; +}); + +const useEvent = (propsProvider: () => UseEventProps): UseEvent => { + const props = createMemo(propsProvider); + + const query = createQuery( + () => ['useEvent', props()] as const, + ({ queryKey, signal }) => { + const [, currentProps] = queryKey; + return exec(currentProps, signal); + }, + { + // 5 minutes + staleTime: 5 * 60 * 1000, + cacheTime: 15 * 60 * 1000, + }, + ); + + const event = () => query.data; return { event, query }; }; diff --git a/src/clients/useProfile.ts b/src/clients/useProfile.ts index 57af445..240565e 100644 --- a/src/clients/useProfile.ts +++ b/src/clients/useProfile.ts @@ -1,13 +1,10 @@ -import { type Accessor } from 'solid-js'; +import { createMemo, type Accessor } from 'solid-js'; import { type Event as NostrEvent } from 'nostr-tools/event'; -import { type CreateQueryResult } from '@tanstack/solid-query'; +import { createQuery, type CreateQueryResult } from '@tanstack/solid-query'; -import useCachedEvents from '@/clients/useCachedEvents'; - -type UseProfileProps = { - relayUrls: string[]; - pubkey: string; -}; +import useConfig from '@/clients/useConfig'; +import useBatch, { type Task } from '@/clients/useBatch'; +import useSubscription from '@/clients/useSubscription'; // TODO zodにする // deleted等の特殊なもの @@ -27,28 +24,65 @@ type NonStandardProfile = { type Profile = StandardProfile & NonStandardProfile; -type UseProfile = { - profile: Accessor; - query: CreateQueryResult; +type UseProfileProps = { + relayUrls: string[]; + pubkey: string; }; -const useProfile = (propsProvider: () => UseProfileProps): UseProfile => { - const query = useCachedEvents(() => { - const { relayUrls, pubkey } = propsProvider(); - return { - relayUrls, - filters: [ - { - kinds: [0], - authors: [pubkey], - limit: 1, +type UseProfile = { + profile: Accessor; + query: CreateQueryResult; +}; + +const { exec } = useBatch(() => { + return { + executor: (tasks) => { + // TODO relayUrlsを考慮する + const [config] = useConfig(); + const pubkeyTaskMap = new Map>( + tasks.map((task) => [task.args.pubkey, task]), + ); + const pubkeys = Array.from(pubkeyTaskMap.keys()); + + useSubscription(() => ({ + relayUrls: config().relayUrls, + filters: [ + { + kinds: [0], + authors: pubkeys, + }, + ], + continuous: false, + onEvent: (event: NostrEvent) => { + if (event.id == null) return; + const task = pubkeyTaskMap.get(event.pubkey); + // possibly, the new event received + if (task == null) return; + task.resolve(event); }, - ], - }; - }); + })); + }, + }; +}); + +const useProfile = (propsProvider: () => UseProfileProps): UseProfile => { + const props = createMemo(propsProvider); + + const query = createQuery( + () => ['useProfile', props()] as const, + ({ queryKey, signal }) => { + const [, currentProps] = queryKey; + return exec(currentProps, signal); + }, + { + // 5 minutes + staleTime: 5 * 60 * 1000, + cacheTime: 15 * 60 * 1000, + }, + ); const profile = () => { - const maybeProfile = query.data?.[0]; + const maybeProfile = query.data; if (maybeProfile == null) return undefined; // TODO 大きすぎたりしないかどうか、JSONかどうかのチェック diff --git a/src/clients/useSubscription.ts b/src/clients/useSubscription.ts index 6bea6a8..187de45 100644 --- a/src/clients/useSubscription.ts +++ b/src/clients/useSubscription.ts @@ -8,6 +8,11 @@ export type UseSubscriptionProps = { relayUrls: string[]; filters: Filter[]; options?: SubscriptionOptions; + // subscribe not only stored events but also new events published after the subscription + // default is true + continuous?: boolean; + onEvent?: (event: NostrEvent) => void; + signal?: AbortSignal; }; const sortEvents = (events: NostrEvent[]) => @@ -21,7 +26,7 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | undefined) const props = propsProvider(); if (props == null) return; - const { relayUrls, filters, options } = props; + const { relayUrls, filters, options, onEvent, continuous = true } = props; const sub = pool().sub(relayUrls, filters, options); let pushed = false; @@ -29,6 +34,9 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | undefined) const storedEvents: NostrEvent[] = []; sub.on('event', (event: NostrEvent) => { + if (onEvent != null) { + onEvent(event); + } if (!eose) { pushed = true; storedEvents.push(event); @@ -41,6 +49,10 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | undefined) sub.on('eose', () => { eose = true; setEvents(sortEvents(storedEvents)); + + if (!continuous) { + sub.unsub(); + } }); // avoid updating an array too rapidly while this is fetching stored events diff --git a/src/components/DeprecatedRepost.tsx b/src/components/DeprecatedRepost.tsx index e2dcc99..487a848 100644 --- a/src/components/DeprecatedRepost.tsx +++ b/src/components/DeprecatedRepost.tsx @@ -37,7 +37,10 @@ const DeprecatedRepost: Component = (props) => { {' Reposted'} - loading}> + loading {eventId()}} + > diff --git a/src/components/TextNote.tsx b/src/components/TextNote.tsx index e9d77eb..0a6bd08 100644 --- a/src/components/TextNote.tsx +++ b/src/components/TextNote.tsx @@ -11,7 +11,7 @@ import useProfile from '@/clients/useProfile'; import useConfig from '@/clients/useConfig'; import usePubkey from '@/clients/usePubkey'; import useCommands from '@/clients/useCommands'; -import useReactions from '@/clients/useReactions'; +// import useReactions from '@/clients/useReactions'; import useDatePulser from '@/hooks/useDatePulser'; import { formatRelative } from '@/utils/formatDate'; import ColumnItem from '@/components/ColumnItem'; @@ -33,6 +33,7 @@ const TextNote: Component = (props) => { pubkey: props.event.pubkey, })); + /* const { reactions, isReactedBy, @@ -43,6 +44,8 @@ const TextNote: Component = (props) => { })); const isReactedByMe = createMemo(() => isReactedBy(pubkey())); + */ + const isReactedByMe = () => false; const replyingToPubKeys = createMemo(() => props.event.tags.filter((tag) => tag[0] === 'p').map((e) => e[1]), @@ -61,10 +64,12 @@ const TextNote: Component = (props) => { }; const handleReaction: JSX.EventHandler = (ev) => { + /* if (isReactedByMe()) { // TODO remove reaction return; } + */ ev.preventDefault(); commands .publishReaction({ @@ -75,7 +80,7 @@ const TextNote: Component = (props) => { notifyPubkey: props.event.pubkey, }) .then(() => { - reactionsQuery.refetch(); + // reactionsQuery.refetch(); }); }; @@ -141,7 +146,7 @@ const TextNote: Component = (props) => { -
{reactions().length}
+ {/*
{reactions().length}
*/}