Merge branch 'paginated-list' into 'main'

Paginate list events (kind 3, 10000)

See merge request soapbox-pub/ditto!343
This commit is contained in:
Alex Gleason 2024-05-31 20:37:33 +00:00
commit 97d629cf07
3 changed files with 53 additions and 8 deletions

View File

@ -16,7 +16,7 @@ const mutesController: AppController = async (c) => {
if (event10000) { if (event10000) {
const pubkeys = getTagSet(event10000.tags, 'p'); const pubkeys = getTagSet(event10000.tags, 'p');
return renderAccounts(c, [...pubkeys].reverse()); return renderAccounts(c, [...pubkeys]);
} else { } else {
return c.json([]); return c.json([]);
} }

View File

@ -138,8 +138,8 @@ async function parseBody(req: Request): Promise<unknown> {
/** Schema to parse pagination query params. */ /** Schema to parse pagination query params. */
const paginationSchema = z.object({ const paginationSchema = z.object({
since: z.coerce.number().optional().catch(undefined), since: z.coerce.number().nonnegative().optional().catch(undefined),
until: z.coerce.number().optional().catch(undefined), until: z.coerce.number().nonnegative().optional().catch(undefined),
limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)), limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)),
}); });
@ -179,6 +179,47 @@ function paginated(c: AppContext, events: NostrEvent[], entities: (Entity | unde
return c.json(results, 200, headers); return c.json(results, 200, headers);
} }
/** Query params for paginating a list. */
const listPaginationSchema = z.object({
offset: z.coerce.number().nonnegative().catch(0),
limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)),
});
/** Build HTTP Link header for paginating Nostr lists. */
function buildListLinkHeader(url: string, params: { offset: number; limit: number }): string | undefined {
const { origin } = Conf.url;
const { pathname, search } = new URL(url);
const { offset, limit } = params;
const next = new URL(pathname + search, origin);
const prev = new URL(pathname + search, origin);
next.searchParams.set('offset', String(offset + limit));
prev.searchParams.set('offset', String(Math.max(offset - limit, 0)));
next.searchParams.set('limit', String(limit));
prev.searchParams.set('limit', String(limit));
return `<${next}>; rel="next", <${prev}>; rel="prev"`;
}
/** paginate a list of tags. */
function paginatedList(
c: AppContext,
params: { offset: number; limit: number },
entities: (Entity | undefined)[],
headers: HeaderRecord = {},
) {
const link = buildListLinkHeader(c.req.url, params);
if (link) {
headers.link = link;
}
// Filter out undefined entities.
const results = entities.filter((entity): entity is Entity => Boolean(entity));
return c.json(results, 200, headers);
}
/** JSON-LD context. */ /** JSON-LD context. */
type LDContext = (string | Record<string, string | Record<string, string>>)[]; type LDContext = (string | Record<string, string | Record<string, string>>)[];
@ -209,8 +250,10 @@ export {
createAdminEvent, createAdminEvent,
createEvent, createEvent,
type EventStub, type EventStub,
listPaginationSchema,
localRequest, localRequest,
paginated, paginated,
paginatedList,
type PaginationParams, type PaginationParams,
paginationSchema, paginationSchema,
parseBody, parseBody,

View File

@ -4,7 +4,7 @@ import { AppContext } from '@/app.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { renderAccount } from '@/views/mastodon/accounts.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts';
import { renderStatus } from '@/views/mastodon/statuses.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts';
import { paginated, paginationSchema } from '@/utils/api.ts'; import { listPaginationSchema, paginated, paginatedList, paginationSchema } from '@/utils/api.ts';
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';
import { accountFromPubkey } from '@/views/mastodon/accounts.ts'; import { accountFromPubkey } from '@/views/mastodon/accounts.ts';
@ -42,12 +42,14 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], opts?:
return paginated(c, events, accounts); return paginated(c, events, accounts);
} }
async function renderAccounts(c: AppContext, authors: string[], signal = AbortSignal.timeout(1000)) { async function renderAccounts(c: AppContext, pubkeys: string[]) {
const { since, until, limit } = paginationSchema.parse(c.req.query()); const { offset, limit } = listPaginationSchema.parse(c.req.query());
const authors = pubkeys.reverse().slice(offset, offset + limit);
const store = await Storages.db(); const store = await Storages.db();
const signal = c.req.raw.signal;
const events = await store.query([{ kinds: [0], authors, since, until, limit }], { signal }) const events = await store.query([{ kinds: [0], authors }], { signal })
.then((events) => hydrateEvents({ events, store, signal })); .then((events) => hydrateEvents({ events, store, signal }));
const accounts = await Promise.all( const accounts = await Promise.all(
@ -61,7 +63,7 @@ async function renderAccounts(c: AppContext, authors: string[], signal = AbortSi
}), }),
); );
return paginated(c, events, accounts); return paginatedList(c, { offset, limit }, accounts);
} }
/** Render statuses by event IDs. */ /** Render statuses by event IDs. */