Skip to content

createService

ts
createService<T, R = void>(definition: ServiceDefinition<T>): Service<T, R>

The primitive every built-in service is built on.

The definition

ts
interface ServiceDefinition<T> {
  props: () => T;
  hasProps?: () => boolean;
  start: (emit: (props: T) => void) => Unsubscribe;
}
FieldDoes
propsreads the current props
hasPropssays whether there is a current value, which is what { immediate: true } asks
startstarts the source on the first subscriber, and returns its teardown

Usage

ts
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. start runs 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 isActive flag, 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:

ts
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:

ts
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.

MIT Licensed