Skip to content

Commit

Permalink
Rewrite execute reducer with RTK
Browse files Browse the repository at this point in the history
We now use the Flux standard action `meta` property instead of our own
`extra` property when sending to the backend server.
  • Loading branch information
shepmaster committed Mar 29, 2023
1 parent c2e0ec6 commit c6191cf
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 170 deletions.
2 changes: 2 additions & 0 deletions ui/frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ module.exports = {
'PopButton.tsx',
'editor/AceEditor.tsx',
'editor/SimpleEditor.tsx',
'reducers/output/execute.ts',
'reducers/output/format.ts',
'reducers/output/gist.ts',
'websocketActions.ts',
'websocketMiddleware.ts',
],
extends: ['prettier'],
Expand Down
2 changes: 2 additions & 0 deletions ui/frontend/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ node_modules
!PopButton.tsx
!editor/AceEditor.tsx
!editor/SimpleEditor.tsx
!reducers/output/execute.ts
!reducers/output/format.ts
!reducers/output/gist.ts
!websocketActions.ts
!websocketMiddleware.ts
97 changes: 3 additions & 94 deletions ui/frontend/actions.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import fetch from 'isomorphic-fetch';
import { ThunkAction as ReduxThunkAction } from '@reduxjs/toolkit';
import { ThunkAction as ReduxThunkAction, AnyAction } from '@reduxjs/toolkit';
import { z } from 'zod';

import {
codeSelector,
clippyRequestSelector,
getCrateType,
runAsTest,
useWebsocketSelector,
} from './selectors';
import State from './state';
import {
Expand All @@ -33,6 +32,7 @@ import {
Crate,
} from './types';

import { ExecuteRequestBody, performCommonExecute, wsExecuteRequest } from './reducers/output/execute';
import { performGistLoad } from './reducers/output/gist';

export const routes = {
Expand All @@ -58,6 +58,7 @@ export const routes = {
};

export type ThunkAction<T = void> = ReduxThunkAction<T, State, {}, Action>;
export type SimpleThunkAction<T = void> = ReduxThunkAction<T, State, {}, AnyAction>;

const createAction = <T extends string, P extends {}>(type: T, props?: P) => (
Object.assign({ type }, props)
Expand All @@ -82,9 +83,6 @@ export enum ActionType {
ChangeEdition = 'CHANGE_EDITION',
ChangeBacktrace = 'CHANGE_BACKTRACE',
ChangeFocus = 'CHANGE_FOCUS',
ExecuteRequest = 'EXECUTE_REQUEST',
ExecuteSucceeded = 'EXECUTE_SUCCEEDED',
ExecuteFailed = 'EXECUTE_FAILED',
CompileAssemblyRequest = 'COMPILE_ASSEMBLY_REQUEST',
CompileAssemblySucceeded = 'COMPILE_ASSEMBLY_SUCCEEDED',
CompileAssemblyFailed = 'COMPILE_ASSEMBLY_FAILED',
Expand Down Expand Up @@ -126,8 +124,6 @@ export enum ActionType {
WebSocketConnected = 'WEBSOCKET_CONNECTED',
WebSocketDisconnected = 'WEBSOCKET_DISCONNECTED',
WebSocketFeatureFlagEnabled = 'WEBSOCKET_FEATURE_FLAG_ENABLED',
WSExecuteRequest = 'WS_EXECUTE_REQUEST',
WSExecuteResponse = 'WS_EXECUTE_RESPONSE',
}

export const WebSocketError = z.object({
Expand All @@ -136,20 +132,6 @@ export const WebSocketError = z.object({
});
export type WebSocketError = z.infer<typeof WebSocketError>;

const ExecuteExtra = z.object({
sequenceNumber: z.number(),
});
type ExecuteExtra = z.infer<typeof ExecuteExtra>;

export const WSExecuteResponse = z.object({
type: z.literal(ActionType.WSExecuteResponse),
success: z.boolean(),
stdout: z.string(),
stderr: z.string(),
extra: ExecuteExtra,
});
export type WSExecuteResponse = z.infer<typeof WSExecuteResponse>;

export const initializeApplication = () => createAction(ActionType.InitializeApplication);

export const disableSyncChangesToStorage = () => createAction(ActionType.DisableSyncChangesToStorage);
Expand Down Expand Up @@ -210,20 +192,6 @@ export const reExecuteWithBacktrace = (): ThunkAction => dispatch => {
export const changeFocus = (focus?: Focus) =>
createAction(ActionType.ChangeFocus, { focus });

interface ExecuteResponseBody {
stdout: string;
stderr: string;
}

const requestExecute = () =>
createAction(ActionType.ExecuteRequest);

const receiveExecuteSuccess = ({ stdout, stderr }: ExecuteResponseBody) =>
createAction(ActionType.ExecuteSucceeded, { stdout, stderr });

const receiveExecuteFailure = ({ error }: { error?: string }) =>
createAction(ActionType.ExecuteFailed, { error });

type FetchArg = Parameters<typeof fetch>[0];

export function jsonGet(url: FetchArg) {
Expand Down Expand Up @@ -298,35 +266,6 @@ export const adaptFetchError = async <R>(cb: () => Promise<R>): Promise<R> => {
}
}

interface ExecuteRequestBody {
channel: string;
mode: string;
crateType: string;
tests: boolean;
code: string;
edition: string;
backtrace: boolean;
}

const performCommonExecute = (crateType: string, tests: boolean): ThunkAction => (dispatch, getState) => {
const state = getState();
const code = codeSelector(state);
const { configuration: { channel, mode, edition } } = state;
const backtrace = state.configuration.backtrace === Backtrace.Enabled;

if (useWebsocketSelector(state)) {
return dispatch(wsExecuteRequest(channel, mode, edition, crateType, tests, code, backtrace));
} else {
dispatch(requestExecute());

const body: ExecuteRequestBody = { channel, mode, edition, crateType, tests, code, backtrace };

return jsonPost<ExecuteResponseBody>(routes.execute, body)
.then(json => dispatch(receiveExecuteSuccess(json)))
.catch(json => dispatch(receiveExecuteFailure(json)));
}
};

function performAutoOnly(): ThunkAction {
return function(dispatch, getState) {
const state = getState();
Expand Down Expand Up @@ -506,32 +445,6 @@ const PRIMARY_ACTIONS: { [index in PrimaryAction]: () => ThunkAction } = {
[PrimaryActionCore.Wasm]: performCompileToNightlyWasmOnly,
};

let sequenceNumber = 0;
const nextSequenceNumber = () => sequenceNumber++;
const makeExtra = (): ExecuteExtra => ({
sequenceNumber: nextSequenceNumber(),
});

const wsExecuteRequest = (
channel: Channel,
mode: Mode,
edition: Edition,
crateType: string,
tests: boolean,
code: string,
backtrace: boolean
) =>
createAction(ActionType.WSExecuteRequest, {
channel,
mode,
edition,
crateType,
tests,
code,
backtrace,
extra: makeExtra(),
});

export const performPrimaryAction = (): ThunkAction => (dispatch, getState) => {
const state = getState();
const primaryAction = PRIMARY_ACTIONS[state.configuration.primaryAction];
Expand Down Expand Up @@ -871,9 +784,6 @@ export type Action =
| ReturnType<typeof changeProcessAssembly>
| ReturnType<typeof changeAceTheme>
| ReturnType<typeof changeMonacoTheme>
| ReturnType<typeof requestExecute>
| ReturnType<typeof receiveExecuteSuccess>
| ReturnType<typeof receiveExecuteFailure>
| ReturnType<typeof requestCompileAssembly>
| ReturnType<typeof receiveCompileAssemblySuccess>
| ReturnType<typeof receiveCompileAssemblyFailure>
Expand Down Expand Up @@ -916,5 +826,4 @@ export type Action =
| ReturnType<typeof websocketDisconnected>
| ReturnType<typeof websocketFeatureFlagEnabled>
| ReturnType<typeof wsExecuteRequest>
| WSExecuteResponse
;
164 changes: 127 additions & 37 deletions ui/frontend/reducers/output/execute.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { Action, ActionType } from '../../actions';
import { finish, start } from './sharedStateManagement';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import * as z from 'zod';

const DEFAULT: State = {
import { SimpleThunkAction, adaptFetchError, jsonPost, routes } from '../../actions';
import { executeRequestPayloadSelector, useWebsocketSelector } from '../../selectors';
import { Channel, Edition, Mode } from '../../types';
import {
WsPayloadAction,
createWebsocketResponseAction,
createWebsocketResponseSchema,
makeWebSocketMeta,
} from '../../websocketActions';

const initialState: State = {
requestsInProgress: 0,
};

Expand All @@ -13,38 +23,118 @@ interface State {
error?: string;
}

export default function execute(state = DEFAULT, action: Action) {
switch (action.type) {
case ActionType.ExecuteRequest:
return start(DEFAULT, state);
case ActionType.WSExecuteRequest: {
const { extra: { sequenceNumber } } = action;
if (sequenceNumber >= (state.sequenceNumber ?? 0)) {
const requestsInProgress = 1; // Only tracking one request
return {...state, sequenceNumber, requestsInProgress };
} else {
return state;
}
}
case ActionType.ExecuteSucceeded: {
const { stdout = '', stderr = '' } = action;
return finish(state, { stdout, stderr });
}
case ActionType.ExecuteFailed: {
const { error } = action;
return finish(state, { error });
}
case ActionType.WSExecuteResponse: {
const { stdout, stderr, extra: { sequenceNumber } } = action;

if (sequenceNumber >= (state.sequenceNumber ?? 0)) {
const requestsInProgress = 0; // Only tracking one request
return { ...state, stdout, stderr, requestsInProgress };
} else {
return state;
}
}
default:
return state;
}
const wsExecuteResponsePayloadSchema = z.object({
success: z.boolean(),
stdout: z.string(),
stderr: z.string(),
});
type wsExecuteResponsePayload = z.infer<typeof wsExecuteResponsePayloadSchema>;

type wsExecuteRequestPayload = {
channel: Channel;
mode: Mode;
edition: Edition;
crateType: string;
tests: boolean;
code: string;
backtrace: boolean;
};

const wsExecuteResponse = createWebsocketResponseAction<wsExecuteResponsePayload>(
'output/execute/wsExecuteResponse',
);

const sliceName = 'output/execute';

export interface ExecuteRequestBody {
channel: string;
mode: string;
crateType: string;
tests: boolean;
code: string;
edition: string;
backtrace: boolean;
}

interface ExecuteResponseBody {
success: boolean;
stdout: string;
stderr: string;
}

export const performExecute = createAsyncThunk(sliceName, async (payload: ExecuteRequestBody) =>
adaptFetchError(() => jsonPost<ExecuteResponseBody>(routes.execute, payload)),
);

const slice = createSlice({
name: 'output/execute',
initialState,
reducers: {
wsExecuteRequest: {
reducer: (state, action: WsPayloadAction<wsExecuteRequestPayload>) => {
const { sequenceNumber } = action.meta;
if (sequenceNumber >= (state.sequenceNumber ?? 0)) {
state.sequenceNumber = sequenceNumber;
state.requestsInProgress = 1; // Only tracking one request
}
},

prepare: (payload: wsExecuteRequestPayload) => ({
payload,
meta: makeWebSocketMeta(),
}),
},
},
extraReducers: (builder) => {
builder
.addCase(performExecute.pending, (state) => {
state.requestsInProgress += 1;
})
.addCase(performExecute.fulfilled, (state, action) => {
const { stdout, stderr } = action.payload;
Object.assign(state, { stdout, stderr });
state.requestsInProgress -= 1;
})
.addCase(performExecute.rejected, (state, action) => {
if (action.payload) {
} else {
state.error = action.error.message;
}
state.requestsInProgress -= 1;
})
.addCase(wsExecuteResponse, (state, action) => {
const {
payload: { stdout, stderr },
meta: { sequenceNumber },
} = action;

if (sequenceNumber >= (state.sequenceNumber ?? 0)) {
Object.assign(state, { stdout, stderr });
state.requestsInProgress = 0; // Only tracking one request
}
});
},
});

export const { wsExecuteRequest } = slice.actions;

export const performCommonExecute =
(crateType: string, tests: boolean): SimpleThunkAction =>
(dispatch, getState) => {
const state = getState();
const body = executeRequestPayloadSelector(state, { crateType, tests });
const useWebSocket = useWebsocketSelector(state);

if (useWebSocket) {
dispatch(wsExecuteRequest(body));
} else {
dispatch(performExecute(body));
}
};

export const wsExecuteResponseSchema = createWebsocketResponseSchema(
wsExecuteResponse,
wsExecuteResponsePayloadSchema,
);

export default slice.reducer;
5 changes: 3 additions & 2 deletions ui/frontend/reducers/output/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Action, ActionType } from '../../actions';
import { Focus } from '../../types';
import { performGistLoad, performGistSave } from './gist';
import { performFormat } from './format';
import { performExecute, wsExecuteRequest } from './execute';

const DEFAULT: State = {
};
Expand Down Expand Up @@ -39,8 +40,8 @@ export default function meta(state = DEFAULT, action: Action) {
case ActionType.CompileAssemblyRequest:
return { ...state, focus: Focus.Asm };

case ActionType.ExecuteRequest:
case ActionType.WSExecuteRequest:
case performExecute.pending.type:
case wsExecuteRequest.type:
return { ...state, focus: Focus.Execute };

default: {
Expand Down
Loading

0 comments on commit c6191cf

Please sign in to comment.