mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-19 06:54:23 +01:00
update
This commit is contained in:
85
src/clients/useBatch.ts
Normal file
85
src/clients/useBatch.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { createSignal, createEffect, onCleanup } from 'solid-js';
|
||||
|
||||
export type Task<TaskArgs, TaskResult> = {
|
||||
id: number;
|
||||
args: TaskArgs;
|
||||
resolve: (result: TaskResult) => void;
|
||||
reject: (error: any) => void;
|
||||
};
|
||||
|
||||
export type UseBatchProps<TaskArgs, TaskResult> = {
|
||||
executor: (task: Task<TaskArgs, TaskResult>[]) => void;
|
||||
interval?: number;
|
||||
// batchSize: number;
|
||||
};
|
||||
|
||||
export type PromiseWithCallbacks<T> = {
|
||||
promise: Promise<T>;
|
||||
resolve: (e: T) => void;
|
||||
reject: (e: any) => void;
|
||||
};
|
||||
|
||||
const promiseWithCallbacks = <T>(): PromiseWithCallbacks<T> => {
|
||||
let resolve: ((e: T) => void) | undefined;
|
||||
let reject: ((e: any) => void) | undefined;
|
||||
|
||||
const promise = new Promise<T>((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 = <TaskArgs, TaskResult>(
|
||||
propsProvider: () => UseBatchProps<TaskArgs, TaskResult>,
|
||||
) => {
|
||||
const [seqId, setSeqId] = createSignal<number>(0);
|
||||
const [taskQueue, setTaskQueue] = createSignal<Task<TaskArgs, TaskResult>[]>([]);
|
||||
|
||||
createEffect(() => {
|
||||
const { executor, interval = 1000 } = propsProvider();
|
||||
let timeoutId: ReturnType<typeof setTimeout> | 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<TaskResult> => {
|
||||
const { promise, resolve, reject } = promiseWithCallbacks<TaskResult>();
|
||||
const id = nextId();
|
||||
const newTask: Task<TaskArgs, TaskResult> = { 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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<NostrEvent | undefined>;
|
||||
query: CreateQueryResult<NostrEvent[]>;
|
||||
query: CreateQueryResult<NostrEvent>;
|
||||
};
|
||||
|
||||
const useEvent = (propsProvider: () => UseEventProps): UseEvent => {
|
||||
const query = useCachedEvents(() => {
|
||||
const { relayUrls, eventId } = propsProvider();
|
||||
return {
|
||||
relayUrls,
|
||||
filters: [
|
||||
{
|
||||
ids: [eventId],
|
||||
kinds: [1],
|
||||
limit: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
const { exec } = useBatch<UseEventProps, NostrEvent>(() => {
|
||||
return {
|
||||
executor: (tasks) => {
|
||||
// TODO relayUrlsを考慮する
|
||||
const [config] = useConfig();
|
||||
const eventIdTaskMap = new Map<string, Task<UseEventProps, NostrEvent>>(
|
||||
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 };
|
||||
};
|
||||
|
||||
@@ -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<Profile | undefined>;
|
||||
query: CreateQueryResult<NostrEvent[]>;
|
||||
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<Profile | undefined>;
|
||||
query: CreateQueryResult<NostrEvent>;
|
||||
};
|
||||
|
||||
const { exec } = useBatch<UseProfileProps, NostrEvent>(() => {
|
||||
return {
|
||||
executor: (tasks) => {
|
||||
// TODO relayUrlsを考慮する
|
||||
const [config] = useConfig();
|
||||
const pubkeyTaskMap = new Map<string, Task<UseProfileProps, NostrEvent>>(
|
||||
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かどうかのチェック
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,7 +37,10 @@ const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => {
|
||||
{' Reposted'}
|
||||
</div>
|
||||
</div>
|
||||
<Show when={event() != null} fallback={<Show when={eventQuery.isLoading}>loading</Show>}>
|
||||
<Show
|
||||
when={event() != null}
|
||||
fallback={<Show when={eventQuery.isLoading}>loading {eventId()}</Show>}
|
||||
>
|
||||
<TextNote event={event()} />
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -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<TextNoteProps> = (props) => {
|
||||
pubkey: props.event.pubkey,
|
||||
}));
|
||||
|
||||
/*
|
||||
const {
|
||||
reactions,
|
||||
isReactedBy,
|
||||
@@ -43,6 +44,8 @@ const TextNote: Component<TextNoteProps> = (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<TextNoteProps> = (props) => {
|
||||
};
|
||||
|
||||
const handleReaction: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||
/*
|
||||
if (isReactedByMe()) {
|
||||
// TODO remove reaction
|
||||
return;
|
||||
}
|
||||
*/
|
||||
ev.preventDefault();
|
||||
commands
|
||||
.publishReaction({
|
||||
@@ -75,7 +80,7 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
||||
notifyPubkey: props.event.pubkey,
|
||||
})
|
||||
.then(() => {
|
||||
reactionsQuery.refetch();
|
||||
// reactionsQuery.refetch();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -141,7 +146,7 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
||||
<HeartSolid />
|
||||
</Show>
|
||||
</button>
|
||||
<div class="text-sm text-zinc-400">{reactions().length}</div>
|
||||
{/* <div class="text-sm text-zinc-400">{reactions().length}</div> */}
|
||||
</div>
|
||||
<button class="h-4 w-4 text-zinc-400">
|
||||
<EllipsisHorizontal />
|
||||
|
||||
@@ -61,7 +61,7 @@ const Reaction: Component<ReactionProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification-event">
|
||||
<Show when={reactedEvent() != null} fallback="loading">
|
||||
<Show when={reactedEvent() != null} fallback={<>loading {eventId()}</>}>
|
||||
<TextNote event={reactedEvent()} />
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ const [currentDate, setCurrentDate] = createSignal(new Date());
|
||||
|
||||
setInterval(() => {
|
||||
setCurrentDate(new Date());
|
||||
}, 10000);
|
||||
}, 7000);
|
||||
|
||||
const useDatePulser = (): Accessor<Date> => {
|
||||
return currentDate;
|
||||
|
||||
@@ -32,6 +32,21 @@ useShortcutKeys({
|
||||
onShortcut: (s) => console.log(s),
|
||||
});
|
||||
|
||||
const dummyTextNote = (
|
||||
<TextNote
|
||||
event={
|
||||
{
|
||||
id: 12345,
|
||||
kind: 1,
|
||||
pubkey: pubkeyHex,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: 'hello',
|
||||
} as NostrEvent
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const Home: Component = () => {
|
||||
const [config] = useConfig();
|
||||
const commands = useCommands();
|
||||
@@ -47,6 +62,7 @@ const Home: Component = () => {
|
||||
kinds: [1, 6],
|
||||
authors: followings()?.map((f) => f.pubkey) ?? [pubkeyHex],
|
||||
limit: 25,
|
||||
since: Math.floor(Date.now() / 1000) - 12 * 60 * 60,
|
||||
},
|
||||
],
|
||||
}));
|
||||
@@ -117,18 +133,6 @@ const Home: Component = () => {
|
||||
<SideBar postForm={() => <NotePostForm onPost={handlePost} />} />
|
||||
<div class="flex flex-row overflow-y-hidden overflow-x-scroll">
|
||||
<Column name="ホーム" width="widest">
|
||||
<TextNote
|
||||
event={
|
||||
{
|
||||
id: 12345,
|
||||
kind: 1,
|
||||
pubkey: pubkeyHex,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: 'hello',
|
||||
} as NostrEvent
|
||||
}
|
||||
/>
|
||||
<Timeline events={followingsPosts()} />
|
||||
</Column>
|
||||
<Column name="通知" width="medium">
|
||||
|
||||
Reference in New Issue
Block a user