From c5616a5ecbf83ad063986871cb362e15a74fc35c Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 5 Oct 2024 21:30:51 -0400 Subject: [PATCH 01/10] feat: dispose lang worker on unmount --- .../workspace/CodeEditor/CodeEditor.tsx | 67 +++++++++++-------- .../CodeEditor/autocomplete/register.ts | 22 ++++-- web/src/workers/language/client.ts | 18 ++--- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx index baa45bbf..ba50ff9d 100644 --- a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx +++ b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx @@ -12,14 +12,16 @@ import { newMonacoParamsChangeDispatcher, runFileDispatcher, type StateDispatch, + type State, } from '~/store' import { type WorkspaceState, dispatchFormatFile, dispatchResetWorkspace, dispatchUpdateFile } from '~/store/workspace' +import type { VimState } from '~/store/vim/state' +import { spawnLanguageWorker } from '~/workers/language' import { getTimeNowUsageMarkers, asyncDebounce, debounce } from './utils' import { attachCustomCommands } from './commands' import { LANGUAGE_GOLANG, stateToOptions } from './props' import { configureMonacoLoader } from './loader' import { DocumentMetadataCache, registerGoLanguageProviders } from './autocomplete' -import type { VimState } from '~/store/vim/state' import classes from './CodeEditor.module.css' const ANALYZE_DEBOUNCE_TIME = 500 @@ -27,36 +29,19 @@ const ANALYZE_DEBOUNCE_TIME = 500 // ask monaco-editor/react to use our own Monaco instance. configureMonacoLoader() -const mapWorkspaceProps = ({ files, selectedFile, snippet }: WorkspaceState) => { - const projectId = snippet?.id ?? '' - if (!selectedFile) { - return { - projectId, - code: '', - fileName: '', - } - } - - return { - projectId, - code: files?.[selectedFile], - fileName: selectedFile, - } -} - interface CodeEditorState { code?: string loading?: boolean fileName: string projectId: string -} - -interface Props extends CodeEditorState { darkMode: boolean vimModeEnabled: boolean isServerEnvironment: boolean options: MonacoSettings vim?: VimState | null +} + +interface Props extends CodeEditorState { dispatch: StateDispatch } @@ -66,14 +51,18 @@ class CodeEditor extends React.Component { private vimAdapter?: VimModeKeymap private vimCommandAdapter?: StatusBarAdapter private monaco?: Monaco - private disposables?: monaco.IDisposable[] private saveTimeoutId?: ReturnType + private readonly disposables: monaco.IDisposable[] = [] private readonly metadataCache = new DocumentMetadataCache() private readonly debouncedAnalyzeFunc = asyncDebounce(async (fileName: string, code: string) => { return await this.doAnalyze(fileName, code) }, ANALYZE_DEBOUNCE_TIME) + private addDisposer(...disposers: monaco.IDisposable[]) { + this.disposables.push(...disposers) + } + private readonly persistFontSize = debounce((fontSize: number) => { this.props.dispatch( newMonacoParamsChangeDispatcher({ @@ -83,7 +72,10 @@ class CodeEditor extends React.Component { }, 1000) editorDidMount(editorInstance: monaco.editor.IStandaloneCodeEditor, monacoInstance: Monaco) { - this.disposables = registerGoLanguageProviders(this.props.dispatch, this.metadataCache) + const [langWorker, workerDisposer] = spawnLanguageWorker() + + this.addDisposer(workerDisposer) + this.addDisposer(...registerGoLanguageProviders(this.props.dispatch, this.metadataCache, langWorker)) this.editorInstance = editorInstance this.monaco = monacoInstance @@ -142,7 +134,7 @@ class CodeEditor extends React.Component { ] // Persist font size on zoom - this.disposables.push( + this.addDisposer( editorInstance.onDidChangeConfiguration((e) => { if (e.hasChanged(monaco.editor.EditorOption.fontSize)) { const newFontSize = editorInstance.getOption(monaco.editor.EditorOption.fontSize) @@ -160,13 +152,13 @@ class CodeEditor extends React.Component { void this.debouncedAnalyzeFunc(fileName, code) } - private isFileOrEnvironmentChanged(prevProps) { + private isFileOrEnvironmentChanged(prevProps: Props) { return ( prevProps.isServerEnvironment !== this.props.isServerEnvironment || prevProps.fileName !== this.props.fileName ) } - private applyVimModeChanges(prevProps) { + private applyVimModeChanges(prevProps: Props) { if (prevProps?.vimModeEnabled === this.props.vimModeEnabled) { return } @@ -295,7 +287,7 @@ class CodeEditor extends React.Component { } } -export const ConnectedCodeEditor = connect(({ workspace, ...s }) => ({ +const mapStateToProps = ({ workspace, ...s }: State): CodeEditorState => ({ ...mapWorkspaceProps(workspace), darkMode: s.settings.darkMode, vimModeEnabled: s.settings.enableVimMode, @@ -303,4 +295,23 @@ export const ConnectedCodeEditor = connect(({ workspace, .. loading: s.status?.loading, options: s.monaco, vim: s.vim, -}))(CodeEditor) +}) + +const mapWorkspaceProps = ({ files, selectedFile, snippet }: WorkspaceState) => { + const projectId = snippet?.id ?? '' + if (!selectedFile) { + return { + projectId, + code: '', + fileName: '', + } + } + + return { + projectId, + code: files?.[selectedFile], + fileName: selectedFile, + } +} + +export const ConnectedCodeEditor = connect(mapStateToProps)(CodeEditor) diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/register.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/register.ts index 2e7153c7..e8c34110 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/register.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/register.ts @@ -5,19 +5,27 @@ import { GoImportsCompletionProvider } from './imports' import { GoHoverProvider } from './hover' import type { StateDispatch } from '~/store' import type { DocumentMetadataCache } from './cache' -import { getLanguageWorker } from '~/workers/language' +import type { LanguageWorker } from '~/workers/language' + +const LANG_GO = 'go' /** * Registers all Go autocomplete providers for Monaco editor. */ -export const registerGoLanguageProviders = (dispatcher: StateDispatch, cache: DocumentMetadataCache) => { - const worker = getLanguageWorker() +export const registerGoLanguageProviders = ( + dispatcher: StateDispatch, + cache: DocumentMetadataCache, + langWorker: LanguageWorker, +) => { return [ monaco.languages.registerCompletionItemProvider( - 'go', - new GoSymbolsCompletionItemProvider(dispatcher, cache, worker), + LANG_GO, + new GoSymbolsCompletionItemProvider(dispatcher, cache, langWorker), + ), + monaco.languages.registerCompletionItemProvider( + LANG_GO, + new GoImportsCompletionProvider(dispatcher, cache, langWorker), ), - monaco.languages.registerCompletionItemProvider('go', new GoImportsCompletionProvider(dispatcher, cache, worker)), - monaco.languages.registerHoverProvider('go', new GoHoverProvider(worker, cache)), + monaco.languages.registerHoverProvider(LANG_GO, new GoHoverProvider(langWorker, cache)), ] } diff --git a/web/src/workers/language/client.ts b/web/src/workers/language/client.ts index 19458e8c..1390fdb0 100644 --- a/web/src/workers/language/client.ts +++ b/web/src/workers/language/client.ts @@ -1,19 +1,21 @@ import * as Comlink from 'comlink' import type { WorkerHandler } from './language.worker' +import type { IDisposable } from 'monaco-editor' export type LanguageWorker = Comlink.Remote -let worker: LanguageWorker - -const spawnWorker = (): LanguageWorker => { +export const spawnLanguageWorker = (): [LanguageWorker, IDisposable] => { const worker = new Worker(new URL('./language.worker.ts', import.meta.url), { type: 'module', }) - return Comlink.wrap(worker) -} + const proxy = Comlink.wrap(worker) + const dispose = { + dispose: () => { + proxy[Comlink.releaseProxy]() + worker.terminate() + }, + } -export const getLanguageWorker = () => { - worker ??= spawnWorker() - return worker + return [proxy, dispose] } From c932447b83c53a339afe9bbc6f27122564818b95 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 5 Oct 2024 21:43:32 -0400 Subject: [PATCH 02/10] fix: fix import paths --- .../elements/tabs/TabLabel/TabLabel.tsx | 2 +- .../components/elements/tabs/TabView/TabView.tsx | 2 +- .../features/workspace/CodeEditor/CodeEditor.tsx | 16 ++++++++-------- .../CodeEditor/autocomplete/symbols/provider.ts | 2 +- .../workspace/CodeEditor/{ => utils}/commands.ts | 0 .../workspace/CodeEditor/{ => utils}/loader.ts | 0 .../workspace/CodeEditor/{ => utils}/props.ts | 0 .../workspace/CodeEditor/{ => utils}/utils.ts | 0 .../features/workspace/Workspace/Workspace.tsx | 4 ++-- web/src/services/examples/index.ts | 6 +++--- 10 files changed, 16 insertions(+), 16 deletions(-) rename web/src/components/features/workspace/CodeEditor/{ => utils}/commands.ts (100%) rename web/src/components/features/workspace/CodeEditor/{ => utils}/loader.ts (100%) rename web/src/components/features/workspace/CodeEditor/{ => utils}/props.ts (100%) rename web/src/components/features/workspace/CodeEditor/{ => utils}/utils.ts (100%) diff --git a/web/src/components/elements/tabs/TabLabel/TabLabel.tsx b/web/src/components/elements/tabs/TabLabel/TabLabel.tsx index bf6313f9..0e9cf639 100644 --- a/web/src/components/elements/tabs/TabLabel/TabLabel.tsx +++ b/web/src/components/elements/tabs/TabLabel/TabLabel.tsx @@ -11,7 +11,7 @@ import { type IStackStyles, type IButtonStyles, } from '@fluentui/react' -import type { TabIconStyles } from '../types.ts' +import type { TabIconStyles } from '../types' const BUTTON_PRIMARY = 0 const BUTTON_WHEEL = 1 diff --git a/web/src/components/elements/tabs/TabView/TabView.tsx b/web/src/components/elements/tabs/TabView/TabView.tsx index 6f830301..d46d2e2c 100644 --- a/web/src/components/elements/tabs/TabView/TabView.tsx +++ b/web/src/components/elements/tabs/TabView/TabView.tsx @@ -6,7 +6,7 @@ import { TabHeader } from '../TabHeader' import type { TabBarAction, TabInfo, TabIconStyles } from '../types' import { containerStyles, tabHeaderStyles, getTabContentStyles } from './styles' -import { debounce } from './debounce.ts' +import { debounce } from './debounce' interface Props { actions?: TabBarAction[] diff --git a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx index ba50ff9d..fab77579 100644 --- a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx +++ b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx @@ -17,10 +17,10 @@ import { import { type WorkspaceState, dispatchFormatFile, dispatchResetWorkspace, dispatchUpdateFile } from '~/store/workspace' import type { VimState } from '~/store/vim/state' import { spawnLanguageWorker } from '~/workers/language' -import { getTimeNowUsageMarkers, asyncDebounce, debounce } from './utils' -import { attachCustomCommands } from './commands' -import { LANGUAGE_GOLANG, stateToOptions } from './props' -import { configureMonacoLoader } from './loader' +import { getTimeNowUsageMarkers, asyncDebounce, debounce } from './utils/utils' +import { attachCustomCommands } from './utils/commands' +import { LANGUAGE_GOLANG, stateToOptions } from './utils/props' +import { configureMonacoLoader } from './utils/loader' import { DocumentMetadataCache, registerGoLanguageProviders } from './autocomplete' import classes from './CodeEditor.module.css' @@ -29,7 +29,7 @@ const ANALYZE_DEBOUNCE_TIME = 500 // ask monaco-editor/react to use our own Monaco instance. configureMonacoLoader() -interface CodeEditorState { +export interface CodeEditorState { code?: string loading?: boolean fileName: string @@ -41,11 +41,11 @@ interface CodeEditorState { vim?: VimState | null } -interface Props extends CodeEditorState { +export interface Props extends CodeEditorState { dispatch: StateDispatch } -class CodeEditor extends React.Component { +class CodeEditorView extends React.Component { private analyzer?: Analyzer private editorInstance?: monaco.editor.IStandaloneCodeEditor private vimAdapter?: VimModeKeymap @@ -314,4 +314,4 @@ const mapWorkspaceProps = ({ files, selectedFile, snippet }: WorkspaceState) => } } -export const ConnectedCodeEditor = connect(mapStateToProps)(CodeEditor) +export const CodeEditor = connect(mapStateToProps)(CodeEditorView) diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts index b4aba4fa..b8638c2c 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts @@ -1,6 +1,6 @@ import type * as monaco from 'monaco-editor' import type { SuggestionContext, SuggestionQuery } from '~/workers/language' -import { asyncDebounce } from '../../utils' +import { asyncDebounce } from '../../utils/utils' import snippets from './snippets' import { parseExpression } from './parse' import { CacheBasedCompletionProvider } from '../base' diff --git a/web/src/components/features/workspace/CodeEditor/commands.ts b/web/src/components/features/workspace/CodeEditor/utils/commands.ts similarity index 100% rename from web/src/components/features/workspace/CodeEditor/commands.ts rename to web/src/components/features/workspace/CodeEditor/utils/commands.ts diff --git a/web/src/components/features/workspace/CodeEditor/loader.ts b/web/src/components/features/workspace/CodeEditor/utils/loader.ts similarity index 100% rename from web/src/components/features/workspace/CodeEditor/loader.ts rename to web/src/components/features/workspace/CodeEditor/utils/loader.ts diff --git a/web/src/components/features/workspace/CodeEditor/props.ts b/web/src/components/features/workspace/CodeEditor/utils/props.ts similarity index 100% rename from web/src/components/features/workspace/CodeEditor/props.ts rename to web/src/components/features/workspace/CodeEditor/utils/props.ts diff --git a/web/src/components/features/workspace/CodeEditor/utils.ts b/web/src/components/features/workspace/CodeEditor/utils/utils.ts similarity index 100% rename from web/src/components/features/workspace/CodeEditor/utils.ts rename to web/src/components/features/workspace/CodeEditor/utils/utils.ts diff --git a/web/src/components/features/workspace/Workspace/Workspace.tsx b/web/src/components/features/workspace/Workspace/Workspace.tsx index e1ee0519..702b2c33 100644 --- a/web/src/components/features/workspace/Workspace/Workspace.tsx +++ b/web/src/components/features/workspace/Workspace/Workspace.tsx @@ -12,7 +12,7 @@ import { import { TabView } from '~/components/elements/tabs/TabView' import type { TabBarAction, TabIconStyles } from '~/components/elements/tabs/types' -import { ConnectedCodeEditor } from '../CodeEditor' +import { CodeEditor } from '../CodeEditor' import { FlexContainer } from '../FlexContainer' import { NewFileModal } from '../NewFileModal' import { ContentPlaceholder } from '../ContentPlaceholder' @@ -117,7 +117,7 @@ const Workspace: React.FC = ({ dispatch, files, selectedFile, snippet }) > {tabs?.length ? ( - + ) : ( snippets as Snippets From 9be1665fbdf430453ef2ec283be045e3a610f623 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 5 Oct 2024 22:59:14 -0400 Subject: [PATCH 03/10] feat: lazy-load workspace --- .../SuspenseBoundary/SuspenseBoundary.tsx | 85 +++++++++++++++++++ .../elements/misc/SuspenseBoundary/index.ts | 1 + .../Workspace/LazyLoadedWorkspace.tsx | 10 +++ .../workspace/Workspace/Workspace.tsx | 4 +- .../features/workspace/Workspace/index.ts | 2 +- .../pages/PlaygroundPage/PlaygroundPage.tsx | 4 +- 6 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx create mode 100644 web/src/components/elements/misc/SuspenseBoundary/index.ts create mode 100644 web/src/components/features/workspace/Workspace/LazyLoadedWorkspace.tsx diff --git a/web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx b/web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx new file mode 100644 index 00000000..d94440b1 --- /dev/null +++ b/web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx @@ -0,0 +1,85 @@ +import React, { Suspense } from 'react' +import { IStackStyles, mergeStyleSets, Spinner, Stack, useTheme } from "@fluentui/react"; +import { Poster } from '../Poster' + +interface ContainerProps { + styles?: IStackStyles +} + +const SuspenseContainer: React.FC = ({ children , styles }) => { + const theme = useTheme() + const stackStyles: IStackStyles = mergeStyleSets({ + root: { + background: theme.semanticColors.bodyBackground, + } + }, styles) + + return ( + + {children} + + ) +} + +interface ErrorProps extends ContainerProps { + errorLabel?: string + error?: Error +} + +const SuspenseErrorBoundary: React.FC = ({ styles, children, error, errorLabel}) => { + return ( + error ? ( + + + + ) : ( + <>{children} + ) + ) +} + +interface PreloaderProps { + preloaderText?: string + styles?: IStackStyles +} + +const SuspensePreloader: React.FC = ({ preloaderText }) => { + return ( + + + + ) +} + +export interface SuspenseProps extends PreloaderProps { + errorLabel?: string +} + +interface State { + error?: Error +} + +export class SuspenseBoundary extends React.Component { + state: State = {} + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('failed to render a component', error, errorInfo) + this.setState({ error }) + } + + render() { + return ( + + }> + {this.props.children} + + + ) + } + + static getDerivedStateFromError(err: Error) { + return { + error: err, + } + } +} diff --git a/web/src/components/elements/misc/SuspenseBoundary/index.ts b/web/src/components/elements/misc/SuspenseBoundary/index.ts new file mode 100644 index 00000000..5023224d --- /dev/null +++ b/web/src/components/elements/misc/SuspenseBoundary/index.ts @@ -0,0 +1 @@ +export * from './SuspenseBoundary.tsx' diff --git a/web/src/components/features/workspace/Workspace/LazyLoadedWorkspace.tsx b/web/src/components/features/workspace/Workspace/LazyLoadedWorkspace.tsx new file mode 100644 index 00000000..9b10674d --- /dev/null +++ b/web/src/components/features/workspace/Workspace/LazyLoadedWorkspace.tsx @@ -0,0 +1,10 @@ +import React, { lazy } from 'react' +import { SuspenseBoundary } from '~/components/elements/misc/SuspenseBoundary' + +const LazyWorkspace = lazy(() => import('./Workspace.tsx')) + +export const LazyLoadedWorkspace: React.FC = () => ( + + + +) diff --git a/web/src/components/features/workspace/Workspace/Workspace.tsx b/web/src/components/features/workspace/Workspace/Workspace.tsx index 702b2c33..81240e19 100644 --- a/web/src/components/features/workspace/Workspace/Workspace.tsx +++ b/web/src/components/features/workspace/Workspace/Workspace.tsx @@ -152,4 +152,6 @@ const Workspace: React.FC = ({ dispatch, files, selectedFile, snippet }) ) } -export const ConnectedWorkspace = connect(({ workspace }) => ({ ...workspace }))(Workspace) +const ConnectedWorkspace = connect(({ workspace }) => ({ ...workspace }))(Workspace) + +export default ConnectedWorkspace diff --git a/web/src/components/features/workspace/Workspace/index.ts b/web/src/components/features/workspace/Workspace/index.ts index 3e077425..69759a4a 100644 --- a/web/src/components/features/workspace/Workspace/index.ts +++ b/web/src/components/features/workspace/Workspace/index.ts @@ -1 +1 @@ -export * from './Workspace' +export * from './LazyLoadedWorkspace' diff --git a/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx b/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx index 3aa131ea..e9c3d0fd 100644 --- a/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx +++ b/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx @@ -5,7 +5,6 @@ import { connect } from 'react-redux' import { dispatchPanelLayoutChange } from '~/store' import { dispatchLoadSnippet } from '~/store/workspace' import { Header } from '~/components/layout/Header' -import { ConnectedWorkspace } from '~/components/features/workspace/Workspace' import { InspectorPanel } from '~/components/features/inspector/InspectorPanel/InspectorPanel' import { NotificationHost } from '~/components/modals/Notification' import { Layout } from '~/components/layout/Layout/Layout' @@ -14,6 +13,7 @@ import { computeSizePercentage } from './utils' import styles from './PlaygroundPage.module.css' import { ConfirmProvider } from '~/components/modals/ConfirmModal' +import { LazyLoadedWorkspace } from '~/components/features/workspace/Workspace' interface PageParams { snippetID: string @@ -31,7 +31,7 @@ export const PlaygroundPage = connect(({ panel }: any) => ({ panelProps: panel }
- + { From 41f5e1fa16bb23fa627afa4e5aeab754c49d8862 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 5 Oct 2024 22:59:47 -0400 Subject: [PATCH 04/10] fix: linter --- .../SuspenseBoundary/SuspenseBoundary.tsx | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx b/web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx index d94440b1..d164fa79 100644 --- a/web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx +++ b/web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx @@ -1,18 +1,21 @@ import React, { Suspense } from 'react' -import { IStackStyles, mergeStyleSets, Spinner, Stack, useTheme } from "@fluentui/react"; +import { type IStackStyles, mergeStyleSets, Spinner, Stack, useTheme } from '@fluentui/react' import { Poster } from '../Poster' interface ContainerProps { styles?: IStackStyles } -const SuspenseContainer: React.FC = ({ children , styles }) => { +const SuspenseContainer: React.FC = ({ children, styles }) => { const theme = useTheme() - const stackStyles: IStackStyles = mergeStyleSets({ - root: { - background: theme.semanticColors.bodyBackground, - } - }, styles) + const stackStyles: IStackStyles = mergeStyleSets( + { + root: { + background: theme.semanticColors.bodyBackground, + }, + }, + styles, + ) return ( @@ -26,15 +29,13 @@ interface ErrorProps extends ContainerProps { error?: Error } -const SuspenseErrorBoundary: React.FC = ({ styles, children, error, errorLabel}) => { - return ( - error ? ( - - - - ) : ( - <>{children} - ) +const SuspenseErrorBoundary: React.FC = ({ styles, children, error, errorLabel }) => { + return error ? ( + + + + ) : ( + <>{children} ) } From 1d30a2a698481d0e54de4e938f942088b881624b Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 5 Oct 2024 23:01:57 -0400 Subject: [PATCH 05/10] fix: linter --- .../elements/misc/SuspenseBoundary/SuspenseBoundary.tsx | 8 ++++---- .../features/workspace/Workspace/LazyLoadedWorkspace.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx b/web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx index d164fa79..717afb1b 100644 --- a/web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx +++ b/web/src/components/elements/misc/SuspenseBoundary/SuspenseBoundary.tsx @@ -1,10 +1,10 @@ -import React, { Suspense } from 'react' +import React, { type PropsWithChildren, Suspense } from 'react' import { type IStackStyles, mergeStyleSets, Spinner, Stack, useTheme } from '@fluentui/react' import { Poster } from '../Poster' -interface ContainerProps { +type ContainerProps = PropsWithChildren<{ styles?: IStackStyles -} +}> const SuspenseContainer: React.FC = ({ children, styles }) => { const theme = useTheme() @@ -60,7 +60,7 @@ interface State { error?: Error } -export class SuspenseBoundary extends React.Component { +export class SuspenseBoundary extends React.Component, State> { state: State = {} componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { diff --git a/web/src/components/features/workspace/Workspace/LazyLoadedWorkspace.tsx b/web/src/components/features/workspace/Workspace/LazyLoadedWorkspace.tsx index 9b10674d..b684f8d7 100644 --- a/web/src/components/features/workspace/Workspace/LazyLoadedWorkspace.tsx +++ b/web/src/components/features/workspace/Workspace/LazyLoadedWorkspace.tsx @@ -1,7 +1,7 @@ import React, { lazy } from 'react' import { SuspenseBoundary } from '~/components/elements/misc/SuspenseBoundary' -const LazyWorkspace = lazy(() => import('./Workspace.tsx')) +const LazyWorkspace = lazy(async () => await import('./Workspace.tsx')) export const LazyLoadedWorkspace: React.FC = () => ( From 7e57b15a57d00141707e6c78d73901cf5385fcee Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 5 Oct 2024 23:27:34 -0400 Subject: [PATCH 06/10] feat: lazyload page content --- .../PlaygroundPage/PlaygroundContainer.tsx | 52 +++++++++++++++++++ .../pages/PlaygroundPage/PlaygroundPage.tsx | 46 ++++------------ 2 files changed, 63 insertions(+), 35 deletions(-) create mode 100644 web/src/components/pages/PlaygroundPage/PlaygroundContainer.tsx diff --git a/web/src/components/pages/PlaygroundPage/PlaygroundContainer.tsx b/web/src/components/pages/PlaygroundPage/PlaygroundContainer.tsx new file mode 100644 index 00000000..08c99eeb --- /dev/null +++ b/web/src/components/pages/PlaygroundPage/PlaygroundContainer.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { type State, dispatchPanelLayoutChange } from '~/store' +import { InspectorPanel } from '~/components/features/inspector/InspectorPanel/InspectorPanel' +import { NotificationHost } from '~/components/modals/Notification' +import { Layout } from '~/components/layout/Layout/Layout' +import { computeSizePercentage } from './utils' + +import { ConfirmProvider } from '~/components/modals/ConfirmModal' +import { LazyLoadedWorkspace } from '~/components/features/workspace/Workspace' + +export interface PlaygroundContainerProps { + parentRef: React.RefObject +} + +const PlaygroundContainer: React.FC = ({ parentRef }) => { + const dispatch = useDispatch() + const panelProps = useSelector((state) => state.panel) + return ( + + + + { + dispatch(dispatchPanelLayoutChange({ layout })) + }} + onCollapsed={(collapsed) => { + dispatch(dispatchPanelLayoutChange({ collapsed })) + }} + onResize={(changes) => { + if ('height' in changes) { + // Height percentage is buggy on resize. Use percents only for width. + dispatch(dispatchPanelLayoutChange(changes)) + return + } + + if (!parentRef.current) { + return + } + + const result = computeSizePercentage(changes, parentRef.current) + dispatch(dispatchPanelLayoutChange(result)) + }} + /> + + + + ) +} + +export default PlaygroundContainer diff --git a/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx b/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx index e9c3d0fd..bcc7c9d3 100644 --- a/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx +++ b/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx @@ -1,25 +1,22 @@ -import React, { useEffect, useRef } from 'react' +import React, { lazy, useEffect, useRef } from 'react' import { useParams } from 'react-router-dom' -import { connect } from 'react-redux' +import { useDispatch } from 'react-redux' -import { dispatchPanelLayoutChange } from '~/store' import { dispatchLoadSnippet } from '~/store/workspace' import { Header } from '~/components/layout/Header' -import { InspectorPanel } from '~/components/features/inspector/InspectorPanel/InspectorPanel' -import { NotificationHost } from '~/components/modals/Notification' -import { Layout } from '~/components/layout/Layout/Layout' import { ConnectedStatusBar } from '~/components/layout/StatusBar' -import { computeSizePercentage } from './utils' import styles from './PlaygroundPage.module.css' -import { ConfirmProvider } from '~/components/modals/ConfirmModal' -import { LazyLoadedWorkspace } from '~/components/features/workspace/Workspace' +import { SuspenseBoundary } from '~/components/elements/misc/SuspenseBoundary' + +const LazyPlaygroundContent = lazy(async () => await import('./PlaygroundContainer.tsx')) interface PageParams { snippetID: string } -export const PlaygroundPage = connect(({ panel }: any) => ({ panelProps: panel }))(({ panelProps, dispatch }: any) => { +export const PlaygroundPage: React.FC = () => { + const dispatch = useDispatch() const containerRef = useRef(null) const { snippetID } = useParams() useEffect(() => { @@ -29,31 +26,10 @@ export const PlaygroundPage = connect(({ panel }: any) => ({ panelProps: panel } return (
- - - - { - dispatch(dispatchPanelLayoutChange({ layout })) - }} - onCollapsed={(collapsed) => { - dispatch(dispatchPanelLayoutChange({ collapsed })) - }} - onResize={(changes) => { - if ('height' in changes) { - // Height percentage is buggy on resize. Use percents only for width. - dispatch(dispatchPanelLayoutChange(changes)) - return - } - const result = computeSizePercentage(changes, containerRef.current!) - dispatch(dispatchPanelLayoutChange(result)) - }} - /> - - - + + +
) -}) +} From b94fd1b0e07b54a2ca81262ea5dc3a55acdcb266 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 5 Oct 2024 23:34:28 -0400 Subject: [PATCH 07/10] fix: use modern redux hooks --- .../workspace/Workspace/Workspace.tsx | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/web/src/components/features/workspace/Workspace/Workspace.tsx b/web/src/components/features/workspace/Workspace/Workspace.tsx index 81240e19..19e27896 100644 --- a/web/src/components/features/workspace/Workspace/Workspace.tsx +++ b/web/src/components/features/workspace/Workspace/Workspace.tsx @@ -1,13 +1,8 @@ import React, { useState, useMemo, useRef } from 'react' import { useTheme } from '@fluentui/react' -import { type StateDispatch, connect } from '~/store' -import { - type WorkspaceState, - dispatchCreateFile, - dispatchRemoveFile, - dispatchImportFile, - newFileSelectAction, -} from '~/store/workspace' +import { useDispatch, useSelector } from 'react-redux' +import { type State } from '~/store' +import { dispatchCreateFile, dispatchRemoveFile, dispatchImportFile, newFileSelectAction } from '~/store/workspace' import { TabView } from '~/components/elements/tabs/TabView' import type { TabBarAction, TabIconStyles } from '~/components/elements/tabs/types' @@ -19,11 +14,9 @@ import { ContentPlaceholder } from '../ContentPlaceholder' import { newEmptyFileContent } from './utils' import { useConfirmModal } from '~/components/modals/ConfirmModal' -interface Props extends WorkspaceState { - dispatch: StateDispatch -} - -const Workspace: React.FC = ({ dispatch, files, selectedFile, snippet }) => { +const Workspace: React.FC = () => { + const dispatch = useDispatch() + const { files, selectedFile, snippet } = useSelector((state) => state.workspace) const { palette, semanticColors } = useTheme() const uploadRef = useRef(null) const [modalOpen, setModalOpen] = useState(false) @@ -152,6 +145,4 @@ const Workspace: React.FC = ({ dispatch, files, selectedFile, snippet }) ) } -const ConnectedWorkspace = connect(({ workspace }) => ({ ...workspace }))(Workspace) - -export default ConnectedWorkspace +export default Workspace From ce51fc1c05984711d4a0454512c5e0653dbdff70 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 5 Oct 2024 23:42:42 -0400 Subject: [PATCH 08/10] fix: autodetect language name --- .../features/workspace/CodeEditor/CodeEditor.tsx | 4 ++-- .../features/workspace/CodeEditor/utils/props.ts | 12 +----------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx index fab77579..d8db8f36 100644 --- a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx +++ b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx @@ -19,7 +19,7 @@ import type { VimState } from '~/store/vim/state' import { spawnLanguageWorker } from '~/workers/language' import { getTimeNowUsageMarkers, asyncDebounce, debounce } from './utils/utils' import { attachCustomCommands } from './utils/commands' -import { LANGUAGE_GOLANG, stateToOptions } from './utils/props' +import { languageFromFilename, stateToOptions } from './utils/props' import { configureMonacoLoader } from './utils/loader' import { DocumentMetadataCache, registerGoLanguageProviders } from './autocomplete' import classes from './CodeEditor.module.css' @@ -273,7 +273,7 @@ class CodeEditorView extends React.Component { return ( (fname.endsWith('.mod') ? 'gomod' : 'go') // stateToOptions converts MonacoState to IEditorOptions export const stateToOptions = (state: MonacoSettings): monaco.editor.IEditorOptions => { From 725543e824840ff9c692d1063608df7629090155 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sun, 6 Oct 2024 00:12:14 -0400 Subject: [PATCH 09/10] feat: lazy-load 404 --- ....module.css => NotFoundContent.module.css} | 0 .../pages/NotFoundPage/NotFoundContent.tsx | 61 +++++++++++++++++ .../pages/NotFoundPage/NotFoundPage.tsx | 66 +++---------------- 3 files changed, 69 insertions(+), 58 deletions(-) rename web/src/components/pages/NotFoundPage/{NotFoundPage.module.css => NotFoundContent.module.css} (100%) create mode 100644 web/src/components/pages/NotFoundPage/NotFoundContent.tsx diff --git a/web/src/components/pages/NotFoundPage/NotFoundPage.module.css b/web/src/components/pages/NotFoundPage/NotFoundContent.module.css similarity index 100% rename from web/src/components/pages/NotFoundPage/NotFoundPage.module.css rename to web/src/components/pages/NotFoundPage/NotFoundContent.module.css diff --git a/web/src/components/pages/NotFoundPage/NotFoundContent.tsx b/web/src/components/pages/NotFoundPage/NotFoundContent.tsx new file mode 100644 index 00000000..a30c82fd --- /dev/null +++ b/web/src/components/pages/NotFoundPage/NotFoundContent.tsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react' +import { Link } from 'react-router-dom' +import styles from './NotFoundContent.module.css' + +const NotFoundContent: React.FC = () => { + const [catVisible, setCatVisible] = useState(false) + return ( +
+
+
+

404!

+

Page Not Found

+
+

Requested page does not exist or was deleted.

+ {/* eslint-disable-next-line react/no-unescaped-entities */} +

That's all we know 🤷

+
+
+ {!catVisible && ( + + )} + + Go To Home + +
+
+
+ {catVisible ? ( + 😺 + ) : ( + Gopher + )} +
+
+ + + + + + + + + +
+ ) +} + +export default NotFoundContent diff --git a/web/src/components/pages/NotFoundPage/NotFoundPage.tsx b/web/src/components/pages/NotFoundPage/NotFoundPage.tsx index ed8a0e5f..4c174ca1 100644 --- a/web/src/components/pages/NotFoundPage/NotFoundPage.tsx +++ b/web/src/components/pages/NotFoundPage/NotFoundPage.tsx @@ -1,59 +1,9 @@ -import React, { useState } from 'react' -import { Link } from 'react-router-dom' -import styles from './NotFoundPage.module.css' +import React from 'react' -export const NotFoundPage: React.FC = () => { - const [catVisible, setCatVisible] = useState(false) - return ( -
-
-
-

404!

-

Page Not Found

-
-

Requested page does not exist or was deleted.

- {/* eslint-disable-next-line react/no-unescaped-entities */} -

That's all we know 🤷

-
-
- {!catVisible && ( - - )} - - Go To Home - -
-
-
- {catVisible ? ( - 😺 - ) : ( - Gopher - )} -
-
- - - - - - - - - -
- ) -} +const NotFoundContent = React.lazy(async () => await import('./NotFoundContent.tsx')) + +export const NotFoundPage: React.FC = () => ( + Loading...}> + + +) From 0c6bf228f8e998824d65bcdbb7d5fa30b7be09e9 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sun, 6 Oct 2024 00:44:45 -0400 Subject: [PATCH 10/10] feat: goodbye Axios --- web/package.json | 1 - web/src/services/api/client.ts | 61 +++++++++++++++++++------------ web/src/services/api/interface.ts | 3 -- web/yarn.lock | 19 ---------- 4 files changed, 37 insertions(+), 47 deletions(-) diff --git a/web/package.json b/web/package.json index ecfdf68c..836e4700 100644 --- a/web/package.json +++ b/web/package.json @@ -25,7 +25,6 @@ "@xterm/addon-image": "^0.7.0-beta.1", "@xterm/addon-webgl": "^0.17.0-beta.1", "@xterm/xterm": "^5.4.0-beta.1", - "axios": "^1.7.4", "clsx": "^1.1.1", "comlink": "^4.4.1", "connected-react-router": "^6.9.2", diff --git a/web/src/services/api/client.ts b/web/src/services/api/client.ts index 4de22648..bd2f1beb 100644 --- a/web/src/services/api/client.ts +++ b/web/src/services/api/client.ts @@ -1,4 +1,3 @@ -import * as axios from 'axios' import { Backend, type VersionResponse, @@ -11,15 +10,7 @@ import { import type { IAPIClient } from './interface' export class Client implements IAPIClient { - private readonly client: axios.AxiosInstance - - get axiosClient() { - return this.client - } - - constructor(private readonly baseUrl: string) { - this.client = axios.default.create({ baseURL: baseUrl }) - } + constructor(private readonly baseUrl: string) {} async getVersion(): Promise { return await this.get(`/version?=${Date.now()}`) @@ -60,24 +51,46 @@ export class Client implements IAPIClient { } private async get(uri: string): Promise { - try { - const resp = await this.client.get(uri) - return resp.data - } catch (err) { - throw Client.extractAPIError(err) - } + return await this.doRequest(uri, { + headers: { + Accept: 'application/json', + }, + }) } - private async post(uri: string, data: any, cfg?: axios.AxiosRequestConfig): Promise { + private async post(uri: string, data: any): Promise { + return await this.doRequest(uri, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + } + + private async doRequest(uri: string, reqInit?: RequestInit): Promise { + const reqUrl = this.baseUrl + uri + const rsp = await fetch(reqUrl, reqInit) + if (rsp.ok) { + return (await rsp.json()) as T + } + + const isJson = rsp.headers.get('content-type') + if (!isJson) { + throw new Error(`${rsp.status} ${rsp.statusText}`) + } + + let errBody: { error: string } try { - const resp = await this.client.post(uri, data, cfg) - return resp.data - } catch (err) { - throw Client.extractAPIError(err) + errBody = await rsp.json() + } catch (_) { + // Fallback in case of malformed response + errBody = { + error: `${rsp.status} ${rsp.statusText}`, + } } - } - private static extractAPIError(err: any): Error { - return new Error(err?.response?.data?.error ?? err.message) + throw new Error(errBody.error) } } diff --git a/web/src/services/api/interface.ts b/web/src/services/api/interface.ts index edac4e46..e0fbf279 100644 --- a/web/src/services/api/interface.ts +++ b/web/src/services/api/interface.ts @@ -1,9 +1,6 @@ -import type { AxiosInstance } from 'axios' import type { VersionResponse, RunResponse, BuildResponse, ShareResponse, VersionsInfo, FilesPayload } from './models' export interface IAPIClient { - readonly axiosClient: AxiosInstance - getVersion: () => Promise run: (files: Record, vet: boolean) => Promise diff --git a/web/yarn.lock b/web/yarn.lock index a4ccce0a..9e4250d1 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2815,15 +2815,6 @@ axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== -axios@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2" - integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -4423,11 +4414,6 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== -follow-redirects@^1.15.6: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== - for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -6112,11 +6098,6 @@ prop-types@^15.7.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - public-encrypt@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"