@realitycollective/service-framework
The core runtime of the Reality Collective Service Framework for TypeScript - dependency injection, service lifecycle, events, schedulers and configuration, for browser and app runtimes.
npm install @realitycollective/service-framework
A TypeScript-first implementation of the same architecture as the Reality Collective Unity Service Framework: services are registered into a profile, resolved by token, and driven by a scheduler that the host environment owns.
What it provides
| Area | Detail |
|---|---|
| Service manager | Registration, dependency-ordered start/stop, resolution by token, wait-for-service |
| Base services | BaseService and BaseServiceModule - lifecycle hooks with typed configuration |
| Tokens | createServiceToken<T>() - type-safe resolution with no string keys at the call site |
| Scheduler | Named channels such as renderTick that services subscribe to. Your app decides what drives them: a timer, a render loop, or manual ticks |
| Events | An in-framework event service for service-to-service messaging |
| Configuration | Profile-based configuration with environment awareness |
| Runtime adapter | RuntimeAdapter - the host seam a service depends on: a per-frame fan-out, XR capability flags, and an optional session facet (request, end, state and visibility). Host bindings implement it |
| Session features | SessionRequestOptions.requiredFeatures and optionalFeatures - WebXR feature strings for one request, merged over the host's own defaults by mergeSessionInit(init, options). An app that swaps mode mid-session needs them, because the host's defaults were chosen for the mode it is leaving |
| Capability derivation | deriveCapabilities(session) - reads immersive, handTracking, planeDetection, passthrough and environmentBlendMode off a live XR session. Shared by every host binding, so the same session reports the same flags under IWSDK and three.js. Its input, CapabilitySessionLike, is structural: no WebXR types, no DOM |
| State-owning services | SnapshotService<TConfig, TSnapshot> - one immutable snapshot plus pub/sub; subscribers get the current value immediately, then every publish |
| Headless testing | MockRuntimeAdapter - drives frames, capabilities and session lifecycle with no engine, no WebXR and no headset |
| Adapter conformance | runtimeAdapterContractCases() - the checks every RuntimeAdapter must pass, shipped as data rather than as a test file, so an adapter written outside this repository can prove it conforms |
EnvironmentDescriptor here means the platform environment - the host's name and its capability strings, such as "dom" or "render-loop" - and is not the same thing as EnvironmentSpec in @realitycollective/webxr-environment, which describes the visual environment of sky, fog and lighting. An app can hold both at once, so the two names are worth keeping apart.
Usage
import {
ManualScheduler,
ServiceManager,
createServiceProfile,
} from "@realitycollective/service-framework";
const scheduler = new ManualScheduler();
const manager = new ServiceManager({ scheduler });
manager.initializeProfile(createServiceProfile("my-app", [/* your service registrations */]));
manager.start();
The host decides what drives the scheduler - a timer, a render loop, or an XR frame source.
Host bindings
The core is host-agnostic. Add exactly one binding for your runtime:
| Package | Host |
|---|---|
service-framework-react | React provider and hooks |
service-framework-three | three.js render loop, plus a WebXR runtime adapter for any page that owns its renderer |
service-framework-babylon | Babylon.js render loop |
service-framework-iwsdk | Meta IWSDK (WebXR) frame source |
service-framework-client | React + three.js, already wired together |
Writing your own binding
An adapter for a host nobody has covered yet has to behave exactly as the bindings above do, or a service that passes its unit tests stops behaving the same way on a headset. runtimeAdapterContractCases() is the set of checks that says so, shipped as data rather than as a test file so it can run in your repository under your own runner. Each case returns silently on success and throws a plain Error describing the failure otherwise:
import { runtimeAdapterContractCases } from "@realitycollective/service-framework";
for (const contractCase of runtimeAdapterContractCases()) {
it(contractCase.name, () => contractCase.run(makeSubject()));
}
makeSubject() returns a RuntimeAdapterSubject: your adapter, plus a RuntimeAdapterDriver that pushes a frame, sets capability flags and - if your host owns sessions - starts and ends one. Build a fresh subject per case, because the session cases drive a session through its whole lifecycle. Some cases are asynchronous, so the runner has to await what run returns. An adapter with no session facet passes the session cases without running them, since session is optional.
Live examples
- Weather client walkthrough: service-framework-weather.pages.dev
- Client runtime reference: service-framework-client-app.pages.dev
Documentation
Architecture, service authoring and consumption patterns are documented in the repository, including a Unity-to-web migration guide. A runnable example ships in this package's Examples/ folder.
License
MIT - see LICENSE.
Classes
| Class | Description |
|---|---|
| BaseEventService | - |
| BaseService | - |
| BaseServiceModule | - |
| EnvironmentDescriptor | The platform environment a service is running in: a name plus the capability strings the host offers, such as "dom" or "render-loop". Configuration profiles gate on it, so a service can register differently on a browser page and in a headless test. |
| ManualScheduler | - |
| MockRuntimeAdapter | - |
| ServiceManager | - |
| ServiceToken | - |
| SnapshotService | - |
| TimerScheduler | - |
Interfaces
| Interface | Description |
|---|---|
| AdapterCapabilities | XR capabilities services gate on (e.g. passthrough requires immersive). |
| CapabilityInputSourceLike | One entry of XRSession.inputSources; hand is set for a tracked hand. |
| CapabilitySessionLike | The slice of a live XR session capability derivation reads. Every member is optional, so a host that reports less than a full XRSession still fits. |
| DependencyGraphNode | - |
| FocusChangeContext | - |
| FrameInfo | - |
| IEnvironmentDescriptor | - |
| IEventService | - |
| IScheduler | - |
| IService | - |
| IServiceModule | - |
| LifecycleContext | - |
| PauseChangeContext | - |
| RuntimeAdapter | - |
| RuntimeAdapterContractCase | One check a RuntimeAdapter implementation must pass. run returns silently on success and throws an Error describing the failure otherwise, so any test runner can host it. Some cases are asynchronous, so a runner must await whatever run returns. |
| RuntimeAdapterDriver | How a case drives the adapter's host, whatever that host happens to be. |
| RuntimeAdapterSubject | One adapter plus the means to drive its host, as a case receives it. |
| SchedulerEventMap | - |
| ServiceActivationContext | - |
| ServiceDiagnostics | - |
| ServiceModuleRegistration | - |
| ServiceProfile | - |
| ServiceRegistration | - |
| ServiceSnapshot | - |
| SessionFacet | Optional session lifecycle facet. Present on adapters whose host owns an XR session; absent on hosts that do not (a plain render loop, for instance). Check adapter.session before using it. |
| SessionInitLike | The two members of an XRSessionInit this merge touches. |
| SessionRequestOptions | - |
| TimerSchedulerOptions | - |
Type Aliases
| Type Alias | Description |
|---|---|
| CapabilitiesListener | - |
| EnvironmentBlendMode | The three blend modes WebXR defines for XRSession.environmentBlendMode. |
| EventHandler | - |
| FrameListener | - |
| RuntimeAdapterFacet | - |
| SchedulerChannel | - |
| SchedulerHandler | - |
| ServiceClass | - |
| ServiceContext | The activation-context type a service constructor receives. Aliased here so every service uses one consistent, framework-correct shape (matches the useFactory(context) parameter the ServiceManager passes). |
| ServiceFactory | - |
| SessionFailureReason | Why a session request did not produce a session. |
| SessionMode | The session kinds a host can be asked for; the WebXR session mode strings. |
| SessionResult | The outcome of SessionFacet.request. A request never rejects: a host that cannot start a session is a normal runtime condition, not a bug, so the caller gets a result to branch on rather than an exception to catch. |
| SessionState | Where the host is in the session lifecycle. |
| SessionVisibility | Session visibility as the host reports it. non-immersive means the page is running in 2D with no session at all, which WebXR itself has no value for. |
| SnapshotListener | - |
| TelemetryLevel | Severity of a record. Sinks route on this rather than parsing the name. |
| TelemetryLog | A telemetry emitter. Supply one to ServiceManager to receive the framework's own records; every service then reaches the same emitter through logEvent. |
| Unsubscribe | Runtime adapter contract - the seam between a host runtime and services. |
Variables
| Variable | Description |
|---|---|
| DEFAULT_CAPABILITIES | - |
| DEFAULT_SESSION_TIMEOUT_MS | How long SessionFacet.request waits before reporting a timeout. |
| NO_OP_TELEMETRY_LOG | The emitter used when no telemetry is configured. One shared function for the whole application, so an unconfigured manager allocates nothing per call and a consumer can identity-compare against it to detect the default. |
| RUNTIME_ADAPTER_FACETS | Every optional facet a RuntimeAdapter can carry. A conformance test walks this list, so an adapter that grows a facet without implementing it in the mock fails the suite rather than being found by a consumer. |
Functions
| Function | Description |
|---|---|
| createBrowserEnvironment | - |
| createEnvironmentDescriptor | - |
| createServiceProfile | - |
| createServiceToken | - |
| deriveCapabilities | Derive the capability flags from a live session. |
| mergeSessionInit | Fold a request's features into the host's XRSessionInit. |
| runtimeAdapterContractCases | The whole suite, in a stable order. |