mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Add paginationMiddleware, getting warmer
This commit is contained in:
parent
316e3e287f
commit
e2ce9d0dab
8 changed files with 126 additions and 65 deletions
|
|
@ -4,7 +4,7 @@
|
|||
"exports": {
|
||||
".": "./mod.ts",
|
||||
"./middleware": "./middleware/mod.ts",
|
||||
"./pagination": "./pagination.ts",
|
||||
"./pagination": "./pagination/mod.ts",
|
||||
"./routes": "./routes/mod.ts",
|
||||
"./schema": "./schema.ts",
|
||||
"./views": "./views/mod.ts"
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { paginationMiddleware } from './paginationMiddleware.ts';
|
||||
export { requireVar } from './requireVar.ts';
|
||||
export { userMiddleware } from './userMiddleware.ts';
|
||||
|
|
|
|||
80
packages/api/middleware/paginationMiddleware.ts
Normal file
80
packages/api/middleware/paginationMiddleware.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { paginated, paginatedList, paginationSchema } from '@ditto/api/pagination';
|
||||
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import type { DittoMiddleware } from '../DittoMiddleware.ts';
|
||||
|
||||
interface Pagination {
|
||||
since?: number;
|
||||
until?: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
interface ListPagination {
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
type HeaderRecord = Record<string, string | string[]>;
|
||||
type PaginateFn = (events: NostrEvent[], body: object | unknown[], headers?: HeaderRecord) => Response;
|
||||
type ListPaginateFn = (params: ListPagination, body: object | unknown[], headers?: HeaderRecord) => Response;
|
||||
|
||||
/** Fixes compatibility with Mastodon apps by that don't use `Link` headers. */
|
||||
// @ts-ignore Types are right.
|
||||
export function paginationMiddleware(): DittoMiddleware<{ pagination: Pagination; paginate: PaginateFn }>;
|
||||
export function paginationMiddleware(
|
||||
type: 'list',
|
||||
): DittoMiddleware<{ pagination: ListPagination; paginate: ListPaginateFn }>;
|
||||
export function paginationMiddleware(
|
||||
type?: string,
|
||||
): DittoMiddleware<{ pagination?: Pagination | ListPagination; paginate: PaginateFn | ListPaginateFn }> {
|
||||
return async (c, next) => {
|
||||
const { relay } = c.var;
|
||||
|
||||
const pagination = paginationSchema.parse(c.req.query());
|
||||
|
||||
const {
|
||||
max_id: maxId,
|
||||
min_id: minId,
|
||||
since,
|
||||
until,
|
||||
} = pagination;
|
||||
|
||||
if ((maxId && !until) || (minId && !since)) {
|
||||
const ids: string[] = [];
|
||||
|
||||
if (maxId) ids.push(maxId);
|
||||
if (minId) ids.push(minId);
|
||||
|
||||
if (ids.length) {
|
||||
const events = await relay.query(
|
||||
[{ ids, limit: ids.length }],
|
||||
{ signal: c.req.raw.signal },
|
||||
);
|
||||
|
||||
for (const event of events) {
|
||||
if (!until && maxId === event.id) pagination.until = event.created_at;
|
||||
if (!since && minId === event.id) pagination.since = event.created_at;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'list') {
|
||||
c.set('pagination', {
|
||||
limit: pagination.limit,
|
||||
offset: pagination.offset,
|
||||
});
|
||||
const fn: ListPaginateFn = (params, body, headers) => paginatedList(c, params, body, headers);
|
||||
c.set('paginate', fn);
|
||||
} else {
|
||||
c.set('pagination', {
|
||||
since: pagination.since,
|
||||
until: pagination.until,
|
||||
limit: pagination.limit,
|
||||
});
|
||||
const fn: PaginateFn = (events, body, headers) => paginated(c, events, body, headers);
|
||||
c.set('paginate', fn);
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { buildLinkHeader, buildListLinkHeader } from './link-header.ts';
|
||||
export { paginated, paginatedList } from './paginate.ts';
|
||||
export { paginationSchema } from './schema.ts';
|
||||
23
packages/api/pagination/schema.test.ts
Normal file
23
packages/api/pagination/schema.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { assertEquals } from '@std/assert';
|
||||
|
||||
import { paginationSchema } from './schema.ts';
|
||||
|
||||
Deno.test('paginationSchema', () => {
|
||||
const pagination = paginationSchema.parse({
|
||||
limit: '10',
|
||||
offset: '20',
|
||||
max_id: '1',
|
||||
min_id: '2',
|
||||
since: '3',
|
||||
until: '4',
|
||||
});
|
||||
|
||||
assertEquals(pagination, {
|
||||
limit: 10,
|
||||
offset: 20,
|
||||
max_id: '1',
|
||||
min_id: '2',
|
||||
since: 3,
|
||||
until: 4,
|
||||
});
|
||||
});
|
||||
|
|
@ -1,18 +1,21 @@
|
|||
import { DittoRoute } from '@ditto/api';
|
||||
import { userMiddleware } from '@ditto/api/middleware';
|
||||
import { paginationMiddleware, userMiddleware } from '@ditto/api/middleware';
|
||||
import { booleanParamSchema, languageSchema } from '@ditto/api/schema';
|
||||
import { getTagSet } from '@ditto/utils/tags';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { Context } from '@hono/hono';
|
||||
import type { NostrFilter } from '@nostrify/nostrify';
|
||||
import type { DittoEnv } from '../DittoEnv.ts';
|
||||
|
||||
const route = new DittoRoute().use(userMiddleware({ privileged: false, required: true }));
|
||||
const route = new DittoRoute().use(paginationMiddleware());
|
||||
|
||||
const homeQuerySchema = z.object({
|
||||
exclude_replies: booleanParamSchema.optional(),
|
||||
only_media: booleanParamSchema.optional(),
|
||||
});
|
||||
|
||||
route.get('/home', async (c) => {
|
||||
route.get('/home', userMiddleware({ privileged: false, required: true }), async (c) => {
|
||||
const { user, pagination } = c.var;
|
||||
|
||||
const pubkey = await user?.signer.getPublicKey()!;
|
||||
|
|
@ -50,7 +53,7 @@ const publicQuerySchema = z.object({
|
|||
language: languageSchema.optional(),
|
||||
});
|
||||
|
||||
const publicTimelineController: AppController = (c) => {
|
||||
route.get('/public', (c) => {
|
||||
const { conf, pagination } = c.var;
|
||||
const result = publicQuerySchema.safeParse(c.req.query());
|
||||
|
||||
|
|
@ -79,30 +82,29 @@ const publicTimelineController: AppController = (c) => {
|
|||
}
|
||||
|
||||
return renderStatuses(c, [filter]);
|
||||
};
|
||||
});
|
||||
|
||||
const hashtagTimelineController: AppController = (c) => {
|
||||
route.get('/tag/:hashtag', (c) => {
|
||||
const { pagination } = c.var;
|
||||
const hashtag = c.req.param('hashtag')!.toLowerCase();
|
||||
const hashtag = c.req.param('hashtag').toLowerCase();
|
||||
|
||||
return renderStatuses(c, [{ kinds: [1, 20], '#t': [hashtag], ...pagination }]);
|
||||
};
|
||||
});
|
||||
|
||||
const suggestedTimelineController: AppController = async (c) => {
|
||||
const { conf } = c.var;
|
||||
const store = c.get('store');
|
||||
const params = c.get('pagination');
|
||||
route.get('/suggested', async (c) => {
|
||||
const { conf, relay, pagination } = c.var;
|
||||
|
||||
const [follows] = await store.query(
|
||||
const [follows] = await relay.query(
|
||||
[{ kinds: [3], authors: [conf.pubkey], limit: 1 }],
|
||||
);
|
||||
|
||||
const authors = [...getTagSet(follows?.tags ?? [], 'p')];
|
||||
|
||||
return renderStatuses(c, [{ authors, kinds: [1, 20], ...params }]);
|
||||
};
|
||||
return renderStatuses(c, [{ authors, kinds: [1, 20], ...pagination }]);
|
||||
});
|
||||
|
||||
/** Render statuses for timelines. */
|
||||
async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
|
||||
async function renderStatuses(c: Context<DittoEnv>, filters: NostrFilter[]) {
|
||||
const { conf, store, user, signal } = c.var;
|
||||
const opts = { signal, timeout: conf.db.timeouts.timelines };
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import { AppMiddleware } from '@/app.ts';
|
||||
import { paginationSchema } from '@/schemas/pagination.ts';
|
||||
|
||||
/** Fixes compatibility with Mastodon apps by that don't use `Link` headers. */
|
||||
export const paginationMiddleware: AppMiddleware = async (c, next) => {
|
||||
const { store } = c.var;
|
||||
|
||||
const pagination = paginationSchema.parse(c.req.query());
|
||||
|
||||
const {
|
||||
max_id: maxId,
|
||||
min_id: minId,
|
||||
since,
|
||||
until,
|
||||
} = pagination;
|
||||
|
||||
if ((maxId && !until) || (minId && !since)) {
|
||||
const ids: string[] = [];
|
||||
|
||||
if (maxId) ids.push(maxId);
|
||||
if (minId) ids.push(minId);
|
||||
|
||||
if (ids.length) {
|
||||
const events = await store.query(
|
||||
[{ ids, limit: ids.length }],
|
||||
{ signal: c.req.raw.signal },
|
||||
);
|
||||
|
||||
for (const event of events) {
|
||||
if (!until && maxId === event.id) pagination.until = event.created_at;
|
||||
if (!since && minId === event.id) pagination.since = event.created_at;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.set('pagination', {
|
||||
since: pagination.since,
|
||||
until: pagination.until,
|
||||
limit: pagination.limit,
|
||||
});
|
||||
|
||||
c.set('listPagination', {
|
||||
limit: pagination.limit,
|
||||
offset: pagination.offset,
|
||||
});
|
||||
|
||||
await next();
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue