Skip to content

Commit a9c3041

Browse files
leeyi45martin-henz
andauthored
Remove Concurrent and Interpreter (#3130)
* Remove concurrent variant * Fix broken tests * Begin fixing * Fix all broken workspace reducer tests * FIx more broken workspace saga tests * Refactor eval code to utilize more type safety * Minor refactoring * Fix broken tsc * Upgrade saga handler to allow using takeLatest or takeLeading * Modify sagas to use new combineSagaHandlers function * Add redux tests * Add more tests * Modify actions and sagas to use new redux utils * Fix Linting --------- Co-authored-by: Martin Henz <[email protected]>
1 parent 2f5bc86 commit a9c3041

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1864
-1951
lines changed

src/commons/__tests__/Markdown.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ test('Markdown page renders correct Source information', () => {
3131
const source3Default = <Markdown {...mockProps(Chapter.SOURCE_3, Variant.DEFAULT)} />;
3232
expect(source3Default.props.content).toContain('Source \xa73');
3333

34-
const source3Concurrent = <Markdown {...mockProps(Chapter.SOURCE_3, Variant.CONCURRENT)} />;
35-
expect(source3Concurrent.props.content).toContain('Source \xa73 Concurrent');
36-
3734
const source4Default = <Markdown {...mockProps(Chapter.SOURCE_4, Variant.DEFAULT)} />;
3835
expect(source4Default.props.content).toContain('Source \xa74');
3936
});

src/commons/application/ApplicationTypes.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
import { Chapter, Language, SourceError, Variant } from 'js-slang/dist/types';
1+
import { Chapter, Language, type SourceError, type Value, Variant } from 'js-slang/dist/types';
22

3-
import { AchievementState } from '../../features/achievement/AchievementTypes';
4-
import { DashboardState } from '../../features/dashboard/DashboardTypes';
5-
import { PlaygroundState } from '../../features/playground/PlaygroundTypes';
3+
import type { AchievementState } from '../../features/achievement/AchievementTypes';
4+
import type { DashboardState } from '../../features/dashboard/DashboardTypes';
5+
import type { PlaygroundState } from '../../features/playground/PlaygroundTypes';
66
import { PlaybackStatus, RecordingStatus } from '../../features/sourceRecorder/SourceRecorderTypes';
7-
import { StoriesEnvState, StoriesState } from '../../features/stories/StoriesTypes';
7+
import type { StoriesEnvState, StoriesState } from '../../features/stories/StoriesTypes';
88
import { freshSortState } from '../../pages/academy/grading/subcomponents/GradingSubmissionsTable';
99
import { WORKSPACE_BASE_PATHS } from '../../pages/fileSystem/createInBrowserFileSystem';
1010
import { defaultFeatureFlags, FeatureFlagsState } from '../featureFlags';
11-
import { FileSystemState } from '../fileSystem/FileSystemTypes';
12-
import { SideContentManagerState, SideContentState } from '../sideContent/SideContentTypes';
11+
import type { FileSystemState } from '../fileSystem/FileSystemTypes';
12+
import type { SideContentManagerState, SideContentState } from '../sideContent/SideContentTypes';
1313
import Constants from '../utils/Constants';
1414
import { createContext } from '../utils/JsSlangHelper';
15-
import {
15+
import type {
1616
DebuggerContext,
1717
WorkspaceLocation,
1818
WorkspaceManagerState,
1919
WorkspaceState
2020
} from '../workspace/WorkspaceTypes';
21-
import { RouterState } from './types/CommonsTypes';
21+
import type { RouterState } from './types/CommonsTypes';
2222
import { ExternalLibraryName } from './types/ExternalTypes';
23-
import { SessionState } from './types/SessionTypes';
24-
import { VscodeState as VscodeState } from './types/VscodeTypes';
23+
import type { SessionState } from './types/SessionTypes';
24+
import type { VscodeState as VscodeState } from './types/VscodeTypes';
2525

2626
export type OverallState = {
2727
readonly router: RouterState;
@@ -74,7 +74,7 @@ export type CodeOutput = {
7474
*/
7575
export type ResultOutput = {
7676
type: 'result';
77-
value: any;
77+
value: Value;
7878
consoleLogs: string[];
7979
runtime?: number;
8080
isProgram?: boolean;
@@ -160,7 +160,6 @@ type LanguageFeatures = Partial<{
160160
const variantDisplay: Map<Variant, string> = new Map([
161161
[Variant.TYPED, 'Typed'],
162162
[Variant.WASM, 'WebAssembly'],
163-
[Variant.CONCURRENT, 'Concurrent'],
164163
[Variant.NATIVE, 'Native'],
165164
[Variant.EXPLICIT_CONTROL, 'Explicit-Control']
166165
]);
@@ -266,7 +265,6 @@ const sourceSubLanguages: Array<Pick<SALanguage, 'chapter' | 'variant'>> = [
266265

267266
{ chapter: Chapter.SOURCE_3, variant: Variant.DEFAULT },
268267
{ chapter: Chapter.SOURCE_3, variant: Variant.TYPED },
269-
{ chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT },
270268
{ chapter: Chapter.SOURCE_3, variant: Variant.NATIVE },
271269

272270
{ chapter: Chapter.SOURCE_4, variant: Variant.DEFAULT },
@@ -288,13 +286,13 @@ export const sourceLanguages: SALanguage[] = sourceSubLanguages.map(sublang => {
288286
(variant === Variant.DEFAULT || variant === Variant.NATIVE || variant === Variant.TYPED);
289287

290288
// Enable CSE Machine for Source Chapter 3 and above
291-
supportedFeatures.cseMachine = chapter >= Chapter.SOURCE_3 && variant !== Variant.CONCURRENT;
289+
supportedFeatures.cseMachine = chapter >= Chapter.SOURCE_3;
292290

293291
// Local imports/exports require Source 2+ as Source 1 does not have lists.
294292
supportedFeatures.multiFile = chapter >= Chapter.SOURCE_2;
295293

296294
// Disable REPL for concurrent variants
297-
supportedFeatures.repl = variant !== Variant.CONCURRENT;
295+
supportedFeatures.repl = true;
298296

299297
return {
300298
...sublang,

src/commons/application/__tests__/ApplicationTypes.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,6 @@ describe('available Source language configurations', () => {
5555
variant: Variant.TYPED,
5656
supports: { dataVisualizer: true, cseMachine: true }
5757
},
58-
{
59-
chapter: Chapter.SOURCE_3,
60-
variant: Variant.CONCURRENT,
61-
supports: { dataVisualizer: true }
62-
},
6358
{
6459
chapter: Chapter.SOURCE_3,
6560
variant: Variant.NATIVE,

src/commons/application/__tests__/__snapshots__/ApplicationTypes.ts.snap

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -188,19 +188,6 @@ Array [
188188
},
189189
"variant": "typed",
190190
},
191-
Object {
192-
"chapter": 3,
193-
"displayName": "Source §3 Concurrent",
194-
"mainLanguage": "JavaScript",
195-
"supports": Object {
196-
"cseMachine": false,
197-
"dataVisualizer": true,
198-
"multiFile": true,
199-
"repl": false,
200-
"substVisualizer": false,
201-
},
202-
"variant": "concurrent",
203-
},
204191
Object {
205192
"chapter": 3,
206193
"displayName": "Source §3 Native",

src/commons/application/actions/SessionActions.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,27 @@ import {
44
unpublishedToBackendParams
55
} from 'src/features/grading/GradingUtils';
66
import { freshSortState } from 'src/pages/academy/grading/subcomponents/GradingSubmissionsTable';
7-
import { OptionType } from 'src/pages/academy/teamFormation/subcomponents/TeamFormationForm';
7+
import type { OptionType } from 'src/pages/academy/teamFormation/subcomponents/TeamFormationForm';
88

9-
import {
9+
import type {
1010
AllColsSortStates,
1111
GradingOverviews,
1212
GradingQuery
1313
} from '../../../features/grading/GradingTypes';
14-
import { TeamFormationOverview } from '../../../features/teamFormation/TeamFormationTypes';
15-
import {
14+
import type { TeamFormationOverview } from '../../../features/teamFormation/TeamFormationTypes';
15+
import type {
1616
Assessment,
1717
AssessmentConfiguration,
1818
AssessmentOverview,
1919
ContestEntry
2020
} from '../../assessment/AssessmentTypes';
21-
import {
21+
import type {
2222
Notification,
2323
NotificationFilterFunction
2424
} from '../../notificationBadge/NotificationBadgeTypes';
2525
import { generateOctokitInstance } from '../../utils/GitHubPersistenceHelper';
2626
import { Role, StoriesRole } from '../ApplicationTypes';
27-
import {
27+
import type {
2828
AdminPanelCourseRegistration,
2929
CourseRegistration,
3030
Tokens,
@@ -153,6 +153,4 @@ const SessionActions = createActions('session', {
153153
});
154154

155155
// For compatibility with existing code (actions helper)
156-
export default {
157-
...SessionActions
158-
};
156+
export default SessionActions;

src/commons/mocks/ContextMocks.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { parse } from 'acorn';
2-
import { FunctionExpression, Node } from 'estree';
2+
import type { Node } from 'estree';
33
import { ACORN_PARSE_OPTIONS } from 'js-slang/dist/constants';
44
import createContext, { EnvTree } from 'js-slang/dist/createContext';
5-
import Closure from 'js-slang/dist/interpreter/closure';
6-
import { Context, Environment } from 'js-slang/dist/types';
5+
import type { Context } from 'js-slang/dist/types';
76
import { TypeError } from 'js-slang/dist/utils/rttc';
87

98
export function mockContext(chapter = 1): Context {
@@ -44,10 +43,6 @@ export function mockRuntimeContext(): Context {
4443
return context;
4544
}
4645

47-
export function mockClosure(): Closure {
48-
return new Closure({} as FunctionExpression, {} as Environment, {} as Context);
49-
}
50-
5146
export function mockTypeError(): TypeError {
5247
// Typecast to Node to fix estree-acorn compatability.
5348
return new TypeError(parse('', ACORN_PARSE_OPTIONS) as Node, '', '', '');

src/commons/redux/__tests__/utils.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { testSaga } from 'redux-saga-test-plan';
2+
import WorkspaceActions from 'src/commons/workspace/WorkspaceActions';
3+
4+
import { combineSagaHandlers, createActions } from '../utils';
5+
6+
// Would have used spyOn, but for some reason that doesn't work properly
7+
jest.mock('src/commons/sagas/SafeEffects', () => ({
8+
...jest.requireActual('src/commons/sagas/SafeEffects'),
9+
// Mock wrap saga to just be a passthrough so that the identity
10+
// checking that testSaga uses will pass
11+
wrapSaga: (x: any) => x
12+
}));
13+
14+
test('test combineSagaHandlers', () => {
15+
const mockTakeEveryHandler = jest.fn();
16+
const mockTakeLatestHandler = jest.fn();
17+
const mockTakeLeadingHandler = jest.fn();
18+
19+
const saga = combineSagaHandlers({
20+
[WorkspaceActions.toggleUsingUpload.type]: mockTakeEveryHandler,
21+
[WorkspaceActions.toggleFolderMode.type]: {
22+
takeEvery: mockTakeEveryHandler
23+
},
24+
[WorkspaceActions.toggleUsingCse.type]: {
25+
takeLatest: mockTakeLatestHandler
26+
},
27+
[WorkspaceActions.toggleUsingSubst.type]: {
28+
takeLeading: mockTakeLeadingHandler
29+
},
30+
[WorkspaceActions.toggleEditorAutorun.type]: {
31+
takeEvery: mockTakeEveryHandler,
32+
takeLeading: mockTakeLeadingHandler
33+
}
34+
});
35+
36+
testSaga(saga)
37+
.next()
38+
.takeEvery(WorkspaceActions.toggleUsingUpload.type, mockTakeEveryHandler)
39+
.next()
40+
.takeEvery(WorkspaceActions.toggleFolderMode.type, mockTakeEveryHandler)
41+
.next()
42+
.takeLatest(WorkspaceActions.toggleUsingCse.type, mockTakeLatestHandler)
43+
.next()
44+
.takeLeading(WorkspaceActions.toggleUsingSubst.type, mockTakeLeadingHandler)
45+
.next()
46+
.takeEvery(WorkspaceActions.toggleEditorAutorun.type, mockTakeEveryHandler)
47+
.next()
48+
.takeLeading(WorkspaceActions.toggleEditorAutorun.type, mockTakeLeadingHandler)
49+
.next()
50+
.isDone();
51+
});
52+
53+
test('createActions', () => {
54+
const actions = createActions('workspace', {
55+
act0: false,
56+
act1: (value: string) => ({ value }),
57+
act2: 525600
58+
});
59+
60+
const act0 = actions.act0();
61+
expect(act0.type).toEqual('workspace/act0');
62+
63+
const act1 = actions.act1('test');
64+
expect(act1.type).toEqual('workspace/act1');
65+
expect(act1.payload).toMatchObject({ value: 'test' });
66+
67+
const act2 = actions.act2();
68+
expect(act2.type).toEqual('workspace/act2');
69+
});

src/commons/redux/utils.ts

Lines changed: 46 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import {
2-
ActionCreatorWithOptionalPayload,
3-
ActionCreatorWithoutPayload,
4-
ActionCreatorWithPreparedPayload,
2+
type ActionCreatorWithOptionalPayload,
3+
type ActionCreatorWithoutPayload,
4+
type ActionCreatorWithPreparedPayload,
55
createAction
66
} from '@reduxjs/toolkit';
7-
import * as Sentry from '@sentry/browser';
8-
import { SagaIterator } from 'redux-saga';
9-
import { StrictEffect, takeEvery } from 'redux-saga/effects';
7+
import type { SagaIterator } from 'redux-saga';
8+
import { type StrictEffect, takeEvery, takeLatest, takeLeading } from 'redux-saga/effects';
9+
10+
import { safeTakeEvery, wrapSaga } from '../sagas/SafeEffects';
11+
import type { SourceActionType } from '../utils/ActionsHelper';
12+
import { type ActionTypeToCreator, objectEntries } from '../utils/TypeHelper';
1013

1114
/**
1215
* Creates actions, given a base name and base actions
1316
* @param baseName The base name of the actions
14-
* @param baseActions The base actions. Use a falsy value to create an action without a payload.
17+
* @param baseActions The base actions. Use a non function value to create an action without a payload.
1518
* @returns An object containing the actions
1619
*/
1720
export function createActions<BaseName extends string, BaseActions extends Record<string, any>>(
@@ -21,11 +24,12 @@ export function createActions<BaseName extends string, BaseActions extends Recor
2124
return Object.entries(baseActions).reduce(
2225
(res, [name, func]) => ({
2326
...res,
24-
[name]: func
25-
? createAction(`${baseName}/${name}`, (...args: any) => ({ payload: func(...args) }))
26-
: createAction(`${baseName}/${name}`)
27+
[name]:
28+
typeof func === 'function'
29+
? createAction(`${baseName}/${name}`, (...args: any) => ({ payload: func(...args) }))
30+
: createAction(`${baseName}/${name}`)
2731
}),
28-
{} as {
32+
{} as Readonly<{
2933
[K in keyof BaseActions]: K extends string
3034
? BaseActions[K] extends (...args: any) => any
3135
? ActionCreatorWithPreparedPayload<
@@ -35,35 +39,40 @@ export function createActions<BaseName extends string, BaseActions extends Recor
3539
>
3640
: ActionCreatorWithoutPayload<`${BaseName}/${K}`>
3741
: never;
38-
}
42+
}>
3943
);
4044
}
4145

42-
export function combineSagaHandlers<
43-
TActions extends Record<
44-
string,
45-
ActionCreatorWithPreparedPayload<any, any> | ActionCreatorWithoutPayload<any>
46-
>
47-
>(
48-
actions: TActions,
49-
handlers: {
50-
// TODO: Maybe this can be stricter? And remove the optional type after
51-
// migration is fully done
52-
[K in keyof TActions]?: (action: ReturnType<TActions[K]>) => SagaIterator;
53-
},
54-
others?: (takeEvery: typeof saferTakeEvery) => SagaIterator
55-
): () => SagaIterator {
56-
const sagaHandlers = Object.entries(handlers).map(([actionName, saga]) =>
57-
saferTakeEvery(actions[actionName], saga)
58-
);
46+
type SagaHandler<T extends SourceActionType['type']> = (
47+
action: ReturnType<ActionTypeToCreator<T>>
48+
) => Generator<StrictEffect>;
49+
50+
type SagaHandlers = {
51+
[K in SourceActionType['type']]?:
52+
| SagaHandler<K>
53+
| Partial<Record<'takeEvery' | 'takeLatest' | 'takeLeading', SagaHandler<K>>>;
54+
};
55+
56+
export function combineSagaHandlers(handlers: SagaHandlers) {
5957
return function* (): SagaIterator {
60-
yield* sagaHandlers;
61-
if (others) {
62-
const obj = others(saferTakeEvery);
63-
while (true) {
64-
const { done, value } = obj.next();
65-
if (done) break;
66-
yield value;
58+
for (const [actionName, saga] of objectEntries(handlers)) {
59+
if (saga === undefined) {
60+
continue;
61+
} else if (typeof saga === 'function') {
62+
yield takeEvery(actionName, wrapSaga(saga));
63+
continue;
64+
}
65+
66+
if (saga.takeEvery) {
67+
yield takeEvery(actionName, wrapSaga(saga.takeEvery));
68+
}
69+
70+
if (saga.takeLeading) {
71+
yield takeLeading(actionName, wrapSaga(saga.takeLeading));
72+
}
73+
74+
if (saga.takeLatest) {
75+
yield takeLatest(actionName, wrapSaga(saga.takeLatest));
6776
}
6877
}
6978
};
@@ -74,26 +83,5 @@ export function saferTakeEvery<
7483
| ActionCreatorWithOptionalPayload<any>
7584
| ActionCreatorWithPreparedPayload<any[], any>
7685
>(actionPattern: Action, fn: (action: ReturnType<Action>) => Generator<StrictEffect<any>>) {
77-
function* wrapper(action: ReturnType<Action>) {
78-
try {
79-
yield* fn(action);
80-
} catch (error) {
81-
handleUncaughtError(error);
82-
}
83-
}
84-
85-
return takeEvery(actionPattern.type, wrapper);
86-
}
87-
88-
function handleUncaughtError(error: any) {
89-
if (process.env.NODE_ENV === 'development') {
90-
// react-error-overlay is a "special" package that's automatically included
91-
// in development mode by CRA
92-
93-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
94-
// @ts-ignore
95-
import('react-error-overlay').then(reo => reo.reportRuntimeError(error));
96-
}
97-
Sentry.captureException(error);
98-
console.error(error);
86+
return safeTakeEvery(actionPattern.type, fn);
9987
}

0 commit comments

Comments
 (0)