Skip to content

Modules System Documentation (DEPRECATED)

Bryan Loh edited this page Apr 18, 2021 · 1 revision

Dynamic Module aims to enhance Side Content flexibility and capabilities, as well as to make each Side Content Tab to be able to load dynamically based on the program ran.

The spawning and despawning of module tab will be triggered whenever a program is evaluated, regardless whether it is from the REPL or the Editor.

Dynamic Module consists of two components:

  1. React Component for front end, which is the tab that will spawn or despawn on the SideContent. The tab can be used to display the output of the module file e.g. visualisation of wave, rune display, etc.
  2. Module file, at the modules repo e.g. game.js

A module creator can implement just the module file if there is no need for any visualisation on front-end, or just the React component if there is no need for any additional module file. Of course, implementing both React Component and module file is also possible.

User Guide/Module Creator Guide

1. Creating a New Module

React Component

Side Content accepts SideContentTab (defined here). As such, the module tab must be of SideContentTab type for it to be integrated into Side Content.

  1. Create a new .ts file, prepend the name file with SideContent and save it under src/commons/sideContent/.
  2. Create a (functional) React component.
  3. Create a separate function that specify when to spawn the module OUTSIDE the React component within the same .ts file, following the signature (debuggerContext: DebuggerContext) => boolean.
  4. Wrap the React component into type of SideContentTab.

For an example of the .ts file, refer to this sample module tab.

Module File

Write a body of anonymous function in javascript and upload them to the modules repo e.g. game.js


2. Accessing Front End Data

Commonly, we would want to make use of front end data e.g. the code to be run, Source version, etc. to be used by the module file.

The information of the raw code, external libraries used, context (among other things) are packaged into debuggerContext and stored at the application store, specifically at state.workspaces.<YOUR CURRENT WORKSPACE LOCATION>.debuggerContext, e.g. state.workspaces.playground.debuggerContext.

The most up-to-date definition of debuggerContext is defined here.

export type DebuggerContext = {
  result: any;
  lastDebuggerResult: any;
  code: string;
  context: Context;
  workspaceLocation?: WorkspaceLocation;
};

By default, all integrated module tab will receive a props.workspaceLocation as one of its props. By using this, it is possible to access the debuggerContext. Of course, being a React component, we can also access anything under the store.

Below is an example of how to access the debuggerContext.

const SideContentSampleTab = (props: any) => {
  let workspaces = useSelector((state: OverallState) => state.workspaces);

  // Require workspace location
  if (!props.workspaceLocation) {
    return <></>;
  }
  const debuggerContext = workspaces[props.workspaceLocation].debuggerContext;
  
  ... more code ...
}

3. Supplying the Module file in Modules Repo with Front End Data

Most likely, there is a need to provide the module file with Front End Data, e.g. the code to be run, Source version, etc. The question is, how do we do that?

We can provide the front end data to the module file as a property of the context used by the js-slang evaluator.

When evaluating a js-slang code, js-slang evaluator require a context to run the code in. Context encapsulate all the necessary information to run the code e.g. library used, module file used, existing variables, etc.

By using runInContext function provided by the js-slang, we can run the js-slang code within our desired context. The context creation can be done with createContext function provided by js-slang as well.

Within the debuggerContext, we save the latest code to run. Using the js-slang evaluator, we can evaluate the js-slang code within desired context.

Below is the sample code of evaluating the js-slang code while providing front end data as part of the context:

// At the .ts file, within the body of the React Component
const context = createContext(4, [], {}, 'gpu', {
      studentCodeLength: debuggerContext.code.length,
      windowToUse: (window as any).MyLibrary,
    });

const result = await runInContext(debuggerContext.code, context); // run the js-slang code

The above snippet will run the code (debuggerContext.code) with the desired context. The module file will be fed this context and can make use of it i.e. at the module file itself:

// At the module file
(_params) => {
    const code_length = _params.studentCodeLength;
    const window = _params.windowToUse

    ... rest of module file ...
}

The module file then can make use of the given parameters e.g. using the given window to display the result.

IMPORTANT: the module file will only be used by the js-slang if student has imported the module file:

import { ... } from 'my_library';

4. Integration into Source Academy

  1. Open src/commons/sideContent/SideContentHelper.ts.
  2. Import your SideContentTab, e.g. yourOwnTab: SideContentTab and include it into the array potentialTabs.

e.g. potentialTabs = [sampleTab, yourOwnTab].


5. Module Tab Template

We provide sample code as a template. Below, the module tab is spawned when program output is sample, and despawned when program output is unsample. It simply outputs the program output as its content.

At SideContentSampleTab.ts:

import { IconNames } from '@blueprintjs/icons';
import * as React from 'react';
import { useSelector } from 'react-redux';

import { OverallState } from '../application/ApplicationTypes';
import { DebuggerContext } from '../workspace/WorkspaceTypes';
import { isCurrentlyActive,isEmptyDebuggerContext } from './SideContentHelper';
import { SideContentTab } from './SideContentTypes';

////////////////////////
//   React Component  //
////////////////////////

const SideContentSampleTab = (props: any) => {
  const workspaces = useSelector((state: OverallState) => state.workspaces);

  // Require workspace location
  if (!props.workspaceLocation) {
    return <></>;
  }

  const debuggerContext = workspaces[props.workspaceLocation].debuggerContext;

  const replOutput = isEmptyDebuggerContext(debuggerContext)
    ? ''
    : debuggerContext.result.value;

  return (
    <div>
      <p>
        {replOutput}
      </p>
    </div>
  );
};

////////////////////////
// Spawning Behaviour //
////////////////////////

// Outside the react component
const toSpawnSampleTab = (debuggerContext: DebuggerContext): boolean => {
  if (debuggerContext.result === undefined || debuggerContext.result.value === 'unsample') {
    return false;
  }
  const isToSpawn = isCurrentlyActive('Sample', debuggerContext.workspaceLocation)
    || debuggerContext.result.value === 'sample';
  return isToSpawn;
};

////////////////////////
//        Wrap        //
////////////////////////

// Wrap it within SideContentTab
export const sampleTab: SideContentTab = {
  label: 'Sample',
  iconName: IconNames.ASTERISK,
  body: <SideContentSampleTab />,
  toSpawn: toSpawnSampleTab // indicate the spawning behaviour function
};

export default SideContentSampleTab;

At SideContentHelper.ts:

import { sampleTab } from './SideContentSampleTab';

////////////////////////
//     Integration    //
////////////////////////
...
const potentialTabs: SideContentTab[] = [sampleTab];
...

Developer Guide

1. Data Flow

The debuggerContext is collected from SagaMiddleware, specifically at WorkspaceSaga during evaluation of any code. After evaluation of any code, we dispatch an action loaded with debuggerContext. The corresponding reducer, WorkspaceReducer, saves the debuggerContext into the store.


2. Previous Architecture Data Flow Problem and Solution

Previously, there is almost no information passed by front end to the module tabs. In other words, module tabs and the module back end do not have direct access to the program being run.

 ---> = data flow
 ---+ = child component (no data flow)               
                                    
program ---> sagas ---> front-end ---+ module front end

In the event that the module tab require additional information, they have to invent a way to collect the information and pass it to their corresponding module tab. This introduces many 'inventive' ways throughout the code base for their data collection.

 ---> = data flow
 ---+ = child component (no data flow)
       
                      _______________________
                     |                       |  'inventive' collection of data from sagas
                     |                       v               
program ---> sagas ---> front-end ---+ module front end

As such, the solution is to introduce a formalised pipeline such that module tabs are able to easily access their required information. The front-end now saves the required data and save it into the store, where module tabs can easily access the required information. This eliminate the need for other 'inventive' ways to be deployed and clutter the code base.

 ---> = data flow
 ---+ = child component (no data flow)

                                 ------> store     
                                 |         |               
program ---> sagas ---> front-end          | fetch required data from store, just like any React Component
                                 |         v
                                 ---+ module front end

3. Triggering the spawning and despawning of Module Tabs

On every program run, the debuggerContext will be updated automatically. Side Content will be notified as it is subscribed to debuggerContext and will prompt all of the potential module tabs with the new information. All potential module tabs that wants to spawn will be added into tabs to be rendered.


4. Passing workspaceLocation as a props

We pass the workSpaceLocation as props into the module tab by directly modifying the JSX element (body attribute in SideContentTab).

const tabBody: JSX.Element = workspaceLocation
  ? {
      ...tab.body,
      props: {
        ...tab.body.props,
        workspaceLocation
      }
    }
  : tab.body;

5. Future Improvement

Passing the body as JSX element is a bad practice which makes it difficult to pass in props. Unfortunately, this is the current adopted way of Side Content, and Dynamic Module simply extends the bad practice. This should be changed in the future to not pass a JSX element.

Clone this wiki locally