---
description: Understand the core systems about the viewer.
---

# Prerequisite Knowledge

## Introduction - Profiles

All the source code for the viewer can be found in `path_to_viewer/resources/app` .

In the majority of cases this code is not relevant, and we are only interested in the profiles which the viewer loads and uses for parsing and rendering replays. These can be found in `path_to_viewer/resources/app/assets/profiles` where each folder here represents a separate profile.

Out of the box you should find 4 profiles:

* `vanilla` - This is the base profile that gets automatically loaded and is used for handling vanilla gameplay.
* `template` - A template profile which can be copied and pasted for creating new profiles. By default loads vanilla.
* `duo trials` - Profile for the custom rundown [Duo Trials](https://thunderstore.io/c/gtfo/p/ProjectZaero/Duo_Trials/).
* `descent` - Profile for the custom rundown [Descent](https://thunderstore.io/c/gtfo/p/Yukitty/Descent/).

When a profile is loaded, all the JavaScript files within the profile folder is executed and will be watched for hot reloading via a system called the [Async Script Loader (ASL)](#async-script-loader-asl):

## Async Script Loader (ASL)&#x20;

{% hint style="info" %}
Understanding of ASL requires decent knowledge of [JavaScript ES6 Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) and [JavaScript Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). \
\
If you are not making complex script changes then feel free to skip ahead. Sections that require knowledge about ASL will link back to this page.&#x20;
{% endhint %}

ASL is the core system that handles the dynamic loading of scripts, it is used over the existing JavaScript module system, ES6 Modules, to support hot reloading for the ease of use for developers.

Since it does not rely on ES6 Modules, it has its own syntax for performing corresponding actions. Lets take a look at a simple ASL module and its ES6 Modules counterpart:

```javascript
const { Vector3 } = await require("three", "esm");
const { foo } = await require("./B.js", "asl");

exports.bar = function() {
    return foo(new Vector3(1, 2, 3));
}
```

The ES6 Modules counterpart: &#x20;

```javascript
import { Vector3 } from "three";
import { foo } from "./B.js";

export function bar() {
    return foo(new Vector3(1, 2, 3));
}
```

As you can see there are not many differences between ES6 Modules and ASL apart from the syntax.&#x20;

### Async

ASL modules all execute in an Asynchronous context. Under the hood, this is done via an `eval()` call within an `async function` :

```javascript
(async function() { /* Your Module Code ... */ })()
```

This allows for the use of the `await` keyword to be used which is useful with async methods that fetch content such as models / other ASL modules.

### Importing Modules

To import other modules you use the function `require` :

```typescript
function require(path: string, type: "esm" | "asl" = "asl"): Promise<any>;
```

***

`require` accepts a path to the module you wish to import and an optional parameter to specify the import type. If no import type is specified, `"asl"` is assumed by default.

If the `path` provided is an absolute path (does not start with `.` ) then it will load the path relative to the `baseURI` of the page. This will be from `path_to_viewer/resources/app/assets/main` .&#x20;

For example, the following code will import the module located in `path_to_viewer/resources/app/assets/main/B.js` :

```javascript
require("B.js");
```

Relative paths are resolved as expected, importing the module relative ot the current module.

***

Sometimes you may wish to import regular ES6 Modules and not just ASL modules, this is where the `type` parameter comes in. By default, it is assumed that you are loading an ASL module, however if you specify `"esm"` ,  you can import regular ES6 modules.

Importing ES6 modules follows the same path resolution as if you were using the `import` statement. This includes aliases specified by the [`importmap`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) .

For example, the viewer frequently uses the [THREE JS](https://threejs.org/docs/#manual/en/introduction/Installation) library which is an ES6 module:

```javascript
const { Vector3 } = await require("three", "esm");
```

***

`require` returns a [Promise ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)which resolves once a module finishes importing. Once resolved, it returns an object containing the exports of the imported module.

Because it returns a Promise, and modules execute within an Async context, you can `await` a `require` call:

```javascript
const B = await require("./B.js");
```

### Exports

ASL supports exporting items from one module such that they are accessible from another. This is done using the `exports` syntax:

<pre class="language-javascript"><code class="lang-javascript"><strong>// Module A
</strong><strong>exports.foo = function() {
</strong>    console.log("Hello from A");
}

exports.bar = 1;
</code></pre>

```javascript
// Module B
const { foo, bar } = await require("./A.js");

foo(); // "Hello from A"
console.log(bar); // 1
```

Unlike ES6 module exports, features such as ["default exports" or "aggregation"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) are not supported. ASL exports are more akin to using the `this` keyword to assign properties to an instance.

Because of this, care needs to be taken to ensure the correct behaviour when changing non-referenced values:

```javascript
// Module A
let foo = 1;
exports.foo = foo;
foo = 2;
// As foo is not an object, it gets copied and thus changing foo
// does not change exports.foo

exports.bar = 1;
exports.bar = 2;
```

```javascript
// Module B
const { foo, bar } = await require("./A.js");

console.log(foo); // 1
console.log(bar); // 2
```

Further more, exports are immutable. The decision to do so is to prevent reference gymnastics with import syntax:

```javascript
let { foo } = await require("./A.js");

foo = 10; // This does not actually change the value of `foo` in module `A`, 
          // due to how javascript object decomposition works.
          
const A = await require("./A.js");

A.foo = 10; // Although this would work, the syntax is a gotcha that is annoying
            // to account for when mixed with decomposition syntax and is just
            // a source of bugs in the event you forget about this behaviour.
            // 
            // As a result, I don't allow this behaviour at all and ASL
            // will throw an Error.
```

{% hint style="success" %}
The correct pattern to handle the above would be:

```javascript
// ** Module A **

// object reference pattern
exports.foo = { value: 10 };

// getter / setter pattern
let bar = 10;
exports.bar = (...args) => {
    if (args.length > 0) bar = args[0];
    else return bar;
}
```

```javascript
// ** Module B **
const { foo, bar } = await require("./A.js");

console.log(foo.value); // 10
foo.value = 20;
console.log(foo.value); // 20

console.log(bar()); // 10
bar(20);
console.log(bar()); // 20
```
{% endhint %}

### Circular Dependencies

ES6 will still load modules even if there are circular dependencies which can cause bugs due to an export not yet being available within said loop.

ASL, on the other hand, simply will continue execution until a module has fully loaded.  This means that in the event of a circular dependency, the module will not complete execution:

```javascript
// Module A
exports.A = 1;

console.log("waiting for B"); // "waiting for B"
const { B } = await require("./B.js");

console.log(exports.A + B); // does not get reached
```

```javascript
// Module B
exports.B = 2;

console.log("waiting for A"); // "waiting for A"
const { A } = await require("./A.js");

console.log(A + exports.B); // does not get reached
```

In the above case, both modules will pause on there `await require()` after logging `waiting for A` and `waiting for B` . This is because `require` cannot import either module until both have completed, but as both depend on each other, this will never happen.

***

To get around this, ASL provides a the method `module.ready()` which tells ASL that all exports are finished and the module is ready for use even though it has not yet finished executing.&#x20;

```javascript
// Module A
exports.A = 1;
module.ready();

console.log("waiting for B"); // "waiting for B"
const { B } = await require("./B.js");

console.log(exports.A + B); // 3
```

```javascript
// Module B
exports.B = 2;
module.ready();

console.log("waiting for A"); // "waiting for A"
const { A } = await require("./A.js");

console.log(A + exports.B); // 3
```

{% hint style="warning" %}
Once a module has been marked as `ready` it's exports can no longer be changed:

```javascript
exports.A = 1;
module.ready();
exports.A = 2; // Error! - Immutable exports
exports.B = 2; // Error! - Cannot add new exports
```
{% endhint %}

***

Circular dependencies can be debugged in the electron [browser developer tools](https://developer.chrome.com/docs/devtools/) and typing `vm.requires` . This will output a `Map` where the keys represent modules that are waiting and the values specify which modules they are waiting for.

### Destructor

As ASL provides hot reloading support, modules need to manage being unloaded to free resources and prevent memory leaks. To do this, you can assign a destructor which gets called when a module is unloaded:

```javascript
module.destructor = function() {
    console.log("This module is being destructed!");
};
```

### Error Handling

ASL tries to provide as helpful error messages as possibly:

<figure><img src="https://4046408204-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fq6xD7htHf8NjtJZNTdPN%2Fuploads%2Fn6znXA5a6O5Q189Q7MMp%2FScreenshot%202024-07-14%20165351.png?alt=media&#x26;token=0815cfab-4b7e-41a6-8fee-0bac6dce75b7" alt=""><figcaption><p>Verbose error message provided by ASL</p></figcaption></figure>

However, sometimes this is not possible as the error is not caught by ASL such as in the case of [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). In these cases, for better error handling, you can catch the error and throw it again throw `module.raise()`:

```javascript
promise.catch((error) => throw module.error(error));
```

This will wrap the error in a verbose message as shown above.

You may also include an additional message to the error to provide more context, this will be logged along side the actual error:

```javascript
promise.catch((error) => throw module.error(error, "Context"));
```

{% hint style="warning" %}
Wrapping an error multiple times will not do anything, and thus trying to add multiple context messages will only result in the first being visible:

```javascript
throw module.error(module.error(error, "1"), "2");
```

The above will only log `"1"` and `"2"` will never be visible.
{% endhint %}

### Memory Leaks

Due to the nature of hot reloading and switching profiles, it can be very easy to leak memory.&#x20;

JavaScript is a garbage collected language which means that memory is freed once all references to objects are lost. The issue is that there are a lot of hidden references that are held by the browser which can stop modules from being properly garbage collected after hot reloading / changing profile.

{% hint style="danger" %}
```javascript
let A = 0;
exports.A = () => A;

const element = document.getElementById("some-element");
element.addEventListener("click", () => {
    ++A;
    element.innerHTML = `${exports.A()}`;
});
```
{% endhint %}

The above module will leak as it can never be garbage collected since the `element` on the browser will continue to hold a reference to it via the event listener.&#x20;

A simple fix can be achieved by clearing the event listener on module destruction.

{% hint style="success" %}
```javascript
let A = 0;
exports.A = () => A;

const element = document.getElementById("some-element");
const onClick = () => {
    ++A;
    element.innerHTML = `${exports.A()}`;
};
element.addEventListener("click", onClick);

module.destructor = () => {
    element.removeEventListener("click", onClick);
};
```
{% endhint %}

A useful pattern for this is the [AbortController ](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)pattern which allows you to pass an abort signal to each event listener such that it automatically is cleaned on destruction:

{% hint style="success" %}
```javascript
const dispose = new AbortController();
module.destructor = () => {
    dispose.abort();
};

let A = 0;
exports.A = () => A;

const element = document.getElementById("some-element");
element.addEventListener("click", () => {
    ++A;
    element.innerHTML = `${exports.A()}`;
}, { signal: dispose.signal });
```
{% endhint %}

To simply the above, ASL provides the abort signal for you in `module.dispose`such that you do not need to create one yourself:

```javascript
let A = 0;
exports.A = () => A;

const element = document.getElementById("some-element");
element.addEventListener("click", () => {
    ++A;
    element.innerHTML = `${exports.A()}`;
}, { signal: module.dispose }); // module.dispose signal is triggered after
                                // module destructor automatically
```

Despite this, the use of [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) is still useful to be aware of in the event you want to dispose of event listeners manually.

### Additional Helpers

ASL provides additional helper functions:

* `module.src` - provides the path to the current module file.
* `module.rel(path: string)` - converts a relative path from this module to an absolute path relative to the baseURI. This is useful when targeting relative files for `fetch` or model loaders such as [`GLTFLoader`](https://threejs.org/docs/#examples/en/loaders/GLTFLoader) .
