@blac/react 2.0.19 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.cjs +69 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +71 -24
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +11 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -51,12 +51,12 @@ BlaC follows one rule:
|
|
|
51
51
|
> **A component becomes reactive only to the paths it reads during its own render.**
|
|
52
52
|
|
|
53
53
|
`useBloc` returns a proxy that records which paths you read; the component
|
|
54
|
-
re-renders only when one of
|
|
54
|
+
re-renders only when one of _those_ paths changes. Reads outside render — effects,
|
|
55
55
|
event handlers, `setTimeout`, `await` — record nothing.
|
|
56
56
|
|
|
57
57
|
### Props are snapshots — `untracked()`
|
|
58
58
|
|
|
59
|
-
A tracking proxy stays bound to the component that
|
|
59
|
+
A tracking proxy stays bound to the component that _created_ it. If a parent passes
|
|
60
60
|
a proxied value to a child and the child reads from it, that read is attributed to
|
|
61
61
|
the **parent** — so the parent over-subscribes and re-renders for paths only the
|
|
62
62
|
child uses.
|
|
@@ -82,7 +82,7 @@ function List() {
|
|
|
82
82
|
|
|
83
83
|
The trade-off is intentional and React-like: a component that receives an
|
|
84
84
|
`untracked()` value is **not** reactive to it — a change triggers nothing, memo or
|
|
85
|
-
not. If no component reads a path during its
|
|
85
|
+
not. If no component reads a path during its _own_ render, a change to that path
|
|
86
86
|
re-renders nothing. To react to data, a component must be a consumer itself (call
|
|
87
87
|
its own `useBloc`). Nobody up the tree re-renders for a path unless they read it
|
|
88
88
|
themselves.
|
package/dist/index.cjs
CHANGED
|
@@ -113,6 +113,30 @@ function buildTrackedProxy(instance, trackedStateRef, onDepHandle) {
|
|
|
113
113
|
//#region src/useBloc.ts
|
|
114
114
|
let nextConsumerId = 0;
|
|
115
115
|
const ARGS_UNSET = Symbol("blac.argsKeyUnset");
|
|
116
|
+
/**
|
|
117
|
+
* `useRef` with a lazily-built initial value. `useRef(new Map())` discards its
|
|
118
|
+
* argument after the first render but still EVALUATES it on every render, so
|
|
119
|
+
* each such ref costs one dead allocation per render. Passing a module-level
|
|
120
|
+
* factory instead makes the per-render cost a single identity read.
|
|
121
|
+
*
|
|
122
|
+
* The factory MUST be a stable module-level function (an inline arrow would
|
|
123
|
+
* just move the per-render allocation from the value to the closure). The
|
|
124
|
+
* returned `current` is non-nullable, so read sites keep their original types.
|
|
125
|
+
*/
|
|
126
|
+
function useLazyRef(create) {
|
|
127
|
+
const ref = (0, react.useRef)(null);
|
|
128
|
+
if (ref.current === null) ref.current = create();
|
|
129
|
+
return ref;
|
|
130
|
+
}
|
|
131
|
+
const newComponentRef = () => ({});
|
|
132
|
+
const newArgsKeyEntry = () => ({
|
|
133
|
+
ref: ARGS_UNSET,
|
|
134
|
+
key: void 0
|
|
135
|
+
});
|
|
136
|
+
const newSessionMap = () => /* @__PURE__ */ new Map();
|
|
137
|
+
const newDepSubsMap = () => /* @__PURE__ */ new Map();
|
|
138
|
+
const newDepWrapperCache = () => /* @__PURE__ */ new WeakMap();
|
|
139
|
+
const newProxyCache = () => new _dirtytalk_structural.ProxyCache();
|
|
116
140
|
const primaryRefId = (consumerId) => `useBloc@${consumerId}`;
|
|
117
141
|
const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
118
142
|
/**
|
|
@@ -166,24 +190,25 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
|
166
190
|
* ```
|
|
167
191
|
*/
|
|
168
192
|
function useBloc(BlocClass, options) {
|
|
169
|
-
const componentRef = (
|
|
193
|
+
const componentRef = useLazyRef(newComponentRef);
|
|
170
194
|
const consumerIdRef = (0, react.useRef)(null);
|
|
171
195
|
if (consumerIdRef.current === null) consumerIdRef.current = `useBloc-${nextConsumerId++}`;
|
|
172
196
|
const consumerId = consumerIdRef.current;
|
|
173
|
-
(0,
|
|
197
|
+
const isolated = options?.isolated === true || (0, _blac_core.isIsolatedClass)(BlocClass);
|
|
198
|
+
const isolationKey = isolated ? consumerId : void 0;
|
|
174
199
|
const selectRef = (0, react.useRef)(options?.select);
|
|
175
200
|
selectRef.current = options?.select;
|
|
201
|
+
const isSelectMode = options?.select !== void 0;
|
|
176
202
|
const onMountRef = (0, react.useRef)(options?.onMount);
|
|
177
203
|
onMountRef.current = options?.onMount;
|
|
178
204
|
const onUnmountRef = (0, react.useRef)(options?.onUnmount);
|
|
179
205
|
onUnmountRef.current = options?.onUnmount;
|
|
206
|
+
const isolatedRef = (0, react.useRef)(isolated);
|
|
207
|
+
isolatedRef.current = isolated;
|
|
180
208
|
const ownArgs = options?.args;
|
|
181
209
|
const ownArgsRef = (0, react.useRef)(ownArgs);
|
|
182
210
|
ownArgsRef.current = ownArgs;
|
|
183
|
-
const ownArgsKeyRef = (
|
|
184
|
-
ref: ARGS_UNSET,
|
|
185
|
-
key: void 0
|
|
186
|
-
});
|
|
211
|
+
const ownArgsKeyRef = useLazyRef(newArgsKeyEntry);
|
|
187
212
|
if (!Object.is(ownArgsKeyRef.current.ref, ownArgs)) ownArgsKeyRef.current = {
|
|
188
213
|
ref: ownArgs,
|
|
189
214
|
key: ownArgs === void 0 ? void 0 : JSON.stringify(ownArgs)
|
|
@@ -192,25 +217,23 @@ function useBloc(BlocClass, options) {
|
|
|
192
217
|
const providerArgs = useProvidedArgs(BlocClass);
|
|
193
218
|
const providerArgsRef = (0, react.useRef)(providerArgs);
|
|
194
219
|
providerArgsRef.current = providerArgs;
|
|
195
|
-
const providerArgsKeyRef = (
|
|
196
|
-
ref: ARGS_UNSET,
|
|
197
|
-
key: void 0
|
|
198
|
-
});
|
|
220
|
+
const providerArgsKeyRef = useLazyRef(newArgsKeyEntry);
|
|
199
221
|
if (!Object.is(providerArgsKeyRef.current.ref, providerArgs)) providerArgsKeyRef.current = {
|
|
200
222
|
ref: providerArgs,
|
|
201
223
|
key: providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs)
|
|
202
224
|
};
|
|
203
225
|
const providerArgsKey = providerArgsKeyRef.current.key;
|
|
204
226
|
const trackedStateRef = (0, react.useRef)(null);
|
|
205
|
-
const sessionRef = (
|
|
206
|
-
const depSubsRef = (
|
|
207
|
-
const depWrapperCacheRef = (
|
|
208
|
-
const proxyCacheRef = (
|
|
227
|
+
const sessionRef = useLazyRef(newSessionMap);
|
|
228
|
+
const depSubsRef = useLazyRef(newDepSubsMap);
|
|
229
|
+
const depWrapperCacheRef = useLazyRef(newDepWrapperCache);
|
|
230
|
+
const proxyCacheRef = useLazyRef(newProxyCache);
|
|
209
231
|
const lastReconcileRef = (0, react.useRef)(null);
|
|
210
232
|
const [rebindNonce, bumpRebind] = (0, react.useReducer)((x) => x + 1, 0);
|
|
211
233
|
const ownedBlocRef = (0, react.useRef)(null);
|
|
212
234
|
const { bloc, instanceKey, trackedBloc } = (0, react.useMemo)(() => {
|
|
213
|
-
const
|
|
235
|
+
const baseArgs = ownArgsRef.current !== void 0 ? ownArgsRef.current : providerArgsRef.current;
|
|
236
|
+
const effectiveArgs = isolatedRef.current ? withIsolation(baseArgs, consumerId) : baseArgs;
|
|
214
237
|
const resolvedKey = (0, _blac_core.resolveInstanceKey)(BlocClass, effectiveArgs);
|
|
215
238
|
const instance = (0, _blac_core.getRegistry)().acquire(BlocClass, resolvedKey, {
|
|
216
239
|
canCreate: true,
|
|
@@ -235,17 +258,18 @@ function useBloc(BlocClass, options) {
|
|
|
235
258
|
BlocClass,
|
|
236
259
|
ownArgsKey,
|
|
237
260
|
providerArgsKey,
|
|
261
|
+
isolationKey,
|
|
238
262
|
rebindNonce
|
|
239
263
|
]);
|
|
240
264
|
const [, force] = (0, react.useReducer)((x) => x + 1, 0);
|
|
241
|
-
const pathRef = (
|
|
242
|
-
const expandedInterestRef = (
|
|
265
|
+
const pathRef = useLazyRef(_dirtytalk_structural.emptyPathSet);
|
|
266
|
+
const expandedInterestRef = useLazyRef(_dirtytalk_structural.emptyPathSet);
|
|
243
267
|
const lastSelectionRef = (0, react.useRef)(null);
|
|
244
268
|
const renderStateRef = (0, react.useRef)(void 0);
|
|
245
269
|
const prevBlocRef = (0, react.useRef)(null);
|
|
246
270
|
(0, react.useEffect)(() => {
|
|
247
271
|
const channel = bloc.channel;
|
|
248
|
-
if (
|
|
272
|
+
if (isSelectMode) {
|
|
249
273
|
const unsub = channel.subscribe(() => _dirtytalk_structural.ALL_PATHS, () => {
|
|
250
274
|
const select = selectRef.current;
|
|
251
275
|
if (!select) {
|
|
@@ -276,7 +300,11 @@ function useBloc(BlocClass, options) {
|
|
|
276
300
|
unsub();
|
|
277
301
|
bloc.unregisterConsumer(consumerId);
|
|
278
302
|
};
|
|
279
|
-
}, [
|
|
303
|
+
}, [
|
|
304
|
+
bloc,
|
|
305
|
+
consumerId,
|
|
306
|
+
isSelectMode
|
|
307
|
+
]);
|
|
280
308
|
(0, react.useLayoutEffect)(() => {
|
|
281
309
|
const registry = (0, _blac_core.getRegistry)();
|
|
282
310
|
const live = registry.acquire(BlocClass, instanceKey, {
|
|
@@ -402,7 +430,7 @@ function useBloc(BlocClass, options) {
|
|
|
402
430
|
}
|
|
403
431
|
subs.clear();
|
|
404
432
|
};
|
|
405
|
-
}, [consumerId]);
|
|
433
|
+
}, [consumerId, depSubsRef]);
|
|
406
434
|
return [
|
|
407
435
|
state,
|
|
408
436
|
trackedBloc,
|
|
@@ -435,11 +463,17 @@ function makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, onDepHa
|
|
|
435
463
|
const brand = handle[_blac_core.DEP_BRAND];
|
|
436
464
|
const refId = depRefId(consumerId);
|
|
437
465
|
const registry = (0, _blac_core.getRegistry)();
|
|
438
|
-
const perDep = /* @__PURE__ */ new
|
|
466
|
+
const perDep = /* @__PURE__ */ new WeakMap();
|
|
439
467
|
const proxyCache = new _dirtytalk_structural.ProxyCache();
|
|
468
|
+
let lastArgs = ARGS_UNSET;
|
|
469
|
+
let lastKey = "";
|
|
440
470
|
const resolve = (options) => {
|
|
441
471
|
const args = options?.args ?? brand.defaultArgs;
|
|
442
|
-
|
|
472
|
+
if (!Object.is(lastArgs, args)) {
|
|
473
|
+
lastArgs = args;
|
|
474
|
+
lastKey = registry.resolveKey(brand.Type, void 0, args);
|
|
475
|
+
}
|
|
476
|
+
const key = lastKey;
|
|
443
477
|
return {
|
|
444
478
|
dep: registry.ensure(brand.Type, key, args),
|
|
445
479
|
key,
|
|
@@ -490,6 +524,19 @@ function unionPaths(a, b) {
|
|
|
490
524
|
for (const id of b) out.add(id);
|
|
491
525
|
return out;
|
|
492
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Fold a per-mount isolation id into `args`. MERGES rather than replaces
|
|
529
|
+
* (plan Q2=B): a class-level `static isolated` must not be silently cancelled
|
|
530
|
+
* by a call site that happens to pass args. Non-object args are nested rather
|
|
531
|
+
* than spread, so a primitive arg can't be destructured into index keys.
|
|
532
|
+
*/
|
|
533
|
+
const withIsolation = (base, id) => base === void 0 ? { _blacIsolated: id } : typeof base === "object" && base !== null ? {
|
|
534
|
+
...base,
|
|
535
|
+
_blacIsolated: id
|
|
536
|
+
} : {
|
|
537
|
+
_blacArgs: base,
|
|
538
|
+
_blacIsolated: id
|
|
539
|
+
};
|
|
493
540
|
const shallowArrayEqual = (a, b) => {
|
|
494
541
|
if (a === b) return true;
|
|
495
542
|
if (a.length !== b.length) return false;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["DEP_BRAND","ProxyCache","ALL_PATHS","DEP_BRAND"],"sources":["../src/BlocProvider.tsx","../src/buildTrackedProxy.ts","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\n\n/**\n * Internal context value: a Map from bloc constructor to the args object\n * provided by the nearest BlocProvider for that bloc class.\n *\n * Stored as a WeakMap-keyed record so nested providers for different blocs\n * compose without clobbering each other.\n */\ntype ProvidedArgsMap = Map<StateContainerConstructor, unknown>;\n\nconst ProvidedArgsContext = createContext<ProvidedArgsMap>(new Map());\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps<T extends StateContainerConstructor> {\n /**\n * The bloc class whose args are being provided to descendants.\n */\n bloc: T;\n /**\n * Args that descendant `useBloc(bloc)` calls will resolve to when no\n * own `args` are given.\n */\n args: ExtractArgs<T>;\n children: ReactNode;\n}\n\n/**\n * Provides args to descendant `useBloc` calls for a specific bloc class via\n * React context.\n *\n * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the\n * args supplied here. Own `args` on the `useBloc` call always win.\n *\n * Multiple `BlocProvider` wrappers for different bloc classes compose: each\n * provider merges its entry into the inherited map, so nested providers for\n * different blocs do not interfere.\n *\n * @example\n * ```tsx\n * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>\n * <UserProfile />\n * </BlocProvider>\n * ```\n *\n * @example Per-mount private instance\n * ```tsx\n * const id = useId();\n * <BlocProvider bloc={CartBloc} args={{ _id: id }}>\n * <CartWidget />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider<T extends StateContainerConstructor>({\n bloc,\n args,\n children,\n}: BlocProviderProps<T>): ReactElement {\n const parentMap = useContext(ProvidedArgsContext);\n\n // Merge our entry into a new Map so sibling/parent providers for other blocs\n // are preserved. Memoised on (parentMap, bloc, args) identity.\n const mergedMap = useMemo(() => {\n const next = new Map(parentMap);\n next.set(bloc, args);\n return next;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parentMap, bloc, args]);\n\n return (\n <ProvidedArgsContext.Provider value={mergedMap}>\n {children}\n </ProvidedArgsContext.Provider>\n );\n}\n\n/**\n * Returns the args provided by the nearest {@link BlocProvider} for the given\n * bloc class, or `undefined` when called outside a matching provider.\n *\n * Used by `useBloc` to inherit provider args when no own `args` are given.\n */\nexport function useProvidedArgs<T extends StateContainerConstructor>(\n BlocClass: T,\n): ExtractArgs<T> | undefined {\n const map = useContext(ProvidedArgsContext);\n return map.get(BlocClass) as ExtractArgs<T> | undefined;\n}\n","import { DEP_BRAND } from '@blac/core';\n\n/**\n * Build a per-consumer proxy pair for a bloc instance.\n *\n * Returns two proxies:\n * - `proxy` — the stable outer proxy returned to the consumer. Getter\n * properties are invoked with `thisProxy` as `this` so that `this.state`\n * reads inside getters are redirected to the current render's tracking\n * proxy. Non-getter properties fall through to the live instance.\n * - `thisProxy` — the inner `this`-proxy used as the receiver for getter\n * calls. Intercepts `state` to return `trackedStateRef.current` when a\n * tracking context is active; otherwise falls through to the live value.\n * When `onDepHandle` is supplied, a read off `this` whose value carries the\n * `DEP_BRAND` symbol (i.e. a `depend()` handle) is routed through\n * `onDepHandle`, which returns a per-consumer wrapper whose `.track()` opts\n * the consumer into cross-bloc reactivity. Core stays decoupled — detection\n * is purely by the `DEP_BRAND` symbol.\n *\n * Both allocations happen exactly once per bloc acquisition (inside `useMemo`)\n * so the proxies are stable across renders.\n *\n * @param onDepHandle - Optional callback invoked when a getter reads a branded\n * dep handle off `this`. Receives the original handle and returns the value\n * to expose in its place (the session-bound wrapper). The callback is\n * responsible for caching wrappers per handle to avoid re-allocation.\n */\nexport function buildTrackedProxy<T extends object>(\n instance: T,\n trackedStateRef: { current: unknown },\n onDepHandle?: (handle: object) => unknown,\n): { proxy: T; thisProxy: T } {\n // Build a map of getter descriptors from the prototype chain (excluding\n // Object.prototype). This is computed once per bloc acquisition so that\n // the proxy's get trap is O(1) per property access. Both string- and\n // symbol-keyed getters are collected. Arrow-function class properties\n // (own, bound in the constructor) are not getters and pass through\n // unmodified.\n const getterDescs = new Map<string | symbol, PropertyDescriptor>();\n let proto = Object.getPrototypeOf(instance);\n while (proto && proto !== Object.prototype) {\n const keys: (string | symbol)[] = [\n ...Object.getOwnPropertyNames(proto),\n ...Object.getOwnPropertySymbols(proto),\n ];\n for (const key of keys) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc?.get && !getterDescs.has(key)) getterDescs.set(key, desc);\n }\n proto = Object.getPrototypeOf(proto);\n }\n\n // `this`-proxy for getter invocations, allocated ONCE per acquisition (the\n // trap closes over the stable `trackedStateRef`, so it never needs to be\n // rebuilt per access). Redirects `this.state` to the current render's\n // tracking proxy so getter reads during JSX record paths; outside render\n // `trackedStateRef.current` is null and it falls through to live state.\n // The receiver `r` (this proxy) is threaded through Reflect.get so chained\n // getter calls (getters reading other getters) stay in tracked context.\n const thisProxy = new Proxy(instance as object, {\n get(t, k, r) {\n if (k === 'state') return trackedStateRef.current ?? Reflect.get(t, k, r);\n const value = Reflect.get(t, k, r);\n // A branded dep handle read off `this` (e.g. `this.price`) is routed\n // through onDepHandle so the consumer's session can wrap `.track()`.\n if (\n onDepHandle !== undefined &&\n (typeof value === 'function' || typeof value === 'object') &&\n value !== null &&\n (value as Record<symbol, unknown>)[DEP_BRAND] !== undefined\n ) {\n return onDepHandle(value as object);\n }\n return value;\n },\n });\n\n // Stable proxy: one allocation per bloc acquisition. Non-getter access is\n // a single Map lookup + Reflect.get — no prototype walk on the hot path.\n const proxy = new Proxy(instance as object, {\n get(target, key, receiver) {\n const desc = getterDescs.get(key);\n if (desc?.get) return desc.get.call(thisProxy);\n return Reflect.get(target, key, receiver);\n },\n }) as T;\n\n return { proxy, thisProxy: thisProxy as T };\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n DEP_BRAND,\n getRegistry,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n pathSetEquals,\n trackRender,\n PathInterner,\n ProxyCache,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useProvidedArgs } from './BlocProvider';\nimport { buildTrackedProxy } from './buildTrackedProxy';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n// Sentinel that can never `Object.is`-equal a real args value (including\n// `undefined`). Used to lazily seed args-key refs so the structural key is\n// computed only when the guard actually runs, never on every render.\nconst ARGS_UNSET: unique symbol = Symbol('blac.argsKeyUnset');\n\n// Registry refId formats for a consumer's primary bloc and its tracked deps.\n// Centralised so the `acquire` and `release` sites can never drift apart — a\n// mismatch would leak the ref and keep the bloc alive past unmount.\nconst primaryRefId = (consumerId: string): string => `useBloc@${consumerId}`;\nconst depRefId = (consumerId: string): string => `useBloc@${consumerId}:dep`;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`).\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is derived from `options.args` (own args),\n * then the surrounding {@link BlocProvider} context args for this bloc,\n * then the default key (no args).\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * Per-mount private instance:\n * ```ts\n * const id = useId();\n * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });\n * ```\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Args-based shared instance\n * ```ts\n * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Priority: own args > provider args (for this bloc class) > none.\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const ownArgs = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const ownArgsRef = useRef(ownArgs);\n ownArgsRef.current = ownArgs;\n // Fast-path: only recompute the structural key when the args REFERENCE\n // changes. Callers commonly pass a stable args object (e.g. memoised or\n // module-level), so this skips a JSON.stringify call every render.\n const ownArgsKeyRef = useRef<{ ref: unknown; key: string | undefined }>({\n ref: ARGS_UNSET,\n key: undefined,\n });\n if (!Object.is(ownArgsKeyRef.current.ref, ownArgs)) {\n ownArgsKeyRef.current = {\n ref: ownArgs,\n key: ownArgs === undefined ? undefined : JSON.stringify(ownArgs),\n };\n }\n const ownArgsKey = ownArgsKeyRef.current.key;\n\n // Read provided args from the nearest BlocProvider for this bloc class.\n const providerArgs = useProvidedArgs(BlocClass);\n const providerArgsRef = useRef(providerArgs);\n providerArgsRef.current = providerArgs;\n // Same reference fast-path as ownArgsKey above.\n const providerArgsKeyRef = useRef<{ ref: unknown; key: string | undefined }>(\n {\n ref: ARGS_UNSET,\n key: undefined,\n },\n );\n if (!Object.is(providerArgsKeyRef.current.ref, providerArgs)) {\n providerArgsKeyRef.current = {\n ref: providerArgs,\n key: providerArgs === undefined ? undefined : JSON.stringify(providerArgs),\n };\n }\n const providerArgsKey = providerArgsKeyRef.current.key;\n\n // Current render's tracking proxy. Declared before the memo so the stable\n // ref object can be passed to buildTrackedProxy at acquisition time. The\n // proxy trap only reads `.current` at invocation time (not during creation),\n // so it is safe to pass on first mount even though the value is null.\n // Populated during each auto-track render snapshot (below); cleared to null\n // by useLayoutEffect after commit.\n const trackedStateRef = useRef<unknown>(null);\n\n // ---------------------------------------------------------------------------\n // Per-consumer cross-bloc session.\n //\n // Each render rebuilds a map of every container this consumer is currently\n // interested in. The PRIMARY bloc is the first uniform entry; every dep\n // reached through `this.<handle>.track()` inside a tracked getter adds an\n // entry. The layout-effect reconcile (below) diffs this map vs the previous\n // render to subscribe new deps and release dropped ones. The session lives in\n // this hook's refs only — there is no global ambient state, so sibling\n // renders never cross-contaminate.\n // ---------------------------------------------------------------------------\n const sessionRef = useRef<Map<StateContainer, SessionEntry>>(new Map());\n // Channel subscriptions for DEP containers (the primary keeps its own\n // dedicated effect). container -> { unsubscribe, interestRef, refId held }.\n const depSubsRef = useRef<Map<StateContainer, DepSub>>(new Map());\n // Per-handle wrapper cache, allocated once per bloc acquisition (in the memo)\n // so wrappers are stable across renders. handle -> session-bound wrapper.\n const depWrapperCacheRef = useRef<Map<object, unknown>>(new Map());\n const proxyCacheRef = useRef(new ProxyCache());\n // Snapshot of the last FULL reconcile's shape, used by the layout-effect\n // below to short-circuit when nothing actually changed. `null` means \"no\n // prior full run to compare against\" (first commit, or the previous commit\n // was in select-mode) — always forces a full reconcile in that case.\n const lastReconcileRef = useRef<ReconcileSignature | null>(null);\n\n // Rebind nonce: bumped by the ownership layout-effect when the instance the\n // render captured was disposed + recreated out from under us. This happens on\n // a same-commit ownership handoff of a shared (non-keepAlive) key — the sole\n // prior owner's effect cleanup releases refs→0 and SYNCHRONOUSLY disposes the\n // instance before this consumer's layout setup re-acquires (creating a fresh\n // one) — and equivalently under StrictMode's setup→cleanup→setup double-invoke\n // for a lone owner. Threaded into the memo deps so bumping it re-ensures `bloc`\n // against the LIVE registry entry instead of the disposed instance.\n const [rebindNonce, bumpRebind] = useReducer((x: number) => x + 1, 0);\n // The live instance actually owned (ref held) by the ownership layout-effect,\n // read by its cleanup so onUnmount always fires with the owned instance.\n const ownedBlocRef = useRef<TBloc | null>(null);\n\n const { bloc, instanceKey, trackedBloc } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n trackedBloc: TBloc;\n }>(() => {\n // Own args win over provider args; provider args win over no args.\n const effectiveArgs =\n ownArgsRef.current !== undefined\n ? ownArgsRef.current\n : providerArgsRef.current;\n\n const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);\n const registry = getRegistry();\n // Render only ENSUREs the instance exists (no ref). Ownership is claimed in\n // the layout effect below, so an abandoned/uncommitted render can never\n // leak a ref and a memo re-run can never double-count one (R3/R4).\n const instance = registry.acquire(BlocClass, resolvedKey, {\n canCreate: true,\n countRef: false,\n args: effectiveArgs,\n }) as TBloc;\n\n // Build a session-bound wrapper for a dep handle the first time a getter\n // reads it off `this`; cache per handle so the wrapper identity is stable.\n // `onDepHandle` is threaded into each dep's tracked proxy too, so a nested\n // `this.<otherHandle>.track()` inside a dep's getter records into the SAME\n // consumer session — that is what makes deep chains (A→B→C) reactive.\n const onDepHandle = (handle: object): unknown => {\n const cache = depWrapperCacheRef.current;\n const cached = cache.get(handle);\n if (cached !== undefined) return cached;\n const wrapper = makeDepWrapper(\n handle as DepHandleLike,\n consumerId,\n trackedStateRef,\n sessionRef,\n onDepHandle,\n );\n cache.set(handle, wrapper);\n return wrapper;\n };\n\n const { proxy } = buildTrackedProxy(\n instance as object,\n trackedStateRef,\n onDepHandle,\n );\n\n return {\n bloc: instance,\n instanceKey: resolvedKey,\n trackedBloc: proxy as TBloc,\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, ownArgsKey, providerArgsKey, rebindNonce]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We talk directly to `bloc.channel` — the StructuralContainer's path-scoped\n // DirtyChannel. Subscribing with a dynamic interest function lets us narrow\n // wakeups per consumer, so a component only re-renders when a path it\n // actually read changes (rather than on every state change).\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // Expanded interest: leaf paths PLUS *ancestor-watch* ids for their parents,\n // so that when `patch` atomically replaces a parent (e.g. the array 'items')\n // and the consumer tracked a child (e.g. 'items.length'), the intersection\n // still fires — while a structural pulse-up of a plain-object parent (e.g.\n // 'user' when a sibling 'user.name' changed) does NOT, because pulse-up marks\n // are normal ids and ancestor-watch ids only intersect their own lane.\n // Updated in useLayoutEffect after each render once pathRef.current is\n // populated.\n const expandedInterestRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n // Render-time raw-state snapshot, seeded each render (below) and read by the\n // subscription effect to close the mount gap (R2): an emit landing between the\n // render read and the passive subscribe would otherwise be lost.\n const renderStateRef = useRef<unknown>(undefined);\n // Bloc identity from the previous render, so select-mode can reset its cached\n // selection when the underlying instance changes (re-key).\n const prevBlocRef = useRef<unknown>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n //\n // Self-healing note: this effect can run once against a pre-rebind (possibly\n // disposed) `bloc` before the ownership layout-effect's nonce bump triggers a\n // re-render that swaps `bloc` to the live instance (the memo dep array\n // includes `rebindNonce`). That's benign — `channel.subscribe` and\n // `unregisterConsumer` are plain Map operations that don't check disposal\n // state and never throw on a disposed container — so this effect's cleanup\n // runs cleanly and the re-render re-subscribes against the live instance.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n // Close the mount gap (R2): an emit between the render's selector seed and\n // this subscribe would be lost. Recompute against LIVE state and force if\n // the selection advanced.\n const select = selectRef.current;\n if (select) {\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev === null || !shallowArrayEqual(prev, next)) {\n lastSelectionRef.current = next;\n force();\n }\n }\n return unsub;\n }\n\n // Auto-track mode: subscribe with the expanded interest (leaf paths + their\n // ancestors). This ensures that when `patch` marks a parent path (e.g.\n // 'items') and the consumer tracked a child (e.g. 'items.length'), the\n // channel intersection still fires a re-render.\n //\n // `expandedInterestRef.current` is updated by the useLayoutEffect below\n // after each render, so the interest is always fresh at flush time.\n const unsub = channel.subscribe(\n () => expandedInterestRef.current,\n () => force(),\n );\n // Register the consumer's leaf paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n // Close the mount gap (R2): if the container's state advanced between the\n // render snapshot (renderStateRef) and this subscribe, the emit was lost —\n // force one re-render so we don't stay stale.\n if ((bloc as unknown as StateContainer).state !== renderStateRef.current) {\n force();\n }\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Ownership + mount / unmount lifecycle.\n //\n // The ownership ref is claimed HERE (a layout effect), not in the render/memo,\n // so acquire and release are perfectly paired: a memo re-run can no longer\n // double-count (R3) and an uncommitted render can no longer leak (R4). Keyed\n // on [BlocClass, instanceKey, consumerId] — NOT `bloc` — so a genuine re-key\n // (args change, OR a BlocClass swap) releases the old ref and acquires the new\n // one, while a pure rebind (same class+key, instance replaced under us) does\n // NOT re-run this effect and therefore never releases the single ref we just\n // took (which, for a sole owner, would synchronously dispose the\n // freshly-created instance and churn indefinitely).\n //\n // `BlocClass` MUST stay in the dep array even though `instanceKey` alone often\n // determines identity: `resolveInstanceKey`/`resolveKey` collapse to the same\n // `DEFAULT_STRUCTURAL_KEY` sentinel across DIFFERENT classes when neither has\n // args nor a `static key` (e.g. `useBloc(cond ? AdminBloc : UserBloc)`). Without\n // `BlocClass` here, swapping classes at that shared key would never re-run this\n // effect: the old class's ref would leak until unmount, and the new class's\n // instance (only `ensure`d by the render memo, countRef:false) would be held\n // with zero ownership refs — exposed to disposal by unrelated traffic on that\n // class+key, with no rebind path to recover it.\n //\n // `acquire` returns the authoritative LIVE instance for the key. If it differs\n // from the instance the render captured (`bloc`), the render read a disposed /\n // replaced instance (same-commit shared-key handoff, or StrictMode remount);\n // we bump the rebind nonce so the memo re-ensures `bloc` against this live one.\n // onMount/onUnmount fire with the owned live instance and stay co-located with\n // acquire/release so onUnmount(bloc) runs BEFORE release(...) within one\n // cleanup, keeping the instance alive while the callback runs.\n // ---------------------------------------------------------------------------\n useLayoutEffect(() => {\n const registry = getRegistry();\n const live = registry.acquire(BlocClass, instanceKey, {\n canCreate: true,\n countRef: true,\n refId: primaryRefId(consumerId),\n }) as TBloc;\n ownedBlocRef.current = live;\n onMountRef.current?.(live as InstanceType<T>);\n // Rebind if the render captured a stale (disposed/replaced) instance so the\n // component renders + subscribes against the live registry entry, not a\n // disposed one. Only bumps on an actual mismatch, so it can fire at most once\n // per handoff and never loops (the re-ensured `bloc` equals `live`, and this\n // effect is not keyed on `bloc` so it won't re-run and re-release).\n if (live !== bloc) {\n bumpRebind();\n }\n return () => {\n onUnmountRef.current?.((ownedBlocRef.current ?? bloc) as InstanceType<T>);\n registry.release(\n BlocClass,\n instanceKey,\n false,\n primaryRefId(consumerId),\n );\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, instanceKey, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n // Seed the render-time snapshot so the subscription effect can detect an emit\n // that landed in the render→subscribe window (R2 mount gap).\n renderStateRef.current = rawState;\n // Reset the cached selection when the bloc identity changes (re-key) so the\n // select seed below re-seeds against the NEW instance instead of comparing\n // against a stale selection from the previous instance.\n if (prevBlocRef.current !== bloc) {\n prevBlocRef.current = bloc;\n lastSelectionRef.current = null;\n }\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n proxyCacheRef.current,\n );\n state = tracked.value as ExtractState<T>;\n trackedStateRef.current = tracked.value;\n pathRef.current = tracked.paths;\n // Rebuild the per-consumer session for this render. The primary bloc is the\n // first uniform entry; its `paths` are the SAME PathSet object the proxy\n // mutates during JSX (so it stays live as getters record leaves). Dep\n // entries are appended during JSX as `this.<handle>.track()` runs. Cleared\n // here (not in the layout effect) so a render that no longer tracks a dep\n // produces a session without it, and the reconcile drops it.\n const session = sessionRef.current;\n session.clear();\n session.set(bloc as unknown as StateContainer, {\n kind: 'primary',\n paths: tracked.paths,\n });\n // Freeze this render's tracking proxy after the synchronous render+commit\n // pass. The microtask fires only once the current task unwinds, so all\n // render-time JSX reads still record; reads afterwards — effects, event\n // handlers, async callbacks, devtools inspecting `state` — hit the\n // disarmed proxy and record nothing, so this render's path set can't be\n // polluted by work that outlives the render that owns it.\n queueMicrotask(tracked.disarm);\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits, pathRef.current is the consumer's actual leaf\n // interest (populated by the proxy during JSX evaluation). Re-register with\n // the container so the skeleton reflects the latest paths, and expand the\n // interest to include ancestor paths for the channel subscription.\n //\n // useLayoutEffect runs before the browser paints (and before any emit\n // triggered by another effect), so the skeleton is fresh by the time the\n // next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n // Clear the render-time tracking proxy now that JSX has been evaluated and\n // committed. Getters invoked after this point (event handlers, effects,\n // method→getter chains) fall through to live state instead of reading this\n // render's frozen snapshot. The render body re-seeds it next render.\n trackedStateRef.current = null;\n if (selectRef.current !== undefined) {\n // Switching into (or staying in) select-mode: invalidate any prior full\n // reconcile so a later switch back to auto-track mode never mistakes a\n // stale signature for \"unchanged\" and skips a needed reconcile.\n lastReconcileRef.current = null;\n return;\n }\n const container = bloc as unknown as StateContainer;\n const paths = pathRef.current;\n const session = sessionRef.current;\n\n // ---------------------------------------------------------------------\n // Short-circuit: if the primary path set AND the full dep session are\n // set-equal (paths + key/refId/args) to the last FULL reconcile, none of\n // registerConsumerPaths/subscribe/unsubscribe/expandWithAncestors below\n // can have anything new to do — skip the whole block. Any mismatch, or\n // `lastReconcileRef.current === null` (first commit, or the immediately\n // preceding commit was select-mode / uncertain), falls through to the\n // full reconcile. Never skip on uncertainty — a missed re-subscribe would\n // leave a stale/dropped subscription.\n // ---------------------------------------------------------------------\n const last = lastReconcileRef.current;\n if (\n last !== null &&\n last.primaryContainer === container &&\n pathSetEquals(last.primaryPaths, paths)\n ) {\n let unchanged = last.deps.size === session.size - 1;\n if (unchanged) {\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n const prevEntry = last.deps.get(depContainer);\n if (\n prevEntry === undefined ||\n prevEntry.key !== entry.key ||\n prevEntry.refId !== entry.refId ||\n !Object.is(prevEntry.args, entry.args) ||\n !pathSetEquals(prevEntry.paths, entry.paths)\n ) {\n unchanged = false;\n break;\n }\n }\n }\n if (unchanged) return;\n }\n\n container.registerConsumerPaths(consumerId, paths);\n // Register the *normal* leaf paths above for the source-side skeleton, then\n // build the channel interest as leaves + ancestor-watch ids so that an\n // atomic `patch` replacement of a parent (e.g. the array 'items') wakes a\n // consumer that tracked a child (e.g. 'items.length').\n expandedInterestRef.current = expandWithAncestors(\n paths,\n container.interner,\n );\n\n // -----------------------------------------------------------------------\n // Reconcile DEP containers (cross-bloc `.track()` interest).\n //\n // The primary bloc keeps its own dedicated subscription effect above; this\n // block manages only the *dep* containers recorded in the session this\n // render. We diff the new dep set vs the previously-subscribed set:\n // - new dep -> acquire was already done in `.track()`; subscribe its\n // channel + registerConsumerPaths + seed interest.\n // - surviving -> refresh its interest ref (subscribe closure reads it).\n // - dropped -> unsubscribe, unregisterConsumer, and release its ref.\n // -----------------------------------------------------------------------\n const subs = depSubsRef.current;\n\n // Pass 1: drop containers no longer in the session.\n for (const [depContainer, sub] of subs) {\n if (!session.has(depContainer)) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n subs.delete(depContainer);\n }\n }\n\n // Pass 2: add/refresh containers in the session (skip the primary).\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n const interest = expandWithAncestors(entry.paths, depContainer.interner);\n depContainer.registerConsumerPaths(consumerId, entry.paths);\n const existing = subs.get(depContainer);\n if (existing) {\n existing.interestRef.current = interest;\n continue;\n }\n // First commit that sees this dep: take the ownership ref HERE (not in\n // render/`.track()`), so an uncommitted render can never leak it (R4).\n getRegistry().acquire(entry.Type, entry.key, {\n canCreate: true,\n countRef: true,\n refId: entry.refId,\n args: entry.args,\n });\n const interestRef: { current: PathSet } = { current: interest };\n const unsubscribe = depContainer.channel.subscribe(\n () => interestRef.current,\n () => force(),\n );\n subs.set(depContainer, {\n unsubscribe,\n interestRef,\n Type: entry.Type,\n key: entry.key,\n refId: entry.refId,\n args: entry.args,\n });\n }\n\n // Capture this full reconcile's shape for the NEXT commit's short-circuit\n // check above. `paths`/`entry.paths` are fresh Sets seeded this render\n // (trackRender/unionPaths always allocate new Sets, never mutate one from\n // a prior render) — safe to keep direct references without cloning.\n const depsSignature = new Map<StateContainer, ReconcileDepSignature>();\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n depsSignature.set(depContainer, {\n paths: entry.paths,\n key: entry.key,\n refId: entry.refId,\n args: entry.args,\n });\n }\n lastReconcileRef.current = {\n primaryContainer: container,\n primaryPaths: paths,\n deps: depsSignature,\n };\n });\n\n // Unmount: tear down every dep subscription + ref exactly once. Kept in its\n // own effect (consumerId is stable for the component's lifetime, so this only\n // runs on final unmount, not on every reconcile). depSubsRef is mutated in\n // place by the reconcile, so the captured Map reference still holds the live\n // set at unmount.\n useEffect(() => {\n // Capture the ref's Map (mutated in place across renders) so the cleanup\n // reads the captured reference rather than depSubsRef.current directly.\n const subs = depSubsRef.current;\n return () => {\n for (const [depContainer, sub] of subs) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n }\n subs.clear();\n };\n }, [consumerId]);\n\n return [\n state,\n trackedBloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\n// ---------------------------------------------------------------------------\n// Cross-bloc session types + dep-handle wrapper.\n// ---------------------------------------------------------------------------\n\n/**\n * One entry in a consumer's per-render session map. Discriminated on `kind`:\n * the primary bloc is managed by its own dedicated effect, while dep entries\n * carry the registry coordinates the reconcile needs to release their ref.\n */\ntype SessionEntry =\n | {\n kind: 'primary';\n /** Tracked leaf paths recorded against the primary this render. */\n paths: PathSet;\n }\n | {\n kind: 'dep';\n /** Tracked leaf paths recorded against this dep this render. */\n paths: PathSet;\n /** Constructor for registry release. */\n Type: StateContainerConstructor;\n /** Resolved instance key for registry release. */\n key: string;\n /** refId held for this dep (released on drop/unmount). */\n refId: string;\n /** Construction args, used by the reconcile pass to acquire the ref. */\n args: unknown;\n };\n\n/** A live dep-channel subscription tracked between renders for reconciliation. */\ninterface DepSub {\n unsubscribe: () => void;\n interestRef: { current: PathSet };\n Type: StateContainerConstructor;\n key: string;\n refId: string;\n args: unknown;\n}\n\n/**\n * Snapshot of one dep's shape from the last FULL reconcile, compared against\n * the current session entry to decide whether the dep-reconcile layout effect\n * can short-circuit (see `lastReconcileRef` in `useBloc`).\n */\ninterface ReconcileDepSignature {\n paths: PathSet;\n key: string;\n refId: string;\n args: unknown;\n}\n\n/** Snapshot of the last FULL dep-reconcile layout effect run. */\ninterface ReconcileSignature {\n /** The primary container this signature was captured against (identity\n * check — a rebind/re-key swaps this even if the tracked paths happen to\n * be textually identical, and must never be mistaken for \"unchanged\"). */\n primaryContainer: StateContainer;\n primaryPaths: PathSet;\n deps: Map<StateContainer, ReconcileDepSignature>;\n}\n\n/** Per-access options shared by both dep accessors. */\ninterface DepAccessOptionsLike {\n args?: unknown;\n}\n\n/** Structural shape of a branded `depend()` handle as seen from React. */\ninterface DepHandleLike {\n track(options?: DepAccessOptionsLike): [unknown, StateContainer];\n untracked(options?: DepAccessOptionsLike): StateContainer;\n readonly [DEP_BRAND]: {\n Type: StateContainerConstructor;\n defaultArgs?: unknown;\n };\n}\n\n/**\n * Build the per-consumer wrapper that replaces a branded dep handle inside a\n * tracked getter's `this`. The wrapper exposes the same accessors as the core\n * handle and overrides `.track()`:\n *\n * - **Inside a render** (`trackedStateRef.current != null`): resolve (ENSURE,\n * no ref) the dep, `trackRender` its state, merge the recorded paths into the\n * session entry, build/reuse a tracked proxy for the dep so its OWN getters\n * track too, and return `[trackedValue, depProxy]`. The ownership ref is taken\n * by the layout-effect reconcile pass, not here.\n * - **Outside a render**: degrade to live `[dep.state, dep]` — matches the core\n * base impl, safe in event handlers/effects/methods.\n *\n * `.untracked()` always returns the live instance with no subscription.\n *\n * Args resolve at call time (`options.args ?? defaultArgs`), so a single handle\n * can resolve different dep instances across calls; tracked-proxy state is\n * therefore cached per resolved instance, not per handle. Guards against a\n * container re-entering tracking within the same render (mutual A↔B deps): if\n * the dep already has a non-primary session entry this render, reuse its proxy\n * + union its paths instead of re-acquiring.\n */\nfunction makeDepWrapper(\n handle: DepHandleLike,\n consumerId: string,\n trackedStateRef: { current: unknown },\n sessionRef: { current: Map<StateContainer, SessionEntry> },\n onDepHandle: (handle: object) => unknown,\n): DepHandleLike {\n const brand = handle[DEP_BRAND];\n const refId = depRefId(consumerId);\n const registry = getRegistry();\n // Per-resolved-instance tracked-state ref + proxy. Call-time args mean one\n // handle can resolve several instances, so cache is keyed by the instance.\n const perDep = new Map<\n StateContainer,\n { ref: { current: unknown }; proxy: StateContainer }\n >();\n // One ProxyCache shared across every instance this handle resolves to\n // (call-time args can resolve different dep instances across calls) — safe\n // because ProxyCache's internal map is keyed by target object identity, so\n // unrelated instances' objects never collide in it.\n const proxyCache = new ProxyCache();\n\n const resolve = (options?: DepAccessOptionsLike) => {\n const args = options?.args ?? brand.defaultArgs;\n const key = registry.resolveKey(brand.Type, undefined, args);\n const dep = registry.ensure(\n brand.Type,\n key,\n args,\n ) as unknown as StateContainer;\n return { dep, key, args };\n };\n\n const wrapper = {\n untracked: (options?: DepAccessOptionsLike) => resolve(options).dep,\n track: (options?: DepAccessOptionsLike) => {\n const { dep, key, args } = resolve(options);\n\n // Outside a render: live values, no subscription (core base behavior).\n if (trackedStateRef.current == null) {\n return [dep.state, dep];\n }\n\n const session = sessionRef.current;\n const existing = session.get(dep);\n\n // Render only ENSUREs the dep instance (via `resolve()` above); it does\n // NOT take a ref. Ownership is claimed by the layout-effect reconcile\n // pass-2 the first commit it sees this dep, and released on drop/unmount.\n // This keeps acquire/release paired so an uncommitted render can't leak a\n // dep ref (R4).\n\n const tracked = trackRender(dep.state, dep.interner, proxyCache);\n let cache = perDep.get(dep);\n if (cache === undefined) {\n const ref = { current: tracked.value as unknown };\n cache = { ref, proxy: buildTrackedProxy(dep, ref, onDepHandle).proxy };\n perDep.set(dep, cache);\n } else {\n cache.ref.current = tracked.value;\n }\n\n if (existing !== undefined) {\n // Re-entry this render (`.track()` twice, or a mutual cycle): union the\n // new paths into the existing entry rather than re-acquiring.\n existing.paths = unionPaths(existing.paths, tracked.paths);\n } else {\n session.set(dep, {\n kind: 'dep',\n paths: tracked.paths,\n Type: brand.Type,\n key,\n refId,\n args,\n });\n }\n\n return [tracked.value, cache.proxy];\n },\n } as DepHandleLike;\n\n Object.defineProperty(wrapper, DEP_BRAND, {\n value: brand,\n enumerable: false,\n writable: false,\n configurable: false,\n });\n\n return wrapper;\n}\n\n/** Union two PathSets (ALL_PATHS dominates). */\nfunction unionPaths(a: PathSet, b: PathSet): PathSet {\n if (a === ALL_PATHS || b === ALL_PATHS) return ALL_PATHS;\n const out = new Set<number>(a as Set<number>);\n for (const id of b as Set<number>) out.add(id);\n return out;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n\n/**\n * Expand a PathSet to include an *ancestor-watch* id for every ancestor of\n * every tracked leaf.\n *\n * The auto-tracker records leaf paths (e.g. `'items.length'`), but\n * `StructuralContainer.patch` can only mark the parent (`'items'`) when it\n * replaces a value atomically (arrays, `null`, primitives — it can't see\n * inside). Without expansion, a subscriber with interest `{'items.length'}`\n * would miss a `patch`-triggered atomic-replacement of `items`.\n *\n * Ancestors are added under the interner's *ancestor-watch* lane\n * (`internAncestor`), NOT as normal ids. The source emits a matching\n * ancestor-watch mark only for paths it replaces atomically — never for a\n * plain-object structural pulse-up. So `{'items.length'}` wakes when the array\n * `items` is replaced, but `{'user.email'}` does NOT wake when a sibling\n * `user.name` changes and pulses `user` up: pulse-up `user` is a normal id and\n * the ancestor-watch `user` only intersects another ancestor-watch `user`.\n *\n * Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but\n * NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,\n * and `''` would wake this consumer on every field change).\n */\nfunction expandWithAncestors(paths: PathSet, interner: PathInterner): PathSet {\n if (paths === ALL_PATHS) return ALL_PATHS;\n const leafPaths = paths as Set<number>;\n if (leafPaths.size === 0) return paths;\n\n const expanded = new Set<number>(leafPaths);\n for (const id of leafPaths) {\n const str = interner.lookup(id);\n // Add all non-root ancestor segments as *ancestor-watch* ids: 'a.b.c' →\n // watch 'a.b' and 'a'. These live in the interner's ancestor lane so they\n // only intersect the source's atomic-replacement marks (`internAncestor`),\n // never a structural pulse-up mark of the same path. That is what lets a\n // descendant-reader (e.g. `items.length`) wake on an array/null replacement\n // without a sibling-leaf reader (`user.email`) waking when a sibling\n // (`user.name`) changes and pulses up through `user`.\n let idx = str.lastIndexOf('.');\n while (idx > 0) {\n const ancestor = str.slice(0, idx);\n expanded.add(interner.internAncestor(ancestor));\n idx = ancestor.lastIndexOf('.');\n }\n }\n return expanded;\n}\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["DEP_BRAND","ProxyCache","emptyPathSet","ALL_PATHS","DEP_BRAND"],"sources":["../src/BlocProvider.tsx","../src/buildTrackedProxy.ts","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\n\n/**\n * Internal context value: a Map from bloc constructor to the args object\n * provided by the nearest BlocProvider for that bloc class.\n *\n * Stored as a WeakMap-keyed record so nested providers for different blocs\n * compose without clobbering each other.\n */\ntype ProvidedArgsMap = Map<StateContainerConstructor, unknown>;\n\nconst ProvidedArgsContext = createContext<ProvidedArgsMap>(new Map());\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps<T extends StateContainerConstructor> {\n /**\n * The bloc class whose args are being provided to descendants.\n */\n bloc: T;\n /**\n * Args that descendant `useBloc(bloc)` calls will resolve to when no\n * own `args` are given.\n */\n args: ExtractArgs<T>;\n children: ReactNode;\n}\n\n/**\n * Provides args to descendant `useBloc` calls for a specific bloc class via\n * React context.\n *\n * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the\n * args supplied here. Own `args` on the `useBloc` call always win.\n *\n * Multiple `BlocProvider` wrappers for different bloc classes compose: each\n * provider merges its entry into the inherited map, so nested providers for\n * different blocs do not interfere.\n *\n * @example\n * ```tsx\n * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>\n * <UserProfile />\n * </BlocProvider>\n * ```\n *\n * @example Per-mount private instance\n * ```tsx\n * const id = useId();\n * <BlocProvider bloc={CartBloc} args={{ _id: id }}>\n * <CartWidget />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider<T extends StateContainerConstructor>({\n bloc,\n args,\n children,\n}: BlocProviderProps<T>): ReactElement {\n const parentMap = useContext(ProvidedArgsContext);\n\n // Merge our entry into a new Map so sibling/parent providers for other blocs\n // are preserved. Memoised on (parentMap, bloc, args) identity.\n const mergedMap = useMemo(() => {\n const next = new Map(parentMap);\n next.set(bloc, args);\n return next;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parentMap, bloc, args]);\n\n return (\n <ProvidedArgsContext.Provider value={mergedMap}>\n {children}\n </ProvidedArgsContext.Provider>\n );\n}\n\n/**\n * Returns the args provided by the nearest {@link BlocProvider} for the given\n * bloc class, or `undefined` when called outside a matching provider.\n *\n * Used by `useBloc` to inherit provider args when no own `args` are given.\n */\nexport function useProvidedArgs<T extends StateContainerConstructor>(\n BlocClass: T,\n): ExtractArgs<T> | undefined {\n const map = useContext(ProvidedArgsContext);\n return map.get(BlocClass) as ExtractArgs<T> | undefined;\n}\n","import { DEP_BRAND } from '@blac/core';\n\n/**\n * Build a per-consumer proxy pair for a bloc instance.\n *\n * Returns two proxies:\n * - `proxy` — the stable outer proxy returned to the consumer. Getter\n * properties are invoked with `thisProxy` as `this` so that `this.state`\n * reads inside getters are redirected to the current render's tracking\n * proxy. Non-getter properties fall through to the live instance.\n * - `thisProxy` — the inner `this`-proxy used as the receiver for getter\n * calls. Intercepts `state` to return `trackedStateRef.current` when a\n * tracking context is active; otherwise falls through to the live value.\n * When `onDepHandle` is supplied, a read off `this` whose value carries the\n * `DEP_BRAND` symbol (i.e. a `depend()` handle) is routed through\n * `onDepHandle`, which returns a per-consumer wrapper whose `.track()` opts\n * the consumer into cross-bloc reactivity. Core stays decoupled — detection\n * is purely by the `DEP_BRAND` symbol.\n *\n * Both allocations happen exactly once per bloc acquisition (inside `useMemo`)\n * so the proxies are stable across renders.\n *\n * @param onDepHandle - Optional callback invoked when a getter reads a branded\n * dep handle off `this`. Receives the original handle and returns the value\n * to expose in its place (the session-bound wrapper). The callback is\n * responsible for caching wrappers per handle to avoid re-allocation.\n */\nexport function buildTrackedProxy<T extends object>(\n instance: T,\n trackedStateRef: { current: unknown },\n onDepHandle?: (handle: object) => unknown,\n): { proxy: T; thisProxy: T } {\n // Build a map of getter descriptors from the prototype chain (excluding\n // Object.prototype). This is computed once per bloc acquisition so that\n // the proxy's get trap is O(1) per property access. Both string- and\n // symbol-keyed getters are collected. Arrow-function class properties\n // (own, bound in the constructor) are not getters and pass through\n // unmodified.\n const getterDescs = new Map<string | symbol, PropertyDescriptor>();\n let proto = Object.getPrototypeOf(instance);\n while (proto && proto !== Object.prototype) {\n const keys: (string | symbol)[] = [\n ...Object.getOwnPropertyNames(proto),\n ...Object.getOwnPropertySymbols(proto),\n ];\n for (const key of keys) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc?.get && !getterDescs.has(key)) getterDescs.set(key, desc);\n }\n proto = Object.getPrototypeOf(proto);\n }\n\n // `this`-proxy for getter invocations, allocated ONCE per acquisition (the\n // trap closes over the stable `trackedStateRef`, so it never needs to be\n // rebuilt per access). Redirects `this.state` to the current render's\n // tracking proxy so getter reads during JSX record paths; outside render\n // `trackedStateRef.current` is null and it falls through to live state.\n // The receiver `r` (this proxy) is threaded through Reflect.get so chained\n // getter calls (getters reading other getters) stay in tracked context.\n const thisProxy = new Proxy(instance as object, {\n get(t, k, r) {\n if (k === 'state') return trackedStateRef.current ?? Reflect.get(t, k, r);\n const value = Reflect.get(t, k, r);\n // A branded dep handle read off `this` (e.g. `this.price`) is routed\n // through onDepHandle so the consumer's session can wrap `.track()`.\n if (\n onDepHandle !== undefined &&\n (typeof value === 'function' || typeof value === 'object') &&\n value !== null &&\n (value as Record<symbol, unknown>)[DEP_BRAND] !== undefined\n ) {\n return onDepHandle(value as object);\n }\n return value;\n },\n });\n\n // Stable proxy: one allocation per bloc acquisition. Non-getter access is\n // a single Map lookup + Reflect.get — no prototype walk on the hot path.\n const proxy = new Proxy(instance as object, {\n get(target, key, receiver) {\n const desc = getterDescs.get(key);\n if (desc?.get) return desc.get.call(thisProxy);\n return Reflect.get(target, key, receiver);\n },\n }) as T;\n\n return { proxy, thisProxy: thisProxy as T };\n}\n","import {\n useEffect,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n DEP_BRAND,\n getRegistry,\n isIsolatedClass,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n pathSetEquals,\n trackRender,\n PathInterner,\n ProxyCache,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useProvidedArgs } from './BlocProvider';\nimport { buildTrackedProxy } from './buildTrackedProxy';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n// Sentinel that can never `Object.is`-equal a real args value (including\n// `undefined`). Used to lazily seed args-key refs so the structural key is\n// computed only when the guard actually runs, never on every render.\nconst ARGS_UNSET: unique symbol = Symbol('blac.argsKeyUnset');\n\n/**\n * `useRef` with a lazily-built initial value. `useRef(new Map())` discards its\n * argument after the first render but still EVALUATES it on every render, so\n * each such ref costs one dead allocation per render. Passing a module-level\n * factory instead makes the per-render cost a single identity read.\n *\n * The factory MUST be a stable module-level function (an inline arrow would\n * just move the per-render allocation from the value to the closure). The\n * returned `current` is non-nullable, so read sites keep their original types.\n */\nfunction useLazyRef<T>(create: () => T): { current: T } {\n const ref = useRef<T | null>(null);\n if (ref.current === null) {\n ref.current = create();\n }\n return ref as { current: T };\n}\n\n// Stable factories for the lazy refs below — one per shape, module-level so the\n// reference passed to `useLazyRef` never changes. Each hook instance still gets\n// its OWN object: nothing here is shared between components (a shared session\n// map or ProxyCache would break per-consumer render isolation).\nconst newComponentRef = (): ComponentRef => ({});\nconst newArgsKeyEntry = (): { ref: unknown; key: string | undefined } => ({\n ref: ARGS_UNSET,\n key: undefined,\n});\nconst newSessionMap = (): Map<StateContainer, SessionEntry> => new Map();\nconst newDepSubsMap = (): Map<StateContainer, DepSub> => new Map();\nconst newDepWrapperCache = (): WeakMap<object, unknown> => new WeakMap();\nconst newProxyCache = (): ProxyCache => new ProxyCache();\n\n// Registry refId formats for a consumer's primary bloc and its tracked deps.\n// Centralised so the `acquire` and `release` sites can never drift apart — a\n// mismatch would leak the ref and keep the bloc alive past unmount.\nconst primaryRefId = (consumerId: string): string => `useBloc@${consumerId}`;\nconst depRefId = (consumerId: string): string => `useBloc@${consumerId}:dep`;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`).\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is derived from `options.args` (own args),\n * then the surrounding {@link BlocProvider} context args for this bloc,\n * then the default key (no args).\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * Per-mount private instance:\n * ```ts\n * const id = useId();\n * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });\n * ```\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Args-based shared instance\n * ```ts\n * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useLazyRef(newComponentRef);\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n\n // `isolated` is sugar over args identity: it injects a per-mount id into the\n // args used for keying. `consumerId` is already stable for this mount, so this\n // costs no extra hook slot (a `useId()` here would cost one in EVERY consumer).\n const isolated = options?.isolated === true || isIsolatedClass(BlocClass);\n // Memo dep: flips when isolation is toggled at runtime (`{ isolated: cond }`)\n // so the instance re-keys. Constant for the common case.\n const isolationKey = isolated ? consumerId : undefined;\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const isSelectMode = options?.select !== undefined;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n const isolatedRef = useRef(isolated);\n isolatedRef.current = isolated;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Priority: own args > provider args (for this bloc class) > none.\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const ownArgs = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const ownArgsRef = useRef(ownArgs);\n ownArgsRef.current = ownArgs;\n // Fast-path: only recompute the structural key when the args REFERENCE\n // changes. Callers commonly pass a stable args object (e.g. memoised or\n // module-level), so this skips a JSON.stringify call every render.\n const ownArgsKeyRef = useLazyRef(newArgsKeyEntry);\n if (!Object.is(ownArgsKeyRef.current.ref, ownArgs)) {\n ownArgsKeyRef.current = {\n ref: ownArgs,\n key: ownArgs === undefined ? undefined : JSON.stringify(ownArgs),\n };\n }\n const ownArgsKey = ownArgsKeyRef.current.key;\n\n // Read provided args from the nearest BlocProvider for this bloc class.\n const providerArgs = useProvidedArgs(BlocClass);\n const providerArgsRef = useRef(providerArgs);\n providerArgsRef.current = providerArgs;\n // Same reference fast-path as ownArgsKey above.\n const providerArgsKeyRef = useLazyRef(newArgsKeyEntry);\n if (!Object.is(providerArgsKeyRef.current.ref, providerArgs)) {\n providerArgsKeyRef.current = {\n ref: providerArgs,\n key:\n providerArgs === undefined ? undefined : JSON.stringify(providerArgs),\n };\n }\n const providerArgsKey = providerArgsKeyRef.current.key;\n\n // Current render's tracking proxy. Declared before the memo so the stable\n // ref object can be passed to buildTrackedProxy at acquisition time. The\n // proxy trap only reads `.current` at invocation time (not during creation),\n // so it is safe to pass on first mount even though the value is null.\n // Populated during each auto-track render snapshot (below); cleared to null\n // by useLayoutEffect after commit.\n const trackedStateRef = useRef<unknown>(null);\n\n // ---------------------------------------------------------------------------\n // Per-consumer cross-bloc session.\n //\n // Each render rebuilds a map of every container this consumer is currently\n // interested in. The PRIMARY bloc is the first uniform entry; every dep\n // reached through `this.<handle>.track()` inside a tracked getter adds an\n // entry. The layout-effect reconcile (below) diffs this map vs the previous\n // render to subscribe new deps and release dropped ones. The session lives in\n // this hook's refs only — there is no global ambient state, so sibling\n // renders never cross-contaminate.\n // ---------------------------------------------------------------------------\n const sessionRef = useLazyRef(newSessionMap);\n // Channel subscriptions for DEP containers (the primary keeps its own\n // dedicated effect). container -> { unsubscribe, interestRef, refId held }.\n const depSubsRef = useLazyRef(newDepSubsMap);\n // Per-handle wrapper cache, allocated once per bloc acquisition (in the memo)\n // so wrappers are stable across renders. handle -> session-bound wrapper.\n const depWrapperCacheRef = useLazyRef(newDepWrapperCache);\n const proxyCacheRef = useLazyRef(newProxyCache);\n // Snapshot of the last FULL reconcile's shape, used by the layout-effect\n // below to short-circuit when nothing actually changed. `null` means \"no\n // prior full run to compare against\" (first commit, or the previous commit\n // was in select-mode) — always forces a full reconcile in that case.\n const lastReconcileRef = useRef<ReconcileSignature | null>(null);\n\n // Rebind nonce: bumped by the ownership layout-effect when the instance the\n // render captured was disposed + recreated out from under us. This happens on\n // a same-commit ownership handoff of a shared (non-keepAlive) key — the sole\n // prior owner's effect cleanup releases refs→0 and SYNCHRONOUSLY disposes the\n // instance before this consumer's layout setup re-acquires (creating a fresh\n // one) — and equivalently under StrictMode's setup→cleanup→setup double-invoke\n // for a lone owner. Threaded into the memo deps so bumping it re-ensures `bloc`\n // against the LIVE registry entry instead of the disposed instance.\n const [rebindNonce, bumpRebind] = useReducer((x: number) => x + 1, 0);\n // The live instance actually owned (ref held) by the ownership layout-effect,\n // read by its cleanup so onUnmount always fires with the owned instance.\n const ownedBlocRef = useRef<TBloc | null>(null);\n\n const { bloc, instanceKey, trackedBloc } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n trackedBloc: TBloc;\n }>(() => {\n // Own args win over provider args; provider args win over no args.\n const baseArgs =\n ownArgsRef.current !== undefined\n ? ownArgsRef.current\n : providerArgsRef.current;\n // Isolation MERGES with resolved args rather than losing to them (plan Q2=B):\n // a class-level `static isolated` must not be silently cancelled by a call\n // site that happens to pass args.\n // The cast is the point of the documented behavior note: the injected\n // `_blacIsolated` key is NOT part of the bloc's declared `Args` type, but it\n // does reach `init(args)` — exactly as the `{ args: { _id: useId() } }`\n // idiom this replaces always did.\n const effectiveArgs = isolatedRef.current\n ? (withIsolation(baseArgs, consumerId) as ExtractArgs<T>)\n : baseArgs;\n\n const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);\n const registry = getRegistry();\n // Render only ENSUREs the instance exists (no ref). Ownership is claimed in\n // the layout effect below, so an abandoned/uncommitted render can never\n // leak a ref and a memo re-run can never double-count one (R3/R4).\n const instance = registry.acquire(BlocClass, resolvedKey, {\n canCreate: true,\n countRef: false,\n args: effectiveArgs,\n }) as TBloc;\n\n // Build a session-bound wrapper for a dep handle the first time a getter\n // reads it off `this`; cache per handle so the wrapper identity is stable.\n // `onDepHandle` is threaded into each dep's tracked proxy too, so a nested\n // `this.<otherHandle>.track()` inside a dep's getter records into the SAME\n // consumer session — that is what makes deep chains (A→B→C) reactive.\n const onDepHandle = (handle: object): unknown => {\n const cache = depWrapperCacheRef.current;\n const cached = cache.get(handle);\n if (cached !== undefined) return cached;\n const wrapper = makeDepWrapper(\n handle as DepHandleLike,\n consumerId,\n trackedStateRef,\n sessionRef,\n onDepHandle,\n );\n cache.set(handle, wrapper);\n return wrapper;\n };\n\n const { proxy } = buildTrackedProxy(\n instance as object,\n trackedStateRef,\n onDepHandle,\n );\n\n return {\n bloc: instance,\n instanceKey: resolvedKey,\n trackedBloc: proxy as TBloc,\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, ownArgsKey, providerArgsKey, isolationKey, rebindNonce]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We talk directly to `bloc.channel` — the StructuralContainer's path-scoped\n // DirtyChannel. Subscribing with a dynamic interest function lets us narrow\n // wakeups per consumer, so a component only re-renders when a path it\n // actually read changes (rather than on every state change).\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useLazyRef(emptyPathSet);\n // Expanded interest: leaf paths PLUS *ancestor-watch* ids for their parents,\n // so that when `patch` atomically replaces a parent (e.g. the array 'items')\n // and the consumer tracked a child (e.g. 'items.length'), the intersection\n // still fires — while a structural pulse-up of a plain-object parent (e.g.\n // 'user' when a sibling 'user.name' changed) does NOT, because pulse-up marks\n // are normal ids and ancestor-watch ids only intersect their own lane.\n // Updated in useLayoutEffect after each render once pathRef.current is\n // populated.\n const expandedInterestRef = useLazyRef(emptyPathSet);\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n // Render-time raw-state snapshot, seeded each render (below) and read by the\n // subscription effect to close the mount gap (R2): an emit landing between the\n // render read and the passive subscribe would otherwise be lost.\n const renderStateRef = useRef<unknown>(undefined);\n // Bloc identity from the previous render, so select-mode can reset its cached\n // selection when the underlying instance changes (re-key).\n const prevBlocRef = useRef<unknown>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n //\n // Self-healing note: this effect can run once against a pre-rebind (possibly\n // disposed) `bloc` before the ownership layout-effect's nonce bump triggers a\n // re-render that swaps `bloc` to the live instance (the memo dep array\n // includes `rebindNonce`). That's benign — `channel.subscribe` and\n // `unregisterConsumer` are plain Map operations that don't check disposal\n // state and never throw on a disposed container — so this effect's cleanup\n // runs cleanly and the re-render re-subscribes against the live instance.\n const channel = (bloc as unknown as StateContainer).channel;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n // Close the mount gap (R2): an emit between the render's selector seed and\n // this subscribe would be lost. Recompute against LIVE state and force if\n // the selection advanced.\n const select = selectRef.current;\n if (select) {\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev === null || !shallowArrayEqual(prev, next)) {\n lastSelectionRef.current = next;\n force();\n }\n }\n return unsub;\n }\n\n // Auto-track mode: subscribe with the expanded interest (leaf paths + their\n // ancestors). This ensures that when `patch` marks a parent path (e.g.\n // 'items') and the consumer tracked a child (e.g. 'items.length'), the\n // channel intersection still fires a re-render.\n //\n // `expandedInterestRef.current` is updated by the useLayoutEffect below\n // after each render, so the interest is always fresh at flush time.\n const unsub = channel.subscribe(\n () => expandedInterestRef.current,\n () => force(),\n );\n // Register the consumer's leaf paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n // Close the mount gap (R2): if the container's state advanced between the\n // render snapshot (renderStateRef) and this subscribe, the emit was lost —\n // force one re-render so we don't stay stale.\n if ((bloc as unknown as StateContainer).state !== renderStateRef.current) {\n force();\n }\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, consumerId, isSelectMode]);\n\n // ---------------------------------------------------------------------------\n // Ownership + mount / unmount lifecycle.\n //\n // The ownership ref is claimed HERE (a layout effect), not in the render/memo,\n // so acquire and release are perfectly paired: a memo re-run can no longer\n // double-count (R3) and an uncommitted render can no longer leak (R4). Keyed\n // on [BlocClass, instanceKey, consumerId] — NOT `bloc` — so a genuine re-key\n // (args change, OR a BlocClass swap) releases the old ref and acquires the new\n // one, while a pure rebind (same class+key, instance replaced under us) does\n // NOT re-run this effect and therefore never releases the single ref we just\n // took (which, for a sole owner, would synchronously dispose the\n // freshly-created instance and churn indefinitely).\n //\n // `BlocClass` MUST stay in the dep array even though `instanceKey` alone often\n // determines identity: `resolveInstanceKey`/`resolveKey` collapse to the same\n // `DEFAULT_STRUCTURAL_KEY` sentinel across DIFFERENT classes when neither has\n // args nor a `static key` (e.g. `useBloc(cond ? AdminBloc : UserBloc)`). Without\n // `BlocClass` here, swapping classes at that shared key would never re-run this\n // effect: the old class's ref would leak until unmount, and the new class's\n // instance (only `ensure`d by the render memo, countRef:false) would be held\n // with zero ownership refs — exposed to disposal by unrelated traffic on that\n // class+key, with no rebind path to recover it.\n //\n // `acquire` returns the authoritative LIVE instance for the key. If it differs\n // from the instance the render captured (`bloc`), the render read a disposed /\n // replaced instance (same-commit shared-key handoff, or StrictMode remount);\n // we bump the rebind nonce so the memo re-ensures `bloc` against this live one.\n // onMount/onUnmount fire with the owned live instance and stay co-located with\n // acquire/release so onUnmount(bloc) runs BEFORE release(...) within one\n // cleanup, keeping the instance alive while the callback runs.\n // ---------------------------------------------------------------------------\n useLayoutEffect(() => {\n const registry = getRegistry();\n const live = registry.acquire(BlocClass, instanceKey, {\n canCreate: true,\n countRef: true,\n refId: primaryRefId(consumerId),\n }) as TBloc;\n ownedBlocRef.current = live;\n onMountRef.current?.(live as InstanceType<T>);\n // Rebind if the render captured a stale (disposed/replaced) instance so the\n // component renders + subscribes against the live registry entry, not a\n // disposed one. Only bumps on an actual mismatch, so it can fire at most once\n // per handoff and never loops (the re-ensured `bloc` equals `live`, and this\n // effect is not keyed on `bloc` so it won't re-run and re-release).\n if (live !== bloc) {\n bumpRebind();\n }\n return () => {\n onUnmountRef.current?.((ownedBlocRef.current ?? bloc) as InstanceType<T>);\n registry.release(BlocClass, instanceKey, false, primaryRefId(consumerId));\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, instanceKey, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n // Seed the render-time snapshot so the subscription effect can detect an emit\n // that landed in the render→subscribe window (R2 mount gap).\n renderStateRef.current = rawState;\n // Reset the cached selection when the bloc identity changes (re-key) so the\n // select seed below re-seeds against the NEW instance instead of comparing\n // against a stale selection from the previous instance.\n if (prevBlocRef.current !== bloc) {\n prevBlocRef.current = bloc;\n lastSelectionRef.current = null;\n }\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n proxyCacheRef.current,\n );\n state = tracked.value as ExtractState<T>;\n trackedStateRef.current = tracked.value;\n pathRef.current = tracked.paths;\n // Rebuild the per-consumer session for this render. The primary bloc is the\n // first uniform entry; its `paths` are the SAME PathSet object the proxy\n // mutates during JSX (so it stays live as getters record leaves). Dep\n // entries are appended during JSX as `this.<handle>.track()` runs. Cleared\n // here (not in the layout effect) so a render that no longer tracks a dep\n // produces a session without it, and the reconcile drops it.\n const session = sessionRef.current;\n session.clear();\n session.set(bloc as unknown as StateContainer, {\n kind: 'primary',\n paths: tracked.paths,\n });\n // Freeze this render's tracking proxy after the synchronous render+commit\n // pass. The microtask fires only once the current task unwinds, so all\n // render-time JSX reads still record; reads afterwards — effects, event\n // handlers, async callbacks, devtools inspecting `state` — hit the\n // disarmed proxy and record nothing, so this render's path set can't be\n // polluted by work that outlives the render that owns it.\n queueMicrotask(tracked.disarm);\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits, pathRef.current is the consumer's actual leaf\n // interest (populated by the proxy during JSX evaluation). Re-register with\n // the container so the skeleton reflects the latest paths, and expand the\n // interest to include ancestor paths for the channel subscription.\n //\n // useLayoutEffect runs before the browser paints (and before any emit\n // triggered by another effect), so the skeleton is fresh by the time the\n // next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n // Clear the render-time tracking proxy now that JSX has been evaluated and\n // committed. Getters invoked after this point (event handlers, effects,\n // method→getter chains) fall through to live state instead of reading this\n // render's frozen snapshot. The render body re-seeds it next render.\n trackedStateRef.current = null;\n if (selectRef.current !== undefined) {\n // Switching into (or staying in) select-mode: invalidate any prior full\n // reconcile so a later switch back to auto-track mode never mistakes a\n // stale signature for \"unchanged\" and skips a needed reconcile.\n lastReconcileRef.current = null;\n return;\n }\n const container = bloc as unknown as StateContainer;\n const paths = pathRef.current;\n const session = sessionRef.current;\n\n // ---------------------------------------------------------------------\n // Short-circuit: if the primary path set AND the full dep session are\n // set-equal (paths + key/refId/args) to the last FULL reconcile, none of\n // registerConsumerPaths/subscribe/unsubscribe/expandWithAncestors below\n // can have anything new to do — skip the whole block. Any mismatch, or\n // `lastReconcileRef.current === null` (first commit, or the immediately\n // preceding commit was select-mode / uncertain), falls through to the\n // full reconcile. Never skip on uncertainty — a missed re-subscribe would\n // leave a stale/dropped subscription.\n // ---------------------------------------------------------------------\n const last = lastReconcileRef.current;\n if (\n last !== null &&\n last.primaryContainer === container &&\n pathSetEquals(last.primaryPaths, paths)\n ) {\n let unchanged = last.deps.size === session.size - 1;\n if (unchanged) {\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n const prevEntry = last.deps.get(depContainer);\n if (\n prevEntry === undefined ||\n prevEntry.key !== entry.key ||\n prevEntry.refId !== entry.refId ||\n !Object.is(prevEntry.args, entry.args) ||\n !pathSetEquals(prevEntry.paths, entry.paths)\n ) {\n unchanged = false;\n break;\n }\n }\n }\n if (unchanged) return;\n }\n\n container.registerConsumerPaths(consumerId, paths);\n // Register the *normal* leaf paths above for the source-side skeleton, then\n // build the channel interest as leaves + ancestor-watch ids so that an\n // atomic `patch` replacement of a parent (e.g. the array 'items') wakes a\n // consumer that tracked a child (e.g. 'items.length').\n expandedInterestRef.current = expandWithAncestors(\n paths,\n container.interner,\n );\n\n // -----------------------------------------------------------------------\n // Reconcile DEP containers (cross-bloc `.track()` interest).\n //\n // The primary bloc keeps its own dedicated subscription effect above; this\n // block manages only the *dep* containers recorded in the session this\n // render. We diff the new dep set vs the previously-subscribed set:\n // - new dep -> acquire was already done in `.track()`; subscribe its\n // channel + registerConsumerPaths + seed interest.\n // - surviving -> refresh its interest ref (subscribe closure reads it).\n // - dropped -> unsubscribe, unregisterConsumer, and release its ref.\n // -----------------------------------------------------------------------\n const subs = depSubsRef.current;\n\n // Pass 1: drop containers no longer in the session.\n for (const [depContainer, sub] of subs) {\n if (!session.has(depContainer)) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n subs.delete(depContainer);\n }\n }\n\n // Pass 2: add/refresh containers in the session (skip the primary).\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n const interest = expandWithAncestors(entry.paths, depContainer.interner);\n depContainer.registerConsumerPaths(consumerId, entry.paths);\n const existing = subs.get(depContainer);\n if (existing) {\n existing.interestRef.current = interest;\n continue;\n }\n // First commit that sees this dep: take the ownership ref HERE (not in\n // render/`.track()`), so an uncommitted render can never leak it (R4).\n getRegistry().acquire(entry.Type, entry.key, {\n canCreate: true,\n countRef: true,\n refId: entry.refId,\n args: entry.args,\n });\n const interestRef: { current: PathSet } = { current: interest };\n const unsubscribe = depContainer.channel.subscribe(\n () => interestRef.current,\n () => force(),\n );\n subs.set(depContainer, {\n unsubscribe,\n interestRef,\n Type: entry.Type,\n key: entry.key,\n refId: entry.refId,\n args: entry.args,\n });\n }\n\n // Capture this full reconcile's shape for the NEXT commit's short-circuit\n // check above. `paths`/`entry.paths` are fresh Sets seeded this render\n // (trackRender/unionPaths always allocate new Sets, never mutate one from\n // a prior render) — safe to keep direct references without cloning.\n const depsSignature = new Map<StateContainer, ReconcileDepSignature>();\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n depsSignature.set(depContainer, {\n paths: entry.paths,\n key: entry.key,\n refId: entry.refId,\n args: entry.args,\n });\n }\n lastReconcileRef.current = {\n primaryContainer: container,\n primaryPaths: paths,\n deps: depsSignature,\n };\n });\n\n // Unmount: tear down every dep subscription + ref exactly once. Kept in its\n // own effect (consumerId is stable for the component's lifetime, so this only\n // runs on final unmount, not on every reconcile). depSubsRef is mutated in\n // place by the reconcile, so the captured Map reference still holds the live\n // set at unmount.\n useEffect(() => {\n // Capture the ref's Map (mutated in place across renders) so the cleanup\n // reads the captured reference rather than depSubsRef.current directly.\n const subs = depSubsRef.current;\n return () => {\n for (const [depContainer, sub] of subs) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n }\n subs.clear();\n };\n // `depSubsRef` is a stable ref object (its identity never changes), so\n // listing it keeps this effect an unmount-only effect while satisfying\n // exhaustive-deps — the lint rule can't tell `useLazyRef` returns a ref.\n }, [consumerId, depSubsRef]);\n\n return [\n state,\n trackedBloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\n// ---------------------------------------------------------------------------\n// Cross-bloc session types + dep-handle wrapper.\n// ---------------------------------------------------------------------------\n\n/**\n * One entry in a consumer's per-render session map. Discriminated on `kind`:\n * the primary bloc is managed by its own dedicated effect, while dep entries\n * carry the registry coordinates the reconcile needs to release their ref.\n */\ntype SessionEntry =\n | {\n kind: 'primary';\n /** Tracked leaf paths recorded against the primary this render. */\n paths: PathSet;\n }\n | {\n kind: 'dep';\n /** Tracked leaf paths recorded against this dep this render. */\n paths: PathSet;\n /** Constructor for registry release. */\n Type: StateContainerConstructor;\n /** Resolved instance key for registry release. */\n key: string;\n /** refId held for this dep (released on drop/unmount). */\n refId: string;\n /** Construction args, used by the reconcile pass to acquire the ref. */\n args: unknown;\n };\n\n/** A live dep-channel subscription tracked between renders for reconciliation. */\ninterface DepSub {\n unsubscribe: () => void;\n interestRef: { current: PathSet };\n Type: StateContainerConstructor;\n key: string;\n refId: string;\n args: unknown;\n}\n\n/**\n * Snapshot of one dep's shape from the last FULL reconcile, compared against\n * the current session entry to decide whether the dep-reconcile layout effect\n * can short-circuit (see `lastReconcileRef` in `useBloc`).\n */\ninterface ReconcileDepSignature {\n paths: PathSet;\n key: string;\n refId: string;\n args: unknown;\n}\n\n/** Snapshot of the last FULL dep-reconcile layout effect run. */\ninterface ReconcileSignature {\n /** The primary container this signature was captured against (identity\n * check — a rebind/re-key swaps this even if the tracked paths happen to\n * be textually identical, and must never be mistaken for \"unchanged\"). */\n primaryContainer: StateContainer;\n primaryPaths: PathSet;\n deps: Map<StateContainer, ReconcileDepSignature>;\n}\n\n/** Per-access options shared by both dep accessors. */\ninterface DepAccessOptionsLike {\n args?: unknown;\n}\n\n/** Structural shape of a branded `depend()` handle as seen from React. */\ninterface DepHandleLike {\n track(options?: DepAccessOptionsLike): [unknown, StateContainer];\n untracked(options?: DepAccessOptionsLike): StateContainer;\n readonly [DEP_BRAND]: {\n Type: StateContainerConstructor;\n defaultArgs?: unknown;\n };\n}\n\n/**\n * Build the per-consumer wrapper that replaces a branded dep handle inside a\n * tracked getter's `this`. The wrapper exposes the same accessors as the core\n * handle and overrides `.track()`:\n *\n * - **Inside a render** (`trackedStateRef.current != null`): resolve (ENSURE,\n * no ref) the dep, `trackRender` its state, merge the recorded paths into the\n * session entry, build/reuse a tracked proxy for the dep so its OWN getters\n * track too, and return `[trackedValue, depProxy]`. The ownership ref is taken\n * by the layout-effect reconcile pass, not here.\n * - **Outside a render**: degrade to live `[dep.state, dep]` — matches the core\n * base impl, safe in event handlers/effects/methods.\n *\n * `.untracked()` always returns the live instance with no subscription.\n *\n * Args resolve at call time (`options.args ?? defaultArgs`), so a single handle\n * can resolve different dep instances across calls; tracked-proxy state is\n * therefore cached per resolved instance, not per handle. Guards against a\n * container re-entering tracking within the same render (mutual A↔B deps): if\n * the dep already has a non-primary session entry this render, reuse its proxy\n * + union its paths instead of re-acquiring.\n */\nfunction makeDepWrapper(\n handle: DepHandleLike,\n consumerId: string,\n trackedStateRef: { current: unknown },\n sessionRef: { current: Map<StateContainer, SessionEntry> },\n onDepHandle: (handle: object) => unknown,\n): DepHandleLike {\n const brand = handle[DEP_BRAND];\n const refId = depRefId(consumerId);\n const registry = getRegistry();\n // Per-resolved-instance tracked-state ref + proxy. Call-time args mean one\n // handle can resolve several instances, so cache is keyed by the instance.\n const perDep = new WeakMap<\n StateContainer,\n { ref: { current: unknown }; proxy: StateContainer }\n >();\n // One ProxyCache shared across every instance this handle resolves to\n // (call-time args can resolve different dep instances across calls) — safe\n // because ProxyCache's internal map is keyed by target object identity, so\n // unrelated instances' objects never collide in it.\n const proxyCache = new ProxyCache();\n // One-entry identity cache: skip re-hashing the structural key\n // (`resolveKey` -> `structuralKey` -> JSON.stringify) when `args` is the\n // same reference as last call — callers almost always pass a\n // reference-stable `args` (`options?.args ?? brand.defaultArgs`). Only the\n // derived key is cached; `ensure` still runs every call so a\n // disposed-and-recreated entry is always picked up.\n let lastArgs: unknown = ARGS_UNSET;\n let lastKey = '';\n\n const resolve = (options?: DepAccessOptionsLike) => {\n const args = options?.args ?? brand.defaultArgs;\n // `ARGS_UNSET` is module-private, so no caller's `args` can equal it — the\n // first call always misses and seeds `lastKey` before it is ever read.\n if (!Object.is(lastArgs, args)) {\n lastArgs = args;\n lastKey = registry.resolveKey(brand.Type, undefined, args);\n }\n const key = lastKey;\n const dep = registry.ensure(\n brand.Type,\n key,\n args,\n ) as unknown as StateContainer;\n return { dep, key, args };\n };\n\n const wrapper = {\n untracked: (options?: DepAccessOptionsLike) => resolve(options).dep,\n track: (options?: DepAccessOptionsLike) => {\n const { dep, key, args } = resolve(options);\n\n // Outside a render: live values, no subscription (core base behavior).\n if (trackedStateRef.current == null) {\n return [dep.state, dep];\n }\n\n const session = sessionRef.current;\n const existing = session.get(dep);\n\n // Render only ENSUREs the dep instance (via `resolve()` above); it does\n // NOT take a ref. Ownership is claimed by the layout-effect reconcile\n // pass-2 the first commit it sees this dep, and released on drop/unmount.\n // This keeps acquire/release paired so an uncommitted render can't leak a\n // dep ref (R4).\n\n const tracked = trackRender(dep.state, dep.interner, proxyCache);\n let cache = perDep.get(dep);\n if (cache === undefined) {\n const ref = { current: tracked.value as unknown };\n cache = { ref, proxy: buildTrackedProxy(dep, ref, onDepHandle).proxy };\n perDep.set(dep, cache);\n } else {\n cache.ref.current = tracked.value;\n }\n\n if (existing !== undefined) {\n // Re-entry this render (`.track()` twice, or a mutual cycle): union the\n // new paths into the existing entry rather than re-acquiring.\n existing.paths = unionPaths(existing.paths, tracked.paths);\n } else {\n session.set(dep, {\n kind: 'dep',\n paths: tracked.paths,\n Type: brand.Type,\n key,\n refId,\n args,\n });\n }\n\n return [tracked.value, cache.proxy];\n },\n } as DepHandleLike;\n\n Object.defineProperty(wrapper, DEP_BRAND, {\n value: brand,\n enumerable: false,\n writable: false,\n configurable: false,\n });\n\n return wrapper;\n}\n\n/** Union two PathSets (ALL_PATHS dominates). */\nfunction unionPaths(a: PathSet, b: PathSet): PathSet {\n if (a === ALL_PATHS || b === ALL_PATHS) return ALL_PATHS;\n const out = new Set<number>(a as Set<number>);\n for (const id of b as Set<number>) out.add(id);\n return out;\n}\n\n/**\n * Fold a per-mount isolation id into `args`. MERGES rather than replaces\n * (plan Q2=B): a class-level `static isolated` must not be silently cancelled\n * by a call site that happens to pass args. Non-object args are nested rather\n * than spread, so a primitive arg can't be destructured into index keys.\n */\nconst withIsolation = (base: unknown, id: string): object =>\n base === undefined\n ? { _blacIsolated: id }\n : typeof base === 'object' && base !== null\n ? { ...base, _blacIsolated: id }\n : { _blacArgs: base, _blacIsolated: id };\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n\n/**\n * Expand a PathSet to include an *ancestor-watch* id for every ancestor of\n * every tracked leaf.\n *\n * The auto-tracker records leaf paths (e.g. `'items.length'`), but\n * `StructuralContainer.patch` can only mark the parent (`'items'`) when it\n * replaces a value atomically (arrays, `null`, primitives — it can't see\n * inside). Without expansion, a subscriber with interest `{'items.length'}`\n * would miss a `patch`-triggered atomic-replacement of `items`.\n *\n * Ancestors are added under the interner's *ancestor-watch* lane\n * (`internAncestor`), NOT as normal ids. The source emits a matching\n * ancestor-watch mark only for paths it replaces atomically — never for a\n * plain-object structural pulse-up. So `{'items.length'}` wakes when the array\n * `items` is replaced, but `{'user.email'}` does NOT wake when a sibling\n * `user.name` changes and pulses `user` up: pulse-up `user` is a normal id and\n * the ancestor-watch `user` only intersects another ancestor-watch `user`.\n *\n * Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but\n * NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,\n * and `''` would wake this consumer on every field change).\n */\nfunction expandWithAncestors(paths: PathSet, interner: PathInterner): PathSet {\n if (paths === ALL_PATHS) return ALL_PATHS;\n const leafPaths = paths as Set<number>;\n if (leafPaths.size === 0) return paths;\n\n const expanded = new Set<number>(leafPaths);\n for (const id of leafPaths) {\n const str = interner.lookup(id);\n // Add all non-root ancestor segments as *ancestor-watch* ids: 'a.b.c' →\n // watch 'a.b' and 'a'. These live in the interner's ancestor lane so they\n // only intersect the source's atomic-replacement marks (`internAncestor`),\n // never a structural pulse-up mark of the same path. That is what lets a\n // descendant-reader (e.g. `items.length`) wake on an array/null replacement\n // without a sibling-leaf reader (`user.email`) waking when a sibling\n // (`user.name`) changes and pulses up through `user`.\n let idx = str.lastIndexOf('.');\n while (idx > 0) {\n const ancestor = str.slice(0, idx);\n expanded.add(interner.internAncestor(ancestor));\n idx = ancestor.lastIndexOf('.');\n }\n }\n return expanded;\n}\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createContext, useContext, useEffect,
|
|
2
|
-
import { DEP_BRAND, getRegistry, resolveInstanceKey } from "@blac/core";
|
|
1
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef } from "react";
|
|
2
|
+
import { DEP_BRAND, getRegistry, isIsolatedClass, resolveInstanceKey } from "@blac/core";
|
|
3
3
|
import { ALL_PATHS, ProxyCache, emptyPathSet, pathSetEquals, trackRender, untracked } from "@dirtytalk/structural";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/BlocProvider.tsx
|
|
@@ -112,6 +112,30 @@ function buildTrackedProxy(instance, trackedStateRef, onDepHandle) {
|
|
|
112
112
|
//#region src/useBloc.ts
|
|
113
113
|
let nextConsumerId = 0;
|
|
114
114
|
const ARGS_UNSET = Symbol("blac.argsKeyUnset");
|
|
115
|
+
/**
|
|
116
|
+
* `useRef` with a lazily-built initial value. `useRef(new Map())` discards its
|
|
117
|
+
* argument after the first render but still EVALUATES it on every render, so
|
|
118
|
+
* each such ref costs one dead allocation per render. Passing a module-level
|
|
119
|
+
* factory instead makes the per-render cost a single identity read.
|
|
120
|
+
*
|
|
121
|
+
* The factory MUST be a stable module-level function (an inline arrow would
|
|
122
|
+
* just move the per-render allocation from the value to the closure). The
|
|
123
|
+
* returned `current` is non-nullable, so read sites keep their original types.
|
|
124
|
+
*/
|
|
125
|
+
function useLazyRef(create) {
|
|
126
|
+
const ref = useRef(null);
|
|
127
|
+
if (ref.current === null) ref.current = create();
|
|
128
|
+
return ref;
|
|
129
|
+
}
|
|
130
|
+
const newComponentRef = () => ({});
|
|
131
|
+
const newArgsKeyEntry = () => ({
|
|
132
|
+
ref: ARGS_UNSET,
|
|
133
|
+
key: void 0
|
|
134
|
+
});
|
|
135
|
+
const newSessionMap = () => /* @__PURE__ */ new Map();
|
|
136
|
+
const newDepSubsMap = () => /* @__PURE__ */ new Map();
|
|
137
|
+
const newDepWrapperCache = () => /* @__PURE__ */ new WeakMap();
|
|
138
|
+
const newProxyCache = () => new ProxyCache();
|
|
115
139
|
const primaryRefId = (consumerId) => `useBloc@${consumerId}`;
|
|
116
140
|
const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
117
141
|
/**
|
|
@@ -165,24 +189,25 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
|
165
189
|
* ```
|
|
166
190
|
*/
|
|
167
191
|
function useBloc(BlocClass, options) {
|
|
168
|
-
const componentRef =
|
|
192
|
+
const componentRef = useLazyRef(newComponentRef);
|
|
169
193
|
const consumerIdRef = useRef(null);
|
|
170
194
|
if (consumerIdRef.current === null) consumerIdRef.current = `useBloc-${nextConsumerId++}`;
|
|
171
195
|
const consumerId = consumerIdRef.current;
|
|
172
|
-
|
|
196
|
+
const isolated = options?.isolated === true || isIsolatedClass(BlocClass);
|
|
197
|
+
const isolationKey = isolated ? consumerId : void 0;
|
|
173
198
|
const selectRef = useRef(options?.select);
|
|
174
199
|
selectRef.current = options?.select;
|
|
200
|
+
const isSelectMode = options?.select !== void 0;
|
|
175
201
|
const onMountRef = useRef(options?.onMount);
|
|
176
202
|
onMountRef.current = options?.onMount;
|
|
177
203
|
const onUnmountRef = useRef(options?.onUnmount);
|
|
178
204
|
onUnmountRef.current = options?.onUnmount;
|
|
205
|
+
const isolatedRef = useRef(isolated);
|
|
206
|
+
isolatedRef.current = isolated;
|
|
179
207
|
const ownArgs = options?.args;
|
|
180
208
|
const ownArgsRef = useRef(ownArgs);
|
|
181
209
|
ownArgsRef.current = ownArgs;
|
|
182
|
-
const ownArgsKeyRef =
|
|
183
|
-
ref: ARGS_UNSET,
|
|
184
|
-
key: void 0
|
|
185
|
-
});
|
|
210
|
+
const ownArgsKeyRef = useLazyRef(newArgsKeyEntry);
|
|
186
211
|
if (!Object.is(ownArgsKeyRef.current.ref, ownArgs)) ownArgsKeyRef.current = {
|
|
187
212
|
ref: ownArgs,
|
|
188
213
|
key: ownArgs === void 0 ? void 0 : JSON.stringify(ownArgs)
|
|
@@ -191,25 +216,23 @@ function useBloc(BlocClass, options) {
|
|
|
191
216
|
const providerArgs = useProvidedArgs(BlocClass);
|
|
192
217
|
const providerArgsRef = useRef(providerArgs);
|
|
193
218
|
providerArgsRef.current = providerArgs;
|
|
194
|
-
const providerArgsKeyRef =
|
|
195
|
-
ref: ARGS_UNSET,
|
|
196
|
-
key: void 0
|
|
197
|
-
});
|
|
219
|
+
const providerArgsKeyRef = useLazyRef(newArgsKeyEntry);
|
|
198
220
|
if (!Object.is(providerArgsKeyRef.current.ref, providerArgs)) providerArgsKeyRef.current = {
|
|
199
221
|
ref: providerArgs,
|
|
200
222
|
key: providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs)
|
|
201
223
|
};
|
|
202
224
|
const providerArgsKey = providerArgsKeyRef.current.key;
|
|
203
225
|
const trackedStateRef = useRef(null);
|
|
204
|
-
const sessionRef =
|
|
205
|
-
const depSubsRef =
|
|
206
|
-
const depWrapperCacheRef =
|
|
207
|
-
const proxyCacheRef =
|
|
226
|
+
const sessionRef = useLazyRef(newSessionMap);
|
|
227
|
+
const depSubsRef = useLazyRef(newDepSubsMap);
|
|
228
|
+
const depWrapperCacheRef = useLazyRef(newDepWrapperCache);
|
|
229
|
+
const proxyCacheRef = useLazyRef(newProxyCache);
|
|
208
230
|
const lastReconcileRef = useRef(null);
|
|
209
231
|
const [rebindNonce, bumpRebind] = useReducer((x) => x + 1, 0);
|
|
210
232
|
const ownedBlocRef = useRef(null);
|
|
211
233
|
const { bloc, instanceKey, trackedBloc } = useMemo(() => {
|
|
212
|
-
const
|
|
234
|
+
const baseArgs = ownArgsRef.current !== void 0 ? ownArgsRef.current : providerArgsRef.current;
|
|
235
|
+
const effectiveArgs = isolatedRef.current ? withIsolation(baseArgs, consumerId) : baseArgs;
|
|
213
236
|
const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);
|
|
214
237
|
const instance = getRegistry().acquire(BlocClass, resolvedKey, {
|
|
215
238
|
canCreate: true,
|
|
@@ -234,17 +257,18 @@ function useBloc(BlocClass, options) {
|
|
|
234
257
|
BlocClass,
|
|
235
258
|
ownArgsKey,
|
|
236
259
|
providerArgsKey,
|
|
260
|
+
isolationKey,
|
|
237
261
|
rebindNonce
|
|
238
262
|
]);
|
|
239
263
|
const [, force] = useReducer((x) => x + 1, 0);
|
|
240
|
-
const pathRef =
|
|
241
|
-
const expandedInterestRef =
|
|
264
|
+
const pathRef = useLazyRef(emptyPathSet);
|
|
265
|
+
const expandedInterestRef = useLazyRef(emptyPathSet);
|
|
242
266
|
const lastSelectionRef = useRef(null);
|
|
243
267
|
const renderStateRef = useRef(void 0);
|
|
244
268
|
const prevBlocRef = useRef(null);
|
|
245
269
|
useEffect(() => {
|
|
246
270
|
const channel = bloc.channel;
|
|
247
|
-
if (
|
|
271
|
+
if (isSelectMode) {
|
|
248
272
|
const unsub = channel.subscribe(() => ALL_PATHS, () => {
|
|
249
273
|
const select = selectRef.current;
|
|
250
274
|
if (!select) {
|
|
@@ -275,7 +299,11 @@ function useBloc(BlocClass, options) {
|
|
|
275
299
|
unsub();
|
|
276
300
|
bloc.unregisterConsumer(consumerId);
|
|
277
301
|
};
|
|
278
|
-
}, [
|
|
302
|
+
}, [
|
|
303
|
+
bloc,
|
|
304
|
+
consumerId,
|
|
305
|
+
isSelectMode
|
|
306
|
+
]);
|
|
279
307
|
useLayoutEffect(() => {
|
|
280
308
|
const registry = getRegistry();
|
|
281
309
|
const live = registry.acquire(BlocClass, instanceKey, {
|
|
@@ -401,7 +429,7 @@ function useBloc(BlocClass, options) {
|
|
|
401
429
|
}
|
|
402
430
|
subs.clear();
|
|
403
431
|
};
|
|
404
|
-
}, [consumerId]);
|
|
432
|
+
}, [consumerId, depSubsRef]);
|
|
405
433
|
return [
|
|
406
434
|
state,
|
|
407
435
|
trackedBloc,
|
|
@@ -434,11 +462,17 @@ function makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, onDepHa
|
|
|
434
462
|
const brand = handle[DEP_BRAND];
|
|
435
463
|
const refId = depRefId(consumerId);
|
|
436
464
|
const registry = getRegistry();
|
|
437
|
-
const perDep = /* @__PURE__ */ new
|
|
465
|
+
const perDep = /* @__PURE__ */ new WeakMap();
|
|
438
466
|
const proxyCache = new ProxyCache();
|
|
467
|
+
let lastArgs = ARGS_UNSET;
|
|
468
|
+
let lastKey = "";
|
|
439
469
|
const resolve = (options) => {
|
|
440
470
|
const args = options?.args ?? brand.defaultArgs;
|
|
441
|
-
|
|
471
|
+
if (!Object.is(lastArgs, args)) {
|
|
472
|
+
lastArgs = args;
|
|
473
|
+
lastKey = registry.resolveKey(brand.Type, void 0, args);
|
|
474
|
+
}
|
|
475
|
+
const key = lastKey;
|
|
442
476
|
return {
|
|
443
477
|
dep: registry.ensure(brand.Type, key, args),
|
|
444
478
|
key,
|
|
@@ -489,6 +523,19 @@ function unionPaths(a, b) {
|
|
|
489
523
|
for (const id of b) out.add(id);
|
|
490
524
|
return out;
|
|
491
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Fold a per-mount isolation id into `args`. MERGES rather than replaces
|
|
528
|
+
* (plan Q2=B): a class-level `static isolated` must not be silently cancelled
|
|
529
|
+
* by a call site that happens to pass args. Non-object args are nested rather
|
|
530
|
+
* than spread, so a primitive arg can't be destructured into index keys.
|
|
531
|
+
*/
|
|
532
|
+
const withIsolation = (base, id) => base === void 0 ? { _blacIsolated: id } : typeof base === "object" && base !== null ? {
|
|
533
|
+
...base,
|
|
534
|
+
_blacIsolated: id
|
|
535
|
+
} : {
|
|
536
|
+
_blacArgs: base,
|
|
537
|
+
_blacIsolated: id
|
|
538
|
+
};
|
|
492
539
|
const shallowArrayEqual = (a, b) => {
|
|
493
540
|
if (a === b) return true;
|
|
494
541
|
if (a.length !== b.length) return false;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/BlocProvider.tsx","../src/buildTrackedProxy.ts","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\n\n/**\n * Internal context value: a Map from bloc constructor to the args object\n * provided by the nearest BlocProvider for that bloc class.\n *\n * Stored as a WeakMap-keyed record so nested providers for different blocs\n * compose without clobbering each other.\n */\ntype ProvidedArgsMap = Map<StateContainerConstructor, unknown>;\n\nconst ProvidedArgsContext = createContext<ProvidedArgsMap>(new Map());\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps<T extends StateContainerConstructor> {\n /**\n * The bloc class whose args are being provided to descendants.\n */\n bloc: T;\n /**\n * Args that descendant `useBloc(bloc)` calls will resolve to when no\n * own `args` are given.\n */\n args: ExtractArgs<T>;\n children: ReactNode;\n}\n\n/**\n * Provides args to descendant `useBloc` calls for a specific bloc class via\n * React context.\n *\n * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the\n * args supplied here. Own `args` on the `useBloc` call always win.\n *\n * Multiple `BlocProvider` wrappers for different bloc classes compose: each\n * provider merges its entry into the inherited map, so nested providers for\n * different blocs do not interfere.\n *\n * @example\n * ```tsx\n * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>\n * <UserProfile />\n * </BlocProvider>\n * ```\n *\n * @example Per-mount private instance\n * ```tsx\n * const id = useId();\n * <BlocProvider bloc={CartBloc} args={{ _id: id }}>\n * <CartWidget />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider<T extends StateContainerConstructor>({\n bloc,\n args,\n children,\n}: BlocProviderProps<T>): ReactElement {\n const parentMap = useContext(ProvidedArgsContext);\n\n // Merge our entry into a new Map so sibling/parent providers for other blocs\n // are preserved. Memoised on (parentMap, bloc, args) identity.\n const mergedMap = useMemo(() => {\n const next = new Map(parentMap);\n next.set(bloc, args);\n return next;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parentMap, bloc, args]);\n\n return (\n <ProvidedArgsContext.Provider value={mergedMap}>\n {children}\n </ProvidedArgsContext.Provider>\n );\n}\n\n/**\n * Returns the args provided by the nearest {@link BlocProvider} for the given\n * bloc class, or `undefined` when called outside a matching provider.\n *\n * Used by `useBloc` to inherit provider args when no own `args` are given.\n */\nexport function useProvidedArgs<T extends StateContainerConstructor>(\n BlocClass: T,\n): ExtractArgs<T> | undefined {\n const map = useContext(ProvidedArgsContext);\n return map.get(BlocClass) as ExtractArgs<T> | undefined;\n}\n","import { DEP_BRAND } from '@blac/core';\n\n/**\n * Build a per-consumer proxy pair for a bloc instance.\n *\n * Returns two proxies:\n * - `proxy` — the stable outer proxy returned to the consumer. Getter\n * properties are invoked with `thisProxy` as `this` so that `this.state`\n * reads inside getters are redirected to the current render's tracking\n * proxy. Non-getter properties fall through to the live instance.\n * - `thisProxy` — the inner `this`-proxy used as the receiver for getter\n * calls. Intercepts `state` to return `trackedStateRef.current` when a\n * tracking context is active; otherwise falls through to the live value.\n * When `onDepHandle` is supplied, a read off `this` whose value carries the\n * `DEP_BRAND` symbol (i.e. a `depend()` handle) is routed through\n * `onDepHandle`, which returns a per-consumer wrapper whose `.track()` opts\n * the consumer into cross-bloc reactivity. Core stays decoupled — detection\n * is purely by the `DEP_BRAND` symbol.\n *\n * Both allocations happen exactly once per bloc acquisition (inside `useMemo`)\n * so the proxies are stable across renders.\n *\n * @param onDepHandle - Optional callback invoked when a getter reads a branded\n * dep handle off `this`. Receives the original handle and returns the value\n * to expose in its place (the session-bound wrapper). The callback is\n * responsible for caching wrappers per handle to avoid re-allocation.\n */\nexport function buildTrackedProxy<T extends object>(\n instance: T,\n trackedStateRef: { current: unknown },\n onDepHandle?: (handle: object) => unknown,\n): { proxy: T; thisProxy: T } {\n // Build a map of getter descriptors from the prototype chain (excluding\n // Object.prototype). This is computed once per bloc acquisition so that\n // the proxy's get trap is O(1) per property access. Both string- and\n // symbol-keyed getters are collected. Arrow-function class properties\n // (own, bound in the constructor) are not getters and pass through\n // unmodified.\n const getterDescs = new Map<string | symbol, PropertyDescriptor>();\n let proto = Object.getPrototypeOf(instance);\n while (proto && proto !== Object.prototype) {\n const keys: (string | symbol)[] = [\n ...Object.getOwnPropertyNames(proto),\n ...Object.getOwnPropertySymbols(proto),\n ];\n for (const key of keys) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc?.get && !getterDescs.has(key)) getterDescs.set(key, desc);\n }\n proto = Object.getPrototypeOf(proto);\n }\n\n // `this`-proxy for getter invocations, allocated ONCE per acquisition (the\n // trap closes over the stable `trackedStateRef`, so it never needs to be\n // rebuilt per access). Redirects `this.state` to the current render's\n // tracking proxy so getter reads during JSX record paths; outside render\n // `trackedStateRef.current` is null and it falls through to live state.\n // The receiver `r` (this proxy) is threaded through Reflect.get so chained\n // getter calls (getters reading other getters) stay in tracked context.\n const thisProxy = new Proxy(instance as object, {\n get(t, k, r) {\n if (k === 'state') return trackedStateRef.current ?? Reflect.get(t, k, r);\n const value = Reflect.get(t, k, r);\n // A branded dep handle read off `this` (e.g. `this.price`) is routed\n // through onDepHandle so the consumer's session can wrap `.track()`.\n if (\n onDepHandle !== undefined &&\n (typeof value === 'function' || typeof value === 'object') &&\n value !== null &&\n (value as Record<symbol, unknown>)[DEP_BRAND] !== undefined\n ) {\n return onDepHandle(value as object);\n }\n return value;\n },\n });\n\n // Stable proxy: one allocation per bloc acquisition. Non-getter access is\n // a single Map lookup + Reflect.get — no prototype walk on the hot path.\n const proxy = new Proxy(instance as object, {\n get(target, key, receiver) {\n const desc = getterDescs.get(key);\n if (desc?.get) return desc.get.call(thisProxy);\n return Reflect.get(target, key, receiver);\n },\n }) as T;\n\n return { proxy, thisProxy: thisProxy as T };\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n DEP_BRAND,\n getRegistry,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n pathSetEquals,\n trackRender,\n PathInterner,\n ProxyCache,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useProvidedArgs } from './BlocProvider';\nimport { buildTrackedProxy } from './buildTrackedProxy';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n// Sentinel that can never `Object.is`-equal a real args value (including\n// `undefined`). Used to lazily seed args-key refs so the structural key is\n// computed only when the guard actually runs, never on every render.\nconst ARGS_UNSET: unique symbol = Symbol('blac.argsKeyUnset');\n\n// Registry refId formats for a consumer's primary bloc and its tracked deps.\n// Centralised so the `acquire` and `release` sites can never drift apart — a\n// mismatch would leak the ref and keep the bloc alive past unmount.\nconst primaryRefId = (consumerId: string): string => `useBloc@${consumerId}`;\nconst depRefId = (consumerId: string): string => `useBloc@${consumerId}:dep`;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`).\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is derived from `options.args` (own args),\n * then the surrounding {@link BlocProvider} context args for this bloc,\n * then the default key (no args).\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * Per-mount private instance:\n * ```ts\n * const id = useId();\n * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });\n * ```\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Args-based shared instance\n * ```ts\n * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Priority: own args > provider args (for this bloc class) > none.\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const ownArgs = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const ownArgsRef = useRef(ownArgs);\n ownArgsRef.current = ownArgs;\n // Fast-path: only recompute the structural key when the args REFERENCE\n // changes. Callers commonly pass a stable args object (e.g. memoised or\n // module-level), so this skips a JSON.stringify call every render.\n const ownArgsKeyRef = useRef<{ ref: unknown; key: string | undefined }>({\n ref: ARGS_UNSET,\n key: undefined,\n });\n if (!Object.is(ownArgsKeyRef.current.ref, ownArgs)) {\n ownArgsKeyRef.current = {\n ref: ownArgs,\n key: ownArgs === undefined ? undefined : JSON.stringify(ownArgs),\n };\n }\n const ownArgsKey = ownArgsKeyRef.current.key;\n\n // Read provided args from the nearest BlocProvider for this bloc class.\n const providerArgs = useProvidedArgs(BlocClass);\n const providerArgsRef = useRef(providerArgs);\n providerArgsRef.current = providerArgs;\n // Same reference fast-path as ownArgsKey above.\n const providerArgsKeyRef = useRef<{ ref: unknown; key: string | undefined }>(\n {\n ref: ARGS_UNSET,\n key: undefined,\n },\n );\n if (!Object.is(providerArgsKeyRef.current.ref, providerArgs)) {\n providerArgsKeyRef.current = {\n ref: providerArgs,\n key: providerArgs === undefined ? undefined : JSON.stringify(providerArgs),\n };\n }\n const providerArgsKey = providerArgsKeyRef.current.key;\n\n // Current render's tracking proxy. Declared before the memo so the stable\n // ref object can be passed to buildTrackedProxy at acquisition time. The\n // proxy trap only reads `.current` at invocation time (not during creation),\n // so it is safe to pass on first mount even though the value is null.\n // Populated during each auto-track render snapshot (below); cleared to null\n // by useLayoutEffect after commit.\n const trackedStateRef = useRef<unknown>(null);\n\n // ---------------------------------------------------------------------------\n // Per-consumer cross-bloc session.\n //\n // Each render rebuilds a map of every container this consumer is currently\n // interested in. The PRIMARY bloc is the first uniform entry; every dep\n // reached through `this.<handle>.track()` inside a tracked getter adds an\n // entry. The layout-effect reconcile (below) diffs this map vs the previous\n // render to subscribe new deps and release dropped ones. The session lives in\n // this hook's refs only — there is no global ambient state, so sibling\n // renders never cross-contaminate.\n // ---------------------------------------------------------------------------\n const sessionRef = useRef<Map<StateContainer, SessionEntry>>(new Map());\n // Channel subscriptions for DEP containers (the primary keeps its own\n // dedicated effect). container -> { unsubscribe, interestRef, refId held }.\n const depSubsRef = useRef<Map<StateContainer, DepSub>>(new Map());\n // Per-handle wrapper cache, allocated once per bloc acquisition (in the memo)\n // so wrappers are stable across renders. handle -> session-bound wrapper.\n const depWrapperCacheRef = useRef<Map<object, unknown>>(new Map());\n const proxyCacheRef = useRef(new ProxyCache());\n // Snapshot of the last FULL reconcile's shape, used by the layout-effect\n // below to short-circuit when nothing actually changed. `null` means \"no\n // prior full run to compare against\" (first commit, or the previous commit\n // was in select-mode) — always forces a full reconcile in that case.\n const lastReconcileRef = useRef<ReconcileSignature | null>(null);\n\n // Rebind nonce: bumped by the ownership layout-effect when the instance the\n // render captured was disposed + recreated out from under us. This happens on\n // a same-commit ownership handoff of a shared (non-keepAlive) key — the sole\n // prior owner's effect cleanup releases refs→0 and SYNCHRONOUSLY disposes the\n // instance before this consumer's layout setup re-acquires (creating a fresh\n // one) — and equivalently under StrictMode's setup→cleanup→setup double-invoke\n // for a lone owner. Threaded into the memo deps so bumping it re-ensures `bloc`\n // against the LIVE registry entry instead of the disposed instance.\n const [rebindNonce, bumpRebind] = useReducer((x: number) => x + 1, 0);\n // The live instance actually owned (ref held) by the ownership layout-effect,\n // read by its cleanup so onUnmount always fires with the owned instance.\n const ownedBlocRef = useRef<TBloc | null>(null);\n\n const { bloc, instanceKey, trackedBloc } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n trackedBloc: TBloc;\n }>(() => {\n // Own args win over provider args; provider args win over no args.\n const effectiveArgs =\n ownArgsRef.current !== undefined\n ? ownArgsRef.current\n : providerArgsRef.current;\n\n const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);\n const registry = getRegistry();\n // Render only ENSUREs the instance exists (no ref). Ownership is claimed in\n // the layout effect below, so an abandoned/uncommitted render can never\n // leak a ref and a memo re-run can never double-count one (R3/R4).\n const instance = registry.acquire(BlocClass, resolvedKey, {\n canCreate: true,\n countRef: false,\n args: effectiveArgs,\n }) as TBloc;\n\n // Build a session-bound wrapper for a dep handle the first time a getter\n // reads it off `this`; cache per handle so the wrapper identity is stable.\n // `onDepHandle` is threaded into each dep's tracked proxy too, so a nested\n // `this.<otherHandle>.track()` inside a dep's getter records into the SAME\n // consumer session — that is what makes deep chains (A→B→C) reactive.\n const onDepHandle = (handle: object): unknown => {\n const cache = depWrapperCacheRef.current;\n const cached = cache.get(handle);\n if (cached !== undefined) return cached;\n const wrapper = makeDepWrapper(\n handle as DepHandleLike,\n consumerId,\n trackedStateRef,\n sessionRef,\n onDepHandle,\n );\n cache.set(handle, wrapper);\n return wrapper;\n };\n\n const { proxy } = buildTrackedProxy(\n instance as object,\n trackedStateRef,\n onDepHandle,\n );\n\n return {\n bloc: instance,\n instanceKey: resolvedKey,\n trackedBloc: proxy as TBloc,\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, ownArgsKey, providerArgsKey, rebindNonce]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We talk directly to `bloc.channel` — the StructuralContainer's path-scoped\n // DirtyChannel. Subscribing with a dynamic interest function lets us narrow\n // wakeups per consumer, so a component only re-renders when a path it\n // actually read changes (rather than on every state change).\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // Expanded interest: leaf paths PLUS *ancestor-watch* ids for their parents,\n // so that when `patch` atomically replaces a parent (e.g. the array 'items')\n // and the consumer tracked a child (e.g. 'items.length'), the intersection\n // still fires — while a structural pulse-up of a plain-object parent (e.g.\n // 'user' when a sibling 'user.name' changed) does NOT, because pulse-up marks\n // are normal ids and ancestor-watch ids only intersect their own lane.\n // Updated in useLayoutEffect after each render once pathRef.current is\n // populated.\n const expandedInterestRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n // Render-time raw-state snapshot, seeded each render (below) and read by the\n // subscription effect to close the mount gap (R2): an emit landing between the\n // render read and the passive subscribe would otherwise be lost.\n const renderStateRef = useRef<unknown>(undefined);\n // Bloc identity from the previous render, so select-mode can reset its cached\n // selection when the underlying instance changes (re-key).\n const prevBlocRef = useRef<unknown>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n //\n // Self-healing note: this effect can run once against a pre-rebind (possibly\n // disposed) `bloc` before the ownership layout-effect's nonce bump triggers a\n // re-render that swaps `bloc` to the live instance (the memo dep array\n // includes `rebindNonce`). That's benign — `channel.subscribe` and\n // `unregisterConsumer` are plain Map operations that don't check disposal\n // state and never throw on a disposed container — so this effect's cleanup\n // runs cleanly and the re-render re-subscribes against the live instance.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n // Close the mount gap (R2): an emit between the render's selector seed and\n // this subscribe would be lost. Recompute against LIVE state and force if\n // the selection advanced.\n const select = selectRef.current;\n if (select) {\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev === null || !shallowArrayEqual(prev, next)) {\n lastSelectionRef.current = next;\n force();\n }\n }\n return unsub;\n }\n\n // Auto-track mode: subscribe with the expanded interest (leaf paths + their\n // ancestors). This ensures that when `patch` marks a parent path (e.g.\n // 'items') and the consumer tracked a child (e.g. 'items.length'), the\n // channel intersection still fires a re-render.\n //\n // `expandedInterestRef.current` is updated by the useLayoutEffect below\n // after each render, so the interest is always fresh at flush time.\n const unsub = channel.subscribe(\n () => expandedInterestRef.current,\n () => force(),\n );\n // Register the consumer's leaf paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n // Close the mount gap (R2): if the container's state advanced between the\n // render snapshot (renderStateRef) and this subscribe, the emit was lost —\n // force one re-render so we don't stay stale.\n if ((bloc as unknown as StateContainer).state !== renderStateRef.current) {\n force();\n }\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Ownership + mount / unmount lifecycle.\n //\n // The ownership ref is claimed HERE (a layout effect), not in the render/memo,\n // so acquire and release are perfectly paired: a memo re-run can no longer\n // double-count (R3) and an uncommitted render can no longer leak (R4). Keyed\n // on [BlocClass, instanceKey, consumerId] — NOT `bloc` — so a genuine re-key\n // (args change, OR a BlocClass swap) releases the old ref and acquires the new\n // one, while a pure rebind (same class+key, instance replaced under us) does\n // NOT re-run this effect and therefore never releases the single ref we just\n // took (which, for a sole owner, would synchronously dispose the\n // freshly-created instance and churn indefinitely).\n //\n // `BlocClass` MUST stay in the dep array even though `instanceKey` alone often\n // determines identity: `resolveInstanceKey`/`resolveKey` collapse to the same\n // `DEFAULT_STRUCTURAL_KEY` sentinel across DIFFERENT classes when neither has\n // args nor a `static key` (e.g. `useBloc(cond ? AdminBloc : UserBloc)`). Without\n // `BlocClass` here, swapping classes at that shared key would never re-run this\n // effect: the old class's ref would leak until unmount, and the new class's\n // instance (only `ensure`d by the render memo, countRef:false) would be held\n // with zero ownership refs — exposed to disposal by unrelated traffic on that\n // class+key, with no rebind path to recover it.\n //\n // `acquire` returns the authoritative LIVE instance for the key. If it differs\n // from the instance the render captured (`bloc`), the render read a disposed /\n // replaced instance (same-commit shared-key handoff, or StrictMode remount);\n // we bump the rebind nonce so the memo re-ensures `bloc` against this live one.\n // onMount/onUnmount fire with the owned live instance and stay co-located with\n // acquire/release so onUnmount(bloc) runs BEFORE release(...) within one\n // cleanup, keeping the instance alive while the callback runs.\n // ---------------------------------------------------------------------------\n useLayoutEffect(() => {\n const registry = getRegistry();\n const live = registry.acquire(BlocClass, instanceKey, {\n canCreate: true,\n countRef: true,\n refId: primaryRefId(consumerId),\n }) as TBloc;\n ownedBlocRef.current = live;\n onMountRef.current?.(live as InstanceType<T>);\n // Rebind if the render captured a stale (disposed/replaced) instance so the\n // component renders + subscribes against the live registry entry, not a\n // disposed one. Only bumps on an actual mismatch, so it can fire at most once\n // per handoff and never loops (the re-ensured `bloc` equals `live`, and this\n // effect is not keyed on `bloc` so it won't re-run and re-release).\n if (live !== bloc) {\n bumpRebind();\n }\n return () => {\n onUnmountRef.current?.((ownedBlocRef.current ?? bloc) as InstanceType<T>);\n registry.release(\n BlocClass,\n instanceKey,\n false,\n primaryRefId(consumerId),\n );\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, instanceKey, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n // Seed the render-time snapshot so the subscription effect can detect an emit\n // that landed in the render→subscribe window (R2 mount gap).\n renderStateRef.current = rawState;\n // Reset the cached selection when the bloc identity changes (re-key) so the\n // select seed below re-seeds against the NEW instance instead of comparing\n // against a stale selection from the previous instance.\n if (prevBlocRef.current !== bloc) {\n prevBlocRef.current = bloc;\n lastSelectionRef.current = null;\n }\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n proxyCacheRef.current,\n );\n state = tracked.value as ExtractState<T>;\n trackedStateRef.current = tracked.value;\n pathRef.current = tracked.paths;\n // Rebuild the per-consumer session for this render. The primary bloc is the\n // first uniform entry; its `paths` are the SAME PathSet object the proxy\n // mutates during JSX (so it stays live as getters record leaves). Dep\n // entries are appended during JSX as `this.<handle>.track()` runs. Cleared\n // here (not in the layout effect) so a render that no longer tracks a dep\n // produces a session without it, and the reconcile drops it.\n const session = sessionRef.current;\n session.clear();\n session.set(bloc as unknown as StateContainer, {\n kind: 'primary',\n paths: tracked.paths,\n });\n // Freeze this render's tracking proxy after the synchronous render+commit\n // pass. The microtask fires only once the current task unwinds, so all\n // render-time JSX reads still record; reads afterwards — effects, event\n // handlers, async callbacks, devtools inspecting `state` — hit the\n // disarmed proxy and record nothing, so this render's path set can't be\n // polluted by work that outlives the render that owns it.\n queueMicrotask(tracked.disarm);\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits, pathRef.current is the consumer's actual leaf\n // interest (populated by the proxy during JSX evaluation). Re-register with\n // the container so the skeleton reflects the latest paths, and expand the\n // interest to include ancestor paths for the channel subscription.\n //\n // useLayoutEffect runs before the browser paints (and before any emit\n // triggered by another effect), so the skeleton is fresh by the time the\n // next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n // Clear the render-time tracking proxy now that JSX has been evaluated and\n // committed. Getters invoked after this point (event handlers, effects,\n // method→getter chains) fall through to live state instead of reading this\n // render's frozen snapshot. The render body re-seeds it next render.\n trackedStateRef.current = null;\n if (selectRef.current !== undefined) {\n // Switching into (or staying in) select-mode: invalidate any prior full\n // reconcile so a later switch back to auto-track mode never mistakes a\n // stale signature for \"unchanged\" and skips a needed reconcile.\n lastReconcileRef.current = null;\n return;\n }\n const container = bloc as unknown as StateContainer;\n const paths = pathRef.current;\n const session = sessionRef.current;\n\n // ---------------------------------------------------------------------\n // Short-circuit: if the primary path set AND the full dep session are\n // set-equal (paths + key/refId/args) to the last FULL reconcile, none of\n // registerConsumerPaths/subscribe/unsubscribe/expandWithAncestors below\n // can have anything new to do — skip the whole block. Any mismatch, or\n // `lastReconcileRef.current === null` (first commit, or the immediately\n // preceding commit was select-mode / uncertain), falls through to the\n // full reconcile. Never skip on uncertainty — a missed re-subscribe would\n // leave a stale/dropped subscription.\n // ---------------------------------------------------------------------\n const last = lastReconcileRef.current;\n if (\n last !== null &&\n last.primaryContainer === container &&\n pathSetEquals(last.primaryPaths, paths)\n ) {\n let unchanged = last.deps.size === session.size - 1;\n if (unchanged) {\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n const prevEntry = last.deps.get(depContainer);\n if (\n prevEntry === undefined ||\n prevEntry.key !== entry.key ||\n prevEntry.refId !== entry.refId ||\n !Object.is(prevEntry.args, entry.args) ||\n !pathSetEquals(prevEntry.paths, entry.paths)\n ) {\n unchanged = false;\n break;\n }\n }\n }\n if (unchanged) return;\n }\n\n container.registerConsumerPaths(consumerId, paths);\n // Register the *normal* leaf paths above for the source-side skeleton, then\n // build the channel interest as leaves + ancestor-watch ids so that an\n // atomic `patch` replacement of a parent (e.g. the array 'items') wakes a\n // consumer that tracked a child (e.g. 'items.length').\n expandedInterestRef.current = expandWithAncestors(\n paths,\n container.interner,\n );\n\n // -----------------------------------------------------------------------\n // Reconcile DEP containers (cross-bloc `.track()` interest).\n //\n // The primary bloc keeps its own dedicated subscription effect above; this\n // block manages only the *dep* containers recorded in the session this\n // render. We diff the new dep set vs the previously-subscribed set:\n // - new dep -> acquire was already done in `.track()`; subscribe its\n // channel + registerConsumerPaths + seed interest.\n // - surviving -> refresh its interest ref (subscribe closure reads it).\n // - dropped -> unsubscribe, unregisterConsumer, and release its ref.\n // -----------------------------------------------------------------------\n const subs = depSubsRef.current;\n\n // Pass 1: drop containers no longer in the session.\n for (const [depContainer, sub] of subs) {\n if (!session.has(depContainer)) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n subs.delete(depContainer);\n }\n }\n\n // Pass 2: add/refresh containers in the session (skip the primary).\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n const interest = expandWithAncestors(entry.paths, depContainer.interner);\n depContainer.registerConsumerPaths(consumerId, entry.paths);\n const existing = subs.get(depContainer);\n if (existing) {\n existing.interestRef.current = interest;\n continue;\n }\n // First commit that sees this dep: take the ownership ref HERE (not in\n // render/`.track()`), so an uncommitted render can never leak it (R4).\n getRegistry().acquire(entry.Type, entry.key, {\n canCreate: true,\n countRef: true,\n refId: entry.refId,\n args: entry.args,\n });\n const interestRef: { current: PathSet } = { current: interest };\n const unsubscribe = depContainer.channel.subscribe(\n () => interestRef.current,\n () => force(),\n );\n subs.set(depContainer, {\n unsubscribe,\n interestRef,\n Type: entry.Type,\n key: entry.key,\n refId: entry.refId,\n args: entry.args,\n });\n }\n\n // Capture this full reconcile's shape for the NEXT commit's short-circuit\n // check above. `paths`/`entry.paths` are fresh Sets seeded this render\n // (trackRender/unionPaths always allocate new Sets, never mutate one from\n // a prior render) — safe to keep direct references without cloning.\n const depsSignature = new Map<StateContainer, ReconcileDepSignature>();\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n depsSignature.set(depContainer, {\n paths: entry.paths,\n key: entry.key,\n refId: entry.refId,\n args: entry.args,\n });\n }\n lastReconcileRef.current = {\n primaryContainer: container,\n primaryPaths: paths,\n deps: depsSignature,\n };\n });\n\n // Unmount: tear down every dep subscription + ref exactly once. Kept in its\n // own effect (consumerId is stable for the component's lifetime, so this only\n // runs on final unmount, not on every reconcile). depSubsRef is mutated in\n // place by the reconcile, so the captured Map reference still holds the live\n // set at unmount.\n useEffect(() => {\n // Capture the ref's Map (mutated in place across renders) so the cleanup\n // reads the captured reference rather than depSubsRef.current directly.\n const subs = depSubsRef.current;\n return () => {\n for (const [depContainer, sub] of subs) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n }\n subs.clear();\n };\n }, [consumerId]);\n\n return [\n state,\n trackedBloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\n// ---------------------------------------------------------------------------\n// Cross-bloc session types + dep-handle wrapper.\n// ---------------------------------------------------------------------------\n\n/**\n * One entry in a consumer's per-render session map. Discriminated on `kind`:\n * the primary bloc is managed by its own dedicated effect, while dep entries\n * carry the registry coordinates the reconcile needs to release their ref.\n */\ntype SessionEntry =\n | {\n kind: 'primary';\n /** Tracked leaf paths recorded against the primary this render. */\n paths: PathSet;\n }\n | {\n kind: 'dep';\n /** Tracked leaf paths recorded against this dep this render. */\n paths: PathSet;\n /** Constructor for registry release. */\n Type: StateContainerConstructor;\n /** Resolved instance key for registry release. */\n key: string;\n /** refId held for this dep (released on drop/unmount). */\n refId: string;\n /** Construction args, used by the reconcile pass to acquire the ref. */\n args: unknown;\n };\n\n/** A live dep-channel subscription tracked between renders for reconciliation. */\ninterface DepSub {\n unsubscribe: () => void;\n interestRef: { current: PathSet };\n Type: StateContainerConstructor;\n key: string;\n refId: string;\n args: unknown;\n}\n\n/**\n * Snapshot of one dep's shape from the last FULL reconcile, compared against\n * the current session entry to decide whether the dep-reconcile layout effect\n * can short-circuit (see `lastReconcileRef` in `useBloc`).\n */\ninterface ReconcileDepSignature {\n paths: PathSet;\n key: string;\n refId: string;\n args: unknown;\n}\n\n/** Snapshot of the last FULL dep-reconcile layout effect run. */\ninterface ReconcileSignature {\n /** The primary container this signature was captured against (identity\n * check — a rebind/re-key swaps this even if the tracked paths happen to\n * be textually identical, and must never be mistaken for \"unchanged\"). */\n primaryContainer: StateContainer;\n primaryPaths: PathSet;\n deps: Map<StateContainer, ReconcileDepSignature>;\n}\n\n/** Per-access options shared by both dep accessors. */\ninterface DepAccessOptionsLike {\n args?: unknown;\n}\n\n/** Structural shape of a branded `depend()` handle as seen from React. */\ninterface DepHandleLike {\n track(options?: DepAccessOptionsLike): [unknown, StateContainer];\n untracked(options?: DepAccessOptionsLike): StateContainer;\n readonly [DEP_BRAND]: {\n Type: StateContainerConstructor;\n defaultArgs?: unknown;\n };\n}\n\n/**\n * Build the per-consumer wrapper that replaces a branded dep handle inside a\n * tracked getter's `this`. The wrapper exposes the same accessors as the core\n * handle and overrides `.track()`:\n *\n * - **Inside a render** (`trackedStateRef.current != null`): resolve (ENSURE,\n * no ref) the dep, `trackRender` its state, merge the recorded paths into the\n * session entry, build/reuse a tracked proxy for the dep so its OWN getters\n * track too, and return `[trackedValue, depProxy]`. The ownership ref is taken\n * by the layout-effect reconcile pass, not here.\n * - **Outside a render**: degrade to live `[dep.state, dep]` — matches the core\n * base impl, safe in event handlers/effects/methods.\n *\n * `.untracked()` always returns the live instance with no subscription.\n *\n * Args resolve at call time (`options.args ?? defaultArgs`), so a single handle\n * can resolve different dep instances across calls; tracked-proxy state is\n * therefore cached per resolved instance, not per handle. Guards against a\n * container re-entering tracking within the same render (mutual A↔B deps): if\n * the dep already has a non-primary session entry this render, reuse its proxy\n * + union its paths instead of re-acquiring.\n */\nfunction makeDepWrapper(\n handle: DepHandleLike,\n consumerId: string,\n trackedStateRef: { current: unknown },\n sessionRef: { current: Map<StateContainer, SessionEntry> },\n onDepHandle: (handle: object) => unknown,\n): DepHandleLike {\n const brand = handle[DEP_BRAND];\n const refId = depRefId(consumerId);\n const registry = getRegistry();\n // Per-resolved-instance tracked-state ref + proxy. Call-time args mean one\n // handle can resolve several instances, so cache is keyed by the instance.\n const perDep = new Map<\n StateContainer,\n { ref: { current: unknown }; proxy: StateContainer }\n >();\n // One ProxyCache shared across every instance this handle resolves to\n // (call-time args can resolve different dep instances across calls) — safe\n // because ProxyCache's internal map is keyed by target object identity, so\n // unrelated instances' objects never collide in it.\n const proxyCache = new ProxyCache();\n\n const resolve = (options?: DepAccessOptionsLike) => {\n const args = options?.args ?? brand.defaultArgs;\n const key = registry.resolveKey(brand.Type, undefined, args);\n const dep = registry.ensure(\n brand.Type,\n key,\n args,\n ) as unknown as StateContainer;\n return { dep, key, args };\n };\n\n const wrapper = {\n untracked: (options?: DepAccessOptionsLike) => resolve(options).dep,\n track: (options?: DepAccessOptionsLike) => {\n const { dep, key, args } = resolve(options);\n\n // Outside a render: live values, no subscription (core base behavior).\n if (trackedStateRef.current == null) {\n return [dep.state, dep];\n }\n\n const session = sessionRef.current;\n const existing = session.get(dep);\n\n // Render only ENSUREs the dep instance (via `resolve()` above); it does\n // NOT take a ref. Ownership is claimed by the layout-effect reconcile\n // pass-2 the first commit it sees this dep, and released on drop/unmount.\n // This keeps acquire/release paired so an uncommitted render can't leak a\n // dep ref (R4).\n\n const tracked = trackRender(dep.state, dep.interner, proxyCache);\n let cache = perDep.get(dep);\n if (cache === undefined) {\n const ref = { current: tracked.value as unknown };\n cache = { ref, proxy: buildTrackedProxy(dep, ref, onDepHandle).proxy };\n perDep.set(dep, cache);\n } else {\n cache.ref.current = tracked.value;\n }\n\n if (existing !== undefined) {\n // Re-entry this render (`.track()` twice, or a mutual cycle): union the\n // new paths into the existing entry rather than re-acquiring.\n existing.paths = unionPaths(existing.paths, tracked.paths);\n } else {\n session.set(dep, {\n kind: 'dep',\n paths: tracked.paths,\n Type: brand.Type,\n key,\n refId,\n args,\n });\n }\n\n return [tracked.value, cache.proxy];\n },\n } as DepHandleLike;\n\n Object.defineProperty(wrapper, DEP_BRAND, {\n value: brand,\n enumerable: false,\n writable: false,\n configurable: false,\n });\n\n return wrapper;\n}\n\n/** Union two PathSets (ALL_PATHS dominates). */\nfunction unionPaths(a: PathSet, b: PathSet): PathSet {\n if (a === ALL_PATHS || b === ALL_PATHS) return ALL_PATHS;\n const out = new Set<number>(a as Set<number>);\n for (const id of b as Set<number>) out.add(id);\n return out;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n\n/**\n * Expand a PathSet to include an *ancestor-watch* id for every ancestor of\n * every tracked leaf.\n *\n * The auto-tracker records leaf paths (e.g. `'items.length'`), but\n * `StructuralContainer.patch` can only mark the parent (`'items'`) when it\n * replaces a value atomically (arrays, `null`, primitives — it can't see\n * inside). Without expansion, a subscriber with interest `{'items.length'}`\n * would miss a `patch`-triggered atomic-replacement of `items`.\n *\n * Ancestors are added under the interner's *ancestor-watch* lane\n * (`internAncestor`), NOT as normal ids. The source emits a matching\n * ancestor-watch mark only for paths it replaces atomically — never for a\n * plain-object structural pulse-up. So `{'items.length'}` wakes when the array\n * `items` is replaced, but `{'user.email'}` does NOT wake when a sibling\n * `user.name` changes and pulses `user` up: pulse-up `user` is a normal id and\n * the ancestor-watch `user` only intersects another ancestor-watch `user`.\n *\n * Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but\n * NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,\n * and `''` would wake this consumer on every field change).\n */\nfunction expandWithAncestors(paths: PathSet, interner: PathInterner): PathSet {\n if (paths === ALL_PATHS) return ALL_PATHS;\n const leafPaths = paths as Set<number>;\n if (leafPaths.size === 0) return paths;\n\n const expanded = new Set<number>(leafPaths);\n for (const id of leafPaths) {\n const str = interner.lookup(id);\n // Add all non-root ancestor segments as *ancestor-watch* ids: 'a.b.c' →\n // watch 'a.b' and 'a'. These live in the interner's ancestor lane so they\n // only intersect the source's atomic-replacement marks (`internAncestor`),\n // never a structural pulse-up mark of the same path. That is what lets a\n // descendant-reader (e.g. `items.length`) wake on an array/null replacement\n // without a sibling-leaf reader (`user.email`) waking when a sibling\n // (`user.name`) changes and pulses up through `user`.\n let idx = str.lastIndexOf('.');\n while (idx > 0) {\n const ancestor = str.slice(0, idx);\n expanded.add(interner.internAncestor(ancestor));\n idx = ancestor.lastIndexOf('.');\n }\n }\n return expanded;\n}\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/BlocProvider.tsx","../src/buildTrackedProxy.ts","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\n\n/**\n * Internal context value: a Map from bloc constructor to the args object\n * provided by the nearest BlocProvider for that bloc class.\n *\n * Stored as a WeakMap-keyed record so nested providers for different blocs\n * compose without clobbering each other.\n */\ntype ProvidedArgsMap = Map<StateContainerConstructor, unknown>;\n\nconst ProvidedArgsContext = createContext<ProvidedArgsMap>(new Map());\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps<T extends StateContainerConstructor> {\n /**\n * The bloc class whose args are being provided to descendants.\n */\n bloc: T;\n /**\n * Args that descendant `useBloc(bloc)` calls will resolve to when no\n * own `args` are given.\n */\n args: ExtractArgs<T>;\n children: ReactNode;\n}\n\n/**\n * Provides args to descendant `useBloc` calls for a specific bloc class via\n * React context.\n *\n * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the\n * args supplied here. Own `args` on the `useBloc` call always win.\n *\n * Multiple `BlocProvider` wrappers for different bloc classes compose: each\n * provider merges its entry into the inherited map, so nested providers for\n * different blocs do not interfere.\n *\n * @example\n * ```tsx\n * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>\n * <UserProfile />\n * </BlocProvider>\n * ```\n *\n * @example Per-mount private instance\n * ```tsx\n * const id = useId();\n * <BlocProvider bloc={CartBloc} args={{ _id: id }}>\n * <CartWidget />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider<T extends StateContainerConstructor>({\n bloc,\n args,\n children,\n}: BlocProviderProps<T>): ReactElement {\n const parentMap = useContext(ProvidedArgsContext);\n\n // Merge our entry into a new Map so sibling/parent providers for other blocs\n // are preserved. Memoised on (parentMap, bloc, args) identity.\n const mergedMap = useMemo(() => {\n const next = new Map(parentMap);\n next.set(bloc, args);\n return next;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parentMap, bloc, args]);\n\n return (\n <ProvidedArgsContext.Provider value={mergedMap}>\n {children}\n </ProvidedArgsContext.Provider>\n );\n}\n\n/**\n * Returns the args provided by the nearest {@link BlocProvider} for the given\n * bloc class, or `undefined` when called outside a matching provider.\n *\n * Used by `useBloc` to inherit provider args when no own `args` are given.\n */\nexport function useProvidedArgs<T extends StateContainerConstructor>(\n BlocClass: T,\n): ExtractArgs<T> | undefined {\n const map = useContext(ProvidedArgsContext);\n return map.get(BlocClass) as ExtractArgs<T> | undefined;\n}\n","import { DEP_BRAND } from '@blac/core';\n\n/**\n * Build a per-consumer proxy pair for a bloc instance.\n *\n * Returns two proxies:\n * - `proxy` — the stable outer proxy returned to the consumer. Getter\n * properties are invoked with `thisProxy` as `this` so that `this.state`\n * reads inside getters are redirected to the current render's tracking\n * proxy. Non-getter properties fall through to the live instance.\n * - `thisProxy` — the inner `this`-proxy used as the receiver for getter\n * calls. Intercepts `state` to return `trackedStateRef.current` when a\n * tracking context is active; otherwise falls through to the live value.\n * When `onDepHandle` is supplied, a read off `this` whose value carries the\n * `DEP_BRAND` symbol (i.e. a `depend()` handle) is routed through\n * `onDepHandle`, which returns a per-consumer wrapper whose `.track()` opts\n * the consumer into cross-bloc reactivity. Core stays decoupled — detection\n * is purely by the `DEP_BRAND` symbol.\n *\n * Both allocations happen exactly once per bloc acquisition (inside `useMemo`)\n * so the proxies are stable across renders.\n *\n * @param onDepHandle - Optional callback invoked when a getter reads a branded\n * dep handle off `this`. Receives the original handle and returns the value\n * to expose in its place (the session-bound wrapper). The callback is\n * responsible for caching wrappers per handle to avoid re-allocation.\n */\nexport function buildTrackedProxy<T extends object>(\n instance: T,\n trackedStateRef: { current: unknown },\n onDepHandle?: (handle: object) => unknown,\n): { proxy: T; thisProxy: T } {\n // Build a map of getter descriptors from the prototype chain (excluding\n // Object.prototype). This is computed once per bloc acquisition so that\n // the proxy's get trap is O(1) per property access. Both string- and\n // symbol-keyed getters are collected. Arrow-function class properties\n // (own, bound in the constructor) are not getters and pass through\n // unmodified.\n const getterDescs = new Map<string | symbol, PropertyDescriptor>();\n let proto = Object.getPrototypeOf(instance);\n while (proto && proto !== Object.prototype) {\n const keys: (string | symbol)[] = [\n ...Object.getOwnPropertyNames(proto),\n ...Object.getOwnPropertySymbols(proto),\n ];\n for (const key of keys) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc?.get && !getterDescs.has(key)) getterDescs.set(key, desc);\n }\n proto = Object.getPrototypeOf(proto);\n }\n\n // `this`-proxy for getter invocations, allocated ONCE per acquisition (the\n // trap closes over the stable `trackedStateRef`, so it never needs to be\n // rebuilt per access). Redirects `this.state` to the current render's\n // tracking proxy so getter reads during JSX record paths; outside render\n // `trackedStateRef.current` is null and it falls through to live state.\n // The receiver `r` (this proxy) is threaded through Reflect.get so chained\n // getter calls (getters reading other getters) stay in tracked context.\n const thisProxy = new Proxy(instance as object, {\n get(t, k, r) {\n if (k === 'state') return trackedStateRef.current ?? Reflect.get(t, k, r);\n const value = Reflect.get(t, k, r);\n // A branded dep handle read off `this` (e.g. `this.price`) is routed\n // through onDepHandle so the consumer's session can wrap `.track()`.\n if (\n onDepHandle !== undefined &&\n (typeof value === 'function' || typeof value === 'object') &&\n value !== null &&\n (value as Record<symbol, unknown>)[DEP_BRAND] !== undefined\n ) {\n return onDepHandle(value as object);\n }\n return value;\n },\n });\n\n // Stable proxy: one allocation per bloc acquisition. Non-getter access is\n // a single Map lookup + Reflect.get — no prototype walk on the hot path.\n const proxy = new Proxy(instance as object, {\n get(target, key, receiver) {\n const desc = getterDescs.get(key);\n if (desc?.get) return desc.get.call(thisProxy);\n return Reflect.get(target, key, receiver);\n },\n }) as T;\n\n return { proxy, thisProxy: thisProxy as T };\n}\n","import {\n useEffect,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n DEP_BRAND,\n getRegistry,\n isIsolatedClass,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n pathSetEquals,\n trackRender,\n PathInterner,\n ProxyCache,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useProvidedArgs } from './BlocProvider';\nimport { buildTrackedProxy } from './buildTrackedProxy';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n// Sentinel that can never `Object.is`-equal a real args value (including\n// `undefined`). Used to lazily seed args-key refs so the structural key is\n// computed only when the guard actually runs, never on every render.\nconst ARGS_UNSET: unique symbol = Symbol('blac.argsKeyUnset');\n\n/**\n * `useRef` with a lazily-built initial value. `useRef(new Map())` discards its\n * argument after the first render but still EVALUATES it on every render, so\n * each such ref costs one dead allocation per render. Passing a module-level\n * factory instead makes the per-render cost a single identity read.\n *\n * The factory MUST be a stable module-level function (an inline arrow would\n * just move the per-render allocation from the value to the closure). The\n * returned `current` is non-nullable, so read sites keep their original types.\n */\nfunction useLazyRef<T>(create: () => T): { current: T } {\n const ref = useRef<T | null>(null);\n if (ref.current === null) {\n ref.current = create();\n }\n return ref as { current: T };\n}\n\n// Stable factories for the lazy refs below — one per shape, module-level so the\n// reference passed to `useLazyRef` never changes. Each hook instance still gets\n// its OWN object: nothing here is shared between components (a shared session\n// map or ProxyCache would break per-consumer render isolation).\nconst newComponentRef = (): ComponentRef => ({});\nconst newArgsKeyEntry = (): { ref: unknown; key: string | undefined } => ({\n ref: ARGS_UNSET,\n key: undefined,\n});\nconst newSessionMap = (): Map<StateContainer, SessionEntry> => new Map();\nconst newDepSubsMap = (): Map<StateContainer, DepSub> => new Map();\nconst newDepWrapperCache = (): WeakMap<object, unknown> => new WeakMap();\nconst newProxyCache = (): ProxyCache => new ProxyCache();\n\n// Registry refId formats for a consumer's primary bloc and its tracked deps.\n// Centralised so the `acquire` and `release` sites can never drift apart — a\n// mismatch would leak the ref and keep the bloc alive past unmount.\nconst primaryRefId = (consumerId: string): string => `useBloc@${consumerId}`;\nconst depRefId = (consumerId: string): string => `useBloc@${consumerId}:dep`;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`).\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is derived from `options.args` (own args),\n * then the surrounding {@link BlocProvider} context args for this bloc,\n * then the default key (no args).\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * Per-mount private instance:\n * ```ts\n * const id = useId();\n * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });\n * ```\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Args-based shared instance\n * ```ts\n * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useLazyRef(newComponentRef);\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n\n // `isolated` is sugar over args identity: it injects a per-mount id into the\n // args used for keying. `consumerId` is already stable for this mount, so this\n // costs no extra hook slot (a `useId()` here would cost one in EVERY consumer).\n const isolated = options?.isolated === true || isIsolatedClass(BlocClass);\n // Memo dep: flips when isolation is toggled at runtime (`{ isolated: cond }`)\n // so the instance re-keys. Constant for the common case.\n const isolationKey = isolated ? consumerId : undefined;\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const isSelectMode = options?.select !== undefined;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n const isolatedRef = useRef(isolated);\n isolatedRef.current = isolated;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Priority: own args > provider args (for this bloc class) > none.\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const ownArgs = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const ownArgsRef = useRef(ownArgs);\n ownArgsRef.current = ownArgs;\n // Fast-path: only recompute the structural key when the args REFERENCE\n // changes. Callers commonly pass a stable args object (e.g. memoised or\n // module-level), so this skips a JSON.stringify call every render.\n const ownArgsKeyRef = useLazyRef(newArgsKeyEntry);\n if (!Object.is(ownArgsKeyRef.current.ref, ownArgs)) {\n ownArgsKeyRef.current = {\n ref: ownArgs,\n key: ownArgs === undefined ? undefined : JSON.stringify(ownArgs),\n };\n }\n const ownArgsKey = ownArgsKeyRef.current.key;\n\n // Read provided args from the nearest BlocProvider for this bloc class.\n const providerArgs = useProvidedArgs(BlocClass);\n const providerArgsRef = useRef(providerArgs);\n providerArgsRef.current = providerArgs;\n // Same reference fast-path as ownArgsKey above.\n const providerArgsKeyRef = useLazyRef(newArgsKeyEntry);\n if (!Object.is(providerArgsKeyRef.current.ref, providerArgs)) {\n providerArgsKeyRef.current = {\n ref: providerArgs,\n key:\n providerArgs === undefined ? undefined : JSON.stringify(providerArgs),\n };\n }\n const providerArgsKey = providerArgsKeyRef.current.key;\n\n // Current render's tracking proxy. Declared before the memo so the stable\n // ref object can be passed to buildTrackedProxy at acquisition time. The\n // proxy trap only reads `.current` at invocation time (not during creation),\n // so it is safe to pass on first mount even though the value is null.\n // Populated during each auto-track render snapshot (below); cleared to null\n // by useLayoutEffect after commit.\n const trackedStateRef = useRef<unknown>(null);\n\n // ---------------------------------------------------------------------------\n // Per-consumer cross-bloc session.\n //\n // Each render rebuilds a map of every container this consumer is currently\n // interested in. The PRIMARY bloc is the first uniform entry; every dep\n // reached through `this.<handle>.track()` inside a tracked getter adds an\n // entry. The layout-effect reconcile (below) diffs this map vs the previous\n // render to subscribe new deps and release dropped ones. The session lives in\n // this hook's refs only — there is no global ambient state, so sibling\n // renders never cross-contaminate.\n // ---------------------------------------------------------------------------\n const sessionRef = useLazyRef(newSessionMap);\n // Channel subscriptions for DEP containers (the primary keeps its own\n // dedicated effect). container -> { unsubscribe, interestRef, refId held }.\n const depSubsRef = useLazyRef(newDepSubsMap);\n // Per-handle wrapper cache, allocated once per bloc acquisition (in the memo)\n // so wrappers are stable across renders. handle -> session-bound wrapper.\n const depWrapperCacheRef = useLazyRef(newDepWrapperCache);\n const proxyCacheRef = useLazyRef(newProxyCache);\n // Snapshot of the last FULL reconcile's shape, used by the layout-effect\n // below to short-circuit when nothing actually changed. `null` means \"no\n // prior full run to compare against\" (first commit, or the previous commit\n // was in select-mode) — always forces a full reconcile in that case.\n const lastReconcileRef = useRef<ReconcileSignature | null>(null);\n\n // Rebind nonce: bumped by the ownership layout-effect when the instance the\n // render captured was disposed + recreated out from under us. This happens on\n // a same-commit ownership handoff of a shared (non-keepAlive) key — the sole\n // prior owner's effect cleanup releases refs→0 and SYNCHRONOUSLY disposes the\n // instance before this consumer's layout setup re-acquires (creating a fresh\n // one) — and equivalently under StrictMode's setup→cleanup→setup double-invoke\n // for a lone owner. Threaded into the memo deps so bumping it re-ensures `bloc`\n // against the LIVE registry entry instead of the disposed instance.\n const [rebindNonce, bumpRebind] = useReducer((x: number) => x + 1, 0);\n // The live instance actually owned (ref held) by the ownership layout-effect,\n // read by its cleanup so onUnmount always fires with the owned instance.\n const ownedBlocRef = useRef<TBloc | null>(null);\n\n const { bloc, instanceKey, trackedBloc } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n trackedBloc: TBloc;\n }>(() => {\n // Own args win over provider args; provider args win over no args.\n const baseArgs =\n ownArgsRef.current !== undefined\n ? ownArgsRef.current\n : providerArgsRef.current;\n // Isolation MERGES with resolved args rather than losing to them (plan Q2=B):\n // a class-level `static isolated` must not be silently cancelled by a call\n // site that happens to pass args.\n // The cast is the point of the documented behavior note: the injected\n // `_blacIsolated` key is NOT part of the bloc's declared `Args` type, but it\n // does reach `init(args)` — exactly as the `{ args: { _id: useId() } }`\n // idiom this replaces always did.\n const effectiveArgs = isolatedRef.current\n ? (withIsolation(baseArgs, consumerId) as ExtractArgs<T>)\n : baseArgs;\n\n const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);\n const registry = getRegistry();\n // Render only ENSUREs the instance exists (no ref). Ownership is claimed in\n // the layout effect below, so an abandoned/uncommitted render can never\n // leak a ref and a memo re-run can never double-count one (R3/R4).\n const instance = registry.acquire(BlocClass, resolvedKey, {\n canCreate: true,\n countRef: false,\n args: effectiveArgs,\n }) as TBloc;\n\n // Build a session-bound wrapper for a dep handle the first time a getter\n // reads it off `this`; cache per handle so the wrapper identity is stable.\n // `onDepHandle` is threaded into each dep's tracked proxy too, so a nested\n // `this.<otherHandle>.track()` inside a dep's getter records into the SAME\n // consumer session — that is what makes deep chains (A→B→C) reactive.\n const onDepHandle = (handle: object): unknown => {\n const cache = depWrapperCacheRef.current;\n const cached = cache.get(handle);\n if (cached !== undefined) return cached;\n const wrapper = makeDepWrapper(\n handle as DepHandleLike,\n consumerId,\n trackedStateRef,\n sessionRef,\n onDepHandle,\n );\n cache.set(handle, wrapper);\n return wrapper;\n };\n\n const { proxy } = buildTrackedProxy(\n instance as object,\n trackedStateRef,\n onDepHandle,\n );\n\n return {\n bloc: instance,\n instanceKey: resolvedKey,\n trackedBloc: proxy as TBloc,\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, ownArgsKey, providerArgsKey, isolationKey, rebindNonce]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We talk directly to `bloc.channel` — the StructuralContainer's path-scoped\n // DirtyChannel. Subscribing with a dynamic interest function lets us narrow\n // wakeups per consumer, so a component only re-renders when a path it\n // actually read changes (rather than on every state change).\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useLazyRef(emptyPathSet);\n // Expanded interest: leaf paths PLUS *ancestor-watch* ids for their parents,\n // so that when `patch` atomically replaces a parent (e.g. the array 'items')\n // and the consumer tracked a child (e.g. 'items.length'), the intersection\n // still fires — while a structural pulse-up of a plain-object parent (e.g.\n // 'user' when a sibling 'user.name' changed) does NOT, because pulse-up marks\n // are normal ids and ancestor-watch ids only intersect their own lane.\n // Updated in useLayoutEffect after each render once pathRef.current is\n // populated.\n const expandedInterestRef = useLazyRef(emptyPathSet);\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n // Render-time raw-state snapshot, seeded each render (below) and read by the\n // subscription effect to close the mount gap (R2): an emit landing between the\n // render read and the passive subscribe would otherwise be lost.\n const renderStateRef = useRef<unknown>(undefined);\n // Bloc identity from the previous render, so select-mode can reset its cached\n // selection when the underlying instance changes (re-key).\n const prevBlocRef = useRef<unknown>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n //\n // Self-healing note: this effect can run once against a pre-rebind (possibly\n // disposed) `bloc` before the ownership layout-effect's nonce bump triggers a\n // re-render that swaps `bloc` to the live instance (the memo dep array\n // includes `rebindNonce`). That's benign — `channel.subscribe` and\n // `unregisterConsumer` are plain Map operations that don't check disposal\n // state and never throw on a disposed container — so this effect's cleanup\n // runs cleanly and the re-render re-subscribes against the live instance.\n const channel = (bloc as unknown as StateContainer).channel;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n // Close the mount gap (R2): an emit between the render's selector seed and\n // this subscribe would be lost. Recompute against LIVE state and force if\n // the selection advanced.\n const select = selectRef.current;\n if (select) {\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev === null || !shallowArrayEqual(prev, next)) {\n lastSelectionRef.current = next;\n force();\n }\n }\n return unsub;\n }\n\n // Auto-track mode: subscribe with the expanded interest (leaf paths + their\n // ancestors). This ensures that when `patch` marks a parent path (e.g.\n // 'items') and the consumer tracked a child (e.g. 'items.length'), the\n // channel intersection still fires a re-render.\n //\n // `expandedInterestRef.current` is updated by the useLayoutEffect below\n // after each render, so the interest is always fresh at flush time.\n const unsub = channel.subscribe(\n () => expandedInterestRef.current,\n () => force(),\n );\n // Register the consumer's leaf paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n // Close the mount gap (R2): if the container's state advanced between the\n // render snapshot (renderStateRef) and this subscribe, the emit was lost —\n // force one re-render so we don't stay stale.\n if ((bloc as unknown as StateContainer).state !== renderStateRef.current) {\n force();\n }\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, consumerId, isSelectMode]);\n\n // ---------------------------------------------------------------------------\n // Ownership + mount / unmount lifecycle.\n //\n // The ownership ref is claimed HERE (a layout effect), not in the render/memo,\n // so acquire and release are perfectly paired: a memo re-run can no longer\n // double-count (R3) and an uncommitted render can no longer leak (R4). Keyed\n // on [BlocClass, instanceKey, consumerId] — NOT `bloc` — so a genuine re-key\n // (args change, OR a BlocClass swap) releases the old ref and acquires the new\n // one, while a pure rebind (same class+key, instance replaced under us) does\n // NOT re-run this effect and therefore never releases the single ref we just\n // took (which, for a sole owner, would synchronously dispose the\n // freshly-created instance and churn indefinitely).\n //\n // `BlocClass` MUST stay in the dep array even though `instanceKey` alone often\n // determines identity: `resolveInstanceKey`/`resolveKey` collapse to the same\n // `DEFAULT_STRUCTURAL_KEY` sentinel across DIFFERENT classes when neither has\n // args nor a `static key` (e.g. `useBloc(cond ? AdminBloc : UserBloc)`). Without\n // `BlocClass` here, swapping classes at that shared key would never re-run this\n // effect: the old class's ref would leak until unmount, and the new class's\n // instance (only `ensure`d by the render memo, countRef:false) would be held\n // with zero ownership refs — exposed to disposal by unrelated traffic on that\n // class+key, with no rebind path to recover it.\n //\n // `acquire` returns the authoritative LIVE instance for the key. If it differs\n // from the instance the render captured (`bloc`), the render read a disposed /\n // replaced instance (same-commit shared-key handoff, or StrictMode remount);\n // we bump the rebind nonce so the memo re-ensures `bloc` against this live one.\n // onMount/onUnmount fire with the owned live instance and stay co-located with\n // acquire/release so onUnmount(bloc) runs BEFORE release(...) within one\n // cleanup, keeping the instance alive while the callback runs.\n // ---------------------------------------------------------------------------\n useLayoutEffect(() => {\n const registry = getRegistry();\n const live = registry.acquire(BlocClass, instanceKey, {\n canCreate: true,\n countRef: true,\n refId: primaryRefId(consumerId),\n }) as TBloc;\n ownedBlocRef.current = live;\n onMountRef.current?.(live as InstanceType<T>);\n // Rebind if the render captured a stale (disposed/replaced) instance so the\n // component renders + subscribes against the live registry entry, not a\n // disposed one. Only bumps on an actual mismatch, so it can fire at most once\n // per handoff and never loops (the re-ensured `bloc` equals `live`, and this\n // effect is not keyed on `bloc` so it won't re-run and re-release).\n if (live !== bloc) {\n bumpRebind();\n }\n return () => {\n onUnmountRef.current?.((ownedBlocRef.current ?? bloc) as InstanceType<T>);\n registry.release(BlocClass, instanceKey, false, primaryRefId(consumerId));\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, instanceKey, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n // Seed the render-time snapshot so the subscription effect can detect an emit\n // that landed in the render→subscribe window (R2 mount gap).\n renderStateRef.current = rawState;\n // Reset the cached selection when the bloc identity changes (re-key) so the\n // select seed below re-seeds against the NEW instance instead of comparing\n // against a stale selection from the previous instance.\n if (prevBlocRef.current !== bloc) {\n prevBlocRef.current = bloc;\n lastSelectionRef.current = null;\n }\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n proxyCacheRef.current,\n );\n state = tracked.value as ExtractState<T>;\n trackedStateRef.current = tracked.value;\n pathRef.current = tracked.paths;\n // Rebuild the per-consumer session for this render. The primary bloc is the\n // first uniform entry; its `paths` are the SAME PathSet object the proxy\n // mutates during JSX (so it stays live as getters record leaves). Dep\n // entries are appended during JSX as `this.<handle>.track()` runs. Cleared\n // here (not in the layout effect) so a render that no longer tracks a dep\n // produces a session without it, and the reconcile drops it.\n const session = sessionRef.current;\n session.clear();\n session.set(bloc as unknown as StateContainer, {\n kind: 'primary',\n paths: tracked.paths,\n });\n // Freeze this render's tracking proxy after the synchronous render+commit\n // pass. The microtask fires only once the current task unwinds, so all\n // render-time JSX reads still record; reads afterwards — effects, event\n // handlers, async callbacks, devtools inspecting `state` — hit the\n // disarmed proxy and record nothing, so this render's path set can't be\n // polluted by work that outlives the render that owns it.\n queueMicrotask(tracked.disarm);\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits, pathRef.current is the consumer's actual leaf\n // interest (populated by the proxy during JSX evaluation). Re-register with\n // the container so the skeleton reflects the latest paths, and expand the\n // interest to include ancestor paths for the channel subscription.\n //\n // useLayoutEffect runs before the browser paints (and before any emit\n // triggered by another effect), so the skeleton is fresh by the time the\n // next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n // Clear the render-time tracking proxy now that JSX has been evaluated and\n // committed. Getters invoked after this point (event handlers, effects,\n // method→getter chains) fall through to live state instead of reading this\n // render's frozen snapshot. The render body re-seeds it next render.\n trackedStateRef.current = null;\n if (selectRef.current !== undefined) {\n // Switching into (or staying in) select-mode: invalidate any prior full\n // reconcile so a later switch back to auto-track mode never mistakes a\n // stale signature for \"unchanged\" and skips a needed reconcile.\n lastReconcileRef.current = null;\n return;\n }\n const container = bloc as unknown as StateContainer;\n const paths = pathRef.current;\n const session = sessionRef.current;\n\n // ---------------------------------------------------------------------\n // Short-circuit: if the primary path set AND the full dep session are\n // set-equal (paths + key/refId/args) to the last FULL reconcile, none of\n // registerConsumerPaths/subscribe/unsubscribe/expandWithAncestors below\n // can have anything new to do — skip the whole block. Any mismatch, or\n // `lastReconcileRef.current === null` (first commit, or the immediately\n // preceding commit was select-mode / uncertain), falls through to the\n // full reconcile. Never skip on uncertainty — a missed re-subscribe would\n // leave a stale/dropped subscription.\n // ---------------------------------------------------------------------\n const last = lastReconcileRef.current;\n if (\n last !== null &&\n last.primaryContainer === container &&\n pathSetEquals(last.primaryPaths, paths)\n ) {\n let unchanged = last.deps.size === session.size - 1;\n if (unchanged) {\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n const prevEntry = last.deps.get(depContainer);\n if (\n prevEntry === undefined ||\n prevEntry.key !== entry.key ||\n prevEntry.refId !== entry.refId ||\n !Object.is(prevEntry.args, entry.args) ||\n !pathSetEquals(prevEntry.paths, entry.paths)\n ) {\n unchanged = false;\n break;\n }\n }\n }\n if (unchanged) return;\n }\n\n container.registerConsumerPaths(consumerId, paths);\n // Register the *normal* leaf paths above for the source-side skeleton, then\n // build the channel interest as leaves + ancestor-watch ids so that an\n // atomic `patch` replacement of a parent (e.g. the array 'items') wakes a\n // consumer that tracked a child (e.g. 'items.length').\n expandedInterestRef.current = expandWithAncestors(\n paths,\n container.interner,\n );\n\n // -----------------------------------------------------------------------\n // Reconcile DEP containers (cross-bloc `.track()` interest).\n //\n // The primary bloc keeps its own dedicated subscription effect above; this\n // block manages only the *dep* containers recorded in the session this\n // render. We diff the new dep set vs the previously-subscribed set:\n // - new dep -> acquire was already done in `.track()`; subscribe its\n // channel + registerConsumerPaths + seed interest.\n // - surviving -> refresh its interest ref (subscribe closure reads it).\n // - dropped -> unsubscribe, unregisterConsumer, and release its ref.\n // -----------------------------------------------------------------------\n const subs = depSubsRef.current;\n\n // Pass 1: drop containers no longer in the session.\n for (const [depContainer, sub] of subs) {\n if (!session.has(depContainer)) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n subs.delete(depContainer);\n }\n }\n\n // Pass 2: add/refresh containers in the session (skip the primary).\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n const interest = expandWithAncestors(entry.paths, depContainer.interner);\n depContainer.registerConsumerPaths(consumerId, entry.paths);\n const existing = subs.get(depContainer);\n if (existing) {\n existing.interestRef.current = interest;\n continue;\n }\n // First commit that sees this dep: take the ownership ref HERE (not in\n // render/`.track()`), so an uncommitted render can never leak it (R4).\n getRegistry().acquire(entry.Type, entry.key, {\n canCreate: true,\n countRef: true,\n refId: entry.refId,\n args: entry.args,\n });\n const interestRef: { current: PathSet } = { current: interest };\n const unsubscribe = depContainer.channel.subscribe(\n () => interestRef.current,\n () => force(),\n );\n subs.set(depContainer, {\n unsubscribe,\n interestRef,\n Type: entry.Type,\n key: entry.key,\n refId: entry.refId,\n args: entry.args,\n });\n }\n\n // Capture this full reconcile's shape for the NEXT commit's short-circuit\n // check above. `paths`/`entry.paths` are fresh Sets seeded this render\n // (trackRender/unionPaths always allocate new Sets, never mutate one from\n // a prior render) — safe to keep direct references without cloning.\n const depsSignature = new Map<StateContainer, ReconcileDepSignature>();\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n depsSignature.set(depContainer, {\n paths: entry.paths,\n key: entry.key,\n refId: entry.refId,\n args: entry.args,\n });\n }\n lastReconcileRef.current = {\n primaryContainer: container,\n primaryPaths: paths,\n deps: depsSignature,\n };\n });\n\n // Unmount: tear down every dep subscription + ref exactly once. Kept in its\n // own effect (consumerId is stable for the component's lifetime, so this only\n // runs on final unmount, not on every reconcile). depSubsRef is mutated in\n // place by the reconcile, so the captured Map reference still holds the live\n // set at unmount.\n useEffect(() => {\n // Capture the ref's Map (mutated in place across renders) so the cleanup\n // reads the captured reference rather than depSubsRef.current directly.\n const subs = depSubsRef.current;\n return () => {\n for (const [depContainer, sub] of subs) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n }\n subs.clear();\n };\n // `depSubsRef` is a stable ref object (its identity never changes), so\n // listing it keeps this effect an unmount-only effect while satisfying\n // exhaustive-deps — the lint rule can't tell `useLazyRef` returns a ref.\n }, [consumerId, depSubsRef]);\n\n return [\n state,\n trackedBloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\n// ---------------------------------------------------------------------------\n// Cross-bloc session types + dep-handle wrapper.\n// ---------------------------------------------------------------------------\n\n/**\n * One entry in a consumer's per-render session map. Discriminated on `kind`:\n * the primary bloc is managed by its own dedicated effect, while dep entries\n * carry the registry coordinates the reconcile needs to release their ref.\n */\ntype SessionEntry =\n | {\n kind: 'primary';\n /** Tracked leaf paths recorded against the primary this render. */\n paths: PathSet;\n }\n | {\n kind: 'dep';\n /** Tracked leaf paths recorded against this dep this render. */\n paths: PathSet;\n /** Constructor for registry release. */\n Type: StateContainerConstructor;\n /** Resolved instance key for registry release. */\n key: string;\n /** refId held for this dep (released on drop/unmount). */\n refId: string;\n /** Construction args, used by the reconcile pass to acquire the ref. */\n args: unknown;\n };\n\n/** A live dep-channel subscription tracked between renders for reconciliation. */\ninterface DepSub {\n unsubscribe: () => void;\n interestRef: { current: PathSet };\n Type: StateContainerConstructor;\n key: string;\n refId: string;\n args: unknown;\n}\n\n/**\n * Snapshot of one dep's shape from the last FULL reconcile, compared against\n * the current session entry to decide whether the dep-reconcile layout effect\n * can short-circuit (see `lastReconcileRef` in `useBloc`).\n */\ninterface ReconcileDepSignature {\n paths: PathSet;\n key: string;\n refId: string;\n args: unknown;\n}\n\n/** Snapshot of the last FULL dep-reconcile layout effect run. */\ninterface ReconcileSignature {\n /** The primary container this signature was captured against (identity\n * check — a rebind/re-key swaps this even if the tracked paths happen to\n * be textually identical, and must never be mistaken for \"unchanged\"). */\n primaryContainer: StateContainer;\n primaryPaths: PathSet;\n deps: Map<StateContainer, ReconcileDepSignature>;\n}\n\n/** Per-access options shared by both dep accessors. */\ninterface DepAccessOptionsLike {\n args?: unknown;\n}\n\n/** Structural shape of a branded `depend()` handle as seen from React. */\ninterface DepHandleLike {\n track(options?: DepAccessOptionsLike): [unknown, StateContainer];\n untracked(options?: DepAccessOptionsLike): StateContainer;\n readonly [DEP_BRAND]: {\n Type: StateContainerConstructor;\n defaultArgs?: unknown;\n };\n}\n\n/**\n * Build the per-consumer wrapper that replaces a branded dep handle inside a\n * tracked getter's `this`. The wrapper exposes the same accessors as the core\n * handle and overrides `.track()`:\n *\n * - **Inside a render** (`trackedStateRef.current != null`): resolve (ENSURE,\n * no ref) the dep, `trackRender` its state, merge the recorded paths into the\n * session entry, build/reuse a tracked proxy for the dep so its OWN getters\n * track too, and return `[trackedValue, depProxy]`. The ownership ref is taken\n * by the layout-effect reconcile pass, not here.\n * - **Outside a render**: degrade to live `[dep.state, dep]` — matches the core\n * base impl, safe in event handlers/effects/methods.\n *\n * `.untracked()` always returns the live instance with no subscription.\n *\n * Args resolve at call time (`options.args ?? defaultArgs`), so a single handle\n * can resolve different dep instances across calls; tracked-proxy state is\n * therefore cached per resolved instance, not per handle. Guards against a\n * container re-entering tracking within the same render (mutual A↔B deps): if\n * the dep already has a non-primary session entry this render, reuse its proxy\n * + union its paths instead of re-acquiring.\n */\nfunction makeDepWrapper(\n handle: DepHandleLike,\n consumerId: string,\n trackedStateRef: { current: unknown },\n sessionRef: { current: Map<StateContainer, SessionEntry> },\n onDepHandle: (handle: object) => unknown,\n): DepHandleLike {\n const brand = handle[DEP_BRAND];\n const refId = depRefId(consumerId);\n const registry = getRegistry();\n // Per-resolved-instance tracked-state ref + proxy. Call-time args mean one\n // handle can resolve several instances, so cache is keyed by the instance.\n const perDep = new WeakMap<\n StateContainer,\n { ref: { current: unknown }; proxy: StateContainer }\n >();\n // One ProxyCache shared across every instance this handle resolves to\n // (call-time args can resolve different dep instances across calls) — safe\n // because ProxyCache's internal map is keyed by target object identity, so\n // unrelated instances' objects never collide in it.\n const proxyCache = new ProxyCache();\n // One-entry identity cache: skip re-hashing the structural key\n // (`resolveKey` -> `structuralKey` -> JSON.stringify) when `args` is the\n // same reference as last call — callers almost always pass a\n // reference-stable `args` (`options?.args ?? brand.defaultArgs`). Only the\n // derived key is cached; `ensure` still runs every call so a\n // disposed-and-recreated entry is always picked up.\n let lastArgs: unknown = ARGS_UNSET;\n let lastKey = '';\n\n const resolve = (options?: DepAccessOptionsLike) => {\n const args = options?.args ?? brand.defaultArgs;\n // `ARGS_UNSET` is module-private, so no caller's `args` can equal it — the\n // first call always misses and seeds `lastKey` before it is ever read.\n if (!Object.is(lastArgs, args)) {\n lastArgs = args;\n lastKey = registry.resolveKey(brand.Type, undefined, args);\n }\n const key = lastKey;\n const dep = registry.ensure(\n brand.Type,\n key,\n args,\n ) as unknown as StateContainer;\n return { dep, key, args };\n };\n\n const wrapper = {\n untracked: (options?: DepAccessOptionsLike) => resolve(options).dep,\n track: (options?: DepAccessOptionsLike) => {\n const { dep, key, args } = resolve(options);\n\n // Outside a render: live values, no subscription (core base behavior).\n if (trackedStateRef.current == null) {\n return [dep.state, dep];\n }\n\n const session = sessionRef.current;\n const existing = session.get(dep);\n\n // Render only ENSUREs the dep instance (via `resolve()` above); it does\n // NOT take a ref. Ownership is claimed by the layout-effect reconcile\n // pass-2 the first commit it sees this dep, and released on drop/unmount.\n // This keeps acquire/release paired so an uncommitted render can't leak a\n // dep ref (R4).\n\n const tracked = trackRender(dep.state, dep.interner, proxyCache);\n let cache = perDep.get(dep);\n if (cache === undefined) {\n const ref = { current: tracked.value as unknown };\n cache = { ref, proxy: buildTrackedProxy(dep, ref, onDepHandle).proxy };\n perDep.set(dep, cache);\n } else {\n cache.ref.current = tracked.value;\n }\n\n if (existing !== undefined) {\n // Re-entry this render (`.track()` twice, or a mutual cycle): union the\n // new paths into the existing entry rather than re-acquiring.\n existing.paths = unionPaths(existing.paths, tracked.paths);\n } else {\n session.set(dep, {\n kind: 'dep',\n paths: tracked.paths,\n Type: brand.Type,\n key,\n refId,\n args,\n });\n }\n\n return [tracked.value, cache.proxy];\n },\n } as DepHandleLike;\n\n Object.defineProperty(wrapper, DEP_BRAND, {\n value: brand,\n enumerable: false,\n writable: false,\n configurable: false,\n });\n\n return wrapper;\n}\n\n/** Union two PathSets (ALL_PATHS dominates). */\nfunction unionPaths(a: PathSet, b: PathSet): PathSet {\n if (a === ALL_PATHS || b === ALL_PATHS) return ALL_PATHS;\n const out = new Set<number>(a as Set<number>);\n for (const id of b as Set<number>) out.add(id);\n return out;\n}\n\n/**\n * Fold a per-mount isolation id into `args`. MERGES rather than replaces\n * (plan Q2=B): a class-level `static isolated` must not be silently cancelled\n * by a call site that happens to pass args. Non-object args are nested rather\n * than spread, so a primitive arg can't be destructured into index keys.\n */\nconst withIsolation = (base: unknown, id: string): object =>\n base === undefined\n ? { _blacIsolated: id }\n : typeof base === 'object' && base !== null\n ? { ...base, _blacIsolated: id }\n : { _blacArgs: base, _blacIsolated: id };\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n\n/**\n * Expand a PathSet to include an *ancestor-watch* id for every ancestor of\n * every tracked leaf.\n *\n * The auto-tracker records leaf paths (e.g. `'items.length'`), but\n * `StructuralContainer.patch` can only mark the parent (`'items'`) when it\n * replaces a value atomically (arrays, `null`, primitives — it can't see\n * inside). Without expansion, a subscriber with interest `{'items.length'}`\n * would miss a `patch`-triggered atomic-replacement of `items`.\n *\n * Ancestors are added under the interner's *ancestor-watch* lane\n * (`internAncestor`), NOT as normal ids. The source emits a matching\n * ancestor-watch mark only for paths it replaces atomically — never for a\n * plain-object structural pulse-up. So `{'items.length'}` wakes when the array\n * `items` is replaced, but `{'user.email'}` does NOT wake when a sibling\n * `user.name` changes and pulses `user` up: pulse-up `user` is a normal id and\n * the ancestor-watch `user` only intersects another ancestor-watch `user`.\n *\n * Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but\n * NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,\n * and `''` would wake this consumer on every field change).\n */\nfunction expandWithAncestors(paths: PathSet, interner: PathInterner): PathSet {\n if (paths === ALL_PATHS) return ALL_PATHS;\n const leafPaths = paths as Set<number>;\n if (leafPaths.size === 0) return paths;\n\n const expanded = new Set<number>(leafPaths);\n for (const id of leafPaths) {\n const str = interner.lookup(id);\n // Add all non-root ancestor segments as *ancestor-watch* ids: 'a.b.c' →\n // watch 'a.b' and 'a'. These live in the interner's ancestor lane so they\n // only intersect the source's atomic-replacement marks (`internAncestor`),\n // never a structural pulse-up mark of the same path. That is what lets a\n // descendant-reader (e.g. `items.length`) wake on an array/null replacement\n // without a sibling-leaf reader (`user.email`) waking when a sibling\n // (`user.name`) changes and pulses up through `user`.\n let idx = str.lastIndexOf('.');\n while (idx > 0) {\n const ancestor = str.slice(0, idx);\n expanded.add(interner.internAncestor(ancestor));\n idx = ancestor.lastIndexOf('.');\n }\n }\n return expanded;\n}\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":";;;;;AAkBA,MAAM,sBAAsB,8BAA+B,IAAI,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CpE,SAAgB,aAAkD,EAChE,MACA,MACA,YACqC;CACrC,MAAM,YAAY,WAAW,mBAAmB;CAIhD,MAAM,YAAY,cAAc;EAC9B,MAAM,OAAO,IAAI,IAAI,SAAS;EAC9B,KAAK,IAAI,MAAM,IAAI;EACnB,OAAO;CAET,GAAG;EAAC;EAAW;EAAM;CAAI,CAAC;CAE1B,OACE,oBAAC,oBAAoB,UAArB;EAA8B,OAAO;EAClC;CAC2B,CAAA;AAElC;;;;;;;AAQA,SAAgB,gBACd,WAC4B;CAE5B,OADY,WAAW,mBACd,EAAE,IAAI,SAAS;AAC1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACrEA,SAAgB,kBACd,UACA,iBACA,aAC4B;CAO5B,MAAM,8BAAc,IAAI,IAAyC;CACjE,IAAI,QAAQ,OAAO,eAAe,QAAQ;CAC1C,OAAO,SAAS,UAAU,OAAO,WAAW;EAC1C,MAAM,OAA4B,CAChC,GAAG,OAAO,oBAAoB,KAAK,GACnC,GAAG,OAAO,sBAAsB,KAAK,CACvC;EACA,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,OAAO,OAAO,yBAAyB,OAAO,GAAG;GACvD,IAAI,MAAM,OAAO,CAAC,YAAY,IAAI,GAAG,GAAG,YAAY,IAAI,KAAK,IAAI;EACnE;EACA,QAAQ,OAAO,eAAe,KAAK;CACrC;CASA,MAAM,YAAY,IAAI,MAAM,UAAoB,EAC9C,IAAI,GAAG,GAAG,GAAG;EACX,IAAI,MAAM,SAAS,OAAO,gBAAgB,WAAW,QAAQ,IAAI,GAAG,GAAG,CAAC;EACxE,MAAM,QAAQ,QAAQ,IAAI,GAAG,GAAG,CAAC;EAGjC,IACE,gBAAgB,KAAA,MACf,OAAO,UAAU,cAAc,OAAO,UAAU,aACjD,UAAU,QACT,MAAkC,eAAe,KAAA,GAElD,OAAO,YAAY,KAAe;EAEpC,OAAO;CACT,EACF,CAAC;CAYD,OAAO;EAAE,OAAA,IARS,MAAM,UAAoB,EAC1C,IAAI,QAAQ,KAAK,UAAU;GACzB,MAAM,OAAO,YAAY,IAAI,GAAG;GAChC,IAAI,MAAM,KAAK,OAAO,KAAK,IAAI,KAAK,SAAS;GAC7C,OAAO,QAAQ,IAAI,QAAQ,KAAK,QAAQ;EAC1C,EACF,CAEa;EAAc;CAAe;AAC5C;;;ACxDA,IAAI,iBAAiB;AAKrB,MAAM,aAA4B,OAAO,mBAAmB;;;;;;;;;;;AAY5D,SAAS,WAAc,QAAiC;CACtD,MAAM,MAAM,OAAiB,IAAI;CACjC,IAAI,IAAI,YAAY,MAClB,IAAI,UAAU,OAAO;CAEvB,OAAO;AACT;AAMA,MAAM,yBAAuC,CAAC;AAC9C,MAAM,yBAAoE;CACxE,KAAK;CACL,KAAK,KAAA;AACP;AACA,MAAM,sCAAyD,IAAI,IAAI;AACvE,MAAM,sCAAmD,IAAI,IAAI;AACjE,MAAM,2CAAqD,IAAI,QAAQ;AACvE,MAAM,sBAAkC,IAAI,WAAW;AAKvD,MAAM,gBAAgB,eAA+B,WAAW;AAChE,MAAM,YAAY,eAA+B,WAAW,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDvE,SAAgB,QAGd,WACA,SACmC;CAGnC,MAAM,eAAe,WAAW,eAAe;CAK/C,MAAM,gBAAgB,OAAsB,IAAI;CAChD,IAAI,cAAc,YAAY,MAC5B,cAAc,UAAU,WAAW;CAErC,MAAM,aAAa,cAAc;CAKjC,MAAM,WAAW,SAAS,aAAa,QAAQ,gBAAgB,SAAS;CAGxE,MAAM,eAAe,WAAW,aAAa,KAAA;CAI7C,MAAM,YAAY,OAAO,SAAS,MAAM;CACxC,UAAU,UAAU,SAAS;CAC7B,MAAM,eAAe,SAAS,WAAW,KAAA;CACzC,MAAM,aAAa,OAAO,SAAS,OAAO;CAC1C,WAAW,UAAU,SAAS;CAC9B,MAAM,eAAe,OAAO,SAAS,SAAS;CAC9C,aAAa,UAAU,SAAS;CAChC,MAAM,cAAc,OAAO,QAAQ;CACnC,YAAY,UAAU;CAYtB,MAAM,UAAW,SAAmD;CACpE,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CAIrB,MAAM,gBAAgB,WAAW,eAAe;CAChD,IAAI,CAAC,OAAO,GAAG,cAAc,QAAQ,KAAK,OAAO,GAC/C,cAAc,UAAU;EACtB,KAAK;EACL,KAAK,YAAY,KAAA,IAAY,KAAA,IAAY,KAAK,UAAU,OAAO;CACjE;CAEF,MAAM,aAAa,cAAc,QAAQ;CAGzC,MAAM,eAAe,gBAAgB,SAAS;CAC9C,MAAM,kBAAkB,OAAO,YAAY;CAC3C,gBAAgB,UAAU;CAE1B,MAAM,qBAAqB,WAAW,eAAe;CACrD,IAAI,CAAC,OAAO,GAAG,mBAAmB,QAAQ,KAAK,YAAY,GACzD,mBAAmB,UAAU;EAC3B,KAAK;EACL,KACE,iBAAiB,KAAA,IAAY,KAAA,IAAY,KAAK,UAAU,YAAY;CACxE;CAEF,MAAM,kBAAkB,mBAAmB,QAAQ;CAQnD,MAAM,kBAAkB,OAAgB,IAAI;CAa5C,MAAM,aAAa,WAAW,aAAa;CAG3C,MAAM,aAAa,WAAW,aAAa;CAG3C,MAAM,qBAAqB,WAAW,kBAAkB;CACxD,MAAM,gBAAgB,WAAW,aAAa;CAK9C,MAAM,mBAAmB,OAAkC,IAAI;CAU/D,MAAM,CAAC,aAAa,cAAc,YAAY,MAAc,IAAI,GAAG,CAAC;CAGpE,MAAM,eAAe,OAAqB,IAAI;CAE9C,MAAM,EAAE,MAAM,aAAa,gBAAgB,cAIlC;EAEP,MAAM,WACJ,WAAW,YAAY,KAAA,IACnB,WAAW,UACX,gBAAgB;EAQtB,MAAM,gBAAgB,YAAY,UAC7B,cAAc,UAAU,UAAU,IACnC;EAEJ,MAAM,cAAc,mBAAmB,WAAW,aAAa;EAK/D,MAAM,WAJW,YAIO,EAAE,QAAQ,WAAW,aAAa;GACxD,WAAW;GACX,UAAU;GACV,MAAM;EACR,CAAC;EAOD,MAAM,eAAe,WAA4B;GAC/C,MAAM,QAAQ,mBAAmB;GACjC,MAAM,SAAS,MAAM,IAAI,MAAM;GAC/B,IAAI,WAAW,KAAA,GAAW,OAAO;GACjC,MAAM,UAAU,eACd,QACA,YACA,iBACA,YACA,WACF;GACA,MAAM,IAAI,QAAQ,OAAO;GACzB,OAAO;EACT;EAEA,MAAM,EAAE,UAAU,kBAChB,UACA,iBACA,WACF;EAEA,OAAO;GACL,MAAM;GACN,aAAa;GACb,aAAa;EACf;CAEF,GAAG;EAAC;EAAW;EAAY;EAAiB;EAAc;CAAW,CAAC;CAUtE,MAAM,GAAG,SAAS,YAAY,MAAc,IAAI,GAAG,CAAC;CACpD,MAAM,UAAU,WAAW,YAAY;CASvC,MAAM,sBAAsB,WAAW,YAAY;CAGnD,MAAM,mBAAmB,OAAyB,IAAI;CAItD,MAAM,iBAAiB,OAAgB,KAAA,CAAS;CAGhD,MAAM,cAAc,OAAgB,IAAI;CAExC,gBAAgB;EAYd,MAAM,UAAW,KAAmC;EAEpD,IAAI,cAAc;GAChB,MAAM,QAAQ,QAAQ,gBACd,iBACA;IACJ,MAAM,SAAS,UAAU;IACzB,IAAI,CAAC,QAAQ;KACX,MAAM;KACN;IACF;IACA,MAAM,OAAO,OACV,KAAmC,OACpC,IACF;IACA,MAAM,OAAO,iBAAiB;IAC9B,IAAI,SAAS,QAAQ,kBAAkB,MAAM,IAAI,GAAG;IACpD,iBAAiB,UAAU;IAC3B,MAAM;GACR,CACF;GAIA,MAAM,SAAS,UAAU;GACzB,IAAI,QAAQ;IACV,MAAM,OAAO,OACV,KAAmC,OACpC,IACF;IACA,MAAM,OAAO,iBAAiB;IAC9B,IAAI,SAAS,QAAQ,CAAC,kBAAkB,MAAM,IAAI,GAAG;KACnD,iBAAiB,UAAU;KAC3B,MAAM;IACR;GACF;GACA,OAAO;EACT;EASA,MAAM,QAAQ,QAAQ,gBACd,oBAAoB,eACpB,MAAM,CACd;EAIA,KAAoC,sBAClC,YACA,QAAQ,OACV;EAIA,IAAK,KAAmC,UAAU,eAAe,SAC/D,MAAM;EAER,aAAa;GACX,MAAM;GACN,KAAoC,mBAAmB,UAAU;EACnE;CAEF,GAAG;EAAC;EAAM;EAAY;CAAY,CAAC;CAiCnC,sBAAsB;EACpB,MAAM,WAAW,YAAY;EAC7B,MAAM,OAAO,SAAS,QAAQ,WAAW,aAAa;GACpD,WAAW;GACX,UAAU;GACV,OAAO,aAAa,UAAU;EAChC,CAAC;EACD,aAAa,UAAU;EACvB,WAAW,UAAU,IAAuB;EAM5C,IAAI,SAAS,MACX,WAAW;EAEb,aAAa;GACX,aAAa,UAAW,aAAa,WAAW,IAAwB;GACxE,SAAS,QAAQ,WAAW,aAAa,OAAO,aAAa,UAAU,CAAC;EAC1E;CAEF,GAAG;EAAC;EAAW;EAAa;CAAU,CAAC;CAUvC,MAAM,WAAY,KAAmC;CAGrD,eAAe,UAAU;CAIzB,IAAI,YAAY,YAAY,MAAM;EAChC,YAAY,UAAU;EACtB,iBAAiB,UAAU;CAC7B;CACA,IAAI;CACJ,IAAI,UAAU,YAAY,KAAA,GAAW;EACnC,QAAQ;EAGR,IAAI,iBAAiB,YAAY,MAC/B,iBAAiB,UAAU,UAAU,QACnC,UACA,IACF;CAEJ,OAAO;EACL,MAAM,UAAU,YACd,UACC,KAAmC,UACpC,cAAc,OAChB;EACA,QAAQ,QAAQ;EAChB,gBAAgB,UAAU,QAAQ;EAClC,QAAQ,UAAU,QAAQ;EAO1B,MAAM,UAAU,WAAW;EAC3B,QAAQ,MAAM;EACd,QAAQ,IAAI,MAAmC;GAC7C,MAAM;GACN,OAAO,QAAQ;EACjB,CAAC;EAOD,eAAe,QAAQ,MAAM;CAQ/B;CAWA,sBAAsB;EAKpB,gBAAgB,UAAU;EAC1B,IAAI,UAAU,YAAY,KAAA,GAAW;GAInC,iBAAiB,UAAU;GAC3B;EACF;EACA,MAAM,YAAY;EAClB,MAAM,QAAQ,QAAQ;EACtB,MAAM,UAAU,WAAW;EAY3B,MAAM,OAAO,iBAAiB;EAC9B,IACE,SAAS,QACT,KAAK,qBAAqB,aAC1B,cAAc,KAAK,cAAc,KAAK,GACtC;GACA,IAAI,YAAY,KAAK,KAAK,SAAS,QAAQ,OAAO;GAClD,IAAI,WACF,KAAK,MAAM,CAAC,cAAc,UAAU,SAAS;IAC3C,IAAI,MAAM,SAAS,WAAW;IAC9B,MAAM,YAAY,KAAK,KAAK,IAAI,YAAY;IAC5C,IACE,cAAc,KAAA,KACd,UAAU,QAAQ,MAAM,OACxB,UAAU,UAAU,MAAM,SAC1B,CAAC,OAAO,GAAG,UAAU,MAAM,MAAM,IAAI,KACrC,CAAC,cAAc,UAAU,OAAO,MAAM,KAAK,GAC3C;KACA,YAAY;KACZ;IACF;GACF;GAEF,IAAI,WAAW;EACjB;EAEA,UAAU,sBAAsB,YAAY,KAAK;EAKjD,oBAAoB,UAAU,oBAC5B,OACA,UAAU,QACZ;EAaA,MAAM,OAAO,WAAW;EAGxB,KAAK,MAAM,CAAC,cAAc,QAAQ,MAChC,IAAI,CAAC,QAAQ,IAAI,YAAY,GAAG;GAC9B,IAAI,YAAY;GAChB,aAAa,mBAAmB,UAAU;GAC1C,YAAY,EAAE,QAAQ,IAAI,MAAM,IAAI,KAAK,OAAO,IAAI,KAAK;GACzD,KAAK,OAAO,YAAY;EAC1B;EAIF,KAAK,MAAM,CAAC,cAAc,UAAU,SAAS;GAC3C,IAAI,MAAM,SAAS,WAAW;GAC9B,MAAM,WAAW,oBAAoB,MAAM,OAAO,aAAa,QAAQ;GACvE,aAAa,sBAAsB,YAAY,MAAM,KAAK;GAC1D,MAAM,WAAW,KAAK,IAAI,YAAY;GACtC,IAAI,UAAU;IACZ,SAAS,YAAY,UAAU;IAC/B;GACF;GAGA,YAAY,EAAE,QAAQ,MAAM,MAAM,MAAM,KAAK;IAC3C,WAAW;IACX,UAAU;IACV,OAAO,MAAM;IACb,MAAM,MAAM;GACd,CAAC;GACD,MAAM,cAAoC,EAAE,SAAS,SAAS;GAC9D,MAAM,cAAc,aAAa,QAAQ,gBACjC,YAAY,eACZ,MAAM,CACd;GACA,KAAK,IAAI,cAAc;IACrB;IACA;IACA,MAAM,MAAM;IACZ,KAAK,MAAM;IACX,OAAO,MAAM;IACb,MAAM,MAAM;GACd,CAAC;EACH;EAMA,MAAM,gCAAgB,IAAI,IAA2C;EACrE,KAAK,MAAM,CAAC,cAAc,UAAU,SAAS;GAC3C,IAAI,MAAM,SAAS,WAAW;GAC9B,cAAc,IAAI,cAAc;IAC9B,OAAO,MAAM;IACb,KAAK,MAAM;IACX,OAAO,MAAM;IACb,MAAM,MAAM;GACd,CAAC;EACH;EACA,iBAAiB,UAAU;GACzB,kBAAkB;GAClB,cAAc;GACd,MAAM;EACR;CACF,CAAC;CAOD,gBAAgB;EAGd,MAAM,OAAO,WAAW;EACxB,aAAa;GACX,KAAK,MAAM,CAAC,cAAc,QAAQ,MAAM;IACtC,IAAI,YAAY;IAChB,aAAa,mBAAmB,UAAU;IAC1C,YAAY,EAAE,QAAQ,IAAI,MAAM,IAAI,KAAK,OAAO,IAAI,KAAK;GAC3D;GACA,KAAK,MAAM;EACb;CAIF,GAAG,CAAC,YAAY,UAAU,CAAC;CAE3B,OAAO;EACL;EACA;EACA;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;AAoGA,SAAS,eACP,QACA,YACA,iBACA,YACA,aACe;CACf,MAAM,QAAQ,OAAO;CACrB,MAAM,QAAQ,SAAS,UAAU;CACjC,MAAM,WAAW,YAAY;CAG7B,MAAM,yBAAS,IAAI,QAGjB;CAKF,MAAM,aAAa,IAAI,WAAW;CAOlC,IAAI,WAAoB;CACxB,IAAI,UAAU;CAEd,MAAM,WAAW,YAAmC;EAClD,MAAM,OAAO,SAAS,QAAQ,MAAM;EAGpC,IAAI,CAAC,OAAO,GAAG,UAAU,IAAI,GAAG;GAC9B,WAAW;GACX,UAAU,SAAS,WAAW,MAAM,MAAM,KAAA,GAAW,IAAI;EAC3D;EACA,MAAM,MAAM;EAMZ,OAAO;GAAE,KALG,SAAS,OACnB,MAAM,MACN,KACA,IAES;GAAG;GAAK;EAAK;CAC1B;CAEA,MAAM,UAAU;EACd,YAAY,YAAmC,QAAQ,OAAO,EAAE;EAChE,QAAQ,YAAmC;GACzC,MAAM,EAAE,KAAK,KAAK,SAAS,QAAQ,OAAO;GAG1C,IAAI,gBAAgB,WAAW,MAC7B,OAAO,CAAC,IAAI,OAAO,GAAG;GAGxB,MAAM,UAAU,WAAW;GAC3B,MAAM,WAAW,QAAQ,IAAI,GAAG;GAQhC,MAAM,UAAU,YAAY,IAAI,OAAO,IAAI,UAAU,UAAU;GAC/D,IAAI,QAAQ,OAAO,IAAI,GAAG;GAC1B,IAAI,UAAU,KAAA,GAAW;IACvB,MAAM,MAAM,EAAE,SAAS,QAAQ,MAAiB;IAChD,QAAQ;KAAE;KAAK,OAAO,kBAAkB,KAAK,KAAK,WAAW,EAAE;IAAM;IACrE,OAAO,IAAI,KAAK,KAAK;GACvB,OACE,MAAM,IAAI,UAAU,QAAQ;GAG9B,IAAI,aAAa,KAAA,GAGf,SAAS,QAAQ,WAAW,SAAS,OAAO,QAAQ,KAAK;QAEzD,QAAQ,IAAI,KAAK;IACf,MAAM;IACN,OAAO,QAAQ;IACf,MAAM,MAAM;IACZ;IACA;IACA;GACF,CAAC;GAGH,OAAO,CAAC,QAAQ,OAAO,MAAM,KAAK;EACpC;CACF;CAEA,OAAO,eAAe,SAAS,WAAW;EACxC,OAAO;EACP,YAAY;EACZ,UAAU;EACV,cAAc;CAChB,CAAC;CAED,OAAO;AACT;;AAGA,SAAS,WAAW,GAAY,GAAqB;CACnD,IAAI,MAAM,aAAa,MAAM,WAAW,OAAO;CAC/C,MAAM,MAAM,IAAI,IAAY,CAAgB;CAC5C,KAAK,MAAM,MAAM,GAAkB,IAAI,IAAI,EAAE;CAC7C,OAAO;AACT;;;;;;;AAQA,MAAM,iBAAiB,MAAe,OACpC,SAAS,KAAA,IACL,EAAE,eAAe,GAAG,IACpB,OAAO,SAAS,YAAY,SAAS,OACnC;CAAE,GAAG;CAAM,eAAe;AAAG,IAC7B;CAAE,WAAW;CAAM,eAAe;AAAG;AAE7C,MAAM,qBAAqB,GAAc,MAA0B;CACjE,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAClC,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAC5B,IAAI,CAAC,OAAO,GAAG,EAAE,IAAI,EAAE,EAAE,GAAG,OAAO;CAErC,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAS,oBAAoB,OAAgB,UAAiC;CAC5E,IAAI,UAAU,WAAW,OAAO;CAChC,MAAM,YAAY;CAClB,IAAI,UAAU,SAAS,GAAG,OAAO;CAEjC,MAAM,WAAW,IAAI,IAAY,SAAS;CAC1C,KAAK,MAAM,MAAM,WAAW;EAC1B,MAAM,MAAM,SAAS,OAAO,EAAE;EAQ9B,IAAI,MAAM,IAAI,YAAY,GAAG;EAC7B,OAAO,MAAM,GAAG;GACd,MAAM,WAAW,IAAI,MAAM,GAAG,GAAG;GACjC,SAAS,IAAI,SAAS,eAAe,QAAQ,CAAC;GAC9C,MAAM,SAAS,YAAY,GAAG;EAChC;CACF;CACA,OAAO;AACT;ACj+BA,IAAI,eAAgC,CAAmB;;;;;;AAOvD,SAAgB,mBAAmB,QAAwC;CACzE,eAAe;EAAE,GAAG;EAAc,GAAG;CAAO;AAC9C"}
|
package/dist/types.d.ts
CHANGED
|
@@ -19,7 +19,8 @@ type ArgsOption<T extends StateContainerConstructor> = ExtractArgs<T> extends vo
|
|
|
19
19
|
*
|
|
20
20
|
* @remarks
|
|
21
21
|
* Instance identity is derived entirely from `args`. For a per-mount private
|
|
22
|
-
* instance, pass a stable unique object: `{ args: { _id: useId() } }
|
|
22
|
+
* instance, pass a stable unique object: `{ args: { _id: useId() } }`, or set
|
|
23
|
+
* `isolated: true` (equivalent, no `useId()` needed).
|
|
23
24
|
*
|
|
24
25
|
* To re-render on a fixed set of values rather than on auto-tracked reads, pass
|
|
25
26
|
* `select` — it returns a tuple/array that is compared per-index via
|
|
@@ -46,6 +47,15 @@ export type UseBlocOptions<TBloc extends StateContainerConstructor> = ArgsOption
|
|
|
46
47
|
* registry releases its ref, so the bloc is still alive when this runs.
|
|
47
48
|
*/
|
|
48
49
|
onUnmount?: (bloc: InstanceType<TBloc>) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Give this mount a private instance nobody else shares. Sugar over args
|
|
52
|
+
* identity — it injects a stable per-mount id into `args`; there is no
|
|
53
|
+
* separate identity mechanism. Equivalent to `static isolated = true` on the
|
|
54
|
+
* class; both present is fine.
|
|
55
|
+
*
|
|
56
|
+
* Note: the injected id is visible in the `args` object passed to `init()`.
|
|
57
|
+
*/
|
|
58
|
+
isolated?: boolean;
|
|
49
59
|
};
|
|
50
60
|
/**
|
|
51
61
|
* Tuple return type from {@link useBloc}.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blac/react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Brendan Mullins <jsnanigans@gmail.com>",
|
|
6
6
|
"description": "React bindings for BlaC — useBloc hook with automatic re-render optimization",
|
|
@@ -78,13 +78,13 @@
|
|
|
78
78
|
}
|
|
79
79
|
],
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"@dirtytalk/structural": "^0.0.
|
|
81
|
+
"@dirtytalk/structural": "^0.0.9"
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
84
|
"@testing-library/react": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
85
85
|
"@types/react": "^18.0.0 || ^19.0.0",
|
|
86
86
|
"react": "^18.0.0 || ^19.0.0",
|
|
87
|
-
"@blac/core": "^2.0
|
|
87
|
+
"@blac/core": "^2.1.0"
|
|
88
88
|
},
|
|
89
89
|
"peerDependenciesMeta": {
|
|
90
90
|
"@types/react": {
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
121
121
|
"vite-plus": "^0.1.23",
|
|
122
122
|
"vitest": "npm:@voidzero-dev/vite-plus-test@latest",
|
|
123
|
-
"@blac/core": "2.0
|
|
123
|
+
"@blac/core": "2.1.0"
|
|
124
124
|
},
|
|
125
125
|
"scripts": {
|
|
126
126
|
"dev": "vp pack --watch",
|