newService
The newService
method creates a service mechanism for managing dynamic, multi-provider data flows. It supports consumer callbacks to react to value changes and provides a cleanup mechanism for efficient resource management.
Implementation
// Registers a new service consumer callback and returns a cleanup function
export type ServiceConsumer<T> = (
callback: (...values: T[]) => void,
) => () => void;
// Registers a new service provider and returns two functions:
// - provideService: updates the service value
// - removeProvider: unregisters the provider
export type ServiceProvider<T> = () => [
provideService: (service: T) => void,
removeProvider: () => void,
];
export type Service<T> = [
newConsumer: ServiceConsumer<T>,
newProvider: ServiceProvider<T>,
];
export function newServices<E = unknown>(
onError: (error: E) => void = console.error,
): <T>(key: string) => Service<T> {
const servicesIndex: {
[serviceKey: string]: Service<unknown>;
} = {};
return <T>(key: string) =>
(servicesIndex[key] =
servicesIndex[key] ?? newService<T, E>(onError)) as Service<T>;
}
export function newService<T, E = unknown>(
onError: (error: E) => void = console.error,
): Service<T> {
// Index of values inserted by individual providers
const consumersCallbacks: {
[consumerId: number]: (...values: T[]) => void;
} = {};
// Callbacks to notify consumers about new values
const valuesIndex: {
[providerId: number]: T;
} = {};
// Consumers
let consumerId = 0;
const newConsumer: ServiceConsumer<T> = (callback) => {
// Notify about existing values
try {
callback(...Object.values(valuesIndex));
} catch (error) {
onError(error as E);
}
// Register the callback for future updates
const id = consumerId++;
consumersCallbacks[id] = callback;
// Return a cleanup function removing the callback
return () => {
delete consumersCallbacks[id];
};
};
// Providers
let providerId = 0;
// Notifies all consumers about updated values
function notifyConsumers() {
const values = Object.values(valuesIndex);
for (const callback of Object.values(consumersCallbacks)) {
try {
callback(...values);
} catch (error) {
onError(error as E);
}
}
}
const newProvider: ServiceProvider<T> = () => {
const id = providerId++;
return [
function provideService(service: T) {
valuesIndex[id] = service;
notifyConsumers();
},
function removeProvider() {
delete valuesIndex[id];
notifyConsumers();
},
];
};
return [newConsumer, newProvider];
}
This method is useful when building systems with dynamically changing data shared between multiple producers (providers) and consumers:
- Managing shared state across different parts of an application.
- Implementing subscription-based updates.
- Reactive programming scenarios.
See also a notebook on ObservableHQ “Service Providers and Consumers” with more detailed description of dynamicly extensible systems and usage of the service consumer / provider pattern.
Examples
const [newConsumer, newProvider] = newService<string>();
// Register a consumer
const cleanup = newConsumer((...values) => {
console.log("Received values:", values);
});
// Register a provider
const [provideService, removeProvider] = newProvider();
provideService("Hello, world!");
// Output: Received values: ["Hello, world!"]
// Cleanup
cleanup();
removeProvider();
Advanced example:
const [newConsumer, newProvider] = newService<number>();
// Register multiple consumers
const cleanup1 = newConsumer((...values) => {
console.log("Consumer 1 received:", values);
});
const cleanup2 = newConsumer((...values) => {
console.log("Consumer 2 received:", values);
});
// Register multiple providers
const [provideService1, removeProvider1] = newProvider();
const [provideService2, removeProvider2] = newProvider();
provideService1(10);
provideService2(20);
// Output:
// Consumer 1 received: [10, 20]
// Consumer 2 received: [10, 20]
// Cleanup
cleanup1();
cleanup2();
removeProvider1();
removeProvider2();
Signature
export function newService<T, E = unknown>(
onError: (error: E) => void = console.error,
): Service<T>;
Parameters:
onError
: A function to handle errors that occur in consumer callbacks. Defaults toconsole.error
.
Returns Service<T>
- a tuple containing two methods:
newConsumer(callback: (...values: T[]) => void): () => void
: Registers a consumer callback and returns a cleanup function.newProvider(): [provideService: (service: T) => void, removeProvider: () => void]
: Registers a provider and returns functions to update or remove the provider.
Behavior Details
- Consumers:
- Consumers receive immediate notifications of all current values when registered.
- If multiple providers exist, all their values are passed to the consumer callbacks.
- Consumers can be unregistered using the returned cleanup function.
- Providers:
- Providers can update the shared value, which triggers notifications to all registered consumers.
- Providers can unregister themselves, which removes their values from the notification pool.
- Error Handling:
- Any error occurring in a consumer callback is caught and passed to the
onError
handler.
- Any error occurring in a consumer callback is caught and passed to the
Notes
- The
newService
method is designed to handle multiple providers and consumers efficiently. - All consumer callbacks are invoked synchronously when values are updated.
Caveats
- Ensure proper cleanup of consumers and providers to prevent memory leaks.
- Error handling in
onError
should not introduce additional errors.
Related Methods
newServices
: For managing multiple named services in a shared index.