This commit is contained in:
Shusui MOYATANI
2023-07-08 03:40:44 +09:00
parent c049fdd3a6
commit 7f6ff2368f
8 changed files with 122 additions and 85 deletions

46
src/nostr/query.ts Normal file
View File

@@ -0,0 +1,46 @@
import { QueryClient, QueryKey } from '@tanstack/solid-query';
import { Event as NostrEvent } from 'nostr-tools';
import { BatchedEventsTask, pickLatestEvent, registerTask } from '@/nostr/useBatchedEvents';
import timeout from '@/utils/timeout';
export const latestEventQuery =
<K extends QueryKey>({
taskProvider,
queryClient,
}: {
taskProvider: (arg: K) => BatchedEventsTask | undefined | null;
queryClient: QueryClient;
}) =>
({ queryKey, signal }: { queryKey: K; signal?: AbortSignal }): Promise<NostrEvent | null> => {
const task = taskProvider(queryKey);
if (task == null) return Promise.resolve(null);
const promise = task.firstEventPromise().catch(() => {
throw new Error(`event not found: ${JSON.stringify(queryKey)}`);
});
task.onUpdate((events) => {
const latest = pickLatestEvent(events);
queryClient.setQueryData(queryKey, latest);
});
registerTask({ task, signal });
return timeout(15000, `${JSON.stringify(queryKey)}`)(promise);
};
export const eventsQuery =
<K extends QueryKey>({
taskProvider,
queryClient,
}: {
taskProvider: (arg: K) => BatchedEventsTask | undefined | null;
queryClient: QueryClient;
}) =>
({ queryKey, signal }: { queryKey: K; signal?: AbortSignal }): Promise<NostrEvent[]> => {
const task = taskProvider(queryKey);
if (task == null) return Promise.resolve([]);
const promise = task.toUpdatePromise().catch(() => []);
task.onUpdate((events) => {
queryClient.setQueryData(queryKey, events);
});
registerTask({ task, signal });
return timeout(15000, `${JSON.stringify(queryKey)}`)(promise);
};

View File

@@ -29,6 +29,16 @@ type TaskArg =
| RepostsTask
| ParameterizedReplaceableEventTask;
export const pickLatestEvent = (events: NostrEvent[]): NostrEvent | null => {
if (events.length === 0) return null;
return events.reduce((a, b) => {
const diff = a.created_at - b.created_at;
if (diff > 0) return a;
if (diff < 0) return b;
return a.id < b.id ? a : b;
});
};
export class BatchedEventsTask extends ObservableTask<TaskArg, NostrEvent[]> {
addEvent(event: NostrEvent) {
this.updateWith((current) => [...(current ?? []), event]);
@@ -37,6 +47,14 @@ export class BatchedEventsTask extends ObservableTask<TaskArg, NostrEvent[]> {
firstEventPromise(): Promise<NostrEvent> {
return this.toUpdatePromise().then((events) => events[0]);
}
latestEventPromise(): Promise<NostrEvent> {
return this.toCompletePromise().then((events) => {
const latest = pickLatestEvent(events);
if (latest == null) throw new Error('event not found');
return latest;
});
}
}
let count = 0;
@@ -219,13 +237,3 @@ export const registerTask = ({
addTask(task);
signal?.addEventListener('abort', () => removeTask(task));
};
export const pickLatestEvent = (events: NostrEvent[]): NostrEvent | null => {
if (events.length === 0) return null;
return events.reduce((a, b) => {
const diff = a.created_at - b.created_at;
if (diff > 0) return a;
if (diff < 0) return b;
return a.id < b.id ? a : b;
});
};

View File

@@ -1,4 +1,4 @@
import { createMemo, createSignal } from 'solid-js';
import { createMemo } from 'solid-js';
import uniq from 'lodash/uniq';
import { Kind } from 'nostr-tools';

View File

@@ -1,11 +1,11 @@
import { createMemo, observable } from 'solid-js';
import { createMemo } from 'solid-js';
import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
import { Event as NostrEvent } from 'nostr-tools';
import { genericEvent } from '@/nostr/event';
import { registerTask, BatchedEventsTask, pickLatestEvent } from '@/nostr/useBatchedEvents';
import timeout from '@/utils/timeout';
import { latestEventQuery } from '@/nostr/query';
import { BatchedEventsTask, registerTask } from '@/nostr/useBatchedEvents';
type Following = {
pubkey: string;
@@ -24,6 +24,15 @@ export type UseFollowings = {
query: CreateQueryResult<NostrEvent | null>;
};
export const fetchLatestFollowings = (
{ pubkey }: UseFollowingsProps,
signal?: AbortSignal,
): Promise<NostrEvent> => {
const task = new BatchedEventsTask({ type: 'Followings', pubkey });
registerTask({ task, signal });
return task.latestEventPromise();
};
const useFollowings = (propsProvider: () => UseFollowingsProps | null): UseFollowings => {
const queryClient = useQueryClient();
const props = createMemo(propsProvider);
@@ -31,22 +40,14 @@ const useFollowings = (propsProvider: () => UseFollowingsProps | null): UseFollo
const query = createQuery(
genQueryKey,
({ queryKey, signal }) => {
console.debug('useFollowings');
const [, currentProps] = queryKey;
if (currentProps == null) return Promise.resolve(null);
const { pubkey } = currentProps;
const task = new BatchedEventsTask({ type: 'Followings', pubkey });
const promise = task.firstEventPromise().catch(() => {
throw new Error(`followings not found: ${pubkey}`);
});
task.onUpdate((events) => {
const latest = pickLatestEvent(events);
queryClient.setQueryData(queryKey, latest);
});
registerTask({ task, signal });
return timeout(15000, `useFollowings: ${pubkey}`)(promise);
},
latestEventQuery({
taskProvider: ([, currentProps]) => {
if (currentProps == null) return null;
const { pubkey } = currentProps;
return new BatchedEventsTask({ type: 'Followings', pubkey });
},
queryClient,
}),
{
staleTime: 5 * 60 * 1000, // 5 min
cacheTime: 24 * 60 * 60 * 1000, // 24 hour

View File

@@ -1,16 +1,11 @@
import { createMemo, observable } from 'solid-js';
import { createMemo } from 'solid-js';
import {
createQuery,
useQueryClient,
type CreateQueryResult,
QueryClient,
} from '@tanstack/solid-query';
import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
import { Event as NostrEvent } from 'nostr-tools';
import { Profile, ProfileWithOtherProperties, safeParseProfile } from '@/nostr/event/Profile';
import { BatchedEventsTask, pickLatestEvent, registerTask } from '@/nostr/useBatchedEvents';
import timeout from '@/utils/timeout';
import { latestEventQuery } from '@/nostr/query';
import { BatchedEventsTask } from '@/nostr/useBatchedEvents';
export type UseProfileProps = {
pubkey: string;
@@ -40,21 +35,14 @@ const useProfile = (propsProvider: () => UseProfileProps | null): UseProfile =>
const query = createQuery(
genQueryKey,
({ queryKey, signal }) => {
const [, currentProps] = queryKey;
if (currentProps == null) return null;
const { pubkey } = currentProps;
const task = new BatchedEventsTask({ type: 'Profile', pubkey });
const promise = task.firstEventPromise().catch(() => {
throw new Error(`profile not found: ${pubkey}`);
});
task.onUpdate((events) => {
const latest = pickLatestEvent(events);
queryClient.setQueryData(queryKey, latest);
});
registerTask({ task, signal });
return timeout(3000, `useProfile: ${pubkey}`)(promise);
},
latestEventQuery({
taskProvider: ([, currentProps]) => {
if (currentProps == null) return null;
const { pubkey } = currentProps;
return new BatchedEventsTask({ type: 'Profile', pubkey });
},
queryClient,
}),
{
// Profiles are updated occasionally, so a short staleTime is used here.
// cacheTime is long so that the user see profiles instantly.

View File

@@ -1,11 +1,11 @@
import { createMemo, observable } from 'solid-js';
import { createMemo } from 'solid-js';
import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
import { Event as NostrEvent } from 'nostr-tools';
import useConfig from '@/core/useConfig';
import { registerTask, BatchedEventsTask } from '@/nostr/useBatchedEvents';
import timeout from '@/utils/timeout';
import { eventsQuery } from '@/nostr/query';
import { BatchedEventsTask } from '@/nostr/useBatchedEvents';
export type UseReactionsProps = {
eventId: string;
@@ -30,18 +30,14 @@ const useReactions = (propsProvider: () => UseReactionsProps | null): UseReactio
const query = createQuery(
genQueryKey,
({ queryKey, signal }) => {
const [, currentProps] = queryKey;
if (currentProps == null) return [];
const { eventId: mentionedEventId } = currentProps;
const task = new BatchedEventsTask({ type: 'Reactions', mentionedEventId });
const promise = task.toUpdatePromise().catch(() => []);
task.onUpdate((events) => {
queryClient.setQueryData(queryKey, events);
});
registerTask({ task, signal });
return timeout(15000, `useReactions: ${mentionedEventId}`)(promise);
},
eventsQuery({
taskProvider: ([, currentProps]) => {
if (currentProps == null) return null;
const { eventId: mentionedEventId } = currentProps;
return new BatchedEventsTask({ type: 'Reactions', mentionedEventId });
},
queryClient,
}),
{
staleTime: 1 * 60 * 1000, // 1 min
cacheTime: 4 * 60 * 60 * 1000, // 4 hour

View File

@@ -1,11 +1,11 @@
import { createMemo, observable } from 'solid-js';
import { createMemo } from 'solid-js';
import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
import { Event as NostrEvent } from 'nostr-tools';
import useConfig from '@/core/useConfig';
import { BatchedEventsTask, registerTask } from '@/nostr/useBatchedEvents';
import timeout from '@/utils/timeout';
import { eventsQuery } from '@/nostr/query';
import { BatchedEventsTask } from '@/nostr/useBatchedEvents';
export type UseRepostsProps = {
eventId: string;
@@ -26,18 +26,14 @@ const useReposts = (propsProvider: () => UseRepostsProps): UseReposts => {
const query = createQuery(
genQueryKey,
({ queryKey, signal }) => {
const [, currentProps] = queryKey;
if (currentProps == null) return [];
const { eventId: mentionedEventId } = currentProps;
const task = new BatchedEventsTask({ type: 'Reposts', mentionedEventId });
const promise = task.toUpdatePromise().catch(() => []);
task.onUpdate((events) => {
queryClient.setQueryData(queryKey, events);
});
registerTask({ task, signal });
return timeout(15000, `useReposts: ${mentionedEventId}`)(promise);
},
eventsQuery({
taskProvider: ([, currentProps]) => {
if (currentProps == null) return null;
const { eventId: mentionedEventId } = currentProps;
return new BatchedEventsTask({ type: 'Reposts', mentionedEventId });
},
queryClient,
}),
{
staleTime: 1 * 60 * 1000, // 1 min
cacheTime: 4 * 60 * 60 * 1000, // 4 hour

View File

@@ -1,10 +1,12 @@
export class TimeoutError extends Error {}
const timeout =
(ms: number, info?: string) =>
<T>(promise: Promise<T>): Promise<T> => {
const timeoutPromise = new Promise<T>((_resolve, reject) => {
setTimeout(() => {
const message = info != null ? `TimeoutError: ${info}` : 'TimeoutError';
reject(new Error(message));
reject(new TimeoutError(message));
}, ms);
});