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';
resource() that also publishes responses on a channel.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:
publish(payload: T): void — push a new payload to all subscriberssubscribe(fn: (payload: T) => void): () => void — register a listener; returns unsubscribelast(): T | undefined — read the most recent payload without subscribingWhen 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.
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:
baseUrl?: stringfetcher?: typeof fetchheaders?: HeadersInit | (() => HeadersInit)It returns:
get<T>(path: string, auto = true): { resource: Resource<T>; channel: Channel<T> }post<T>(path: string, body: unknown, auto = true): { resource: Resource<T>; channel: Channel<T> }put<T>(path: string, body: unknown, auto = true): { resource: Resource<T>; channel: Channel<T> }delete<T>(path: string, body?: unknown, auto = true): { resource: Resource<T>; channel: Channel<T> }Each call (like api.get('/user')) does two things:
resource() that runs the request when you call refresh() (or when auto: true).channel that publishes each successful response.So the flow is:
resource.refresh() -> fetcher(url, init) -> response JSON -> channel.publish(data)
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:
fetchresource is the standard Atomica resource() result:
data(): T | undefinederror(): unknownloading(): booleanstate(): 'idle' | 'loading' | 'success' | 'error'refresh(): Promise<void>mutate(next: T | ((prev?: T) => T)): voidclear(): voiddispose(): voidIn the example above:
api.get<T>('/user') defines the request and returns both a resource and a channel.channel.subscribe(...) listens for each successful response.resource.refresh() explicitly triggers the fetch.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()}`
})
});
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)));
If you want a smaller surface, ignore it. Atomica remains fully usable without Companion.
See docs/usage-patterns.md for complete usage recipes.
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)