Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[refactor] Move Toolpad runtime to the RPC mechanism #2582

Merged
merged 9 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/toolpad-app/cli/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ import {
initProject,
} from '../src/server/localMode';
import type { Command as AppDevServerCommand, AppViteServerConfig, WorkerRpc } from './appServer';
import { createRpcHandler, createRpcServer } from '../src/server/rpc';
import { createRpcHandler } from '../src/server/rpc';
import { RUNTIME_CONFIG_WINDOW_PROPERTY } from '../src/constants';
import type { RuntimeConfig } from '../src/config';
import { createWorkerRpcServer } from '../src/server/workerRpc';
import { createRpcServer } from '../src/server/rpcServer';
import { createRpcRuntimeServer } from '../src/server/rpcRuntimeServer';

const DEFAULT_PORT = 3000;

Expand Down Expand Up @@ -86,7 +88,9 @@ async function createDevHandler(
worker.postMessage({ kind: 'reload-components' } satisfies AppDevServerCommand);
});

handler.use('/api/data', project.dataManager.createDataHandler(project));
handler.use('/api/data', project.dataManager.createDataHandler());
const runtimeRpcServer = createRpcRuntimeServer(project);
handler.use('/api/runtime-rpc', createRpcHandler(runtimeRpcServer));
handler.use(
(req, res, next) => {
// Stall the request until the dev server is ready
Expand Down
3 changes: 1 addition & 2 deletions packages/toolpad-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
"semver": "7.5.4",
"serialize-javascript": "6.0.1",
"superjson": "1.13.1",
"typescript": "5.1.6",
"typescript": "5.2.2",
"vite": "4.4.9",
"vm-browserify": "^1.1.2",
"whatwg-url": "13.0.0",
Expand Down Expand Up @@ -159,7 +159,6 @@
"monaco-editor": "0.40.0",
"react-devtools-inline": "4.28.0",
"react-transition-group": "4.4.5",
"typescript": "5.2.2",
"webpack": "5.88.2"
},
"typings": "./index.d.ts",
Expand Down
85 changes: 4 additions & 81 deletions packages/toolpad-app/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import invariant from 'invariant';
import {
QueryClient,
useMutation,
UseMutationOptions,
UseMutationResult,
useQuery,
UseQueryOptions,
UseQueryResult,
} from '@tanstack/react-query';
import type { Definition, MethodsGroup, MethodsOf, ServerDefinition } from './server/rpc';
import { createRpcClient } from './rpcClient';
import { QueryClient } from '@tanstack/react-query';
import { createRpcApi } from './rpcClient';
import type { ServerDefinition } from './server/rpcServer';

export const queryClient = new QueryClient({
defaultOptions: {
Expand All @@ -22,72 +13,4 @@ export const queryClient = new QueryClient({
},
});

export interface UseQueryFnOptions<F extends (...args: any[]) => any>
extends Omit<
UseQueryOptions<Awaited<ReturnType<F>>, unknown, Awaited<ReturnType<F>>, any[]>,
'queryKey' | 'queryFn'
> {}

interface UseQueryFn<M extends MethodsGroup> {
<K extends keyof M & string>(
name: K,
params: Parameters<M[K]> | null,
options?: UseQueryFnOptions<M[K]>,
): UseQueryResult<Awaited<ReturnType<M[K]>>>;
}

interface UseMutationFn<M extends MethodsGroup> {
<K extends keyof M & string>(
name: K,
options?: UseMutationOptions<any, unknown, Parameters<M[K]>>,
): UseMutationResult<Awaited<ReturnType<M[K]>>, unknown, Parameters<M[K]>>;
}

interface RpcClient<D extends Definition> {
query: D['query'];
mutation: D['mutation'];
}

interface ApiClient<D extends Definition> extends RpcClient<D> {
query: D['query'];
mutation: D['mutation'];
useQuery: UseQueryFn<D['query']>;
useMutation: UseMutationFn<D['mutation']>;
refetchQueries: <K extends keyof D['query']>(
key: K,
params?: Parameters<D['query'][K]>,
) => Promise<void>;
invalidateQueries: <K extends keyof D['query']>(
key: K,
params?: Parameters<D['query'][K]>,
) => Promise<void>;
}

function createClient<D extends MethodsOf<any>>(endpoint: string): ApiClient<D> {
const { query, mutation } = createRpcClient<D>(endpoint);

return {
query,
mutation,
useQuery: (key, params, options) => {
return useQuery({
...options,
enabled: !!params && options?.enabled !== false,
queryKey: [key, params],
queryFn: () => {
invariant(params, `"enabled" prop of useQuery should prevent this call'`);
return query[key](...params);
},
});
},
useMutation: (key, options) => useMutation((params) => mutation[key](...params), options),
refetchQueries(key, params?) {
return queryClient.refetchQueries(params ? [key, params] : [key]);
},
invalidateQueries(key, params?) {
return queryClient.invalidateQueries(params ? [key, params] : [key]);
},
};
}

export default createClient<ServerDefinition>('/api/rpc');
export default createRpcApi<ServerDefinition>(queryClient, '/api/rpc');
3 changes: 2 additions & 1 deletion packages/toolpad-app/src/canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import * as React from 'react';
import invariant from 'invariant';
import { throttle } from 'lodash-es';
import { CanvasEventsContext } from '@mui/toolpad-core/runtime';
import ToolpadApp, { LoadComponents, queryClient } from '../runtime/ToolpadApp';
import ToolpadApp, { LoadComponents } from '../runtime/ToolpadApp';
import { queryClient } from '../runtime/api';
import { AppCanvasState } from '../types';
import getPageViewState from './getPageViewState';
import { rectContainsPoint } from '../utils/geometry';
Expand Down
94 changes: 91 additions & 3 deletions packages/toolpad-app/src/rpcClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { parse as superjsonParse } from 'superjson';
import type { MethodsOf, MethodsOfGroup, RpcRequest, RpcResponse } from './server/rpc';
import invariant from 'invariant';
import {
QueryClient,
useMutation,
UseMutationOptions,
UseMutationResult,
useQuery,
UseQueryOptions,
UseQueryResult,
} from '@tanstack/react-query';
import type {
MethodsOf,
MethodsOfGroup,
RpcRequest,
RpcResponse,
Definition,
MethodsGroup,
} from './server/rpc';

function createFetcher(endpoint: string, type: 'query' | 'mutation'): MethodsOfGroup<any> {
function createFetcher(endpoint: string | URL, type: 'query' | 'mutation'): MethodsOfGroup<any> {
return new Proxy(
{},
{
Expand Down Expand Up @@ -46,8 +63,79 @@ export interface RpcClient<D extends MethodsOf<any>> {
mutation: D['mutation'];
}

export function createRpcClient<D extends MethodsOf<any>>(endpoint: string): RpcClient<D> {
export function createRpcClient<D extends MethodsOf<any>>(endpoint: string | URL): RpcClient<D> {
const query = createFetcher(endpoint, 'query');
const mutation = createFetcher(endpoint, 'mutation');
return { query, mutation };
}

export interface UseQueryFnOptions<F extends (...args: any[]) => any>
extends Omit<
UseQueryOptions<Awaited<ReturnType<F>>, unknown, Awaited<ReturnType<F>>, any[]>,
'queryKey' | 'queryFn'
> {}

export interface UseQueryFn<M extends MethodsGroup> {
<K extends keyof M & string>(
name: K,
params: Parameters<M[K]> | null,
options?: UseQueryFnOptions<M[K]>,
): UseQueryResult<Awaited<ReturnType<M[K]>>>;
}

export interface UseMutationFn<M extends MethodsGroup> {
<K extends keyof M & string>(
name: K,
options?: UseMutationOptions<any, unknown, Parameters<M[K]>>,
): UseMutationResult<Awaited<ReturnType<M[K]>>, unknown, Parameters<M[K]>>;
}

export interface RpcPiClient<D extends Definition> {
query: D['query'];
mutation: D['mutation'];
}

export interface ApiClient<D extends Definition> extends RpcPiClient<D> {
query: D['query'];
mutation: D['mutation'];
useQuery: UseQueryFn<D['query']>;
useMutation: UseMutationFn<D['mutation']>;
refetchQueries: <K extends keyof D['query']>(
key: K,
params?: Parameters<D['query'][K]>,
) => Promise<void>;
invalidateQueries: <K extends keyof D['query']>(
key: K,
params?: Parameters<D['query'][K]>,
) => Promise<void>;
}

export function createRpcApi<D extends MethodsOf<any>>(
queryClient: QueryClient,
endpoint: string | URL,
): ApiClient<D> {
const { query, mutation } = createRpcClient<D>(endpoint);

return {
query,
mutation,
useQuery: (key, params, options) => {
return useQuery({
...options,
enabled: !!params && options?.enabled !== false,
queryKey: [key, params],
queryFn: () => {
invariant(params, `"enabled" prop of useQuery should prevent this call'`);
return query[key](...params);
},
});
},
useMutation: (key, options) => useMutation((params) => mutation[key](...params), options),
refetchQueries(key, params?) {
return queryClient.refetchQueries(params ? [key, params] : [key]);
},
invalidateQueries(key, params?) {
return queryClient.invalidateQueries(params ? [key, params] : [key]);
},
};
}
44 changes: 17 additions & 27 deletions packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
import { createProvidedContext, useAssertedContext } from '@mui/toolpad-utils/react';
import { mapProperties, mapValues } from '@mui/toolpad-utils/collections';
import { set as setObjectPath } from 'lodash-es';
import { QueryClient, QueryClientProvider, useMutation } from '@tanstack/react-query';
import { QueryClientProvider, useMutation } from '@tanstack/react-query';
import {
BrowserRouter,
Routes,
Expand Down Expand Up @@ -79,11 +79,12 @@ import evalJsBindings, {
} from './evalJsBindings';
import { HTML_ID_EDITOR_OVERLAY } from './constants';
import { layoutBoxArgTypes } from './toolpadComponents/layoutBox';
import { execDataSourceQuery, useDataQuery, UseFetch } from './useDataQuery';
import { useDataQuery, UseFetch } from './useDataQuery';
import { NavigateToPage } from './CanvasHooksContext';
import PreviewHeader from './PreviewHeader';
import useEvent from '../utils/useEvent';
import { AppLayout } from './AppLayout';
import api, { queryClient } from './api';

const browserJsRuntime = getBrowserRuntime();

Expand Down Expand Up @@ -1266,7 +1267,6 @@ function MutationNode({ node, page }: MutationNodeProps) {

const { bindings } = useAssertedContext(RuntimeScopeContext);

const queryId = node.id;
const { value: params } = resolveBindables(
bindings,
`${node.id}.params`,
Expand All @@ -1279,37 +1279,36 @@ function MutationNode({ node, page }: MutationNodeProps) {
error: fetchError,
mutateAsync,
} = useMutation(
async (overrides: any = {}) =>
execDataSourceQuery({
pageName: page.name,
queryName: node.name,
params: { ...params, ...overrides },
}),
async (overrides: any = {}) => {
return api.mutation.execQuery(page.name, node.name, { ...params, ...overrides });
},
{
mutationKey: [queryId, params],
mutationKey: [node.name, params],
},
);

const { data, error: apiError } = responseData;
const { data, error: apiError } = responseData || EMPTY_OBJECT;

const error = apiError || fetchError;

// Stabilize the mutation and prepare for inclusion in global scope
const mutationResult: UseFetch = React.useMemo(
() => ({
const mutationResult: UseFetch = React.useMemo(() => {
const call = async (overrides: any = {}) => {
await mutateAsync(overrides);
};
return {
isLoading,
isFetching: isLoading,
error,
data,
rows: Array.isArray(data) ? data : EMPTY_ARRAY,
call: mutateAsync,
fetch: mutateAsync,
call,
fetch: call,
refetch: () => {
throw new Error(`refetch is not supported in manual queries`);
},
}),
[isLoading, error, mutateAsync, data],
);
};
}, [isLoading, error, data, mutateAsync]);

React.useEffect(() => {
for (const [key, value] of Object.entries(mutationResult)) {
Expand Down Expand Up @@ -1471,15 +1470,6 @@ function AppError({ error }: FallbackProps) {
);
}

export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: 60 * 1000,
},
},
});

export interface ToolpadAppLayoutProps {
dom: appDom.RenderTree;
hasShell?: boolean;
Expand Down
20 changes: 20 additions & 0 deletions packages/toolpad-app/src/runtime/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { QueryClient } from '@tanstack/react-query';
import { createRpcApi } from '../rpcClient';
import type { ServerDefinition } from '../server/rpcRuntimeServer';

export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
networkMode: 'always',
},
mutations: {
networkMode: 'always',
},
},
});

export default createRpcApi<ServerDefinition>(
queryClient,
new URL(`${process.env.BASE_URL}/api/runtime-rpc`, window.location.href),
);
Loading