Skip to content

Commit

Permalink
Isolate project runtime from editor (#2788)
Browse files Browse the repository at this point in the history
  • Loading branch information
Janpot authored Oct 12, 2023
1 parent 46541cb commit 20933c4
Show file tree
Hide file tree
Showing 23 changed files with 276 additions and 304 deletions.
64 changes: 0 additions & 64 deletions packages/toolpad-app/src/config.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/toolpad-app/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const HTML_ID_EDITOR_OVERLAY = 'editor-overlay';
export const WINDOW_PROP_TOOLPAD_APP_RENDER_PARAMS = '__TOOLPAD_APP_RENDER_PARAMS__';
export const RUNTIME_CONFIG_WINDOW_PROPERTY = '__TOOLPAD_RUNTIME_CONFIG__';
export const APP_URL_WINDOW_PROPERTY = '__TOOLPAD_APP_URL__';
export const INITIAL_STATE_WINDOW_PROPERTY = '__initialToolpadState__';

export const TOOLPAD_TARGET_CE = 'CE';
Expand Down
102 changes: 102 additions & 0 deletions packages/toolpad-app/src/project.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Emitter } from '@mui/toolpad-utils/events';
import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query';
import * as React from 'react';
import { useNonNullableContext } from '@mui/toolpad-utils/react';
import invariant from 'invariant';
import { ProjectEvents } from './types';
import { ServerDefinition } from './server/projectRpcServer';
import { createRpcApi } from './rpcClient';

async function fetchAppDevManifest(url: string) {
const response = await fetch(`${url}/__toolpad_dev__/manifest.json`);
if (!response.ok) {
throw new Error(`Failed to fetch app dev manifest: ${response.status}`);
}
return response.text();
}

function createProject(url: string, serializedManifest: string, queryClient: QueryClient) {
const manifest = JSON.parse(serializedManifest);
const events = new Emitter<ProjectEvents>();

const ws = new WebSocket(manifest.wsUrl);

ws.addEventListener('error', () => console.error(`Websocket failed to connect "${ws.url}"`));

ws.addEventListener('open', () => {
// eslint-disable-next-line no-console
console.log('Socket connected');
});

ws.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
switch (message.kind) {
case 'projectEvent': {
events.emit(message.event, message.payload);
break;
}
default:
throw new Error(`Unknown message kind: ${message.kind}`);
}
});

const api = createRpcApi<ServerDefinition>(queryClient, `${url}/__toolpad_dev__/rpc`);

const unsubExternalChange = events.subscribe('externalChange', () => {
api.invalidateQueries('loadDom', []);
});

const unsubFunctionsChanged = events.subscribe('functionsChanged', () => {
api.invalidateQueries('introspect', []);
});

const dispose = () => {
ws.close();
unsubExternalChange();
unsubFunctionsChanged();
};

return {
url,
rootDir: manifest.rootDir,
api,
events,
dispose,
};
}

type Project = Awaited<ReturnType<typeof createProject>>;

const ProjectContext = React.createContext<Project | undefined>(undefined);

export interface ProjectProps {
url: string;
children: React.ReactNode;
}

export function ProjectProvider({ url, children }: ProjectProps) {
const { data: manifest } = useQuery(['app-dev-manifest', url], () => fetchAppDevManifest(url), {
suspense: true,
});

invariant(manifest, "manifest should be defined, we're using suspense");

const queryClient = useQueryClient();

const project = React.useMemo(
() => createProject(url, manifest, queryClient),
[url, manifest, queryClient],
);

React.useEffect(() => {
return () => {
project.dispose();
};
}, [project]);

return <ProjectContext.Provider value={project}>{children}</ProjectContext.Provider>;
}

export function useProject() {
return useNonNullableContext(ProjectContext);
}
44 changes: 3 additions & 41 deletions packages/toolpad-app/src/projectApi.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,6 @@
import { useQueryClient } from '@tanstack/react-query';
import * as React from 'react';
import { useNonNullableContext } from '@mui/toolpad-utils/react';
import { ApiClient, createRpcApi } from './rpcClient';
import type { ServerDefinition } from './server/projectRpcServer';
import { useProjectEvents } from './projectEvents';

const ApiContext = React.createContext<ApiClient<ServerDefinition> | null>(null);

export interface ApiProviderProps {
url: string;
children: React.ReactNode;
}

export function ProjectApiProvider({ url, children }: ApiProviderProps) {
const queryClient = useQueryClient();
const projectEvents = useProjectEvents();

const api = React.useMemo(
() => createRpcApi<ServerDefinition>(queryClient, url),
[queryClient, url],
);

React.useEffect(() => {
const unsubExternalChange = projectEvents.subscribe('externalChange', () => {
api.invalidateQueries('loadDom', []);
});

const unsubFunctionsChanged = projectEvents.subscribe('functionsChanged', () => {
api.invalidateQueries('introspect', []);
});

return () => {
unsubExternalChange();
unsubFunctionsChanged();
};
}, [projectEvents, api]);

return <ApiContext.Provider value={api}>{children}</ApiContext.Provider>;
}
import { useProject } from './project';

export function useProjectApi() {
return useNonNullableContext(ApiContext);
const project = useProject();
return project.api;
}
64 changes: 0 additions & 64 deletions packages/toolpad-app/src/projectEvents.tsx

This file was deleted.

10 changes: 5 additions & 5 deletions packages/toolpad-app/src/rpcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ export interface ApiClient<D extends MethodResolvers, M extends Methods = Method
methods: M;
useQuery: UseQueryFn<M>;
useMutation: UseMutationFn<M>;
refetchQueries: <K extends keyof M>(key: K, params?: Parameters<M[K]>) => Promise<void>;
invalidateQueries: <K extends keyof M>(key: K, params?: Parameters<M[K]>) => Promise<void>;
refetchQueries: <K extends keyof M>(key: K, params: Parameters<M[K]>) => Promise<void>;
invalidateQueries: <K extends keyof M>(key: K, params: Parameters<M[K]>) => Promise<void>;
}

export function createRpcApi<D extends MethodResolvers>(
Expand All @@ -99,10 +99,10 @@ export function createRpcApi<D extends MethodResolvers>(
},
useMutation: (key, options) => useMutation((params) => methods[key](...params), options),
refetchQueries(key, params?) {
return queryClient.refetchQueries(params ? [key, params] : [key]);
return queryClient.refetchQueries([key, params]);
},
invalidateQueries(key, params?) {
return queryClient.invalidateQueries(params ? [key, params] : [key]);
invalidateQueries(key, params) {
return queryClient.invalidateQueries([key, params]);
},
};
}
5 changes: 2 additions & 3 deletions packages/toolpad-app/src/server/DataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import express, { Router } from 'express';
import cors from 'cors';
import invariant from 'invariant';
import { errorFrom, serializeError, SerializedError } from '@mui/toolpad-utils/errors';
import { Methods, ServerDataSource, ToolpadProjectOptions } from '../types';
import type { RuntimeConfig, Methods, ServerDataSource, ToolpadProjectOptions } from '../types';
import serverDataSources from '../toolpadDataSources/server';
import * as appDom from '../appDom';
import applyTransform from '../toolpadDataSources/applyTransform';
import { asyncHandler } from '../utils/express';
import type FunctionsManager from './FunctionsManager';
import type EnvManager from './EnvManager';
import type { RuntimeConfig } from '../config';

function withSerializedError<T extends { error?: unknown }>(
withError: T,
Expand All @@ -30,7 +29,7 @@ interface IToolpadProject {
saveDom(dom: appDom.AppDom): Promise<void>;
functionsManager: FunctionsManager;
envManager: EnvManager;
getRuntimeConfig: () => RuntimeConfig;
getRuntimeConfig: () => Promise<RuntimeConfig>;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/toolpad-app/src/server/appServerWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import invariant from 'invariant';
import { createServer, Plugin } from 'vite';
import { createRpcClient } from '@mui/toolpad-utils/workerRpc';
import { getHtmlContent, createViteConfig, resolvedComponentsId } from './toolpadAppBuilder';
import type { RuntimeConfig } from '../config';
import type { RuntimeConfig } from '../types';
import type * as appDom from '../appDom';
import type { ComponentEntry } from './localMode';
import createRuntimeState from '../runtime/createRuntimeState';
Expand Down
Loading

0 comments on commit 20933c4

Please sign in to comment.