Skip to content

Service

Azarattum edited this page Nov 24, 2020 · 7 revisions

Service component runs in a separate thread and perfectly suits for heavy computation.

Services are stored in:

src
└───components
    └───app
        └───services

Since services run in a different thread from the application, all their public interfaces have to be asynchronous. Other than that they should feel no different from controllers. All the data passing between threads is managed by comlink and service loaders. Keep in mind that complex structures are structure cloned as it usually happens with workers (except for SharedArrayBuffers). Or you can transfer data with comlink's transfer (see code below).

Example

Exampler service included in the source code:

import Service from "../../common/service.abstract";

/**
 * Example of a service.
 * Best practice to name services as `something(er/or)`
 */
export default class Exampler extends Service<"">() {
	/**
	 * Initialization of Exampler service
	 */
	public async initialize(): Promise<void> {
		///Service initialization logic goes here
	}
}

According to the comment above the component, we should name services as something(er/or). It's nothing more than just a convention to not confuse services with views (which might have the same name). Similar logic applies to controllers. In practice, services and controllers are rarely have similar names, unlike controllers-views or services-views. So, it's just a little tip to help differentiate between them.

Usage

exampler.service.ts:

	...
	public meaning: number = 42;
	...
	public async getMeaning(): Promise<number> {
		return 42;
	}
	...

app.ts:

	...
	const exampler = this.getComponent(Exampler);
	await exampler.getMeaning(); //42
	await exampler.meaning; //42
	...

Where this.getComponent(Exampler) is just a way we get a single instance of Exampler from our application. It looks like we are talking directly to the Exampler object, but actually there is a complex logic hidden behind proxies to make the process seamless.

Note that we use await even with properties! Because loading data from another thread is asynchronous regardless of its origin.

Transferables

If you wish to use transferables, you need to wrap them in comlink's transfer. To do that, you have to install comlink as a direct project dependency:

npm install comlink

Then you will be able to transfer objects, such as OffscreenCanvas:

exampler.service.ts:

	...
	private canvas: OffscreenCanvas | null = null;
	...
	public async setCanvas(canvas: OffscreenCanvas): Promise<void> {
		this.canvas = canvas; //Canvas is set!
	}
	...

app.ts:

import { transfer } from "comlink";
	...
	const exampler = this.getComponent(Exampler); //Get the service
	const canvas = document.getElementsByTagName("canvas")[0]; //Get the canvas
	const offscreen = canvas.transferControlToOffscreen(); //Create offscreen canvas
	const transferable = transfer(offscreen, [offscreen]); //Make it transferable
	await exampler.setCanvas(transferable); //Call set canvas in service
	...

Features

Events

This feature is also applicable to controllers.

An ability to emit and listen for events. Used for communication between components. Handy for passing data around.

//To emit inside a component
this.emit(<event>, ...<args>);
//To listen in the app
component.on(<event>, (...<args>) => {...});

Full example:

exampler.service.ts:

import Service from "../../common/service.abstract";
//                                                 ↓  Events  ↓
export default class Exampler extends Service<"initted" | "closed">() {
	public async initialize(): Promise<void> {
		//Emit "initted" event with args "•_•" and 42
		// (they can be any values of any length)
		this.emit("initted", "•_•", 42);
	}

	/**
	 * Overriding component's close method
	 */
	public async close(): Promise<any> {
		//Emit "closed" event without any arguments
		// before closing
		this.emit("closed");

		//Call the original method
		await super.close();

		//At this moment events are already not available
	}
}

In this example we set <"initted" | "closed"> in the class definition to strictly type the list of events that could be emitted. Then we emit events when the component is initialized and closed using this.emit(<event>, ...<args>).

To listen for these events:

app.ts:

	...
	const exampler = this.getComponent(Exampler); //Get the service
	exampler.on("initted", (arg1: string, arg2: number) => {
		console.log(`Initted with ${arg1} ${arg2}`); //Initted with •_• 42
	});
	exampler.on("closed", () => {
		console.log("Closed..."); //Closed...
	});
	...

The code above is better written with handlers (registering events for every instance of Exampler):

	...
	@handle(Exampler)
	public onExampler(self: Exampler): void {
		self.on("initted", (arg1: string, arg2: number) => {...});
		self.on("closed", () => {...});
	}
	...

Exposing

This feature is also applicable to controllers.

Exposing allows you to share some functions with global context. Useful for debugging or for straight calls from UI.

//To expose an inline function inside a component
this.expose(<name>, <function>);
//To expose <name> function which is a method of the component
this.expose(<name>);
//Decorate component's function to expose it
@expose
public async <name>(): Promise<any> {
    ...
}
//Decorate to expose with custom name
@expose(<name>)
public async myFunction(): Promise<any> {
    ...
}


//To use anywhere
globalThis.<component>.<name>();

Note that the component's name is always lowercased! If there are multiple components (of one type) exposing the same function, then the functions will be applied together and return an array of results of each function, unless a relation is provided (only reasonable for controllers).

Full example:

import { expose } from "../../common/component.interface";
import Service from "../../common/service.abstract";

export default class Exampler extends Service<never>() {
	@expose
	public async say(text: string): Promise<number> {
		console.log(`Service says: ${text} from another thread!`);
		return 42;
	}
}

Then you can just open developer console in browser or interactive mode in node. In there you call:

 await exampler.say("hello");
// Service says: hello from another thread!
 42

Where exampler is a part of the global context.

Clone this wiki locally