Skip to content

Commit c6191cf

Browse files
committed
Rewrite execute reducer with RTK
We now use the Flux standard action `meta` property instead of our own `extra` property when sending to the backend server.
1 parent c2e0ec6 commit c6191cf

File tree

9 files changed

+231
-170
lines changed

9 files changed

+231
-170
lines changed

ui/frontend/.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ module.exports = {
6565
'PopButton.tsx',
6666
'editor/AceEditor.tsx',
6767
'editor/SimpleEditor.tsx',
68+
'reducers/output/execute.ts',
6869
'reducers/output/format.ts',
6970
'reducers/output/gist.ts',
71+
'websocketActions.ts',
7072
'websocketMiddleware.ts',
7173
],
7274
extends: ['prettier'],

ui/frontend/.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ node_modules
1414
!PopButton.tsx
1515
!editor/AceEditor.tsx
1616
!editor/SimpleEditor.tsx
17+
!reducers/output/execute.ts
1718
!reducers/output/format.ts
1819
!reducers/output/gist.ts
20+
!websocketActions.ts
1921
!websocketMiddleware.ts

ui/frontend/actions.ts

Lines changed: 3 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import fetch from 'isomorphic-fetch';
2-
import { ThunkAction as ReduxThunkAction } from '@reduxjs/toolkit';
2+
import { ThunkAction as ReduxThunkAction, AnyAction } from '@reduxjs/toolkit';
33
import { z } from 'zod';
44

55
import {
66
codeSelector,
77
clippyRequestSelector,
88
getCrateType,
99
runAsTest,
10-
useWebsocketSelector,
1110
} from './selectors';
1211
import State from './state';
1312
import {
@@ -33,6 +32,7 @@ import {
3332
Crate,
3433
} from './types';
3534

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

3838
export const routes = {
@@ -58,6 +58,7 @@ export const routes = {
5858
};
5959

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

6263
const createAction = <T extends string, P extends {}>(type: T, props?: P) => (
6364
Object.assign({ type }, props)
@@ -82,9 +83,6 @@ export enum ActionType {
8283
ChangeEdition = 'CHANGE_EDITION',
8384
ChangeBacktrace = 'CHANGE_BACKTRACE',
8485
ChangeFocus = 'CHANGE_FOCUS',
85-
ExecuteRequest = 'EXECUTE_REQUEST',
86-
ExecuteSucceeded = 'EXECUTE_SUCCEEDED',
87-
ExecuteFailed = 'EXECUTE_FAILED',
8886
CompileAssemblyRequest = 'COMPILE_ASSEMBLY_REQUEST',
8987
CompileAssemblySucceeded = 'COMPILE_ASSEMBLY_SUCCEEDED',
9088
CompileAssemblyFailed = 'COMPILE_ASSEMBLY_FAILED',
@@ -126,8 +124,6 @@ export enum ActionType {
126124
WebSocketConnected = 'WEBSOCKET_CONNECTED',
127125
WebSocketDisconnected = 'WEBSOCKET_DISCONNECTED',
128126
WebSocketFeatureFlagEnabled = 'WEBSOCKET_FEATURE_FLAG_ENABLED',
129-
WSExecuteRequest = 'WS_EXECUTE_REQUEST',
130-
WSExecuteResponse = 'WS_EXECUTE_RESPONSE',
131127
}
132128

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

139-
const ExecuteExtra = z.object({
140-
sequenceNumber: z.number(),
141-
});
142-
type ExecuteExtra = z.infer<typeof ExecuteExtra>;
143-
144-
export const WSExecuteResponse = z.object({
145-
type: z.literal(ActionType.WSExecuteResponse),
146-
success: z.boolean(),
147-
stdout: z.string(),
148-
stderr: z.string(),
149-
extra: ExecuteExtra,
150-
});
151-
export type WSExecuteResponse = z.infer<typeof WSExecuteResponse>;
152-
153135
export const initializeApplication = () => createAction(ActionType.InitializeApplication);
154136

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

213-
interface ExecuteResponseBody {
214-
stdout: string;
215-
stderr: string;
216-
}
217-
218-
const requestExecute = () =>
219-
createAction(ActionType.ExecuteRequest);
220-
221-
const receiveExecuteSuccess = ({ stdout, stderr }: ExecuteResponseBody) =>
222-
createAction(ActionType.ExecuteSucceeded, { stdout, stderr });
223-
224-
const receiveExecuteFailure = ({ error }: { error?: string }) =>
225-
createAction(ActionType.ExecuteFailed, { error });
226-
227195
type FetchArg = Parameters<typeof fetch>[0];
228196

229197
export function jsonGet(url: FetchArg) {
@@ -298,35 +266,6 @@ export const adaptFetchError = async <R>(cb: () => Promise<R>): Promise<R> => {
298266
}
299267
}
300268

301-
interface ExecuteRequestBody {
302-
channel: string;
303-
mode: string;
304-
crateType: string;
305-
tests: boolean;
306-
code: string;
307-
edition: string;
308-
backtrace: boolean;
309-
}
310-
311-
const performCommonExecute = (crateType: string, tests: boolean): ThunkAction => (dispatch, getState) => {
312-
const state = getState();
313-
const code = codeSelector(state);
314-
const { configuration: { channel, mode, edition } } = state;
315-
const backtrace = state.configuration.backtrace === Backtrace.Enabled;
316-
317-
if (useWebsocketSelector(state)) {
318-
return dispatch(wsExecuteRequest(channel, mode, edition, crateType, tests, code, backtrace));
319-
} else {
320-
dispatch(requestExecute());
321-
322-
const body: ExecuteRequestBody = { channel, mode, edition, crateType, tests, code, backtrace };
323-
324-
return jsonPost<ExecuteResponseBody>(routes.execute, body)
325-
.then(json => dispatch(receiveExecuteSuccess(json)))
326-
.catch(json => dispatch(receiveExecuteFailure(json)));
327-
}
328-
};
329-
330269
function performAutoOnly(): ThunkAction {
331270
return function(dispatch, getState) {
332271
const state = getState();
@@ -506,32 +445,6 @@ const PRIMARY_ACTIONS: { [index in PrimaryAction]: () => ThunkAction } = {
506445
[PrimaryActionCore.Wasm]: performCompileToNightlyWasmOnly,
507446
};
508447

509-
let sequenceNumber = 0;
510-
const nextSequenceNumber = () => sequenceNumber++;
511-
const makeExtra = (): ExecuteExtra => ({
512-
sequenceNumber: nextSequenceNumber(),
513-
});
514-
515-
const wsExecuteRequest = (
516-
channel: Channel,
517-
mode: Mode,
518-
edition: Edition,
519-
crateType: string,
520-
tests: boolean,
521-
code: string,
522-
backtrace: boolean
523-
) =>
524-
createAction(ActionType.WSExecuteRequest, {
525-
channel,
526-
mode,
527-
edition,
528-
crateType,
529-
tests,
530-
code,
531-
backtrace,
532-
extra: makeExtra(),
533-
});
534-
535448
export const performPrimaryAction = (): ThunkAction => (dispatch, getState) => {
536449
const state = getState();
537450
const primaryAction = PRIMARY_ACTIONS[state.configuration.primaryAction];
@@ -871,9 +784,6 @@ export type Action =
871784
| ReturnType<typeof changeProcessAssembly>
872785
| ReturnType<typeof changeAceTheme>
873786
| ReturnType<typeof changeMonacoTheme>
874-
| ReturnType<typeof requestExecute>
875-
| ReturnType<typeof receiveExecuteSuccess>
876-
| ReturnType<typeof receiveExecuteFailure>
877787
| ReturnType<typeof requestCompileAssembly>
878788
| ReturnType<typeof receiveCompileAssemblySuccess>
879789
| ReturnType<typeof receiveCompileAssemblyFailure>
@@ -916,5 +826,4 @@ export type Action =
916826
| ReturnType<typeof websocketDisconnected>
917827
| ReturnType<typeof websocketFeatureFlagEnabled>
918828
| ReturnType<typeof wsExecuteRequest>
919-
| WSExecuteResponse
920829
;
Lines changed: 127 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1-
import { Action, ActionType } from '../../actions';
2-
import { finish, start } from './sharedStateManagement';
1+
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
2+
import * as z from 'zod';
33

4-
const DEFAULT: State = {
4+
import { SimpleThunkAction, adaptFetchError, jsonPost, routes } from '../../actions';
5+
import { executeRequestPayloadSelector, useWebsocketSelector } from '../../selectors';
6+
import { Channel, Edition, Mode } from '../../types';
7+
import {
8+
WsPayloadAction,
9+
createWebsocketResponseAction,
10+
createWebsocketResponseSchema,
11+
makeWebSocketMeta,
12+
} from '../../websocketActions';
13+
14+
const initialState: State = {
515
requestsInProgress: 0,
616
};
717

@@ -13,38 +23,118 @@ interface State {
1323
error?: string;
1424
}
1525

16-
export default function execute(state = DEFAULT, action: Action) {
17-
switch (action.type) {
18-
case ActionType.ExecuteRequest:
19-
return start(DEFAULT, state);
20-
case ActionType.WSExecuteRequest: {
21-
const { extra: { sequenceNumber } } = action;
22-
if (sequenceNumber >= (state.sequenceNumber ?? 0)) {
23-
const requestsInProgress = 1; // Only tracking one request
24-
return {...state, sequenceNumber, requestsInProgress };
25-
} else {
26-
return state;
27-
}
28-
}
29-
case ActionType.ExecuteSucceeded: {
30-
const { stdout = '', stderr = '' } = action;
31-
return finish(state, { stdout, stderr });
32-
}
33-
case ActionType.ExecuteFailed: {
34-
const { error } = action;
35-
return finish(state, { error });
36-
}
37-
case ActionType.WSExecuteResponse: {
38-
const { stdout, stderr, extra: { sequenceNumber } } = action;
39-
40-
if (sequenceNumber >= (state.sequenceNumber ?? 0)) {
41-
const requestsInProgress = 0; // Only tracking one request
42-
return { ...state, stdout, stderr, requestsInProgress };
43-
} else {
44-
return state;
45-
}
46-
}
47-
default:
48-
return state;
49-
}
26+
const wsExecuteResponsePayloadSchema = z.object({
27+
success: z.boolean(),
28+
stdout: z.string(),
29+
stderr: z.string(),
30+
});
31+
type wsExecuteResponsePayload = z.infer<typeof wsExecuteResponsePayloadSchema>;
32+
33+
type wsExecuteRequestPayload = {
34+
channel: Channel;
35+
mode: Mode;
36+
edition: Edition;
37+
crateType: string;
38+
tests: boolean;
39+
code: string;
40+
backtrace: boolean;
41+
};
42+
43+
const wsExecuteResponse = createWebsocketResponseAction<wsExecuteResponsePayload>(
44+
'output/execute/wsExecuteResponse',
45+
);
46+
47+
const sliceName = 'output/execute';
48+
49+
export interface ExecuteRequestBody {
50+
channel: string;
51+
mode: string;
52+
crateType: string;
53+
tests: boolean;
54+
code: string;
55+
edition: string;
56+
backtrace: boolean;
57+
}
58+
59+
interface ExecuteResponseBody {
60+
success: boolean;
61+
stdout: string;
62+
stderr: string;
5063
}
64+
65+
export const performExecute = createAsyncThunk(sliceName, async (payload: ExecuteRequestBody) =>
66+
adaptFetchError(() => jsonPost<ExecuteResponseBody>(routes.execute, payload)),
67+
);
68+
69+
const slice = createSlice({
70+
name: 'output/execute',
71+
initialState,
72+
reducers: {
73+
wsExecuteRequest: {
74+
reducer: (state, action: WsPayloadAction<wsExecuteRequestPayload>) => {
75+
const { sequenceNumber } = action.meta;
76+
if (sequenceNumber >= (state.sequenceNumber ?? 0)) {
77+
state.sequenceNumber = sequenceNumber;
78+
state.requestsInProgress = 1; // Only tracking one request
79+
}
80+
},
81+
82+
prepare: (payload: wsExecuteRequestPayload) => ({
83+
payload,
84+
meta: makeWebSocketMeta(),
85+
}),
86+
},
87+
},
88+
extraReducers: (builder) => {
89+
builder
90+
.addCase(performExecute.pending, (state) => {
91+
state.requestsInProgress += 1;
92+
})
93+
.addCase(performExecute.fulfilled, (state, action) => {
94+
const { stdout, stderr } = action.payload;
95+
Object.assign(state, { stdout, stderr });
96+
state.requestsInProgress -= 1;
97+
})
98+
.addCase(performExecute.rejected, (state, action) => {
99+
if (action.payload) {
100+
} else {
101+
state.error = action.error.message;
102+
}
103+
state.requestsInProgress -= 1;
104+
})
105+
.addCase(wsExecuteResponse, (state, action) => {
106+
const {
107+
payload: { stdout, stderr },
108+
meta: { sequenceNumber },
109+
} = action;
110+
111+
if (sequenceNumber >= (state.sequenceNumber ?? 0)) {
112+
Object.assign(state, { stdout, stderr });
113+
state.requestsInProgress = 0; // Only tracking one request
114+
}
115+
});
116+
},
117+
});
118+
119+
export const { wsExecuteRequest } = slice.actions;
120+
121+
export const performCommonExecute =
122+
(crateType: string, tests: boolean): SimpleThunkAction =>
123+
(dispatch, getState) => {
124+
const state = getState();
125+
const body = executeRequestPayloadSelector(state, { crateType, tests });
126+
const useWebSocket = useWebsocketSelector(state);
127+
128+
if (useWebSocket) {
129+
dispatch(wsExecuteRequest(body));
130+
} else {
131+
dispatch(performExecute(body));
132+
}
133+
};
134+
135+
export const wsExecuteResponseSchema = createWebsocketResponseSchema(
136+
wsExecuteResponse,
137+
wsExecuteResponsePayloadSchema,
138+
);
139+
140+
export default slice.reducer;

ui/frontend/reducers/output/meta.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Action, ActionType } from '../../actions';
22
import { Focus } from '../../types';
33
import { performGistLoad, performGistSave } from './gist';
44
import { performFormat } from './format';
5+
import { performExecute, wsExecuteRequest } from './execute';
56

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

42-
case ActionType.ExecuteRequest:
43-
case ActionType.WSExecuteRequest:
43+
case performExecute.pending.type:
44+
case wsExecuteRequest.type:
4445
return { ...state, focus: Focus.Execute };
4546

4647
default: {

0 commit comments

Comments
 (0)