Files
pyscript/pyscriptjs/src/runtime.ts
Antonio Cuni c75f885cb4 Refactor py-config and the general initialization logic of the page (#806)
This PR is the first step to improve and rationalize the life-cycle of a pyscript app along the lines of what I described in #763 .
It is not a complete solution, more PRs will follow.
Highlights:

- py-config is no longer a web component: the old code relied on PyConfig.connectedCallback to do some logic, but then if no <py-config> tag was present, we had to introduce a dummy one with the sole goal of activating the callback. Now the logic is much more linear.

- the new pyconfig.ts only contains the code which is needed to parse the config; I also moved some relevant code from utils.ts because it didn't really belong to it

- the old PyConfig class did much more than dealing with the config: in particular, it contained the code to initialize the env and the runtime. Now this logic has been moved directly into main.ts, inside the new PyScriptApp class. I plan to refactor the initialization code in further PRs

- the current code relies too much on global state and global variables, they are everywhere. This PR is a first step to solve the problem by introducing a PyScriptApp class, which will hold all the mutable state of the page. Currently only config is stored there, but eventually I will migrate more state to it, until we will have only one global singleton, globalApp

- thanks to what I described above, I could kill the appConfig svelte store: one less store to kill :).
2022-10-04 14:26:12 +02:00

173 lines
5.1 KiB
TypeScript

import type { AppConfig } from './pyconfig';
import type { PyodideInterface } from 'pyodide';
import type { PyLoader } from './components/pyloader';
import {
runtimeLoaded,
loadedEnvironments,
globalLoader,
initializers,
postInitializers,
Initializer,
scriptsQueue,
} from './stores';
import { createCustomElements } from './components/elements';
import type { PyScript } from './components/pyscript';
import { getLogger } from './logger';
const logger = getLogger('pyscript/runtime');
export const version = "<<VERSION>>";
export type RuntimeInterpreter = PyodideInterface | null;
let loader: PyLoader | undefined;
globalLoader.subscribe(value => {
loader = value;
});
let initializers_: Initializer[];
initializers.subscribe((value: Initializer[]) => {
initializers_ = value;
});
let postInitializers_: Initializer[];
postInitializers.subscribe((value: Initializer[]) => {
postInitializers_ = value;
});
let scriptsQueue_: PyScript[];
scriptsQueue.subscribe((value: PyScript[]) => {
scriptsQueue_ = value;
});
/*
Runtime class is a super class that all different runtimes must respect
and adhere to.
Currently, the only runtime available is Pyodide as indicated by the
`RuntimeInterpreter` type above. This serves as a Union of types of
different runtimes/interpreters which will be added in near future.
The class has abstract methods available which each runtime is supposed
to implement.
Methods available handle loading of the interpreter, initialization,
running code, loading and installation of packages, loading from files etc.
For an example implementation, refer to the `PyodideRuntime` class
in `pyodide.ts`
*/
export abstract class Runtime extends Object {
config: AppConfig;
abstract src: string;
abstract name?: string;
abstract lang?: string;
abstract interpreter: RuntimeInterpreter;
/**
* global symbols table for the underlying interpreter.
* */
abstract globals: any;
constructor(config: AppConfig) {
super();
this.config = config;
}
/**
* loads the interpreter for the runtime and saves an instance of it
* in the `this.interpreter` property along with calling of other
* additional convenience functions.
* */
abstract loadInterpreter(): Promise<void>;
/**
* delegates the code to be run to the underlying interpreter
* (asynchronously) which can call its own API behind the scenes.
* */
abstract run(code: string): Promise<any>;
/**
* delegates the setting of JS objects to
* the underlying interpreter.
* */
abstract registerJsModule(name: string, module: object): void;
/**
* delegates the loading of packages to
* the underlying interpreter.
* */
abstract loadPackage(names: string | string[]): Promise<void>;
/**
* delegates the installation of packages
* (using a package manager, which can be specific to
* the runtime) to the underlying interpreter.
*
* For Pyodide, we use `micropip`
* */
abstract installPackage(package_name: string | string[]): Promise<void>;
/**
* delegates the loading of files to the
* underlying interpreter.
* */
abstract loadFromFile(path: string): Promise<void>;
/**
* initializes the page which involves loading of runtime,
* as well as evaluating all the code inside <py-script> tags
* along with initializers and postInitializers
* */
async initialize(): Promise<void> {
loader?.log('Loading runtime...');
await this.loadInterpreter();
const newEnv = {
id: 'default',
runtime: this,
state: 'loading',
};
runtimeLoaded.set(this);
// Inject the loader into the runtime namespace
// eslint-disable-next-line
this.globals.set('pyscript_loader', loader);
loader?.log('Runtime created...');
loadedEnvironments.update(environments => ({
...environments,
[newEnv['id']]: newEnv,
}));
// now we call all initializers before we actually executed all page scripts
loader?.log('Initializing components...');
for (const initializer of initializers_) {
await initializer();
}
loader?.log('Initializing scripts...');
for (const script of scriptsQueue_) {
void script.evaluate();
}
scriptsQueue.set([]);
// now we call all post initializers AFTER we actually executed all page scripts
loader?.log('Running post initializers...');
// Finally create the custom elements for pyscript such as pybutton
createCustomElements();
if (this.config.autoclose_loader) {
loader?.close();
}
for (const initializer of postInitializers_) {
await initializer();
}
// NOTE: this message is used by integration tests to know that
// pyscript initialization has complete. If you change it, you need to
// change it also in tests/integration/support.py
logger.info('PyScript page fully initialized');
}
}