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:

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:

Returns Service<T> - a tuple containing two methods:

  1. newConsumer(callback: (...values: T[]) => void): () => void: Registers a consumer callback and returns a cleanup function.
  2. newProvider(): [provideService: (service: T) => void, removeProvider: () => void]: Registers a provider and returns functions to update or remove the provider.

Behavior Details

Notes

Caveats