Skip to content

Commit 3aba5a1

Browse files
CopilotRichDom2185
andcommitted
Implement JsSlangContextStore and update core types and hooks
Co-authored-by: RichDom2185 <[email protected]>
1 parent 00aef52 commit 3aba5a1

File tree

15 files changed

+241
-42
lines changed

15 files changed

+241
-42
lines changed

src/commons/application/ApplicationTypes.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { FileSystemState } from '../fileSystem/FileSystemTypes';
1313
import type { SideContentManagerState, SideContentState } from '../sideContent/SideContentTypes';
1414
import Constants from '../utils/Constants';
1515
import { createContext } from '../utils/JsSlangHelper';
16+
import { putJsSlangContext } from '../utils/JsSlangContextStore';
1617
import type {
1718
DebuggerContext,
1819
WorkspaceLocation,
@@ -384,12 +385,12 @@ export const defaultEditorValue = '// Type your program in here!';
384385
*/
385386
export const createDefaultWorkspace = (workspaceLocation: WorkspaceLocation): WorkspaceState => ({
386387
autogradingResults: [],
387-
context: createContext<WorkspaceLocation>(
388+
contextId: putJsSlangContext(createContext<WorkspaceLocation>(
388389
Constants.defaultSourceChapter,
389390
[],
390391
workspaceLocation,
391392
Constants.defaultSourceVariant
392-
),
393+
)),
393394
isFolderModeEnabled: false,
394395
activeEditorTabIndex: 0,
395396
editorTabs: [
@@ -427,7 +428,18 @@ export const createDefaultWorkspace = (workspaceLocation: WorkspaceLocation): Wo
427428
isRunning: false,
428429
isDebugging: false,
429430
enableDebugging: true,
430-
debuggerContext: {} as DebuggerContext,
431+
debuggerContext: {
432+
result: undefined,
433+
lastDebuggerResult: undefined,
434+
code: '',
435+
contextId: putJsSlangContext(createContext<string>(
436+
Constants.defaultSourceChapter,
437+
[],
438+
'debugger',
439+
Constants.defaultSourceVariant
440+
)),
441+
workspaceLocation: workspaceLocation
442+
},
431443
lastDebuggerResult: undefined,
432444
files: {},
433445
updateUserRoleCallback: () => {}

src/commons/sagas/PersistenceSaga.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { combineSagaHandlers } from '../redux/utils';
1313
import { actions } from '../utils/ActionsHelper';
1414
import Constants from '../utils/Constants';
1515
import { showSimpleConfirmDialog, showSimplePromptDialog } from '../utils/DialogHelper';
16+
import { getJsSlangContext } from '../utils/JsSlangContextStore';
1617
import {
1718
dismiss,
1819
showMessage,
@@ -118,15 +119,18 @@ const PersistenceSaga = combineSagaHandlers({
118119
try {
119120
yield call(ensureInitialisedAndAuthorised);
120121

121-
const [activeEditorTabIndex, editorTabs, chapter, variant, external] = yield select(
122+
const [activeEditorTabIndex, editorTabs, contextId, external] = yield select(
122123
(state: OverallState) => [
123124
state.workspaces.playground.activeEditorTabIndex,
124125
state.workspaces.playground.editorTabs,
125-
state.workspaces.playground.context.chapter,
126-
state.workspaces.playground.context.variant,
126+
state.workspaces.playground.contextId,
127127
state.workspaces.playground.externalLibrary
128128
]
129129
);
130+
131+
const context = getJsSlangContext(contextId);
132+
const chapter = context?.chapter || Constants.defaultSourceChapter;
133+
const variant = context?.variant || Constants.defaultSourceVariant;
130134

131135
if (activeEditorTabIndex === null) {
132136
throw new Error('No active editor tab found.');
@@ -249,9 +253,13 @@ const PersistenceSaga = combineSagaHandlers({
249253
const {
250254
activeEditorTabIndex,
251255
editorTabs,
252-
context: { chapter, variant },
256+
contextId,
253257
externalLibrary: external
254258
} = yield* selectWorkspace('playground');
259+
260+
const context = getJsSlangContext(contextId);
261+
const chapter = context?.chapter || Constants.defaultSourceChapter;
262+
const variant = context?.variant || Constants.defaultSourceVariant;
255263

256264
if (activeEditorTabIndex === null) {
257265
throw new Error('No active editor tab found.');

src/commons/sagas/PlaygroundSaga.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
type OverallState
1414
} from '../application/ApplicationTypes';
1515
import { retrieveFilesInWorkspaceAsRecord } from '../fileSystem/utils';
16+
import Constants from '../utils/Constants';
17+
import { getJsSlangContext } from '../utils/JsSlangContextStore';
1618
import { combineSagaHandlers } from '../redux/utils';
1719
import SideContentActions from '../sideContent/SideContentActions';
1820
import { SideContentType } from '../sideContent/SideContentTypes';
@@ -66,9 +68,12 @@ const PlaygroundSaga = combineSagaHandlers({
6668
}
6769

6870
const {
69-
context: { chapter: playgroundSourceChapter },
71+
contextId,
7072
editorTabs
7173
} = yield* selectWorkspace('playground');
74+
75+
const context = getJsSlangContext(contextId);
76+
const playgroundSourceChapter = context?.chapter || Constants.defaultSourceChapter;
7277

7378
if (prevId === SideContentType.substVisualizer) {
7479
if (newId === SideContentType.mobileEditorRun) return;
@@ -131,12 +136,16 @@ function* updateQueryString() {
131136

132137
const {
133138
activeEditorTabIndex,
134-
context: { chapter, variant },
139+
contextId,
135140
editorTabs,
136141
execTime,
137142
externalLibrary: external,
138143
isFolderModeEnabled
139144
} = yield* selectWorkspace('playground');
145+
146+
const context = getJsSlangContext(contextId);
147+
const chapter = context?.chapter || Constants.defaultSourceChapter;
148+
const variant = context?.variant || Constants.defaultSourceVariant;
140149

141150
const editorTabFilePaths = editorTabs
142151
.map(editorTab => editorTab.filePath)

src/commons/sagas/RemoteExecutionSaga.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { ExternalLibraryName } from '../application/types/ExternalTypes';
2626
import { combineSagaHandlers } from '../redux/utils';
2727
import { actions } from '../utils/ActionsHelper';
2828
import type { MaybePromise } from '../utils/TypeHelper';
29+
import { getJsSlangContext } from '../utils/JsSlangContextStore';
2930
import { fetchDevices, getDeviceWSEndpoint } from './RequestsSaga';
3031

3132
const dummyLocation = {
@@ -271,9 +272,13 @@ const RemoteExecutionSaga = combineSagaHandlers({
271272
yield put(actions.clearReplOutput(session.workspace));
272273

273274
const client = session.connection.client;
274-
const context: Context = yield select(
275-
(state: OverallState) => state.workspaces[session.workspace].context
275+
const contextId: string = yield select(
276+
(state: OverallState) => state.workspaces[session.workspace].contextId
276277
);
278+
const context = getJsSlangContext(contextId);
279+
if (!context) {
280+
throw new Error('Context not found');
281+
}
277282
// clear the context of errors (note: the way this works is that the context
278283
// is mutated by js-slang anyway, so it's ok to do it like this)
279284
context.errors.length = 0;

src/commons/sagas/SideContentSaga.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import StoriesActions from 'src/features/stories/StoriesActions';
55
import { combineSagaHandlers } from '../redux/utils';
66
import SideContentActions from '../sideContent/SideContentActions';
77
import { SideContentType } from '../sideContent/SideContentTypes';
8+
import { putJsSlangContext } from '../utils/JsSlangContextStore';
89
import WorkspaceActions from '../workspace/WorkspaceActions';
910

1011
const isSpawnSideContent = (
@@ -34,15 +35,21 @@ const SideContentSaga = combineSagaHandlers({
3435
result: action.payload.result,
3536
lastDebuggerResult: action.payload.lastDebuggerResult,
3637
code: action.payload.code,
37-
context: action.payload.context,
38+
contextId: putJsSlangContext(action.payload.context),
3839
workspaceLocation: action.payload.workspaceLocation
3940
};
4041
yield put(
4142
SideContentActions.spawnSideContent(action.payload.workspaceLocation, debuggerContext)
4243
);
4344
},
4445
[StoriesActions.notifyStoriesEvaluated.type]: function* (action) {
45-
yield put(SideContentActions.spawnSideContent(`stories.${action.payload.env}`, action.payload));
46+
const storiesDebuggerContext = {
47+
...action.payload,
48+
contextId: putJsSlangContext(action.payload.context)
49+
};
50+
// Remove the original context property to avoid type conflicts
51+
delete storiesDebuggerContext.context;
52+
yield put(SideContentActions.spawnSideContent(`stories.${action.payload.env}`, storiesDebuggerContext));
4653
}
4754
});
4855

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { v4 as uuidv4 } from 'uuid';
2+
import type { Context } from 'js-slang/dist/types';
3+
4+
/**
5+
* Global singleton store for js-slang Context objects.
6+
*
7+
* This store manages mutable js-slang contexts outside of Redux,
8+
* solving the issue where storing mutable objects in Redux violates
9+
* immutability requirements and causes problems with Immer's auto-freezing.
10+
*
11+
* Instead of storing Context objects directly in Redux, we store
12+
* UUIDs that reference contexts in this global store.
13+
*/
14+
class JsSlangContextStore {
15+
private contextStore = new Map<string, Context>();
16+
17+
/**
18+
* Stores a js-slang context and returns a UUID to reference it.
19+
* @param context - The js-slang Context object to store
20+
* @returns UUID string that can be stored in Redux state
21+
*/
22+
putJsSlangContext(context: Context): string {
23+
const id = uuidv4();
24+
this.contextStore.set(id, context);
25+
return id;
26+
}
27+
28+
/**
29+
* Retrieves a js-slang context by its UUID.
30+
* @param id - The UUID of the context to retrieve
31+
* @returns The Context object, or undefined if not found
32+
*/
33+
getJsSlangContext(id: string): Context | undefined {
34+
return this.contextStore.get(id);
35+
}
36+
37+
/**
38+
* Removes a js-slang context from the store.
39+
* @param id - The UUID of the context to remove
40+
* @returns true if the context was found and removed, false otherwise
41+
*/
42+
deleteJsSlangContext(id: string): boolean {
43+
return this.contextStore.delete(id);
44+
}
45+
46+
/**
47+
* Gets the number of contexts currently stored.
48+
* Useful for debugging and monitoring.
49+
*/
50+
size(): number {
51+
return this.contextStore.size;
52+
}
53+
54+
/**
55+
* Clears all stored contexts.
56+
* Use with caution - this will invalidate all context references.
57+
*/
58+
clear(): void {
59+
this.contextStore.clear();
60+
}
61+
}
62+
63+
// Export a singleton instance
64+
export const jsSlangContextStore = new JsSlangContextStore();
65+
66+
// Export the class for testing purposes
67+
export { JsSlangContextStore };
68+
69+
// Export utility functions for convenience
70+
export const putJsSlangContext = (context: Context): string =>
71+
jsSlangContextStore.putJsSlangContext(context);
72+
73+
export const getJsSlangContext = (id: string): Context | undefined =>
74+
jsSlangContextStore.getJsSlangContext(id);
75+
76+
export const deleteJsSlangContext = (id: string): boolean =>
77+
jsSlangContextStore.deleteJsSlangContext(id);

src/commons/workspace/WorkspaceReducer.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from '../collabEditing/CollabEditingActions';
2222
import type { SourceActionType } from '../utils/ActionsHelper';
2323
import { createContext } from '../utils/JsSlangHelper';
24+
import { putJsSlangContext } from '../utils/JsSlangContextStore';
2425
import { handleCseAndStepperActions } from './reducers/cseReducer';
2526
import { handleDebuggerActions } from './reducers/debuggerReducer';
2627
import { handleEditorActions } from './reducers/editorReducer';
@@ -95,12 +96,12 @@ const newWorkspaceReducer = createReducer(defaultWorkspaceManager, builder => {
9596
...state,
9697
[workspaceLocation]: {
9798
...state[workspaceLocation],
98-
context: createContext<WorkspaceLocation>(
99+
contextId: putJsSlangContext(createContext<WorkspaceLocation>(
99100
action.payload.library.chapter,
100101
action.payload.library.external.symbols,
101102
workspaceLocation,
102103
action.payload.library.variant
103-
),
104+
)),
104105
globals: action.payload.library.globals,
105106
externalLibrary: action.payload.library.external.name
106107
}
@@ -360,18 +361,19 @@ const newWorkspaceReducer = createReducer(defaultWorkspaceManager, builder => {
360361
};
361362
})
362363
.addCase(WorkspaceActions.updateSublanguage, (state, action) => {
363-
// TODO: Mark for removal
364-
const { chapter, variant } = action.payload.sublang;
365-
state.playground.context.chapter = chapter;
366-
state.playground.context.variant = variant;
364+
// TODO: Mark for removal - this functionality needs to be updated
365+
// to work with the new context store or removed entirely
366+
// const { chapter, variant } = action.payload.sublang;
367+
// state.playground.context.chapter = chapter;
368+
// state.playground.context.variant = variant;
367369
})
368370
.addCase(WorkspaceActions.notifyProgramEvaluated, (state, action) => {
369371
const workspaceLocation = getWorkspaceLocation(action);
370372
const debuggerContext = state[workspaceLocation].debuggerContext;
371373
debuggerContext.result = action.payload.result;
372374
debuggerContext.lastDebuggerResult = action.payload.lastDebuggerResult;
373375
debuggerContext.code = action.payload.code;
374-
debuggerContext.context = action.payload.context;
376+
debuggerContext.contextId = putJsSlangContext(action.payload.context);
375377
debuggerContext.workspaceLocation = action.payload.workspaceLocation;
376378
})
377379
.addCase(WorkspaceActions.toggleUsingUpload, (state, action) => {

src/commons/workspace/WorkspaceTypes.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { CollabEditingAccess } from '@sourceacademy/sharedb-ace/types';
2-
import type { Context } from 'js-slang';
32

43
import type {
54
AllColsSortStates,
@@ -79,7 +78,7 @@ export type EditorTabState = {
7978

8079
export type WorkspaceState = {
8180
readonly autogradingResults: AutogradingResult[];
82-
readonly context: Context;
81+
readonly contextId: string;
8382
readonly isFolderModeEnabled: boolean;
8483
readonly activeEditorTabIndex: number | null;
8584
readonly editorTabs: EditorTabState[];
@@ -120,7 +119,7 @@ export type DebuggerContext = {
120119
result: any;
121120
lastDebuggerResult: any;
122121
code: string;
123-
context: Context;
122+
contextId: string;
124123
workspaceLocation?: WorkspaceLocation;
125124
};
126125

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useSelector } from 'react-redux';
2+
import type { Context } from 'js-slang/dist/types';
3+
4+
import type { OverallState } from '../../application/ApplicationTypes';
5+
import { getJsSlangContext } from '../../utils/JsSlangContextStore';
6+
import type { WorkspaceLocation } from '../WorkspaceTypes';
7+
8+
/**
9+
* Custom hook to get the js-slang context for a specific workspace.
10+
*
11+
* This hook abstracts away the complexity of retrieving contexts from
12+
* the global store. It uses the context ID stored in Redux to fetch
13+
* the actual mutable Context object from the JsSlangContextStore.
14+
*
15+
* @param workspaceLocation - The workspace location to get the context for
16+
* @returns The js-slang Context object, or undefined if not found
17+
*/
18+
export function useJsSlangContext(workspaceLocation: WorkspaceLocation): Context | undefined {
19+
const contextId = useSelector((state: OverallState) =>
20+
state.workspaces[workspaceLocation].contextId
21+
);
22+
23+
return contextId ? getJsSlangContext(contextId) : undefined;
24+
}
25+
26+
/**
27+
* Hook to get the context ID (UUID) for a workspace.
28+
* Useful when you need the ID itself rather than the context object.
29+
*
30+
* @param workspaceLocation - The workspace location to get the context ID for
31+
* @returns The context ID string, or undefined if not set
32+
*/
33+
export function useJsSlangContextId(workspaceLocation: WorkspaceLocation): string | undefined {
34+
return useSelector((state: OverallState) =>
35+
state.workspaces[workspaceLocation].contextId
36+
);
37+
}
38+
39+
/**
40+
* Hook to get the debugger context for a workspace.
41+
* Similar to useJsSlangContext but for the debugger context specifically.
42+
*
43+
* @param workspaceLocation - The workspace location to get the debugger context for
44+
* @returns The debugger's js-slang Context object, or undefined if not found
45+
*/
46+
export function useDebuggerJsSlangContext(workspaceLocation: WorkspaceLocation): Context | undefined {
47+
const debuggerContextId = useSelector((state: OverallState) =>
48+
state.workspaces[workspaceLocation].debuggerContext.contextId
49+
);
50+
51+
return debuggerContextId ? getJsSlangContext(debuggerContextId) : undefined;
52+
}

0 commit comments

Comments
 (0)