Skip to content

Instance management

The registry is a global singleton that manages the lifecycle of state container instances. It handles creation, sharing, ref counting, and disposal. Every signature on this page is quoted from the @blac/core source.

The mental model: a shared library with checkouts

Section titled “The mental model: a shared library with checkouts”

Think of the registry as a lending library for bloc instances:

  • Each (class, instanceKey) pair is one book on the shelf. Ask for the same pair twice and you get the same physical book — that is how two components share state without prop-drilling.
  • A consumer that needs a book checks it out (acquire) and returns it (release) when done. Every checkout is a reference (a “ref”).
  • The library keeps a count of who has each book checked out. When the count drops to zero, the book is discarded (dispose) — its state is gone and any timers or subscriptions it set up are torn down.
  • keepAlive marks a book as a permanent reference copy: it stays on the shelf even at zero checkouts.

Two consequences fall straight out of this model:

  1. Sharing is automatic. The first acquire of a key creates the instance; every later acquire of the same key reuses it. You never wire instances together by hand.
  2. Cleanup is automatic — but only if every checkout is returned. A missing release is like never returning a library book: the count never reaches zero, the instance never disposes, and its memory (and subscriptions) leak.

When you call useBloc(CounterCubit) in React:

  1. The hook calls acquire(CounterCubit), which either creates a new instance or returns the existing one.
  2. The ref count increments (+1). Each consumer gets its own ref (a unique refId), so two components reading the same bloc count as two refs.
  3. When the component unmounts, release(CounterCubit) decrements the ref count (-1).
  4. At ref count zero, the instance is disposed (unless keepAlive is set).

This means shared instances are automatically cleaned up when no component needs them.

acquire ───▶ ref count 1 (created, init() runs, 'created' fires)
acquire ───▶ ref count 2 (same instance reused — shared)
release ───▶ ref count 1 (still alive, one consumer remains)
release ───▶ ref count 0 ─┬─ keepAlive? yes ─▶ stays alive
└─ keepAlive? no ─▶ dispose()
├─ 'dispose' system event fires
├─ subscriptions/handlers torn down
├─ entry removed from registry
└─ ensure-created deps with 0 refs cascade-dispose

Create or return an existing instance, incrementing the ref count.

function acquire<T extends StateContainerConstructor>(
BlocClass: T,
opts?: { args?: ExtractArgs<T>; refId?: string },
): InstanceType<T>;
ParameterTypeRequiredDescription
BlocClassT extends StateContainerConstructoryesThe state-container class to acquire an instance of.
opts.argsExtractArgs<T>noSerializable construction data passed to init(args). Derives the instance key.
opts.refIdstringnoCaller-supplied ref identifier. Used by useBloc to pair with release. Rarely set manually.

Returns: InstanceType<T> — the live instance (newly created or existing).

Behavior. The instance key is derived from opts.args (static key if declared, else the structural hash, else 'default'). If no instance exists for the resolved key, acquire creates one, calls init(args), and fires the internal 'created' event. If an instance already exists, it is returned as-is. The ref count is always incremented. Every acquire must have a matching release — a missing release is the canonical instance leak. In React, useBloc owns this pair; call acquire directly only in server-side or scripting contexts.

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
} from '@blac/core';
import {
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 CounterCubit
CounterCubit
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
<{
count: number
count
: number }> {
constructor() {
super({
count: number
count
: 0 });
}
CounterCubit.increment: () => void
increment
= () => this.
StructuralContainer<{ count: number; }>.update(fn: (state: {
count: number;
}) => {
count: number;
}): void
update
((
s: {
count: number;
}
s
) => ({
count: number
count
:
s: {
count: number;
}
s
.
count: number
count
+ 1 }));
}
// --- outside React ---
const
const counter: CounterCubit
counter
=
acquire<typeof CounterCubit>(BlocClass: typeof CounterCubit, opts?: {
args?: void | undefined;
refId?: string;
} | undefined): CounterCubit

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 CounterCubit
CounterCubit
);
const counter: CounterCubit
counter
.
CounterCubit.increment: () => void
increment
();
var console: Console
console
.
Console.log(...data: any[]): void

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

MDN Reference

log
(
const counter: CounterCubit
counter
.
StructuralContainer<{ count: number; }>.state: {
count: number;
}
state
.
count: number
count
); // => 1
release<typeof CounterCubit>(BlocClass: typeof CounterCubit, 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 CounterCubit
CounterCubit
); // must pair with acquire

Create or return an existing instance without taking a ref.

function ensure<T extends StateContainerConstructor>(
BlocClass: T,
opts?: { args?: ExtractArgs<T> },
): InstanceType<T>;
ParameterTypeRequiredDescription
BlocClassT extends StateContainerConstructoryesThe state-container class.
opts.argsExtractArgs<T>noConstruction data for init(args) and key derivation.

Returns: InstanceType<T> — the live instance.

Behavior. Like acquire, but takes no ref — the instance can be disposed by another caller while you hold the returned reference. Use ensure inside other cubits (via depend()) or in tooling that only needs the instance to exist without pinning its lifecycle. Because no ref is taken, no matching release is needed.

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

Ensure an instance exists without taking ownership (no ref added). Instance identity is derived purely from args, matching acquire.

ensure
} from '@blac/core';
import {
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
<{
userId: string | null
userId
: string | null }> {
constructor() {
super({
userId: string | null
userId
: null });
}
}
// ensure creates AuthCubit if it's not alive yet, but does not keep it alive
const
const auth: AuthCubit
auth
=
ensure<typeof AuthCubit>(BlocClass: typeof AuthCubit, opts?: {
args?: void | undefined;
} | undefined): AuthCubit

Ensure an instance exists without taking ownership (no ref added). Instance identity is derived purely from args, matching acquire.

ensure
(
class AuthCubit
AuthCubit
);
var console: Console
console
.
Console.log(...data: any[]): void

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

MDN Reference

log
(
const auth: AuthCubit
auth
.
StructuralContainer<{ userId: string | null; }>.state: {
userId: string | null;
}
state
.
userId: string | null
userId
); // no release needed

Return an existing instance without taking a ref. Throws if the instance does not exist.

function borrow<T extends StateContainerConstructor>(
BlocClass: T,
opts?: { args?: ExtractArgs<T> },
): InstanceType<T>;
ParameterTypeRequiredDescription
BlocClassT extends StateContainerConstructoryesThe state-container class.
opts.argsExtractArgs<T>noArgs identifying the instance. Defaults to the 'default' key.

Returns: InstanceType<T> — the live instance.

Behavior. Does not create the instance if it is absent; throws an Error instead. Use borrow when the absence of an instance is a programming error — it makes the failure loud and immediate rather than silently returning undefined. No ref is taken, so no release is needed.

import {
function borrow<T extends StateContainerConstructor>(BlocClass: T, target?: BorrowTarget<T>): InstanceType<T>
borrow
} from '@blac/core';
import {
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 CounterCubit
CounterCubit
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
<{
count: number
count
: number }> {
constructor() {
super({
count: number
count
: 0 });
}
}
// Safe read-only access — no acquire/release pair needed
function
function readCount(): number
readCount
(): number {
return
borrow<typeof CounterCubit>(BlocClass: typeof CounterCubit, target?: BorrowTarget<typeof CounterCubit> | undefined): CounterCubit
borrow
(
class CounterCubit
CounterCubit
).
StructuralContainer<{ count: number; }>.state: {
count: number;
}
state
.
count: number
count
;
}

Return an existing instance without taking a ref. Returns an error object instead of throwing.

function borrowSafe<T extends StateContainerConstructor>(
BlocClass: T,
opts?: { args?: ExtractArgs<T> },
):
| { error: Error; instance: null }
| { error: null; instance: InstanceType<T> };
ParameterTypeRequiredDescription
BlocClassT extends StateContainerConstructoryesThe state-container class.
opts.argsExtractArgs<T>noArgs identifying the instance. Defaults to the 'default' key.

Returns: { error: null; instance: InstanceType<T> } when the instance exists, or { error: Error; instance: null } when it does not.

Behavior. Identical to borrow but returns a discriminated union instead of throwing. Prefer borrowSafe in code paths where absence is expected (e.g. optional integrations) and borrow where absence is always a bug.

import {
function borrowSafe<T extends StateContainerConstructor>(BlocClass: T, target?: BorrowTarget<T>): {
error: Error;
instance: null;
} | {
error: null;
instance: InstanceType<T>;
}
borrowSafe
} from '@blac/core';
import {
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
<{
userId: string | null
userId
: string | null }> {
constructor() {
super({
userId: string | null
userId
: null });
}
}
const {
const error: Error | null
error
,
const instance: AuthCubit | null
instance
} =
borrowSafe<typeof AuthCubit>(BlocClass: typeof AuthCubit, target?: BorrowTarget<typeof AuthCubit> | undefined): {
error: Error;
instance: null;
} | {
error: null;
instance: AuthCubit;
}
borrowSafe
(
class AuthCubit
AuthCubit
);
if (
const error: Error | null
error
) {
var console: Console
console
.
Console.log(...data: any[]): void

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

MDN Reference

log
('Auth not initialized yet');
} 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 instance: AuthCubit
instance
.
StructuralContainer<{ userId: string | null; }>.state: {
userId: string | null;
}
state
.
userId: string | null
userId
);
}

Decrement the ref count. Dispose the instance when count reaches zero.

function release<T extends StateContainerConstructor>(
BlocClass: T,
opts?: { args?: ExtractArgs<T>; refId?: string; forceDispose?: boolean },
): void;
ParameterTypeRequiredDescription
BlocClassT extends StateContainerConstructoryesThe state-container class.
opts.argsExtractArgs<T>noThe args used when acquire was called. Must resolve to the same key.
opts.refIdstringnoThe ref identifier passed to acquire. Rarely set manually.
opts.forceDisposebooleannoWhen true, dispose immediately even if keepAlive is set. Default false.

Returns: void.

Behavior. release is idempotent for an already-dropped ref — releasing more times than you acquired won’t throw, it just no-ops once the count is gone. The args you release with must resolve to the same key you acquired with; a mismatch leaves the original ref dangling forever. At ref count zero, the instance is disposed synchronously (unless keepAlive is set or forceDispose is false).

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
} from '@blac/core';
import {
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 SessionCubit
SessionCubit
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 }, {
scope: string
scope
: string }> {
static
SessionCubit.key: (a: {
scope: string;
}) => string
key
= (
a: {
scope: string;
}
a
: {
scope: string
scope
: string }) =>
a: {
scope: string;
}
a
.
scope: string
scope
;
constructor() {
super({
token: string | null
token
: null });
}
}
const
const session: SessionCubit
session
=
acquire<typeof SessionCubit>(BlocClass: typeof SessionCubit, opts?: {
args?: {
scope: string;
} | undefined;
refId?: string;
} | undefined): SessionCubit

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 SessionCubit
SessionCubit
, {
args?: {
scope: string;
} | undefined
args
: {
scope: string
scope
: 'main' } });
// ... use session ...
release<typeof SessionCubit>(BlocClass: typeof SessionCubit, opts?: {
args?: {
scope: string;
} | 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 SessionCubit
SessionCubit
, {
args?: {
scope: string;
} | undefined
args
: {
scope: string
scope
: 'main' } }); // args must match acquire's

To manage distinct named instances, declare an Args shape and pass args to any registry function. The instance key is derived from those args — a static key if the class declares one, otherwise the structural hash of the args:

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
} from '@blac/core';
import {
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 EditorCubit
EditorCubit
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
<{
content: string
content
: string }, {
docId: string
docId
: string }> {
static
EditorCubit.key: (a: {
docId: string;
}) => string
key
= (
a: {
docId: string;
}
a
: {
docId: string
docId
: string }) =>
a: {
docId: string;
}
a
.
docId: string
docId
;
constructor() {
super({
content: string
content
: '' });
}
}
const
const editor1: EditorCubit
editor1
=
acquire<typeof EditorCubit>(BlocClass: typeof EditorCubit, opts?: {
args?: {
docId: string;
} | undefined;
refId?: string;
} | undefined): EditorCubit

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 EditorCubit
EditorCubit
, {
args?: {
docId: string;
} | undefined
args
: {
docId: string
docId
: 'doc-42' } });
const
const editor2: EditorCubit
editor2
=
acquire<typeof EditorCubit>(BlocClass: typeof EditorCubit, opts?: {
args?: {
docId: string;
} | undefined;
refId?: string;
} | undefined): EditorCubit

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 EditorCubit
EditorCubit
, {
args?: {
docId: string;
} | undefined
args
: {
docId: string
docId
: 'doc-99' } });
// These are different instances
const
const areDifferent: boolean
areDifferent
=
const editor1: EditorCubit
editor1
!==
const editor2: EditorCubit
editor2
; // true
release<typeof EditorCubit>(BlocClass: typeof EditorCubit, opts?: {
args?: {
docId: string;
} | 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 EditorCubit
EditorCubit
, {
args?: {
docId: string;
} | undefined
args
: {
docId: string
docId
: 'doc-42' } });
release<typeof EditorCubit>(BlocClass: typeof EditorCubit, opts?: {
args?: {
docId: string;
} | 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 EditorCubit
EditorCubit
, {
args?: {
docId: string;
} | undefined
args
: {
docId: string
docId
: 'doc-99' } });

In React, pass the same args:

const [state] = useBloc(EditorCubit, { args: { docId: 'doc-42' } });

args are the only way to get distinct instances — the meaningful value keys the instance and feeds init(args), so you never pass the same id twice:

// The meaningful value keys the instance AND feeds init(args)
const [s] = useBloc(UserCardCubit, { args: { userId } });

Identity precedence: own args (via static key(args), else structural hash of args) > <BlocProvider> context args > 'default'.

The resolved key is the registry’s single source of truth — acquire and release both run their args through the same resolution, so a ref taken under a given args key is dropped under the same key. See Passing Inputs for the full model and Configuration for static key.

import {
function hasInstance<T extends StateContainerConstructor>(BlocClass: T, opts?: {
args?: ExtractArgs<T>;
}): boolean
hasInstance
,
function getRefCount<T extends StateContainerConstructor>(BlocClass: T, opts?: {
args?: ExtractArgs<T>;
}): number
getRefCount
,
function getAll<T extends StateContainerConstructor>(BlocClass: T): InstanceReadonlyState<T>[]
getAll
,
function forEach<T extends StateContainerConstructor>(BlocClass: T, callback: (instance: InstanceReadonlyState<T>) => void): void
forEach
,
function getStats(): {
registeredTypes: number;
totalInstances: number;
typeBreakdown: Record<string, number>;
}
getStats
,
} from '@blac/core';
import {
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 CounterCubit
CounterCubit
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
<{
count: number
count
: number }> {
constructor() {
super({
count: number
count
: 0 });
}
}
const
const _a: boolean
_a
=
hasInstance<typeof CounterCubit>(BlocClass: typeof CounterCubit, opts?: {
args?: void | undefined;
} | undefined): boolean
hasInstance
(
class CounterCubit
CounterCubit
); // boolean
const
const _b: number
_b
=
getRefCount<typeof CounterCubit>(BlocClass: typeof CounterCubit, opts?: {
args?: void | undefined;
} | undefined): number
getRefCount
(
class CounterCubit
CounterCubit
); // number of distinct active refs
const
const _c: InstanceReadonlyState<typeof CounterCubit>[]
_c
=
getAll<typeof CounterCubit>(BlocClass: typeof CounterCubit): InstanceReadonlyState<typeof CounterCubit>[]
getAll
(
class CounterCubit
CounterCubit
); // all instances of this class
forEach<typeof CounterCubit>(BlocClass: typeof CounterCubit, callback: (instance: InstanceReadonlyState<typeof CounterCubit>) => void): void
forEach
(
class CounterCubit
CounterCubit
, (
_inst: InstanceReadonlyState<typeof CounterCubit>
_inst
) => {}); // iterate instances
const
const _d: {
registeredTypes: number;
totalInstances: number;
typeBreakdown: Record<string, number>;
}
_d
=
function getStats(): {
registeredTypes: number;
totalInstances: number;
typeBreakdown: Record<string, number>;
}
getStats
(); // { registeredTypes, totalInstances, typeBreakdown }
import {
function clear<T extends StateContainerConstructor>(BlocClass: T): void
clear
,
function clearAll(): void
clearAll
} from '@blac/core';
import {
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 CounterCubit
CounterCubit
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
<{
count: number
count
: number }> {
constructor() {
super({
count: number
count
: 0 });
}
}
clear<typeof CounterCubit>(BlocClass: typeof CounterCubit): void
clear
(
class CounterCubit
CounterCubit
); // dispose and remove all instances of this class
function clearAll(): void
clearAll
(); // dispose and remove everything

In React, useBloc handles acquire and release automatically. You rarely call registry functions directly.

Outside React (tests, scripts, server-side code), manage the lifecycle manually:

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
} from '@blac/core';
import {
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 CounterCubit
CounterCubit
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
<{
count: number
count
: number }> {
constructor() {
super({
count: number
count
: 0 });
}
CounterCubit.increment: () => void
increment
= () => this.
StructuralContainer<{ count: number; }>.update(fn: (state: {
count: number;
}) => {
count: number;
}): void
update
((
s: {
count: number;
}
s
) => ({
count: number
count
:
s: {
count: number;
}
s
.
count: number
count
+ 1 }));
}
const
const counter: CounterCubit
counter
=
acquire<typeof CounterCubit>(BlocClass: typeof CounterCubit, opts?: {
args?: void | undefined;
refId?: string;
} | undefined): CounterCubit

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 CounterCubit
CounterCubit
);
const counter: CounterCubit
counter
.
CounterCubit.increment: () => void
increment
();
var console: Console
console
.
Console.log(...data: any[]): void

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

MDN Reference

log
(
const counter: CounterCubit
counter
.
StructuralContainer<{ count: number; }>.state: {
count: number;
}
state
.
count: number
count
);
release<typeof CounterCubit>(BlocClass: typeof CounterCubit, 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 CounterCubit
CounterCubit
);
  • ConfigurationkeepAlive, static key, and the circuit breakers that catch leaks
  • Passing Inputsargs and the full instance-identity model
  • System Events — the dispose event that fires when an instance is torn down
  • Testing core logic — using clear/clearAll to isolate the registry between tests

For the full FAQ see Troubleshooting. Below are the instance-management-specific problems.

Symptom: A component unmounts and remounts and finds the bloc in its initial state — any previous state is gone.

Cause: When the last consumer releases, the instance is disposed immediately (ref count 0 → dispose). The next consumer acquires a brand-new instance with fresh state.

Fix: If state must persist across unmounts, mark the class keepAlive:

import {
function blac(options: BlacOptions): <T extends new (...args: any[]) => any>(target: T, _context?: ClassDecoratorContext) => T

Decorator to configure StateContainer classes.

@example

Decorator syntax (requires experimentalDecorators or TC39 decorators)

@blac({ keepAlive: true }) class AuthBloc extends Cubit {}

@blac

({ excludeFromDevTools: true }) class InternalBloc extends Cubit {}

@example

Function syntax (no decorator support needed)

const AuthBloc = blac({ keepAlive: true })(
class extends Cubit<AuthState> {}
);

blac
,
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';
interface
interface SessionState
SessionState
{
SessionState.token: string | null
token
: string | null;
}
@
function blac(options: BlacOptions): <T extends new (...args: any[]) => any>(target: T, _context?: ClassDecoratorContext) => T

Decorator to configure StateContainer classes.

@example

Decorator syntax (requires experimentalDecorators or TC39 decorators)

@blac({ keepAlive: true }) class AuthBloc extends Cubit {}

@blac

({ excludeFromDevTools: true }) class InternalBloc extends Cubit {}

@example

Function syntax (no decorator support needed)

const AuthBloc = blac({ keepAlive: true })(
class extends Cubit<AuthState> {}
);

blac
({
keepAlive: true
keepAlive
: true })
class
class SessionCubit
SessionCubit
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
<
interface SessionState
SessionState
> {
constructor() {
super({
SessionState.token: string | null
token
: null });
}
}

keepAlive instances are never auto-disposed at ref count 0 — tear them down explicitly with release(Class, { forceDispose: true }) or clear(Class) in teardown. See Configuration.

Symptom: DevTools shows an instance that should have been disposed is still alive, or ref count never returns to 0.

Cause: An acquire outside React has no matching release. Every call to acquire increments the ref count; without a matching release the count never reaches zero.

Fix: Pair every acquire with a release, ideally in a try/finally. If you only need to read without owning the lifecycle, use borrow or ensure — neither takes a ref:

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 borrow<T extends StateContainerConstructor>(BlocClass: T, target?: BorrowTarget<T>): InstanceType<T>
borrow
,
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
} from '@blac/core';
import {
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 CounterCubit
CounterCubit
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
<{
count: number
count
: number }> {
constructor() {
super({
count: number
count
: 0 });
}
}
// Leaks — no release
function
function readOnceBad(): number
readOnceBad
() {
const
const c: CounterCubit
c
=
acquire<typeof CounterCubit>(BlocClass: typeof CounterCubit, opts?: {
args?: void | undefined;
refId?: string;
} | undefined): CounterCubit

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 CounterCubit
CounterCubit
);
return
const c: CounterCubit
c
.
StructuralContainer<{ count: number; }>.state: {
count: number;
}
state
.
count: number
count
;
// BUG: ref count never drops to 0
}
// Fixed — borrow takes no ref
function
function readOnce(): number
readOnce
() {
return
borrow<typeof CounterCubit>(BlocClass: typeof CounterCubit, target?: BorrowTarget<typeof CounterCubit> | undefined): CounterCubit
borrow
(
class CounterCubit
CounterCubit
).
StructuralContainer<{ count: number; }>.state: {
count: number;
}
state
.
count: number
count
;
}

Circuit breaker threw (“max instances” / “max refs”)

Section titled “Circuit breaker threw (“max instances” / “max refs”)”

Symptom: acquire throws with a message like “exceeded the maximum live instances” or “exceeded the maximum refs.”

Cause: Almost always a leak — an unstable instance key (non-serializable args) spawning endless instances, or a missing release accumulating refs. The limit is not too low; the leak is real.

Fix: Find and fix the leak before raising the limit. Common causes: non-serializable values in args (fix: move to deps), acquire without release (fix: pair them), args that differ structurally each render (fix: normalise types). See Configuration: circuit breakers and Troubleshooting.