mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-31 04:41:32 +02:00
[web] Add PythonExecutionContext with per-file output buffers (#32737)
* [web] extract PythonExecutionContext and PythonRunner to manage pyodide execution per file * [web] define worker URL in python execution context in order to avoid breaking cjs-based tests * [web] use null check for doc contents to allow running empty python files * [web] flush buffered editor ops before refreshing snapshot for python execution * [web] catch getExecutionContext errors in python runner to prevent unhandled rejections * [web] add PythonRunner unit tests and extract shared WorkerMock * refactor: rename snapshot to state in PythonRunner * fix: remove unnecessary path normalization in PythonExecutionProvider * fix cypress tests GitOrigin-RevId: 9c55586d982fe8df5b90374227005c6b83e94d1f
This commit is contained in:
committed by
Copybot
parent
e30a2a5beb
commit
415db24ba4
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import {
|
||||
PythonRunner,
|
||||
ExecutionContext,
|
||||
} from '@/features/ide-react/components/editor/python/python-runner'
|
||||
|
||||
// Worker factory lives here (a .tsx file) so that the full
|
||||
// `new Worker(new URL(..., import.meta.url))` expression is in a single place
|
||||
// where webpack 5 can statically detect it and create a proper worker bundle.
|
||||
// Keeping import.meta.url out of .ts files also avoids Node.js 24 switching to
|
||||
// ESM mode and breaking CJS-based test loading via @babel/register.
|
||||
const createPyodideWorker = () =>
|
||||
new Worker(
|
||||
/* webpackChunkName: "pyodide-worker" */
|
||||
new URL('../components/editor/python/pyodide.worker.ts', import.meta.url),
|
||||
{ type: 'module' }
|
||||
)
|
||||
|
||||
export interface PythonExecutionContextValue {
|
||||
getPythonRunner: (fileId: string) => PythonRunner
|
||||
}
|
||||
|
||||
export const PythonExecutionContext = createContext<
|
||||
PythonExecutionContextValue | undefined
|
||||
>(undefined)
|
||||
|
||||
export const PythonExecutionProvider: FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { openDocs } = useEditorManagerContext()
|
||||
const { projectSnapshot } = useProjectContext()
|
||||
const { pathInFolder } = useFileTreePathContext()
|
||||
const runnersRef = useRef(new Map<string, PythonRunner>())
|
||||
const baseAssetPathRef = useRef<string | null>(null)
|
||||
|
||||
const pathInFolderRef = useRef(pathInFolder)
|
||||
pathInFolderRef.current = pathInFolder
|
||||
|
||||
// Refreshes the project snapshot and resolves the source code and all project
|
||||
// files for the given fileId, to be passed to the executor for running.
|
||||
const getExecutionContext = useCallback(
|
||||
async (fileId: string): Promise<ExecutionContext | null> => {
|
||||
await openDocs.awaitBufferedOps(AbortSignal.timeout(5000))
|
||||
await projectSnapshot.refresh()
|
||||
|
||||
const relativePath = pathInFolderRef.current(fileId)
|
||||
if (!relativePath) {
|
||||
return null
|
||||
}
|
||||
|
||||
const code = projectSnapshot.getDocContents(relativePath)
|
||||
if (code == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const docPaths = projectSnapshot.getDocPaths()
|
||||
const files = docPaths
|
||||
.map(docPath => {
|
||||
const content = projectSnapshot.getDocContents(docPath)
|
||||
return content != null ? { relativePath: docPath, content } : null
|
||||
})
|
||||
.filter(
|
||||
(f): f is { relativePath: string; content: string } => f != null
|
||||
)
|
||||
|
||||
return { code, files }
|
||||
},
|
||||
[openDocs, projectSnapshot]
|
||||
)
|
||||
|
||||
const getPythonRunner = useCallback(
|
||||
(fileId: string): PythonRunner => {
|
||||
const existing = runnersRef.current.get(fileId)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
if (!baseAssetPathRef.current) {
|
||||
baseAssetPathRef.current = new URL(
|
||||
getMeta('ol-baseAssetPath'),
|
||||
window.location.href
|
||||
).toString()
|
||||
}
|
||||
|
||||
const runner = new PythonRunner(
|
||||
fileId,
|
||||
baseAssetPathRef.current,
|
||||
() => getExecutionContext(fileId),
|
||||
createPyodideWorker
|
||||
)
|
||||
runner.init()
|
||||
runnersRef.current.set(fileId, runner)
|
||||
return runner
|
||||
},
|
||||
[getExecutionContext]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const runners = runnersRef.current
|
||||
return () => {
|
||||
for (const runner of runners.values()) {
|
||||
runner.destroy()
|
||||
}
|
||||
runners.clear()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const value = useMemo(() => ({ getPythonRunner }), [getPythonRunner])
|
||||
|
||||
return (
|
||||
<PythonExecutionContext.Provider value={value}>
|
||||
{children}
|
||||
</PythonExecutionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const usePythonExecutionContext = (): PythonExecutionContextValue => {
|
||||
const context = useContext(PythonExecutionContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'usePythonExecutionContext is only available inside PythonExecutionContext.Provider'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user