diff --git a/recipes/compiler.tsx b/packages/patterns/compiler.tsx similarity index 81% rename from recipes/compiler.tsx rename to packages/patterns/compiler.tsx index c3cd0f1f2..d14d9588d 100644 --- a/recipes/compiler.tsx +++ b/packages/patterns/compiler.tsx @@ -28,25 +28,28 @@ const updateCode = handler< ); const visit = handler< - { detail: { value: string } }, - { code: string } + unknown, + { result: Cell } >( - (_, state) => { - const { result } = compileAndRun({ - files: [{ name: "/main.tsx", contents: state.code }], - main: "/main.tsx", - }); - - console.log("result", result); - + (_, { result }) => { + console.log("visit: navigating to compiled result", result); return navigateTo(result); }, ); +const handleEditContent = handler< + { code: string }, + { code: Cell } +>( + (event, { code }) => { + code.set(event.code); + }, +); + export default recipe( "Compiler", ({ code }) => { - const { error, errors } = compileAndRun({ + const { result, error, errors } = compileAndRun({ files: [{ name: "/main.tsx", contents: code }], main: "/main.tsx", }); @@ -63,9 +66,9 @@ export default recipe( /> {ifElse( error, - fix the errors, + fix the error: {error}, Navigate To Charm , @@ -73,6 +76,8 @@ export default recipe( ), code, + updateCode: handleEditContent({ code }), + visit: visit({ result }), }; }, ); diff --git a/packages/runner/src/builtins/compile-and-run.ts b/packages/runner/src/builtins/compile-and-run.ts index 231eebc88..ef663b926 100644 --- a/packages/runner/src/builtins/compile-and-run.ts +++ b/packages/runner/src/builtins/compile-and-run.ts @@ -28,12 +28,13 @@ import { CompilerError } from "@commontools/js-runtime/typescript"; export function compileAndRun( inputsCell: Cell>, sendResult: (tx: IExtendedStorageTransaction, result: any) => void, - _addCancel: (cancel: () => void) => void, + addCancel: (cancel: () => void) => void, cause: any, parentCell: Cell, runtime: IRuntime, ): Action { - let currentRun = 0; + let requestId: string | undefined = undefined; + let abortController: AbortController | undefined = undefined; let previousCallHash: string | undefined = undefined; let cellsInitialized = false; let pending: Cell; @@ -52,6 +53,12 @@ export function compileAndRun( | undefined >; + // This is called when the recipe containing this node is being stopped. + addCancel(() => { + // Abort any in-flight compilation if it's still pending. + abortController?.abort("Recipe stopped"); + }); + return (tx: IExtendedStorageTransaction) => { if (!cellsInitialized) { pending = runtime.getCell( @@ -97,7 +104,7 @@ export function compileAndRun( sendResult(tx, { pending, result, error, errors }); cellsInitialized = true; } - const thisRun = ++currentRun; + const pendingWithLog = pending.withTx(tx); const resultWithLog = result.withTx(tx); const errorWithLog = error.withTx(tx); @@ -135,6 +142,11 @@ export function compileAndRun( if (hash === previousCallHash) return; previousCallHash = hash; + // Abort any in-flight compilation before starting a new one + abortController?.abort("New compilation started"); + abortController = new AbortController(); + requestId = crypto.randomUUID(); + runtime.runner.stop(result); resultWithLog.set(undefined); errorWithLog.set(undefined); @@ -156,10 +168,15 @@ export function compileAndRun( // Now we're sure that we have a new file to compile pendingWithLog.set(true); + // Capture requestId for this compilation run + const thisRequestId = requestId; + const compilePromise = runtime.harness.run(program) .catch( (err) => { - if (thisRun !== currentRun) return; + // Only process this error if the request hasn't been superseded + if (requestId !== thisRequestId) return; + if (abortController?.signal.aborted) return; runtime.editWithRetry((asyncTx) => { // Extract structured errors if this is a CompilerError @@ -180,7 +197,9 @@ export function compileAndRun( }); }, ).finally(() => { - if (thisRun !== currentRun) return; + // Only update pending if this is still the current request + if (requestId !== thisRequestId) return; + // Always clear pending state, even if cancelled, to avoid stuck state runtime.editWithRetry((asyncTx) => { pending.withTx(asyncTx).set(false); @@ -188,7 +207,10 @@ export function compileAndRun( }); compilePromise.then((recipe) => { - if (thisRun !== currentRun) return; + // Only run the result if this is still the current request + if (requestId !== thisRequestId) return; + if (abortController?.signal.aborted) return; + if (recipe) { // TODO(ja): to support editting of existing charms / running with // inputs from other charms, we will need to think more about