Advanced API
Factories, messaging, and other advanced utilities.
defineFactory
Create an injectable factory with managed subscriptions, disposal, and a real instance lifetime.
function defineFactory<T>(
setup: (ctx: SetupFactoryContext, ...args: any[]) => T,
lifetime: Lifetime,
typeIdentifier?: guid
): InjectableFunction<T & { dispose: () => void }>;
// Parameterized setups return a FactoryFunction creator instead
// (overloads up to 5 parameters).
Parameters
| Name | Type | Description |
|---|---|---|
setup | (ctx, ...args) => T | Factory setup function. Must return an object or function (primitives throw) |
lifetime | Lifetime | 'singleton', 'scoped', or 'transient' — see Lifetime |
typeIdentifier | guid | Optional unique identifier |
A parameterless factory returns an InjectableFunction (usable like defineInjectable tokens); a parameterized factory returns a FactoryFunction creator. Both carry the provide metadata, so either can be passed to defineProvide / app.defineProvide (they satisfy Providable<T>).
Every instance gets a non-enumerable, idempotent dispose() attached (a dispose returned by the setup is delegated to). Parameterized non-transient factories honor arguments at first creation only — later calls resolve the existing shared instance.
SetupFactoryContext
| Property | Type | Description |
|---|---|---|
onDeactivated | (fn: () => void) => void | Register cleanup callback |
subscriptions | SubscriptionHandler | Subscription manager |
overrideDispose | (fn) => void | Custom disposal logic |
Examples
import { defineFactory } from 'sigx';
// Singleton service — one instance per app, disposed on app.unmount()
const useApi = defineFactory((ctx) => {
const cache = new Map();
ctx.onDeactivated(() => {
cache.clear();
});
return {
fetch: async (url: string) => {
if (cache.has(url)) return cache.get(url);
const data = await fetch(url).then(r => r.json());
cache.set(url, data);
return data;
},
invalidate: (url: string) => cache.delete(url)
};
}, 'singleton');
// Factory with parameters — new instance per call (transient)
const useRepository = defineFactory((ctx, entityName: string) => {
return {
getAll: () => fetch(`/api/${entityName}`),
getById: (id: number) => fetch(`/api/${entityName}/${id}`)
};
}, 'transient');
// Usage
const api = useApi();
const usersRepo = useRepository('users');
Lifetime
String-literal union for factory instance lifetimes. The lifetime you pass is enforced — it controls how many instances exist and when each is disposed.
type Lifetime = 'singleton' | 'scoped' | 'transient';
| Value | Semantics |
|---|---|
'singleton' | One instance per AppContext, created on first resolution and disposed on app.unmount(). Outside any app context, one instance per JS realm |
'scoped' | The nearest instance provided via defineProvide in the component tree; falls back to the app-context instance, and outside any app/component context to the per-realm instance |
'transient' | A new instance per call, disposed with the calling component (or manually via dispose()) |
Examples
import { defineFactory } from 'sigx';
// New instance every call
const useTransient = defineFactory(setup, 'transient');
// Nearest defineProvide provider, falling back to the app instance
const useScoped = defineFactory(setup, 'scoped');
// One instance per app
const useSingleton = defineFactory(setup, 'singleton');
SubscriptionHandler
Helper class for managing cleanup subscriptions.
class SubscriptionHandler {
add(unsub: () => void): void;
unsubscribe(): void;
}
Examples
const useWebSocket = defineFactory((ctx) => {
let ws: WebSocket;
return {
connect: (url: string) => {
ws = new WebSocket(url);
// Auto-cleanup on factory disposal
ctx.subscriptions.add(() => ws.close());
return ws;
}
};
}, 'singleton');
createTopic
Create a pub/sub topic for messaging.
function createTopic<T>(options?: CreateTopicOptions): Topic<T>;
interface CreateTopicOptions {
namespace?: string;
name?: string;
onActivate?(): void;
onDeactivate?(): void;
}
Parameters
| Name | Type | Description |
|---|---|---|
options.namespace | string | Tooling metadata; topics with a namespace register in the inspection registry |
options.name | string | Tooling metadata |
options.onActivate | () => void | Called when subscriberCount transitions 0 → 1 (refCount pattern) |
options.onDeactivate | () => void | Called when subscriberCount transitions back to 0 (last unsubscribe or destroy) |
Returns
Topic<T> — see the Topic interface.
Behavior contract:
publishisolates subscriber errors — a throwing handler is logged viaconsole.errorand neither skips later subscribers nor propagates to the publisher. Publishing to a destroyed topic is a no-op.subscribethrows on a destroyed topic. Called inside a component it auto-unsubscribes on unmount.onActivate/onDeactivatehook errors are isolated too.destroy()is idempotent and unregisters the topic from the inspection registry.
Examples
import { createTopic } from 'sigx';
// Create typed topic
const userEvents = createTopic<{
type: 'login' | 'logout';
userId: number;
}>({
namespace: 'auth', name: 'userEvents', // tooling metadata
onActivate: () => startPolling(), // first subscriber arrived
onDeactivate: () => stopPolling() // last subscriber left
});
// Subscribe
const sub = userEvents.subscribe((event) => {
console.log(`User ${event.userId} ${event.type}`);
});
// Publish (skip work when nobody is listening)
if (userEvents.hasSubscribers) {
userEvents.publish({ type: 'login', userId: 123 });
}
// Unsubscribe
sub.unsubscribe();
// Or destroy all
userEvents.destroy();
createTopicGroup
Create a typed group of topics keyed by an event map — mitt-level DX on the Topic primitive. Topics are created lazily per key on first access and are namespaced/registered like any other topic.
function createTopicGroup<EventMap extends Record<string, any>>(options?: {
namespace?: string;
}): {
topics: { [K in keyof EventMap]: Topic<EventMap[K]> };
destroy(): void;
};
Parameters
| Name | Type | Description |
|---|---|---|
options.namespace | string | Applied to every created topic |
Returns
{ topics, destroy } — destroy() destroys all created topics; accessing a key on a destroyed group throws.
Examples
import { createTopicGroup } from 'sigx';
const group = createTopicGroup<{
loggedIn: { userId: number };
loggedOut: void;
}>({ namespace: 'auth' });
group.topics.loggedIn.subscribe(({ userId }) => console.log(userId));
group.topics.loggedIn.publish({ userId: 123 }); // payload type-checked
group.destroy();
toSubscriber
Create a read-only subscriber from a topic.
function toSubscriber<T>(topic: Topic<T>): {
subscribe: (handler: (data: T) => void) => Subscription;
};
Parameters
| Name | Type | Description |
|---|---|---|
topic | Topic<T> | Full topic to wrap |
Returns
Object with only subscribe method (no publish or destroy).
Examples
import { createTopic, toSubscriber } from 'sigx';
// Internal: full control
const _notifications = createTopic<{ message: string }>();
// External: read-only
export const notifications = toSubscriber(_notifications);
// Can subscribe
notifications.subscribe(({ message }) => alert(message));
// Cannot publish (type error)
notifications.publish({ message: 'Hello' }); // Error!
Topic Interface
interface Topic<T> {
/** Tooling metadata (set via createTopic options). */
readonly namespace?: string;
readonly name?: string;
readonly subscriberCount: number;
readonly hasSubscribers: boolean;
readonly disposed: boolean;
publish(data: T): void;
subscribe(handler: (data: T) => void): Subscription;
destroy(): void;
}
Subscription Interface
interface Subscription {
unsubscribe(): void;
}
Inspection (@sigx/runtime-core/inspect)
A separate @sigx/runtime-core/inspect entry exposes the inspection-only topic registry for tooling (devtools, diagnostics). The DX contract: typed application code holds Topic<T> references; strings are tooling metadata. The registry is deliberately Topic<unknown>-typed and realm-global — do not use it as an app-level lookup mechanism; share typed topic references instead.
Only topics created with a namespace register; destroy() unregisters. Patterns use * wildcards matched over ${namespace}.${name} (e.g. 'todos#1.*', '*.actions.*').
import { getTopic, listTopics, subscribeTopics, onTopicCreated } from '@sigx/runtime-core/inspect';
// Exact lookup by namespace and name
function getTopic(namespace: string, name: string): Topic<unknown> | undefined;
// List live registered topics, optionally filtered by a wildcard pattern
function listTopics(pattern?: string): Topic<unknown>[];
// Subscribe to every matching topic — existing AND created later;
// one Subscription tears everything down
function subscribeTopics(
pattern: string,
handler: (data: unknown, meta: { namespace: string; name: string }) => void
): Subscription;
// Handler for every topic that registers from now on
function onTopicCreated(handler: (topic: Topic<unknown>) => void): Subscription;
subscribeTopics and onTopicCreated auto-unsubscribe on component unmount when called inside a setup.
guid
Generate a unique identifier.
function guid(): string;
type guid = string;
Examples
import { guid } from 'sigx';
const id = guid();
// "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
Patterns
Event Bus
import { createTopicGroup } from 'sigx';
// Centralized, typed event bus
export const eventBus = createTopicGroup<{
userLogin: { userId: number };
userLogout: void;
notification: { message: string; type: 'info' | 'error' };
themeChange: 'light' | 'dark';
}>({ namespace: 'app' });
// Usage
eventBus.topics.userLogin.publish({ userId: 123 });
eventBus.topics.notification.subscribe(({ message }) => toast(message));
Service Layer
import { defineFactory, createTopic, toSubscriber, signal } from 'sigx';
const useCartService = defineFactory((ctx) => {
const state = signal({ items: [] as CartItem[] });
const cartUpdated = createTopic<{ itemCount: number }>();
ctx.onDeactivated(() => {
cartUpdated.destroy();
});
return {
get items() { return state.items; },
get count() { return state.items.length; },
onUpdate: toSubscriber(cartUpdated),
add: (item: CartItem) => {
state.items = [...state.items, item];
cartUpdated.publish({ itemCount: state.items.length });
},
remove: (id: string) => {
state.items = state.items.filter(i => i.id !== id);
cartUpdated.publish({ itemCount: state.items.length });
},
clear: () => {
state.items = [];
cartUpdated.publish({ itemCount: 0 });
}
};
}, 'singleton');
Async Operations
const useAsyncOperation = defineFactory((ctx) => {
const state = signal({
loading: false,
error: null as Error | null,
data: null as any
});
const execute = async <T>(operation: () => Promise<T>): Promise<T | null> => {
state.loading = true;
state.error = null;
try {
const result = await operation();
state.data = result;
return result;
} catch (err) {
state.error = err instanceof Error ? err : new Error(String(err));
return null;
} finally {
state.loading = false;
}
};
return {
get loading() { return state.loading; },
get error() { return state.error; },
get data() { return state.data; },
execute
};
}, 'transient');
