Refactor API data loading to utilize promises for streaming responses in SvelteKit. Update recent bounds calculation for date ranges and enhance loading indicators in package and search pages for improved user experience.

This commit is contained in:
Luke Hagar
2025-08-14 16:59:15 -05:00
parent 94c4c9ad06
commit a42d5a1d06
8 changed files with 158 additions and 140 deletions

View File

@@ -70,11 +70,14 @@ function getRecentBounds(category: string) {
const today = new Date();
let start = new Date(today);
if (category === 'day') {
// include today
// For day, use yesterday since today's data isn't available yet
start = new Date(today.getTime() - 24 * 60 * 60 * 1000);
} else if (category === 'week') {
start = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
// For week, use last 8 days (7 + 1 extra day)
start = new Date(today.getTime() - 8 * 24 * 60 * 60 * 1000);
} else if (category === 'month') {
start = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
// For month, use last 31 days (30 + 1 extra day)
start = new Date(today.getTime() - 31 * 24 * 60 * 60 * 1000);
}
return { start };
}

View File

@@ -0,0 +1,15 @@
<script lang="ts">
interface Props {
size?: 'sm' | 'md' | 'lg';
text?: string;
}
let { size = 'md', text }: Props = $props();
</script>
<div class="flex items-center justify-center gap-2">
<div class="animate-spin rounded-full border-2 border-gray-600 border-t-blue-500 {size === 'sm' ? 'h-4 w-4' : size === 'lg' ? 'h-8 w-8' : 'h-6 w-6'}"></div>
{#if text}
<span class="text-sm text-gray-400">{text}</span>
{/if}
</div>

View File

@@ -2,22 +2,13 @@ import { getPackageCount, getPopularPackages } from '$lib/api.js';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
try {
// Count distinct packages that have any saved data in DB (recent/month as proxy)
const [packageCount, popular] = await Promise.all([
getPackageCount(),
getPopularPackages(10, 30)
]);
return {
packageCount,
popular
};
} catch (error) {
console.error('Error loading page data:', error);
return {
packageCount: 0,
popular: []
};
}
// Return promises directly for streaming - SvelteKit will handle the streaming
const packageCountP = getPackageCount();
const popularP = getPopularPackages(10, 30);
return {
packageCount: packageCountP,
popular: popularP
};
};

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import type { PageData } from './$types';
import LoadingSpinner from '$lib/components/LoadingSpinner.svelte';
const { data } = $props<{ data: PageData }>();
@@ -41,7 +42,7 @@
</div>
<div class="mt-8 text-sm text-gray-400">
Tracking {data.packageCount && data.packageCount > 0 ? data.packageCount.toLocaleString() : '0'} packages
Tracking {#await data.packageCount}...{:then count}{count !== undefined ? count.toLocaleString() : '0'}{:catch}0{/await} packages
</div>
</div>
@@ -50,18 +51,24 @@
<div class="rounded-lg border border-gray-800 bg-gray-900 p-6 shadow-sm">
<h3 class="mb-2 text-lg font-semibold text-gray-100">Popular Packages (last 30 days)</h3>
<p class="mb-4 text-gray-400">Top projects by downloads (without mirrors)</p>
{#if data.popular && data.popular.length > 0}
<ul class="divide-y divide-gray-800">
{#each data.popular as row}
<li class="py-2 flex items-center justify-between">
<a class="font-medium text-blue-400 hover:text-blue-300" href="/packages/{row.package}" data-sveltekit-preload-data="off">{row.package}</a>
<span class="text-sm text-gray-400">{row.downloads.toLocaleString()}</span>
</li>
{/each}
</ul>
{:else}
<div class="text-sm text-gray-400">No data yet.</div>
{/if}
{#await data.popular}
<div class="text-sm text-gray-400"><LoadingSpinner size="sm" text="Loading popular packages..." /></div>
{:then popular}
{#if popular && popular.length > 0}
<ul class="divide-y divide-gray-800">
{#each popular as row}
<li class="py-2 flex items-center justify-between">
<a class="font-medium text-blue-400 hover:text-blue-300" href="/packages/{row.package}" data-sveltekit-preload-data="off">{row.package}</a>
<span class="text-sm text-gray-400">{row.downloads.toLocaleString()}</span>
</li>
{/each}
</ul>
{:else}
<div class="text-sm text-gray-400">No data yet.</div>
{/if}
{:catch}
<div class="text-sm text-gray-400">Failed to load.</div>
{/await}
</div>
<div class="rounded-lg border border-gray-800 bg-gray-900 p-6 shadow-sm">

View File

@@ -9,7 +9,6 @@ import {
getVersionDownloads
} from '$lib/api.js';
import type { PageServerLoad } from './$types';
// Streaming responses: use native promises in returned object (SvelteKit will stream)
export const load: PageServerLoad = async ({ params }) => {
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
@@ -17,73 +16,63 @@ export const load: PageServerLoad = async ({ params }) => {
if (!packageName || packageName === '__all__') {
return {
packageName,
recentStats: null,
overallStats: [],
pythonMajorStats: [],
pythonMinorStats: [],
systemStats: []
recentStats: Promise.resolve(null),
overallStats: Promise.resolve([]),
pythonMajorStats: Promise.resolve([]),
pythonMinorStats: Promise.resolve([]),
systemStats: Promise.resolve([]),
summaryTotals: Promise.resolve({})
};
}
try {
const recentStatsP = getRecentDownloads(packageName).then((recent) => {
const fm: Record<string, number> = {};
for (const stat of recent) fm[`last_${stat.category}`] = Number(stat.downloads);
return fm;
});
// Return promises directly for streaming - SvelteKit will handle the streaming
const recentStatsP = getRecentDownloads(packageName).then((recent) => {
const fm: Record<string, number> = {};
for (const stat of recent) fm[`last_${stat.category}`] = Number(stat.downloads);
return fm;
});
const overallStatsP = getOverallDownloads(packageName);
const pythonMajorStatsP = getPythonMajorDownloads(packageName);
const pythonMinorStatsP = getPythonMinorDownloads(packageName);
const systemStatsP = getSystemDownloads(packageName);
const installerStatsP = getInstallerDownloads(packageName);
const versionStatsP = getVersionDownloads(packageName);
const metaP = getPackageMetadata(packageName);
const overallStatsP = getOverallDownloads(packageName);
const pythonMajorStatsP = getPythonMajorDownloads(packageName);
const pythonMinorStatsP = getPythonMinorDownloads(packageName);
const systemStatsP = getSystemDownloads(packageName);
const installerStatsP = getInstallerDownloads(packageName);
const versionStatsP = getVersionDownloads(packageName);
const metaP = getPackageMetadata(packageName);
const summaryTotalsP = Promise.all([
overallStatsP,
pythonMajorStatsP,
pythonMinorStatsP,
systemStatsP,
installerStatsP,
versionStatsP
]).then(([overall, pyMaj, pyMin, system, installer, version]) => {
const sum = <T extends { category: string; downloads: any }>(rows: T[]) => {
const map: Record<string, number> = {};
for (const r of rows) map[r.category] = (map[r.category] || 0) + Number(r.downloads);
return map;
};
return {
overall: sum(overall),
python_major: sum(pyMaj),
python_minor: sum(pyMin),
system: sum(system),
installer: sum(installer),
version: sum(version)
}
});
return {
packageName,
meta: metaP,
recentStats: recentStatsP,
overallStats: overallStatsP,
pythonMajorStats: pythonMajorStatsP,
pythonMinorStats: pythonMinorStatsP,
systemStats: systemStatsP,
summaryTotals: summaryTotalsP
const summaryTotalsP = Promise.all([
overallStatsP,
pythonMajorStatsP,
pythonMinorStatsP,
systemStatsP,
installerStatsP,
versionStatsP
]).then(([overall, pyMaj, pyMin, system, installer, version]) => {
const sum = <T extends { category: string; downloads: any }>(rows: T[]) => {
const map: Record<string, number> = {};
for (const r of rows) map[r.category] = (map[r.category] || 0) + Number(r.downloads);
return map;
};
} catch (error) {
console.error('Error loading package data:', error);
return {
packageName,
recentStats: null,
overallStats: [],
pythonMajorStats: [],
pythonMinorStats: [],
systemStats: []
};
}
overall: sum(overall),
python_major: sum(pyMaj),
python_minor: sum(pyMin),
system: sum(system),
installer: sum(installer),
version: sum(version)
}
});
return {
packageName,
meta: metaP,
recentStats: recentStatsP,
overallStats: overallStatsP,
pythonMajorStats: pythonMajorStatsP,
pythonMinorStats: pythonMinorStatsP,
systemStats: systemStatsP,
summaryTotals: summaryTotalsP
};
};

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import type { PageData } from './$types';
import { onMount, onDestroy } from 'svelte';
import LoadingSpinner from '$lib/components/LoadingSpinner.svelte';
const { data }: { data: PageData } = $props();
let overallCanvas: HTMLCanvasElement | null = $state(null);
@@ -290,7 +291,7 @@
</thead>
<tbody class="divide-y divide-gray-800 bg-gray-950">
{#await data.recentStats}
<tr><td class="px-6 py-3 text-sm text-gray-400" colspan="2">Loading</td></tr>
<tr><td class="px-6 py-3 text-sm text-gray-400" colspan="2"><LoadingSpinner size="sm" text="Loading recent stats..." /></td></tr>
{:then rs}
{#each [['week', Number((rs as any)?.last_week || 0)], ['month', Number((rs as any)?.last_month || 0)]] as [period, count]}
<tr>
@@ -328,7 +329,7 @@
</thead>
<tbody class="divide-y divide-gray-800 bg-gray-950">
{#await (data as any).summaryTotals}
<tr><td class="px-6 py-3 text-sm text-gray-400" colspan="2">Loading</td></tr>
<tr><td class="px-6 py-3 text-sm text-gray-400" colspan="2"><LoadingSpinner size="sm" text="Loading overall stats..." /></td></tr>
{:then totals}
{#each Object.entries(totals?.overall || {}).sort((a, b) => Number(b[1]) - Number(a[1])) as [k, v]}
<tr>
@@ -368,7 +369,7 @@
</thead>
<tbody class="divide-y divide-gray-800 bg-gray-950">
{#await (data as any).summaryTotals}
<tr><td class="px-6 py-3 text-sm text-gray-400" colspan="2">Loading</td></tr>
<tr><td class="px-6 py-3 text-sm text-gray-400" colspan="2"><LoadingSpinner size="sm" text="Loading system stats..." /></td></tr>
{:then totals}
{#each Object.entries(totals?.system || {}).sort((a, b) => Number(b[1]) - Number(a[1])) as [k, v]}
<tr>
@@ -408,7 +409,7 @@
</thead>
<tbody class="divide-y divide-gray-800 bg-gray-950">
{#await (data as any).summaryTotals}
<tr><td class="px-6 py-3 text-sm text-gray-400" colspan="2">Loading</td></tr>
<tr><td class="px-6 py-3 text-sm text-gray-400" colspan="2"><LoadingSpinner size="sm" text="Loading Python versions..." /></td></tr>
{:then totals}
{#each buildPythonVersionRows(totals?.python_major || {}, totals?.python_minor || {}) as row}
<tr class={row.kind === 'major' ? 'bg-gray-900' : ''}>

View File

@@ -5,24 +5,18 @@ export const load = async ({ url }) => {
if (!searchTerm) {
return {
packages: [],
packages: Promise.resolve([]),
searchTerm: null
};
}
try {
const packages = await searchPackages(searchTerm);
return {
packages,
searchTerm
};
} catch (error) {
console.error('Error searching packages:', error);
return {
packages: [],
searchTerm
};
}
// Return promise directly for streaming - SvelteKit will handle the streaming
const packagesP = searchPackages(searchTerm);
return {
packages: packagesP,
searchTerm
};
};

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import type { PageData } from './$types';
import LoadingSpinner from '$lib/components/LoadingSpinner.svelte';
const { data } = $props<{ data: PageData }>();
let searchTerm = $state(data.searchTerm ?? '');
</script>
@@ -10,7 +11,7 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="max-w-2xl mx-auto">
<h1 class="text-3xl font-bold text-gray-900 mb-8">Search Packages</h1>
<h1 class="text-3xl font-bold text-gray-100 mb-8">Search Packages</h1>
<!-- Search Form -->
<form method="GET" action="/search" class="mb-8">
@@ -20,7 +21,7 @@
name="q"
bind:value={searchTerm}
placeholder="Enter package name..."
class="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="flex-1 px-4 py-2 border border-gray-700 bg-gray-900 text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
<button
@@ -32,35 +33,52 @@
</div>
</form>
{#if data.packages && data.packages.length > 0}
<div class="bg-white rounded-lg shadow-sm border">
<div class="px-6 py-4 border-b">
<h2 class="text-lg font-semibold text-gray-900">
Found {data.packages.length} package{data.packages.length === 1 ? '' : 's'}
</h2>
{#if data.searchTerm}
{#await data.packages}
<div class="text-center py-12">
<div class="text-gray-400">
<LoadingSpinner size="lg" text="Searching packages..." />
</div>
</div>
<div class="divide-y divide-gray-200">
{#each data.packages as pkg}
<div class="px-6 py-4 hover:bg-gray-50">
<a href="/packages/{pkg}" class="block" data-sveltekit-preload-data="off">
<div class="text-lg font-medium text-blue-600 hover:text-blue-800">
{pkg}
</div>
<div class="text-sm text-gray-500">
View download statistics
</div>
</a>
{:then packages}
{#if packages && packages.length > 0}
<div class="bg-gray-900 rounded-lg shadow-sm border border-gray-800">
<div class="px-6 py-4 border-b border-gray-800">
<h2 class="text-lg font-semibold text-gray-100">
Found {packages.length} package{packages.length === 1 ? '' : 's'}
</h2>
</div>
{/each}
<div class="divide-y divide-gray-800">
{#each packages as pkg}
<div class="px-6 py-4 hover:bg-gray-950">
<a href="/packages/{pkg}" class="block" data-sveltekit-preload-data="off">
<div class="text-lg font-medium text-blue-400 hover:text-blue-300">
{pkg}
</div>
<div class="text-sm text-gray-400">
View download statistics
</div>
</a>
</div>
{/each}
</div>
</div>
{:else}
<div class="text-center py-12">
<div class="text-gray-400">
<p class="text-lg mb-2">No packages found</p>
<p class="text-sm">Try searching for a different package name</p>
</div>
</div>
{/if}
{:catch}
<div class="text-center py-12">
<div class="text-red-400">
<p class="text-lg mb-2">Search failed</p>
<p class="text-sm">Please try again</p>
</div>
</div>
</div>
{:else if data.searchTerm}
<div class="text-center py-12">
<div class="text-gray-500">
<p class="text-lg mb-2">No packages found</p>
<p class="text-sm">Try searching for a different package name</p>
</div>
</div>
{/await}
{/if}
</div>
</div>