trails/apps/journal/app/routes/sync.import.$provider.server.ts
Ullrich Schäfer 9c6407423a
fix(journal): remove ineffective dynamic imports
Rollup was warning on 5 modules that were both dynamically and statically
imported. With static importers in the same chunk, the dynamic forms
buy no chunking benefit — they were leftovers from earlier
cycle-avoidance workarounds that no longer apply.

Converted to static:
- @trails-cool/gpx (routes.\$id.server.ts, sync.import.\$provider.server.ts)
- logger.server (boss.server.ts — comment claimed test cycles, but tests pass)
- boss.server (activities.server.ts at two sites)
- connected-services/manager (komoot/importer.ts, wahoo/importer.ts)
- notifications.server (root.tsx)

Kept dynamic: fit-file-parser in sync.import.\$provider.server.ts — it's
heavy and only the FIT ingestion path needs it, no other static
importers exist, so the dynamic actually does chunk-split it.

Build is now warning-free.

Full repo: pnpm typecheck, pnpm lint, pnpm test, pnpm build all green.
192 tests passed (up from 181 — rate-limit test from #424 + others).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:05:08 +02:00

121 lines
4.6 KiB
TypeScript

// Server-only loader/action for /sync/import/:provider. See `home.server.ts`.
import { data, redirect } from "react-router";
import { requireSessionUser } from "~/lib/auth/session.server";
import {
getManifest,
getService,
capabilityContextFor,
} from "~/lib/connected-services";
import { getImportedIds, recordImport } from "~/lib/sync/imports.server";
import { createActivity } from "~/lib/activities.server";
import { generateGpx } from "@trails-cool/gpx";
export async function loadSyncImportProvider(request: Request, provider: string | undefined) {
const user = await requireSessionUser(request);
const manifest = getManifest(provider ?? "");
if (!manifest || !manifest.importer) {
throw data({ error: "Unknown provider" }, { status: 404 });
}
const service = await getService(user.id, manifest.id);
if (!service) throw redirect("/settings");
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") ?? "1");
const ctx = capabilityContextFor(service.id);
const workoutList = await manifest.importer.listImportable(ctx, page);
const importedIds = await getImportedIds(
user.id,
manifest.id,
workoutList.workouts.map((w) => w.id),
);
return {
provider: { id: manifest.id, name: manifest.displayName },
workouts: workoutList.workouts.map((w) => ({
...w,
imported: importedIds.has(w.id),
})),
page: workoutList.page,
totalPages: Math.ceil(workoutList.total / workoutList.perPage),
};
}
export async function syncImportProviderAction(request: Request, provider: string | undefined) {
const user = await requireSessionUser(request);
const manifest = getManifest(provider ?? "");
if (!manifest || !manifest.importer) {
throw data({ error: "Unknown provider" }, { status: 404 });
}
const service = await getService(user.id, manifest.id);
if (!service) return redirect("/settings");
const formData = await request.formData();
const workoutId = formData.get("workoutId") as string;
const workoutName = formData.get("workoutName") as string;
const fileUrl = formData.get("fileUrl") as string;
const startedAt = formData.get("startedAt") as string;
const distance = formData.get("distance") as string;
const duration = formData.get("duration") as string;
// Inline import here (not via Importer.importOne) so we can pass through
// the form-supplied metadata (started_at, distance, duration) the
// capability seam doesn't carry. The seam-shape importer is reserved for
// automatic / webhook-driven imports.
let gpx: string | null = null;
if (fileUrl) {
const ctx = capabilityContextFor(service.id);
// Wahoo CDN URLs are pre-signed; we still go through withFreshCredentials
// so the manager handles refresh of any near-expired token.
const buffer = await ctx.withFreshCredentials(async () => {
const resp = await fetch(fileUrl);
if (!resp.ok) throw new Error(`Download failed: ${resp.status}`);
return Buffer.from(await resp.arrayBuffer());
});
// Lazy-load fit-file-parser only — it's heavy and only the FIT
// ingestion path needs it. @trails-cool/gpx is already pulled in
// statically by other modules in the chunk, so the dynamic import
// there was ineffective.
const { default: FitParser } = await import("fit-file-parser");
const parsed = await new Promise<Record<string, unknown>>((resolve, reject) => {
const parser = new FitParser({ force: true });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parser.parse(buffer as any, (err: unknown, d: any) => (err ? reject(err) : resolve(d ?? {})));
});
const records = (parsed.records ?? []) as Array<{
position_lat?: number;
position_long?: number;
altitude?: number;
timestamp?: string | Date;
}>;
const trackPoints = records
.filter((r) => r.position_lat != null && r.position_long != null)
.map((r) => ({
lat: r.position_lat!,
lon: r.position_long!,
ele: r.altitude,
time: r.timestamp instanceof Date ? r.timestamp.toISOString() : r.timestamp,
}));
if (trackPoints.length >= 2) {
gpx = generateGpx({ name: workoutName, tracks: [trackPoints] });
}
}
const activityId = await createActivity(user.id, {
name: workoutName || `${manifest.displayName} workout`,
gpx: gpx ?? undefined,
distance: distance ? parseFloat(distance) : null,
duration: duration ? parseInt(duration) : null,
startedAt: startedAt ? new Date(startedAt) : null,
});
await recordImport(user.id, manifest.id, workoutId, activityId);
return { imported: workoutId };
}