atomica

Companion (Optional)

Companion is a tiny, opt-in helper layer built on Atomica primitives. It does not add new semantics; it packages a few common patterns: channels, a service wrapper over resource(), and a component registry for diagnostics or debug UI.

If you use it, import from the root to keep a single instance:

import { createChannel, createService, registerComponent } from 'atomica';

Mental model

Channels

Channels are explicit fan-out. They publish data to any number of subscribers.

import { createChannel } from 'atomica';

const channel = createChannel<{ value: number }>();

const unsubscribe = channel.subscribe((payload) => {
  // payload is the exact object passed to publish(...)
  console.log('received', payload.value);
});

channel.publish({ value: 1 });
unsubscribe();

createChannel<T>() returns:

When you call:

channel.subscribe((data) => {
  // data is the published payload (same shape as your T).
  console.log('fresh data', data);
});

you are registering a listener function. The parameter (data) is not something you pass in — it is the value that will be delivered later when someone calls publish(...). Nothing runs on subscribe; it only fires on future publishes.

Service wrapper

The service wrapper is a thin helper around resource(). It has no lifecycle; calls are explicit.

import { createService } from 'atomica';

const api = createService({
  baseUrl: '/api',
  fetcher: fetch
});

type User = { id: string; name: string };
const { resource, channel } = api.get<User>('/user');

channel.subscribe((user) => {
  // user is a User, coming from the latest successful response body
  console.log('fresh data', user.name);
});

resource.refresh();

createService(options?) accepts:

It returns:

How the service call works

Each call (like api.get('/user')) does two things:

So the flow is:

resource.refresh() -> fetcher(url, init) -> response JSON -> channel.publish(data)

What the fetcher does

fetcher is just a function with the same signature as the standard Web fetch API:

fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>

It is called every time the resource runs. Use it to:

resource is the standard Atomica resource() result:

In the example above:

To attach auth headers (for example, a JWT), pass a function so the latest token is read at call time:

const api = createService({
  baseUrl: '/api',
  headers: () => ({
    Authorization: `Bearer ${token.get()}`
  })
});

Component registry

The registry is a signal-backed set you can use for diagnostics panels.

import { registerComponent, listComponents, useComponentRegistry } from 'atomica';

registerComponent('App');
const names = listComponents();
const registry = useComponentRegistry(); // Signal<Set<string>>

listComponents() returns a snapshot; useComponentRegistry() gives you the live signal for reactive UIs:

const registry = useComponentRegistry();
const View = () =>
  h('ul', null, () => Array.from(registry.get()).map((name) => h('li', null, name)));

What Companion is not

If you want a smaller surface, ignore it. Atomica remains fully usable without Companion.

See docs/usage-patterns.md for complete usage recipes.

Helper: bindInput

bindInput is a tiny helper for text inputs. It returns value and onInput props wired to a Signal<string>:

import { bindInput, signal } from 'atomica';

const name = signal('');
const props = bindInput(name);
// h('input', props)