Advanced API#

Factories, messaging, and other advanced utilities.

defineFactory#

Create an injectable factory with managed subscriptions, disposal, and a real instance lifetime.

TSX
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#

NameTypeDescription
setup(ctx, ...args) => TFactory setup function. Must return an object or function (primitives throw)
lifetimeLifetime'singleton', 'scoped', or 'transient' — see Lifetime
typeIdentifierguidOptional 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#

PropertyTypeDescription
onDeactivated(fn: () => void) => voidRegister cleanup callback
subscriptionsSubscriptionHandlerSubscription manager
overrideDispose(fn) => voidCustom disposal logic

Examples#

TSX
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.

TSX
type Lifetime = 'singleton' | 'scoped' | 'transient';
ValueSemantics
'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#

TSX
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.

TSX
class SubscriptionHandler {
    add(unsub: () => void): void;
    unsubscribe(): void;
}

Examples#

TSX
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.

TSX
function createTopic<T>(options?: CreateTopicOptions): Topic<T>;

interface CreateTopicOptions {
    namespace?: string;
    name?: string;
    onActivate?(): void;
    onDeactivate?(): void;
}

Parameters#

NameTypeDescription
options.namespacestringTooling metadata; topics with a namespace register in the inspection registry
options.namestringTooling metadata
options.onActivate() => voidCalled when subscriberCount transitions 0 → 1 (refCount pattern)
options.onDeactivate() => voidCalled when subscriberCount transitions back to 0 (last unsubscribe or destroy)

Returns#

Topic<T> — see the Topic interface.

Behavior contract:

  • publish isolates subscriber errors — a throwing handler is logged via console.error and neither skips later subscribers nor propagates to the publisher. Publishing to a destroyed topic is a no-op.
  • subscribe throws on a destroyed topic. Called inside a component it auto-unsubscribes on unmount.
  • onActivate / onDeactivate hook errors are isolated too.
  • destroy() is idempotent and unregisters the topic from the inspection registry.

Examples#

TSX
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.

TSX
function createTopicGroup<EventMap extends Record<string, any>>(options?: {
    namespace?: string;
}): {
    topics: { [K in keyof EventMap]: Topic<EventMap[K]> };
    destroy(): void;
};

Parameters#

NameTypeDescription
options.namespacestringApplied to every created topic

Returns#

{ topics, destroy }destroy() destroys all created topics; accessing a key on a destroyed group throws.

Examples#

TSX
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.

TSX
function toSubscriber<T>(topic: Topic<T>): {
    subscribe: (handler: (data: T) => void) => Subscription;
};

Parameters#

NameTypeDescription
topicTopic<T>Full topic to wrap

Returns#

Object with only subscribe method (no publish or destroy).

Examples#

TSX
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#

TSX
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#

TSX
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.*').

TSX
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.

TSX
function guid(): string;
type guid = string;

Examples#

TSX
import { guid } from 'sigx';

const id = guid();
// "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

Patterns#

Event Bus#

TSX
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#

TSX
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#

TSX
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');