mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 15:11:29 -07:00
limit slideshow to 10 by default
This commit is contained in:
@@ -299,8 +299,15 @@ export function toImagesPlayer(img: SavedImage): ImagesPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Default number of recent images the slideshow feed returns. */
|
/** How many recent images the slideshow feed returns when the caller doesn't say. */
|
||||||
export const SLIDESHOW_LIMIT = 130
|
export const SLIDESHOW_LIMIT = 10
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The most a caller can ask the slideshow feed for. The endpoint is public and
|
||||||
|
* unauthenticated, so the cap is what keeps an arbitrary `take` from turning into a
|
||||||
|
* scan of the whole image table plus the two batched joins behind it.
|
||||||
|
*/
|
||||||
|
export const SLIDESHOW_MAX_LIMIT = 100
|
||||||
|
|
||||||
/** The slideshow projection of an image — creator username + room name joined in. */
|
/** The slideshow projection of an image — creator username + room name joined in. */
|
||||||
export interface SlideshowImage {
|
export interface SlideshowImage {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
getSlideshowImages,
|
getSlideshowImages,
|
||||||
SavedImageType,
|
SavedImageType,
|
||||||
setImageCheer,
|
setImageCheer,
|
||||||
|
SLIDESHOW_LIMIT,
|
||||||
|
SLIDESHOW_MAX_LIMIT,
|
||||||
toImagesPlayer,
|
toImagesPlayer,
|
||||||
} from '../images-db'
|
} from '../images-db'
|
||||||
import {
|
import {
|
||||||
@@ -312,6 +314,10 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
|||||||
// creator's username and room name. Public (no auth): it only surfaces already-public
|
// creator's username and room name. Public (no auth): it only surfaces already-public
|
||||||
// images and backs the anonymous homepage slideshow. Returns `{ Images, ValidTill }`,
|
// images and backs the anonymous homepage slideshow. Returns `{ Images, ValidTill }`,
|
||||||
// where ValidTill is a short (2-minute) cache hint the client refreshes against.
|
// where ValidTill is a short (2-minute) cache hint the client refreshes against.
|
||||||
|
// Serves 10 by default and never more than SLIDESHOW_MAX_LIMIT (100): it's public and
|
||||||
|
// unauthenticated, so an unclamped `take` would let anyone ask for the whole image
|
||||||
|
// table — and the callers that rotate one photo at a time (the website's hero) don't
|
||||||
|
// want a long feed anyway.
|
||||||
.get(
|
.get(
|
||||||
'/api/images/v1/slideshow',
|
'/api/images/v1/slideshow',
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -323,10 +329,21 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
|||||||
'Deliberately public — it surfaces only already-public images and backs the ' +
|
'Deliberately public — it surfaces only already-public images and backs the ' +
|
||||||
'anonymous homepage slideshow. `ValidTill` is a short (2-minute) cache hint the ' +
|
'anonymous homepage slideshow. `ValidTill` is a short (2-minute) cache hint the ' +
|
||||||
'client refreshes against.',
|
'client refreshes against.',
|
||||||
|
parameters: [
|
||||||
|
intQuery(
|
||||||
|
'take',
|
||||||
|
`How many photos to return (default ${SLIDESHOW_LIMIT}, capped at ${SLIDESHOW_MAX_LIMIT})`
|
||||||
|
),
|
||||||
|
],
|
||||||
responses: { 200: json(SlideshowResponse, 'The feed plus its cache hint') },
|
responses: { 200: json(SlideshowResponse, 'The feed plus its cache hint') },
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const Images = await getSlideshowImages(c.env.DB)
|
// Junk, zero and negative takes fall back to the default rather than 400ing or
|
||||||
|
// serving an empty stage — the caller is a homepage, and no photos reads as the
|
||||||
|
// server being down.
|
||||||
|
const asked = Number.parseInt(c.req.query('take') ?? '', 10)
|
||||||
|
const take = asked > 0 ? Math.min(asked, SLIDESHOW_MAX_LIMIT) : SLIDESHOW_LIMIT
|
||||||
|
const Images = await getSlideshowImages(c.env.DB, take)
|
||||||
const ValidTill = new Date(Date.now() + 2 * 60 * 1000).toISOString()
|
const ValidTill = new Date(Date.now() + 2 * 60 * 1000).toISOString()
|
||||||
return c.json({ Images, ValidTill })
|
return c.json({ Images, ValidTill })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1531,6 +1531,29 @@ describe('images', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// The feed is public and unauthenticated, so `take` is clamped rather than trusted:
|
||||||
|
// without the cap a single anonymous request could pull the whole image table through
|
||||||
|
// the two joins behind it.
|
||||||
|
test('GET /api/images/v1/slideshow serves 10 by default and caps take at 100', async () => {
|
||||||
|
// 120 public ShareCamera photos — more than both the default and the cap.
|
||||||
|
for (let i = 0; i < 120; i++) {
|
||||||
|
await createImage(env.DB, { imageName: `bulkslide${i}.jpg`, playerId: 42 })
|
||||||
|
}
|
||||||
|
const feed = async (query: string) => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/images/v1/slideshow${query}`)
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
return ((await res.json()) as { Images: unknown[] }).Images.length
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await feed('')).toBe(10)
|
||||||
|
expect(await feed('?take=25')).toBe(25)
|
||||||
|
expect(await feed('?take=500')).toBe(100)
|
||||||
|
// Junk and non-positive takes fall back rather than erroring or emptying the stage.
|
||||||
|
expect(await feed('?take=0')).toBe(10)
|
||||||
|
expect(await feed('?take=-5')).toBe(10)
|
||||||
|
expect(await feed('?take=lots')).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
test('POST /api/images/v1/cheer persists, syncs CheerCount, and the bulk lookup reflects it', async () => {
|
test('POST /api/images/v1/cheer persists, syncs CheerCount, and the bulk lookup reflects it', async () => {
|
||||||
// Seed an image to cheer.
|
// Seed an image to cheer.
|
||||||
// Its own player id: 700's photos are asserted on exactly in the player-list test.
|
// Its own player id: 700's photos are asserted on exactly in the player-list test.
|
||||||
|
|||||||
@@ -481,6 +481,14 @@ function NavBar({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many photos the hero asks the feed for. Explicit rather than left to the api's
|
||||||
|
* default, since the count is a design decision here: the stage rotates one photo every
|
||||||
|
* six seconds, so ten is a minute of it — long enough that a repeat visitor sees fresh
|
||||||
|
* photos, short enough that the arrows stay walkable and the payload stays small.
|
||||||
|
*/
|
||||||
|
const SLIDESHOW_TAKE = 10
|
||||||
|
|
||||||
/** A recent public image plus who took it and where. */
|
/** A recent public image plus who took it and where. */
|
||||||
interface Slide {
|
interface Slide {
|
||||||
url: string
|
url: string
|
||||||
@@ -507,7 +515,7 @@ function useSlideshow(config: SiteConfig | undefined) {
|
|||||||
// would take the page down instead of leaving an empty stage behind the fold.
|
// would take the page down instead of leaving an empty stage behind the fold.
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const h = where()
|
const h = where()
|
||||||
const d = await call<Feed>(`${h.api}/api/images/v1/slideshow`)
|
const d = await call<Feed>(`${h.api}/api/images/v1/slideshow?take=${SLIDESHOW_TAKE}`)
|
||||||
setSlides(
|
setSlides(
|
||||||
(d.Images ?? []).map((i) => ({
|
(d.Images ?? []).map((i) => ({
|
||||||
url: `${h.img}/${i.ImageName}`,
|
url: `${h.img}/${i.ImageName}`,
|
||||||
@@ -636,9 +644,9 @@ function Stage({
|
|||||||
{slide.roomName && ` in ${slide.roomName}`}
|
{slide.roomName && ` in ${slide.roomName}`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* Arrows and a count, not a dot per photo: the feed runs to SLIDESHOW_LIMIT
|
{/* Arrows and a count, not a dot per photo: a dot each is wide enough to
|
||||||
(130) images, and a dot each is both unusable and wide enough to shove
|
shove the headline's half of the split off the page, and it would have
|
||||||
the headline's half of the split off the page. */}
|
to be rebuilt the moment SLIDESHOW_TAKE grows. */}
|
||||||
{count > 1 && (
|
{count > 1 && (
|
||||||
<span className="steer">
|
<span className="steer">
|
||||||
<button onClick={() => step(-1)} aria-label="Previous photo">
|
<button onClick={() => step(-1)} aria-label="Previous photo">
|
||||||
|
|||||||
Reference in New Issue
Block a user