Skip to content

Helpers

Jiří Cihelka edited this page Dec 30, 2023 · 2 revisions

Helpers are functions and classes, that can be used in multiple places and are not tightly coupled with the library. They are located in the ./src/helpers/ directory.

Helper classes are usually customized data structures.

Most helpers should be well documented using comments inside the code and by using descriptive names. All can be found in the helpers section of the API documentation. The more complicated ones are also described in the wiki.

Cache

The performance of the library relies heavily on caching. For this reason we have a custom cache interface with a custom implementation, that can however be upgraded when needed. The cache is located in the ./src/helpers/cache/ directory.

Our standard cache (the MemoryMapCache) is a simple map, that stores the values in memory. It is also extended by the MemoryMapCacheWithAutomaticCalculation.
The MemoryMapCacheWithAutomaticCalculation is a cache, that also handles the calculation of the values. When used, reading values consists only of a single function call. The function call checks if the value is already calculated and if not, it calculates it and saves it. This means, that the value is calculated only once. This is useful for values, that are expensive to calculate and are used multiple times. This cache is used as wrapper around almost any potentially expensive calculation in the library.

The Cache interface also supports clearing the cache. This must be done manually, as none of our cache implementations currently clean automatically. This is however expected, as the validity of our data usually doesn't expire unless the inputs change (when they do, we clear the cache). All of our caches are also fixed size, which negates the need for automatic cleaning because the cache will never grow too large.

Typing of cache values

The Cache interface is generic. It has two type parameters: Records and NonEmpty.
The NonEmpty generic parameters needs to extends the boolean type and defaults to false. If it is set to true, it is expected, that the cache will always hold a value for every key defined in the Records type. This can be used, for example, when the constructor of the cache requires default values, or calculates them automatically.
The Records generic parameter is an object type, that defines the keys and values of the cache.

Example use of the Cache interface:

import { Cache } from "./cache";

class MyCache extends Cache<{foo: number, bar: string}, false> {
    // Code for the cache
}

const cache = new MyCache();

cache.readValue("foo"); // number | undefined
cache.readValue("bar"); // string | undefined
cache.readValue("baz"); // Type '"baz"' is not assignable to type '"foo" | "bar"'.

This cache doesn't require default values and doesn't calculate them automatically. It can return undefined for any key, which is also reflected in the return type of the readValue method.

Equality

A big issue encountered when working with floating point numbers is the equality of two numbers. This is because floating point numbers have a finite precision and therefore cannot represent all real numbers. This means, that two numbers, that should be equal, can be represented by two different floating point numbers if they are created by different calculations. This is a problem, because we cannot compare two floating point numbers using the === operator.
This is why we define our own equality function, that compares two numbers to a certain precision.
The equality precision can be set using the setEqualityOptions. The default precision is 43 binary digits. This is 10 bits lower than the precision of the double type, which is 53 binary digits.
Another problem is encountered when comparing a number to zero. This is because we don't know the precision of the number. In this case we use the unit member of the EqualityOptions object. The unit is a number, that is used as a reference for the precision. The default unit is 1.
Checking the equality is then reduced to using the isEqual function.

Get type string

The getTypeString function inside the ./src/helpers/getTypeString.ts file is used to get the type of an object as a string. The library is written in TypeScript, which ensures that the types are correct at compile time. If an error is encountered at runtime however, we don't get any information about the type of the object. This function gets the type of the object as a string, which can be used to debug the error.
This function is not perfect and because we are not sure if it will remain working the same way in the future, it has no defined behavior. Only specification this function has is that it returns a human readable string describing the type of the object and works for any JavaScript value.
This function is recursive and slow. It should only be used on Error for debugging purposes.

Wrappers for external code

If the library uses some external code, it should be wrapped by an object or function inside the ./src/helpers/external/ directory. This is done to make the library more loosely coupled with the external code. This allows us to easily replace the external code with another implementation or with a custom implementation.
Our npm modules and git submodules are all considered external code.