Skip to content

Using BlaC outside React

@blac/core has no dependency on React. You can use blocs in vanilla JavaScript, Node.js scripts, worker threads, or any framework — without @blac/react installed. This page covers the two main patterns: observing state with watch and owning the lifecycle with acquire/release.

watch is the primary way to react to state changes outside a component. It creates a subscription, fires the callback once immediately with the current state, and then on every subsequent change.

import {
const watch: WatchFn
watch
,
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class AuthCubit
AuthCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
token: string | null
token
: string | null }> {
constructor() {
super({
token: string | null
token
: null });
}
AuthCubit.setToken: (token: string) => void
setToken
= (
token: string
token
: string) => this.
StructuralContainer<{ token: string | null; }>.update(fn: (state: {
token: string | null;
}) => {
token: string | null;
}): void
update
(() => ({
token: string | null
token
}));
AuthCubit.logout: () => void
logout
= () => this.
StructuralContainer<{ token: string | null; }>.update(fn: (state: {
token: string | null;
}) => {
token: string | null;
}): void
update
(() => ({
token: string | null
token
: null }));
}
// Subscribe — fires immediately, then on every change.
const
const stop: () => void
stop
=
watch<typeof AuthCubit>(bloc: typeof AuthCubit | BlocRef<typeof AuthCubit>, callback: (bloc: AuthCubit) => void | unique symbol): () => void (+1 overload)
watch
(
class AuthCubit
AuthCubit
, (
auth: AuthCubit
auth
) => {
if (
auth: AuthCubit
auth
.
StructuralContainer<{ token: string | null; }>.state: {
token: string | null;
}
state
.
token: string | null
token
) {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
('Signed in:',
auth: AuthCubit
auth
.
StructuralContainer<{ token: string | null; }>.state: {
token: string | null;
}
state
.
token: string
token
.
String.slice(start?: number, end?: number): string

Returns a section of a string.

@paramstart The index to the beginning of the specified portion of stringObj.

@paramend The index to the end of the specified portion of stringObj. The substring includes the characters up to, but not including, the character indicated by end. If this value is not specified, the substring continues to the end of stringObj.

slice
(0, 8) + '...');
} else {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
('Signed out');
}
});
// Tear down when no longer needed.
const stop: () => void
stop
();

watch returns a stop function. Always call it when the subscription is no longer needed. Forgetting is the most common outside-React leak — a subscription you never stop keeps the bloc alive and runs your callback for the life of the process. See watch for the full API.

Pass a readonly array to observe several blocs at once. The callback fires when any of them changes:

import {
const watch: WatchFn
watch
,
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class UserCubit
UserCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
name: string
name
: string }> {
constructor() {
super({
name: string
name
: '' });
}
}
class
class ThemeCubit
ThemeCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
mode: "light" | "dark"
mode
: 'light' | 'dark' }> {
constructor() {
super({
mode: "light" | "dark"
mode
: 'light' });
}
}
declare function
function updateDocTitle(name: string, mode: string): void
updateDocTitle
(
name: string
name
: string,
mode: string
mode
: string): void;
const
const stop: () => void
stop
=
watch<readonly [typeof UserCubit, typeof ThemeCubit]>(blocs: readonly [typeof UserCubit, typeof ThemeCubit], callback: (blocs: readonly [UserCubit, ThemeCubit]) => void | unique symbol): () => void (+1 overload)
watch
([
class UserCubit
UserCubit
,
class ThemeCubit
ThemeCubit
] as
type const = readonly [typeof UserCubit, typeof ThemeCubit]
const
, ([
user: UserCubit
user
,
theme: ThemeCubit
theme
]) => {
function updateDocTitle(name: string, mode: string): void
updateDocTitle
(
user: UserCubit
user
.
StructuralContainer<{ name: string; }>.state: {
name: string;
}
state
.
name: string
name
,
theme: ThemeCubit
theme
.
StructuralContainer<{ mode: "light" | "dark"; }>.state: {
mode: "light" | "dark";
}
state
.
mode: "light" | "dark"
mode
);
});
// Call stop() when tearing down
const stop: () => void
stop
();

Return watch.STOP from the callback to unsubscribe after a condition is met — useful for one-shot observations:

import {
const watch: WatchFn
watch
,
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class AppCubit
AppCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
ready: boolean
ready
: boolean }> {
constructor() {
super({
ready: boolean
ready
: false });
}
}
declare function
function startWorker(): void
startWorker
(): void;
watch<typeof AppCubit>(bloc: typeof AppCubit | BlocRef<typeof AppCubit>, callback: (bloc: AppCubit) => void | unique symbol): () => void (+1 overload)
watch
(
class AppCubit
AppCubit
, (
app: AppCubit
app
) => {
if (
app: AppCubit
app
.
StructuralContainer<{ ready: boolean; }>.state: {
ready: boolean;
}
state
.
ready: boolean
ready
) {
function startWorker(): void
startWorker
();
return
const watch: WatchFn
watch
.
WatchSingleFn.STOP: unique symbol
STOP
; // unsubscribes after the first match
}
});

Managing lifecycle with acquire and release

Section titled “Managing lifecycle with acquire and release”

In React, useBloc owns the acquire/release pair for you. Outside React you call them directly. Every acquire increments the ref count; the matching release decrements it. At ref count zero the instance is disposed.

import {
function acquire<T extends StateContainerConstructor>(BlocClass: T, opts?: {
args?: ExtractArgs<T>;
refId?: string;
}): InstanceType<T>

Acquire an instance with ref tracking (ownership semantics). Instance identity is derived purely from args (via the class's static key(args), a structural hash of args, or the default sentinel when there are none).

@paramBlocClass - The StateContainer class constructor

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - Named reference ID for debugging; auto-generated if omitted

acquire
,
function release<T extends StateContainerConstructor>(BlocClass: T, opts?: {
args?: ExtractArgs<T>;
refId?: string;
forceDispose?: boolean;
}): void

Release a reference to an instance. Instance identity is derived purely from args (must match the args it was acquired with).

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - The specific ref to drop; drops one arbitrary ref if omitted

@paramopts.forceDispose - Force immediate disposal regardless of refs

release
,
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class TaskQueueCubit
TaskQueueCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
pending: number
pending
: number }> {
constructor() {
super({
pending: number
pending
: 0 });
}
TaskQueueCubit.enqueue: () => void
enqueue
= () => this.
StructuralContainer<{ pending: number; }>.update(fn: (state: {
pending: number;
}) => {
pending: number;
}): void
update
((
s: {
pending: number;
}
s
) => ({
pending: number
pending
:
s: {
pending: number;
}
s
.
pending: number
pending
+ 1 }));
TaskQueueCubit.dequeue: () => void
dequeue
= () => this.
StructuralContainer<{ pending: number; }>.update(fn: (state: {
pending: number;
}) => {
pending: number;
}): void
update
((
s: {
pending: number;
}
s
) => ({
pending: number
pending
:
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.max(...values: number[]): number

Returns the larger of a set of supplied numeric expressions.

@paramvalues Numeric expressions to be evaluated.

max
(0,
s: {
pending: number;
}
s
.
pending: number
pending
- 1) }));
}
// Acquire a ref — the instance is created on first acquire.
const
const queue: TaskQueueCubit
queue
=
acquire<typeof TaskQueueCubit>(BlocClass: typeof TaskQueueCubit, opts?: {
args?: void | undefined;
refId?: string;
} | undefined): TaskQueueCubit

Acquire an instance with ref tracking (ownership semantics). Instance identity is derived purely from args (via the class's static key(args), a structural hash of args, or the default sentinel when there are none).

@paramBlocClass - The StateContainer class constructor

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - Named reference ID for debugging; auto-generated if omitted

acquire
(
class TaskQueueCubit
TaskQueueCubit
);
const queue: TaskQueueCubit
queue
.
TaskQueueCubit.enqueue: () => void
enqueue
();
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const queue: TaskQueueCubit
queue
.
StructuralContainer<{ pending: number; }>.state: {
pending: number;
}
state
.
pending: number
pending
); // 1
// Release when done — drops the ref count.
// At ref count 0 the instance is disposed (unless keepAlive).
release<typeof TaskQueueCubit>(BlocClass: typeof TaskQueueCubit, opts?: {
args?: void | undefined;
refId?: string;
forceDispose?: boolean;
} | undefined): void

Release a reference to an instance. Instance identity is derived purely from args (must match the args it was acquired with).

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - The specific ref to drop; drops one arbitrary ref if omitted

@paramopts.forceDispose - Force immediate disposal regardless of refs

release
(
class TaskQueueCubit
TaskQueueCubit
);

If you only need to read the current state and do not need to keep the instance alive, use borrow (throws if absent) or borrowSafe (returns an error object). Neither takes a ref, so no release is needed:

import {
function borrow<T extends StateContainerConstructor>(BlocClass: T, target?: BorrowTarget<T>): InstanceType<T>
borrow
,
function borrowSafe<T extends StateContainerConstructor>(BlocClass: T, target?: BorrowTarget<T>): {
error: Error;
instance: null;
} | {
error: null;
instance: InstanceType<T>;
}
borrowSafe
,
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class ConfigCubit
ConfigCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
apiUrl: string
apiUrl
: string }> {
constructor() {
super({
apiUrl: string
apiUrl
: '' });
}
}
// borrow: throws if the instance does not exist.
// Use when absence is a programming error.
const
const url: string
url
=
borrow<typeof ConfigCubit>(BlocClass: typeof ConfigCubit, target?: BorrowTarget<typeof ConfigCubit> | undefined): ConfigCubit
borrow
(
class ConfigCubit
ConfigCubit
).
StructuralContainer<{ apiUrl: string; }>.state: {
apiUrl: string;
}
state
.
apiUrl: string
apiUrl
;
// borrowSafe: discriminated union instead of throwing.
// Use when absence is expected.
const
const result: {
error: Error;
instance: null;
} | {
error: null;
instance: ConfigCubit;
}
result
=
borrowSafe<typeof ConfigCubit>(BlocClass: typeof ConfigCubit, target?: BorrowTarget<typeof ConfigCubit> | undefined): {
error: Error;
instance: null;
} | {
error: null;
instance: ConfigCubit;
}
borrowSafe
(
class ConfigCubit
ConfigCubit
);
if (
const result: {
error: Error;
instance: null;
} | {
error: null;
instance: ConfigCubit;
}
result
.
error: Error | null
error
) {
var console: Console
console
.
Console.warn(...data: any[]): void

The console.warn() static method outputs a warning message to the console at the "warning" log level. The message is only displayed to the user if the console is configured to display warning output. In most cases, the log level is configured within the console UI. The message may receive special formatting, such as yellow colors and a warning icon.

MDN Reference

warn
('Config not initialized');
} else {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const result: {
error: null;
instance: ConfigCubit;
}
result
.
instance: ConfigCubit
instance
.
StructuralContainer<{ apiUrl: string; }>.state: {
apiUrl: string;
}
state
.
apiUrl: string
apiUrl
);
}

watch calls ensure internally — it does not take a ref. If you need both a stable subscription and a ref (to keep the instance alive while nothing else holds it), acquire first, then watch:

import {
function acquire<T extends StateContainerConstructor>(BlocClass: T, opts?: {
args?: ExtractArgs<T>;
refId?: string;
}): InstanceType<T>

Acquire an instance with ref tracking (ownership semantics). Instance identity is derived purely from args (via the class's static key(args), a structural hash of args, or the default sentinel when there are none).

@paramBlocClass - The StateContainer class constructor

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - Named reference ID for debugging; auto-generated if omitted

acquire
,
function release<T extends StateContainerConstructor>(BlocClass: T, opts?: {
args?: ExtractArgs<T>;
refId?: string;
forceDispose?: boolean;
}): void

Release a reference to an instance. Instance identity is derived purely from args (must match the args it was acquired with).

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - The specific ref to drop; drops one arbitrary ref if omitted

@paramopts.forceDispose - Force immediate disposal regardless of refs

release
,
const watch: WatchFn
watch
,
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class MetricsCubit
MetricsCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
fps: number
fps
: number }> {
constructor() {
super({
fps: number
fps
: 0 });
}
}
declare function
function updateDashboard(fps: number): void
updateDashboard
(
fps: number
fps
: number): void;
// Keep the instance alive + observe changes.
const
const metrics: MetricsCubit
metrics
=
acquire<typeof MetricsCubit>(BlocClass: typeof MetricsCubit, opts?: {
args?: void | undefined;
refId?: string;
} | undefined): MetricsCubit

Acquire an instance with ref tracking (ownership semantics). Instance identity is derived purely from args (via the class's static key(args), a structural hash of args, or the default sentinel when there are none).

@paramBlocClass - The StateContainer class constructor

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - Named reference ID for debugging; auto-generated if omitted

acquire
(
class MetricsCubit
MetricsCubit
);
const
const stop: () => void
stop
=
watch<typeof MetricsCubit>(bloc: typeof MetricsCubit | BlocRef<typeof MetricsCubit>, callback: (bloc: MetricsCubit) => void | unique symbol): () => void (+1 overload)
watch
(
class MetricsCubit
MetricsCubit
, (
m: MetricsCubit
m
) => {
function updateDashboard(fps: number): void
updateDashboard
(
m: MetricsCubit
m
.
StructuralContainer<{ fps: number; }>.state: {
fps: number;
}
state
.
fps: number
fps
);
});
// On teardown — stop watching first, then release the ref.
function
function teardown(): void
teardown
() {
const stop: () => void
stop
();
release<typeof MetricsCubit>(BlocClass: typeof MetricsCubit, opts?: {
args?: void | undefined;
refId?: string;
forceDispose?: boolean;
} | undefined): void

Release a reference to an instance. Instance identity is derived purely from args (must match the args it was acquired with).

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - The specific ref to drop; drops one arbitrary ref if omitted

@paramopts.forceDispose - Force immediate disposal regardless of refs

release
(
class MetricsCubit
MetricsCubit
);
}

Blocs work in Node.js without any changes. The module-level registry is per-module-graph, so in a single Node process the same singleton rules apply as in a browser tab.

For worker threads (node:worker_threads), each worker has its own module graph and therefore its own registry. Blocs do not cross thread boundaries — pass data between threads via the standard postMessage / MessageChannel API.

For long-running servers that handle multiple requests in the same process, apply the per-request registry isolation pattern to prevent state bleed between concurrent users.

import {
const watch: WatchFn
watch
,
function acquire<T extends StateContainerConstructor>(BlocClass: T, opts?: {
args?: ExtractArgs<T>;
refId?: string;
}): InstanceType<T>

Acquire an instance with ref tracking (ownership semantics). Instance identity is derived purely from args (via the class's static key(args), a structural hash of args, or the default sentinel when there are none).

@paramBlocClass - The StateContainer class constructor

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - Named reference ID for debugging; auto-generated if omitted

acquire
,
function release<T extends StateContainerConstructor>(BlocClass: T, opts?: {
args?: ExtractArgs<T>;
refId?: string;
forceDispose?: boolean;
}): void

Release a reference to an instance. Instance identity is derived purely from args (must match the args it was acquired with).

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - The specific ref to drop; drops one arbitrary ref if omitted

@paramopts.forceDispose - Force immediate disposal regardless of refs

release
,
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class JobCubit
JobCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
status: "idle" | "running" | "done"
status
: 'idle' | 'running' | 'done' }> {
constructor() {
super({
status: "idle" | "running" | "done"
status
: 'idle' });
}
JobCubit.start: () => void
start
= () => this.
StructuralContainer<{ status: "idle" | "running" | "done"; }>.update(fn: (state: {
status: "idle" | "running" | "done";
}) => {
status: "idle" | "running" | "done";
}): void
update
(() => ({
status: "idle" | "running" | "done"
status
: 'running' }));
JobCubit.finish: () => void
finish
= () => this.
StructuralContainer<{ status: "idle" | "running" | "done"; }>.update(fn: (state: {
status: "idle" | "running" | "done";
}) => {
status: "idle" | "running" | "done";
}): void
update
(() => ({
status: "idle" | "running" | "done"
status
: 'done' }));
}
// Node.js CLI script — no React, no browser.
const
const job: JobCubit
job
=
acquire<typeof JobCubit>(BlocClass: typeof JobCubit, opts?: {
args?: void | undefined;
refId?: string;
} | undefined): JobCubit

Acquire an instance with ref tracking (ownership semantics). Instance identity is derived purely from args (via the class's static key(args), a structural hash of args, or the default sentinel when there are none).

@paramBlocClass - The StateContainer class constructor

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - Named reference ID for debugging; auto-generated if omitted

acquire
(
class JobCubit
JobCubit
);
// Watch for completion then exit.
watch<typeof JobCubit>(bloc: typeof JobCubit | BlocRef<typeof JobCubit>, callback: (bloc: JobCubit) => void | unique symbol): () => void (+1 overload)
watch
(
class JobCubit
JobCubit
, (
j: JobCubit
j
) => {
if (
j: JobCubit
j
.
StructuralContainer<{ status: "idle" | "running" | "done"; }>.state: {
status: "idle" | "running" | "done";
}
state
.
status: "idle" | "running" | "done"
status
=== 'done') {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
('Job complete');
return
const watch: WatchFn
watch
.
WatchSingleFn.STOP: unique symbol
STOP
;
}
});
const job: JobCubit
job
.
JobCubit.start: () => void
start
();
// ... do work ...
const job: JobCubit
job
.
JobCubit.finish: () => void
finish
(); // callback fires, prints "Job complete"
release<typeof JobCubit>(BlocClass: typeof JobCubit, opts?: {
args?: void | undefined;
refId?: string;
forceDispose?: boolean;
} | undefined): void

Release a reference to an instance. Instance identity is derived purely from args (must match the args it was acquired with).

@paramopts.args - Construction/identity args; derives the instance key

@paramopts.refId - The specific ref to drop; drops one arbitrary ref if omitted

@paramopts.forceDispose - Force immediate disposal regardless of refs

release
(
class JobCubit
JobCubit
);