createService
createService<T, R = void>(definition: ServiceDefinition<T>): Service<T, R>The primitive every built-in service is built on.
The definition
interface ServiceDefinition<T> {
props: () => T;
hasProps?: () => boolean;
start: (emit: (props: T) => void) => Unsubscribe;
}| Field | Does |
|---|---|
props | reads the current props |
hasProps | says whether there is a current value, which is what { immediate: true } asks |
start | starts the source on the first subscriber, and returns its teardown |
Usage
import { createService } from '@studiometa/js-toolkit';
interface OnlineProps {
readonly isOnline: boolean;
}
const useOnline = createService<OnlineProps>({
props: () => ({ isOnline: navigator.onLine }),
start(emit) {
const publish = () => emit({ isOnline: navigator.onLine });
window.addEventListener('online', publish);
window.addEventListener('offline', publish);
return () => {
window.removeEventListener('online', publish);
window.removeEventListener('offline', publish);
};
},
});What you get for free
- Lazy and reference-counted.
startruns on the first subscriber and its teardown on the last. With no subscriber there is no listener, no observer and no frame. - Symmetric subscriptions.
subscribe(callback)returns the unsubscribe function. A subscription is a record, not a key in a set, so two holders of one function are two subscribers. - A safe fan-out. The fan-out walks a snapshot and each record carries an
isActiveflag, so a subscriber that unsubscribes during a delivery is not called. - Error isolation. A subscriber that throws is skipped and reported as
callback.service-failed.
Publishing is re-entrant, so any code that changes state after a publication must check first that the service is still alive.
hasProps
Omit it and the service is assumed to have a current value. Declare it when it does not:
createService<BatchProps>({
props: () => ({ records: current }),
hasProps: () => current.length > 0, // nothing to deliver between batches
start: (emit) => () => {},
});This is what makes { immediate: true } honest: the frame tick has no current value between two frames, the pointer has none before it is seen, a drag has none outside a gesture, and a mutation service has none between batches.
What a callback may return
R is a type parameter, so a service can require its callbacks to return something. useRaf() uses it to enforce RafRender:
type RafService = Service<RafProps, void | RafRender>;Scoping it to a target
Wrap it in perTarget() to get one instance per target and per options, keyed in a WeakMap — the caching every built-in service has.
Making a mixin over it
See createServiceMixin().
A consumer's service is owned by the consumer
The shared runtime coordinates the built-in service caches across duplicate copies of the package. A createService() or perTarget() call of a consumer stays owned by that consumer.