Getting Started guide
Everything in the framework is code: no editor, no generator, no assets. This guide takes a plain TypeScript project from an empty file to a running service in six steps.
Overview
You will:
- install the core package
- create a
ServiceManagerwith a scheduler and an environment - create the root configuration, a profile
- write a service and its token
- register the service in the profile
- resolve the service and check the manager is ready
Installing the Service Framework
npm install @realitycollective/service-framework
Add the binding for your host when you have one; host runtimes explains each. Plain TypeScript needs nothing else.
Creating the Service Manager
The manager is an object you create. It takes two things. A scheduler is the object the host feeds with frames; the manager turns each frame into update, render and the other per-frame calls on your services. An environment descriptor is a named set of capability strings that says what the host can do, such as dom or render-loop; createBrowserEnvironment() builds the usual one for a browser page.
import { createBrowserEnvironment, ManualScheduler, ServiceManager, TimerScheduler } from "@realitycollective/service-framework";
// Tests and tools: ManualScheduler fires nothing until you emit a channel yourself.
const manual = new ServiceManager({ scheduler: new ManualScheduler() });
// A browser app without a render engine: TimerScheduler runs timers that drive update, lateUpdate and fixedUpdate.
const timed = new ServiceManager({
scheduler: new TimerScheduler(),
environment: createBrowserEnvironment(),
});
Both arguments are optional. The defaults are a ManualScheduler, which emits nothing on its own, and an environment with no capabilities. Those defaults suit a test; a real app passes both.
A React app uses the provider from service-framework-react, which owns the manager for you. A three.js or Babylon.js app bridges its render loop into the scheduler. Both are covered in host runtimes.
Creating the root configuration for the Service Manager
The root configuration is a profile: a named list of registrations.
import { createServiceProfile } from "@realitycollective/service-framework";
export const profile = createServiceProfile("my-app", [
// registrations go here
]);
The profile is plain data, so it can be built in code, loaded from JSON, or assembled from several smaller lists.
The environment switcher
The environment descriptor is how a profile behaves differently on different hosts. Supply your own to test how a profile behaves with a given set of capabilities:
import { createEnvironmentDescriptor, ManualScheduler, ServiceManager } from "@realitycollective/service-framework";
const environment = createEnvironmentDescriptor("test", ["dom", "render-loop"]);
const manager = new ServiceManager({ scheduler: new ManualScheduler(), environment });
Registrations that require a capability the environment lacks are left out. Service design covers the gating rules.
Writing a service
A service is a class, plus a token that identifies it at runtime.
import { BaseService, createServiceToken } from "@realitycollective/service-framework";
export interface LoggerConfig {
readonly level: "info" | "debug";
}
export class LoggerService extends BaseService<LoggerConfig> {
public override initialize(): void {
console.log(`Logger ready at ${this.serviceConfig.level}`);
}
public log(message: string): void {
console.log(`[${this.serviceConfig.level}] ${message}`);
}
}
export const LOGGER_TOKEN = createServiceToken<LoggerService>("LoggerService");
The token exists because TypeScript interfaces are erased when the code is compiled, so the framework needs a runtime identity to look a service up by. The type parameter keeps that lookup strongly typed in your editor.
Things to notice inside the class:
serviceNameandservicePrioritycome from the registrationserviceConfigis the typed configuration object supplied in the profilemanager,scheduler,environmentandabortSignalare available on the base class
Configuring your service
A registration says which class, what name, what priority, what configuration. Add one to the profile's list:
{
token: LOGGER_TOKEN,
name: "Logger", // optional; defaults to the token description
priority: 5, // optional; defaults to 10, lower starts first
config: { level: "debug" },
useClass: LoggerService,
},
useFactory is the alternative to useClass when construction needs more than new:
{
token: LOGGER_TOKEN,
useFactory: (context) => new LoggerService(context),
}
Accessing your service
Check your Service is registered
manager.initializeProfile(profile);
manager.start();
const maybeLogger = manager.tryResolve(LOGGER_TOKEN); // undefined if not registered
Get a reference to your service using resolve
const logger = manager.resolve(LOGGER_TOKEN);
logger.log("Application started");
resolve throws Unable to resolve service "..." when nothing is registered for the token. Loud, and early.
Safer access to getting a service
tryResolve returns undefined instead of throwing:
const logger = manager.tryResolve(LOGGER_TOKEN);
if (logger) {
logger.log("Optional logging enabled");
}
When several services share a token, resolveAll returns every one and resolve(token, name) picks one by registration name. Service patterns covers that model.
Checking the Service Manager is ready
Code that runs before the profile is initialized can wait for it, and a resolve can wait for a service to appear:
await manager.waitUntilInitialized();
// Waits up to two seconds for a service registered later, for example after an async host setup.
const logger = await manager.resolveAsync(LOGGER_TOKEN, 2000);
This matters when composition happens in stages: the React provider mounting, an XR session starting, a profile loaded from the network. Both default to a one second timeout.
See the running state of your service
The manager reports its state as data:
const diagnostics = manager.getDiagnostics(); // initialized, started, and every service with its priority and modules
const graph = manager.getDependencyGraph(); // who depends on whom, in start order
getDiagnostics() answers "what is the state right now". For "what happened, and in what order", give the manager a log function; logging and telemetry explains what it reports.
Check the API docs for more calls
The TypeScript API reference is generated from the source and lists every method on ServiceManager, BaseService and the rest.
Onward
Service design goes deeper into what a service should own, its lifecycle and how it reacts to the environment.
More information
- Introduction
- Service design
- Host runtimes
- Walkthrough: weather client
- Coming from Unity? Migrating from Unity maps each concept