diff --git a/convex/builds.ts b/convex/builds.ts index 7077f66..5d1f902 100644 --- a/convex/builds.ts +++ b/convex/builds.ts @@ -58,17 +58,15 @@ function getR2ArtifactUrl(buildHash: string): string { return `https://${bucketName}.r2.cloudflarestorage.com/${buildHash}.uf2` } -export const triggerBuild = mutation({ +export const triggerFlash = mutation({ args: { profileId: v.id('profiles'), + target: v.string(), }, handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx) - if (!userId) throw new Error('Unauthorized') - const profile = await ctx.db.get(args.profileId) - if (!profile || profile.userId !== userId) { - throw new Error('Unauthorized') + if (!profile) { + throw new Error('Profile not found') } // Convert config object to flags string @@ -84,128 +82,125 @@ export const triggerBuild = mutation({ const flagsString = flags.join(' ') - // Get targets from profileTargets - const profileTargets = await ctx.db - .query('profileTargets') - .withIndex('by_profile', (q) => q.eq('profileId', args.profileId)) - .collect() + // Compute build hash + const buildHash = await computeBuildHash( + profile.version, + args.target, + flagsString + ) - const targets = profileTargets.map((pt) => pt.target) + // Check if build already exists with this hash + const existingBuild = await ctx.db + .query('builds') + .withIndex('by_hash', (q) => q.eq('buildHash', buildHash)) + .first() - // Create build records for each target - for (const target of targets) { - // Compute build hash using the generated flags - const buildHash = await computeBuildHash( - profile.version, - target, - flagsString - ) + let buildId: Id<'builds'> + let shouldDispatch = false - console.log( - `Computed build hash for ${target}: ${buildHash} (Flags: ${flagsString})` - ) + if (existingBuild) { + // Build already exists, use it + buildId = existingBuild._id + } else { + // Check cache for existing build + const cached = await ctx.db + .query('buildCache') + .withIndex('by_hash_target', (q) => + q.eq('buildHash', buildHash).eq('target', args.target) + ) + .first() - // Mutex logic: Check if build already exists with this hash - const build = await ctx.db + if (cached) { + // Use cached artifact, create build with success status + const artifactUrl = getR2ArtifactUrl(buildHash) + buildId = await ctx.db.insert('builds', { + target: args.target, + githubRunId: 0, + status: 'success', + artifactUrl: artifactUrl, + startedAt: Date.now(), + completedAt: Date.now(), + buildHash: buildHash, + }) + } else { + // Not cached, create new build and dispatch workflow + buildId = await ctx.db.insert('builds', { + target: args.target, + githubRunId: 0, + status: 'queued', + startedAt: Date.now(), + buildHash: buildHash, + }) + shouldDispatch = true + } + + // Handle race condition + const raceCheckBuild = await ctx.db .query('builds') .withIndex('by_hash', (q) => q.eq('buildHash', buildHash)) .first() - let buildId: Id<'builds'> - let shouldDispatch = false - - if (build) { - // Build already exists, use it - buildId = build._id - console.log(`Using existing build ${buildId} for hash ${buildHash}`) - } else { - // Check cache for existing build - const cached = await ctx.db - .query('buildCache') - .withIndex('by_hash_target', (q) => - q.eq('buildHash', buildHash).eq('target', target) - ) - .first() - - if (cached) { - // Use cached artifact, create build with success status - const artifactUrl = getR2ArtifactUrl(buildHash) - buildId = await ctx.db.insert('builds', { - target: target, - githubRunId: 0, - status: 'success', - artifactUrl: artifactUrl, - startedAt: Date.now(), - completedAt: Date.now(), - buildHash: buildHash, - }) - console.log(`Created cached build ${buildId} for hash ${buildHash}`) - } else { - // Not cached, create new build and dispatch workflow - buildId = await ctx.db.insert('builds', { - target: target, - githubRunId: 0, - status: 'queued', - startedAt: Date.now(), - buildHash: buildHash, - }) - shouldDispatch = true - console.log(`Created new build ${buildId} for hash ${buildHash}`) - } - - // Handle race condition: if another mutation created the build between our check and insert, - // query again to get the existing build (there might be duplicates, but we'll use the first one) - const existingBuild = await ctx.db - .query('builds') - .withIndex('by_hash', (q) => q.eq('buildHash', buildHash)) - .first() - - if (existingBuild && existingBuild._id !== buildId) { - // Another mutation created the build first, use that one instead - // Delete the duplicate we just created - await ctx.db.delete(buildId) - buildId = existingBuild._id - shouldDispatch = false - console.log( - `Race condition detected: using existing build ${existingBuild._id} instead of duplicate` - ) - } - } - - // Create or update profileBuild record - const existingProfileBuild = await ctx.db - .query('profileBuilds') - .withIndex('by_profile_target', (q) => - q.eq('profileId', args.profileId).eq('target', target) - ) - .first() - - if (existingProfileBuild) { - // Update existing profileBuild to point to the (possibly new) build - await ctx.db.patch(existingProfileBuild._id, { - buildId: buildId, - }) - } else { - // Create new profileBuild record - await ctx.db.insert('profileBuilds', { - profileId: args.profileId, - buildId: buildId, - target: target, - createdAt: Date.now(), - }) - } - - // Only dispatch GitHub workflow if build was newly created and not cached - if (shouldDispatch) { - await ctx.scheduler.runAfter(0, api.actions.dispatchGithubBuild, { - buildId: buildId, - target: target, - flags: flagsString, - version: profile.version, - buildHash: buildHash, - }) + if (raceCheckBuild && raceCheckBuild._id !== buildId) { + await ctx.db.delete(buildId) + buildId = raceCheckBuild._id + shouldDispatch = false } } + + // Create or get profileTarget + let profileTarget = await ctx.db + .query('profileTargets') + .withIndex('by_profile_target', (q) => + q.eq('profileId', args.profileId).eq('target', args.target) + ) + .first() + + if (!profileTarget) { + const newProfileTargetId = await ctx.db.insert('profileTargets', { + profileId: args.profileId, + target: args.target, + createdAt: Date.now(), + }) + const retrieved = await ctx.db.get(newProfileTargetId) + if (!retrieved) { + throw new Error('Failed to create profileTarget') + } + profileTarget = retrieved + } + + // Create or update profileBuild record + const existingProfileBuild = await ctx.db + .query('profileBuilds') + .withIndex('by_profile_target', (q) => + q.eq('profileId', args.profileId).eq('target', args.target) + ) + .first() + + if (existingProfileBuild) { + await ctx.db.patch(existingProfileBuild._id, { + buildId: buildId, + }) + } else { + await ctx.db.insert('profileBuilds', { + profileId: args.profileId, + buildId: buildId, + target: args.target, + createdAt: Date.now(), + }) + } + + // Dispatch GitHub workflow if needed + if (shouldDispatch) { + await ctx.scheduler.runAfter(0, api.actions.dispatchGithubBuild, { + buildId: buildId, + target: args.target, + flags: flagsString, + version: profile.version, + buildHash: buildHash, + }) + } + + return profileTarget._id }, }) diff --git a/convex/profiles.ts b/convex/profiles.ts index 0ef8b2c..ebfac4a 100644 --- a/convex/profiles.ts +++ b/convex/profiles.ts @@ -53,6 +53,31 @@ export const getTargets = query({ }, }) +export const getProfileTarget = query({ + args: { profileTargetId: v.id('profileTargets') }, + handler: async (ctx, args) => { + const profileTarget = await ctx.db.get(args.profileTargetId) + if (!profileTarget) return null + + // Get the associated build via profileBuilds + const profileBuild = await ctx.db + .query('profileBuilds') + .withIndex('by_profile_target', (q) => + q + .eq('profileId', profileTarget.profileId) + .eq('target', profileTarget.target) + ) + .first() + + const build = profileBuild ? await ctx.db.get(profileBuild.buildId) : null + + return { + profileTarget, + build, + } + }, +}) + export const getFlashCount = query({ args: { profileId: v.id('profiles') }, handler: async (ctx, args) => { diff --git a/src/App.tsx b/src/App.tsx index 37b3295..0e7ff08 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,10 @@ function App() { } /> } /> - } /> + } + /> } /> @@ -35,7 +38,10 @@ function App() { } /> } /> } /> - } /> + } + /> } /> diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 84f8820..3217d9a 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -11,7 +11,6 @@ import type { Doc, Id } from '../../convex/_generated/dataModel' export default function Dashboard() { const profiles = useQuery(api.profiles.list) - const triggerBuild = useMutation(api.builds.triggerBuild) const removeProfile = useMutation(api.profiles.remove) const [isEditing, setIsEditing] = useState(false) const [editingProfile, setEditingProfile] = useState | null>( @@ -28,19 +27,6 @@ export default function Dashboard() { setIsEditing(true) } - const handleBuild = async (profileId: Id<'profiles'>) => { - try { - await triggerBuild({ profileId }) - toast.success('Build started', { - description: 'Check the build status below.', - }) - } catch (error) { - toast.error('Build failed', { - description: String(error), - }) - } - } - const handleDelete = async ( profileId: Id<'profiles'>, profileName: string @@ -107,9 +93,6 @@ export default function Dashboard() { > Edit - + + + )}