Skip to content
← Public packages

@kody/hn-pulse

Quick onboarding example: Hacker News top stories via Firebase HN API (zero OAuth).

src/index.ts

74 lines · 2.1 KB · TypeScript
export type TopStoriesInput = {
	/** How many top stories to return (1–30, default 10). */
	limit?: number
}

export type HnStory = {
	id: number
	title: string
	url: string | null
	by: string | null
	score: number | null
	descendants: number | null
}

function clampLimit(limit: unknown): number {
	const n = Number(limit)
	if (!Number.isFinite(n)) return 10
	return Math.min(30, Math.max(1, Math.floor(n)))
}

/**
 * Fetch current Hacker News top stories (Firebase HN API, no auth).
 * Prefer the `./getTopStories` export from callers.
 *
 * @param input.limit - How many top stories to return (1–30, default 10).
 * @returns Hydrated HN stories newest/top ranked.
 *
 * @example
 * import { getTopStories } from 'kody:@kody/hn-pulse/getTopStories'
 * const stories = await getTopStories({ limit: 10 })
 */
export async function getTopStories(
	input: TopStoriesInput = {},
): Promise<HnStory[]> {
	const limit = clampLimit(input?.limit)
	const idsRes = await fetch(
		'https://hacker-news.firebaseio.com/v0/topstories.json',
	)
	const ids = await idsRes.json().catch(() => null)
	if (!idsRes.ok || !Array.isArray(ids) || ids.length === 0) {
		throw new Error('Failed to load Hacker News topstories.json')
	}

	const slice = ids.slice(0, limit)
	const items = await Promise.all(
		slice.map(async (id: number) => {
			const res = await fetch(
				'https://hacker-news.firebaseio.com/v0/item/' + id + '.json',
			)
			const item = await res.json().catch(() => null)
			if (!res.ok || !item || typeof item !== 'object') {
				return null
			}
			const story: HnStory = {
				id: Number(item.id),
				title: String(item.title || '(untitled)'),
				url: item.url ? String(item.url) : null,
				by: item.by ? String(item.by) : null,
				score: Number.isFinite(Number(item.score)) ? Number(item.score) : null,
				descendants: Number.isFinite(Number(item.descendants))
					? Number(item.descendants)
					: null,
			}
			return story
		}),
	)

	return items.filter((story): story is HnStory => story !== null)
}

/** Root dispatcher for packages.invoke({ exportName: '.', params }). */
export default async function hnPulse(input: TopStoriesInput = {}) {
	return getTopStories(input)
}