Skip to content

Commit

Permalink
emmas comments
Browse files Browse the repository at this point in the history
  • Loading branch information
M-Kusumgar committed Dec 19, 2024
1 parent 917330a commit c786d14
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 27 deletions.
19 changes: 18 additions & 1 deletion app/static/src/externalScriptSrc.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
/*
Originally dynamic wodin returned a mustache template from the express server
which had a
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
tag in it but with the introduction of the wodin static build, we no longer
control the html that is shipped to the user (the scientists write the html
research papers with the static build).
We could enforce that they add this script tag manually otherwise mathjax
(latex style rendering in html) won't work but for the sake of the researchers'
experience lets make it easier and less prone to errors by just programmatically
adding the script tag.
To load more external scripts, just append the src to the externalScripts array.
*/
export const externalScripts = [
"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"
];

export const mountScriptTags = () => {
export const loadThirdPartyCDNScripts = () => {
externalScripts.forEach(src => {
const script = document.createElement("script");
script.defer = true;
Expand Down
4 changes: 2 additions & 2 deletions app/static/src/wodin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import help from "./directives/help";
import App from "./components/App.vue";
import { getStoreOptions } from "./wodinStaticUtils";
import { Store, StoreOptions } from "vuex";
import { mountScriptTags } from "./externalScriptSrc";
import { loadThirdPartyCDNScripts } from "./externalScriptSrc";

declare let appType: AppType;

mountScriptTags();
loadThirdPartyCDNScripts();

const { Basic, Fit, Stochastic } = AppType;
export const getComponent = (appType: AppType) => {
Expand Down
26 changes: 17 additions & 9 deletions app/static/src/wodinStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,26 @@ import { getStoreOptions } from "./wodinStaticUtils";
import { Store, StoreOptions } from "vuex";
import "./scss/style.scss"
import { AppState } from "./store/appState/state";
import { mountScriptTags } from "./externalScriptSrc";
import { loadThirdPartyCDNScripts } from "./externalScriptSrc";
import {
componentsAndSelectors, getConfigsAndModels, getDeepCopiedStoreOptions,
componentsAndSelectors, getConfigAndModelForStores, getDeepCopiedStoreOptions,
getStoresInPage, initialiseStore, waitForBlockingScripts
} from "./wodinStaticUtils";

/*
This is the entrypoint to the wodin static build. This boot function just gets called
at the end of the file. Since we are not allowed top level awaits we have to create
an async function and call it at the bottom.
To load any scripts that you need before the runs just append the src to the
blockingScripts array.
*/

const blockingScripts = ["./stores/runnerOde.js", "./stores/runnerDiscrete.js"];

const boot = async () => {
// inject external scripts such as mathjax
mountScriptTags();
loadThirdPartyCDNScripts();

// wait for scripts we can't load without such as runner code
await waitForBlockingScripts(blockingScripts);
Expand All @@ -22,20 +31,19 @@ const boot = async () => {
const storesInPage = getStoresInPage();

// get relevant store configs and model code
const { configs, modelResponses } = await getConfigsAndModels(storesInPage);
const configAndModelObj = await getConfigAndModelForStores(storesInPage);

storesInPage.forEach(async (s, i) => {
const { appType, defaultCode } = configs[i];
const modelResponse = modelResponses[i];
const originalStoreOptions = getStoreOptions(appType) as StoreOptions<AppState>;
storesInPage.forEach(async s => {
const { config, modelResponse } = configAndModelObj[s];
const originalStoreOptions = getStoreOptions(config.appType) as StoreOptions<AppState>;

// recursively deep copy state because without this we would have multiple
// stores writing to the same state objects
const store = new Store(getDeepCopiedStoreOptions(originalStoreOptions));

// manually initialise store because this is the responsibility of apps like
// WodinSession which aren't used in the static build
await initialiseStore(store, appType, defaultCode, modelResponse);
await initialiseStore(store, config, modelResponse);

// mount components to dom elements based on selectors
componentsAndSelectors(s).forEach(({ component, selector }) => {
Expand Down
37 changes: 30 additions & 7 deletions app/static/src/wodinStaticUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,23 @@ export const getStoresInPage = () => {
return storesInPage;
};

export const getConfigsAndModels = async (storesInPage: string[]) => {
// TODO more tolerant error handling, maybe one config didnt work (for error handling PR)
export type StaticConfig = { appType: AppType, defaultCode: string[] };
type ConfigAndModel = { config: StaticConfig, modelResponse: OdinModelResponse };

export const getConfigAndModelForStores = async (storesInPage: string[]) => {
const configPromises = storesInPage.map(s => axios.get(`./stores/${s}/config.json`));
const modelResponsePromises = storesInPage.map(s => axios.get(`./stores/${s}/model.json`));
const configs = (await Promise.all(configPromises)).map(res => res.data) as { appType: AppType, defaultCode: string[] }[];
const configs = (await Promise.all(configPromises)).map(res => res.data) as StaticConfig[];
const modelResponses = (await Promise.all(modelResponsePromises)).map(res => res.data) as OdinModelResponse[];
return { configs, modelResponses };

return Object.fromEntries(storesInPage.map((s, i) => {
const cfgAndModel: ConfigAndModel = {
config: configs[i],
modelResponse: modelResponses[i]
};
return [ s, cfgAndModel ];
}));
};

export const getDeepCopiedStoreOptions = (storeOptions: StoreOptions<AppState>) => {
Expand All @@ -87,14 +98,26 @@ export const getDeepCopiedStoreOptions = (storeOptions: StoreOptions<AppState>)
declare let odinjs: OdinRunnerOde
declare let dust: OdinRunnerDiscrete

/*
Traditionally in dynamic wodin, initialising the store is the responsibility
of the components on mount (such as WodinSession initialising the app config)
however, we do not mount these components anymore in static wodin. Static wodin
may or may not mount these components so we need to guarantee that these bits
of state are initialised.
Note: we have not thoroughly explored mounting components that impact these
specific parts of the store yet as there is no need to in static wodin. In theory
we have disabled the API service so they should not change the state but if you
are getting API related errors on static build then it returning undefined as the
response may be the cause.
*/
export const initialiseStore = async (
store: Store<AppState>, appType: AppType,
defaultCode: string[], modelResponse: OdinModelResponse
store: Store<AppState>, config: StaticConfig, modelResponse: OdinModelResponse
) => {
const appConfigPayload = {
appType,
appType: config.appType,
basicProp: "",
defaultCode,
defaultCode: config.defaultCode,
endTime: 100,
readOnlyCode: true,
stateUploadIntervalMillis: 2_000_000,
Expand Down
4 changes: 2 additions & 2 deletions app/static/tests/unit/externalScriptSrc.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { externalScripts, mountScriptTags } from "../../src/externalScriptSrc"
import { externalScripts, loadThirdPartyCDNScripts } from "../../src/externalScriptSrc"

describe("external script src", () => {
test("mounts enternal scripts to document", () => {
mountScriptTags();
loadThirdPartyCDNScripts();
externalScripts.forEach((src, i) => {
const script = document.body.getElementsByTagName("script").item(i)
expect(script!.src).toBe(src);
Expand Down
19 changes: 13 additions & 6 deletions app/static/tests/unit/wodinStaticUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { AppType } from "@/store/appState/state";
import { componentsAndSelectors, getStoreOptions, waitForBlockingScripts, getStoresInPage, getConfigsAndModels, getDeepCopiedStoreOptions, initialiseStore } from "@/wodinStaticUtils";
import {
componentsAndSelectors, getStoreOptions, waitForBlockingScripts,
getStoresInPage, getConfigAndModelForStores, getDeepCopiedStoreOptions,
initialiseStore
} from "@/wodinStaticUtils";
import { storeOptions as basicStoreOptions } from "@/store/basic/basic";
import { storeOptions as fitStoreOptions } from "@/store/fit/fit";
import { storeOptions as stochasticStoreOptions } from "@/store/stochastic/stochastic";
Expand Down Expand Up @@ -68,9 +72,11 @@ describe("wodin static utils", () => {

test("can get configs and models for stores", async () => {
const testStores = ["store1", "store2"];
const { configs, modelResponses } = await getConfigsAndModels(testStores);
configs.forEach((c, i) => expect(c).toBe(mockConfigRes(testStores[i])));
modelResponses.forEach((m, i) => expect(m).toBe(mockModelRes(testStores[i])));
const configAndModelObj = await getConfigAndModelForStores(testStores);
testStores.forEach(s => {
expect(configAndModelObj[s].config).toBe(mockConfigRes(s));
expect(configAndModelObj[s].modelResponse).toBe(mockModelRes(s));
})
});

test("can deep copy store options", () => {
Expand All @@ -97,8 +103,9 @@ describe("wodin static utils", () => {
const commit = vi.fn();
const dispatch = vi.fn();
await initialiseStore(
{ commit, dispatch } as any, AppType.Basic,
["test", "code"], "test model res" as any
{ commit, dispatch } as any,
{ appType: AppType.Basic, defaultCode: ["test", "code"] },
"test model res" as any
);

expect(commit).toBeCalledTimes(4);
Expand Down

0 comments on commit c786d14

Please sign in to comment.