@blac/react
Advanced tools
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7D,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC"} |
| import type { ExtractState } from '@blac/core'; | ||
| import type { RefObject } from 'react'; | ||
| export interface UseBlocOptions<TBloc> { | ||
| staticProps?: any; | ||
| instanceId?: string | number; | ||
| dependencies?: (state: ExtractState<TBloc>, bloc: TBloc) => unknown[]; | ||
| autoTrack?: boolean; | ||
| disableGetterCache?: boolean; | ||
| onMount?: (bloc: TBloc) => void; | ||
| onUnmount?: (bloc: TBloc) => void; | ||
| } | ||
| export interface UseBlocOptionsWithDependencies<TBloc> extends UseBlocOptions<TBloc> { | ||
| dependencies: (state: ExtractState<TBloc>, bloc: TBloc) => unknown[]; | ||
| autoTrack?: never; | ||
| } | ||
| export type UseBlocReturn<TBloc> = [ | ||
| ExtractState<TBloc>, | ||
| TBloc, | ||
| RefObject<ComponentRef> | ||
| ]; | ||
| export type ComponentRef = { | ||
| __blocInstanceId?: string; | ||
| __bridge?: any; | ||
| }; | ||
| //# sourceMappingURL=types.d.ts.map |
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,cAAc,CAAC,KAAK;IACnC,WAAW,CAAC,EAAE,GAAG,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,KAAK,KAAK,OAAO,EAAE,CAAC;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;IAChC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,8BAA8B,CAAC,KAAK,CACnD,SAAQ,cAAc,CAAC,KAAK,CAAC;IAC7B,YAAY,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,KAAK,KAAK,OAAO,EAAE,CAAC;IACrE,SAAS,CAAC,EAAE,KAAK,CAAC;CACnB;AAED,MAAM,MAAM,aAAa,CAAC,KAAK,IAAI;IACjC,YAAY,CAAC,KAAK,CAAC;IACnB,KAAK;IACL,SAAS,CAAC,YAAY,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB,CAAC"} |
| import { type BlocConstructor, StateContainer } from '@blac/core'; | ||
| import type { UseBlocOptions, UseBlocReturn } from './types'; | ||
| /** | ||
| * Lifecycle: INITIAL MOUNT | ||
| * 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn | ||
| * 2. useSyncExternalStore calls getSnapshotFn (1st time) - lazy creates tracker, starts tracking, returns proxy | ||
| * 3. Component renders - proxy tracks property accesses | ||
| * 4. useSyncExternalStore calls getSnapshotFn (2nd time) - captures tracked paths, starts new tracking, returns proxy | ||
| * 5. useSyncExternalStore calls subscribeFn - sets up state change listener | ||
| * | ||
| * Lifecycle: STATE CHANGE | ||
| * 1. Bloc state changes | ||
| * 2. subscribeFn callback checks hasChanges() - only re-renders if tracked paths changed | ||
| * 3. If re-render: getSnapshotFn captures previous paths, starts tracking, returns proxy | ||
| * | ||
| * Lifecycle: RE-RENDER (parent re-render) | ||
| * 1. useMemo returns cached values (same bloc, subscribeFn, getSnapshotFn) | ||
| * 2. useSyncExternalStore calls getSnapshotFn - captures paths, starts tracking, returns proxy | ||
| */ | ||
| export declare function useBloc<T extends new (...args: any[]) => StateContainer<any>>(BlocClass: T & BlocConstructor<InstanceType<T>>, options?: UseBlocOptions<InstanceType<T>>): UseBlocReturn<InstanceType<T>>; | ||
| //# sourceMappingURL=useBloc.d.ts.map |
| {"version":3,"file":"useBloc.d.ts","sourceRoot":"","sources":["../src/useBloc.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,KAAK,eAAe,EACpB,cAAc,EAef,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAgB,MAAM,SAAS,CAAC;AAuB3E;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,cAAc,CAAC,GAAG,CAAC,EAC3E,SAAS,EAAE,CAAC,GAAG,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAC/C,OAAO,CAAC,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GACxC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAmHhC"} |
| import { type BlocConstructor, StateContainer } from '@blac/core'; | ||
| export interface UseBlocActionsOptions<TBloc> { | ||
| staticProps?: any; | ||
| instanceId?: string | number; | ||
| onMount?: (bloc: TBloc) => void; | ||
| onUnmount?: (bloc: TBloc) => void; | ||
| } | ||
| export declare function useBlocActions<T extends new (...args: any[]) => StateContainer<any>>(BlocClass: T & BlocConstructor<InstanceType<T>>, options?: UseBlocActionsOptions<InstanceType<T>>): InstanceType<T>; | ||
| //# sourceMappingURL=useBlocActions.d.ts.map |
| {"version":3,"file":"useBlocActions.d.ts","sourceRoot":"","sources":["../src/useBlocActions.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,eAAe,EACpB,cAAc,EAEf,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,qBAAqB,CAAC,KAAK;IAC1C,WAAW,CAAC,EAAE,GAAG,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;IAChC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;CACnC;AAED,wBAAgB,cAAc,CAC5B,CAAC,SAAS,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,cAAc,CAAC,GAAG,CAAC,EAErD,SAAS,EAAE,CAAC,GAAG,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAC/C,OAAO,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GAC/C,YAAY,CAAC,CAAC,CAAC,CAiDjB"} |
| /** | ||
| * Instance key generation utilities for React integration | ||
| */ | ||
| import type { ComponentRef } from '../types'; | ||
| /** | ||
| * Generate an instance key for a bloc | ||
| * | ||
| * Logic: | ||
| * - If user provides instanceId, use it (convert number to string) | ||
| * - If isolated, generate or reuse a unique key for this component | ||
| * - Otherwise, return undefined (use default key) | ||
| * | ||
| * @param componentRef - React component reference (persists across remounts) | ||
| * @param isIsolated - Whether the bloc is isolated | ||
| * @param providedId - User-provided instance ID (from options) | ||
| * @returns Instance key string or undefined for default | ||
| */ | ||
| export declare function generateInstanceKey(componentRef: ComponentRef, isIsolated: boolean, providedId?: string | number): string | undefined; | ||
| //# sourceMappingURL=instance-keys.d.ts.map |
| {"version":3,"file":"instance-keys.d.ts","sourceRoot":"","sources":["../../src/utils/instance-keys.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,OAAO,EACnB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAC3B,MAAM,GAAG,SAAS,CAgBpB"} |
+48
-312
@@ -1,195 +0,31 @@ | ||
| //#region rolldown:runtime | ||
| var __create = Object.create; | ||
| var __defProp = Object.defineProperty; | ||
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
| var __getOwnPropNames = Object.getOwnPropertyNames; | ||
| var __getProtoOf = Object.getPrototypeOf; | ||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
| var __copyProps = (to, from, except, desc) => { | ||
| if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { | ||
| key = keys[i]; | ||
| if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { | ||
| get: ((k) => from[k]).bind(null, key), | ||
| enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable | ||
| }); | ||
| } | ||
| return to; | ||
| }; | ||
| var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { | ||
| value: mod, | ||
| enumerable: true | ||
| }) : target, mod)); | ||
| //#endregion | ||
| let react = require("react"); | ||
| react = __toESM(react); | ||
| let __blac_core = require("@blac/core"); | ||
| __blac_core = __toESM(__blac_core); | ||
| //#region src/useBloc.ts | ||
| //#region src/utils/instance-keys.ts | ||
| /** | ||
| * Cache for property descriptors to avoid repeated prototype chain walks | ||
| * Maps from object constructor to a map of property name to descriptor | ||
| * Instance key generation utilities for React integration | ||
| */ | ||
| const descriptorCache = /* @__PURE__ */ new WeakMap(); | ||
| /** | ||
| * Cache for proxied blocs to ensure same proxy is returned for same bloc instance | ||
| * This is important for identity checks (e.g., bloc1 === bloc2) across components | ||
| * using the same shared bloc instance. | ||
| */ | ||
| const blocProxyCache = /* @__PURE__ */ new WeakMap(); | ||
| /** | ||
| * Map to store the currently active tracker during render. | ||
| * This allows the cached proxy to know which component's tracker to use. | ||
| * Set before render, cleared after render. | ||
| */ | ||
| const activeTrackerMap = /* @__PURE__ */ new WeakMap(); | ||
| /** | ||
| * Get property descriptor for a given property, with caching | ||
| * Generate an instance key for a bloc | ||
| * | ||
| * @remarks | ||
| * Walks up the prototype chain once per class to find the descriptor, | ||
| * then caches the result for performance. This is critical because | ||
| * we need to distinguish getters from methods and properties. | ||
| * Logic: | ||
| * - If user provides instanceId, use it (convert number to string) | ||
| * - If isolated, generate or reuse a unique key for this component | ||
| * - Otherwise, return undefined (use default key) | ||
| * | ||
| * @param obj - The object to get the descriptor from | ||
| * @param prop - The property name or symbol | ||
| * @returns The property descriptor if found, undefined otherwise | ||
| * @param componentRef - React component reference (persists across remounts) | ||
| * @param isIsolated - Whether the bloc is isolated | ||
| * @param providedId - User-provided instance ID (from options) | ||
| * @returns Instance key string or undefined for default | ||
| */ | ||
| function getDescriptor(obj, prop) { | ||
| const constructor = obj.constructor; | ||
| let constructorCache = descriptorCache.get(constructor); | ||
| if (constructorCache?.has(prop)) return constructorCache.get(prop); | ||
| let current = obj; | ||
| let descriptor; | ||
| while (current && current !== Object.prototype) { | ||
| descriptor = Object.getOwnPropertyDescriptor(current, prop); | ||
| if (descriptor) break; | ||
| current = Object.getPrototypeOf(current); | ||
| } | ||
| if (!constructorCache) { | ||
| constructorCache = /* @__PURE__ */ new Map(); | ||
| descriptorCache.set(constructor, constructorCache); | ||
| } | ||
| constructorCache.set(prop, descriptor); | ||
| return descriptor; | ||
| } | ||
| /** | ||
| * Check if a property is a getter (has a getter descriptor) | ||
| * | ||
| * @param obj - The object to check | ||
| * @param prop - The property name or symbol | ||
| * @returns True if the property is a getter, false otherwise | ||
| */ | ||
| function isGetter(obj, prop) { | ||
| return getDescriptor(obj, prop)?.get !== void 0; | ||
| } | ||
| /** | ||
| * Create a new getter tracking state | ||
| * | ||
| * @returns A new GetterTrackingState initialized with empty collections | ||
| */ | ||
| function createGetterTracker() { | ||
| return { | ||
| trackedValues: /* @__PURE__ */ new Map(), | ||
| currentlyAccessing: /* @__PURE__ */ new Set(), | ||
| trackedGetters: /* @__PURE__ */ new Set(), | ||
| isTracking: false, | ||
| renderCache: /* @__PURE__ */ new Map(), | ||
| cacheValid: false | ||
| }; | ||
| } | ||
| /** | ||
| * Create a proxy that intercepts getter access on a bloc instance | ||
| * | ||
| * @remarks | ||
| * This proxy wraps the bloc instance to track which getters are accessed | ||
| * during component render. When tracking is enabled (during render phase), | ||
| * it records accessed getters and stores their computed values for later | ||
| * comparison. | ||
| * | ||
| * IMPORTANT: This function caches proxies per bloc instance. Multiple components | ||
| * sharing the same bloc will get the same proxy instance. Each component sets | ||
| * its tracker in activeTrackerMap before render, and the proxy looks it up. | ||
| * | ||
| * @param bloc - The bloc instance to wrap | ||
| * @returns A proxied bloc that tracks getter access | ||
| */ | ||
| function createBlocProxy(bloc) { | ||
| const cached = blocProxyCache.get(bloc); | ||
| if (cached) return cached; | ||
| const proxy = new Proxy(bloc, { get(target, prop, receiver) { | ||
| const tracker = activeTrackerMap.get(target); | ||
| if (tracker?.isTracking && isGetter(target, prop)) { | ||
| tracker.currentlyAccessing.add(prop); | ||
| if (tracker.cacheValid && tracker.renderCache.has(prop)) { | ||
| const cachedValue = tracker.renderCache.get(prop); | ||
| tracker.trackedValues.set(prop, cachedValue); | ||
| return cachedValue; | ||
| } | ||
| const value = getDescriptor(target, prop).get.call(target); | ||
| tracker.trackedValues.set(prop, value); | ||
| return value; | ||
| } | ||
| return Reflect.get(target, prop, receiver); | ||
| } }); | ||
| blocProxyCache.set(bloc, proxy); | ||
| return proxy; | ||
| } | ||
| /** | ||
| * Check if any tracked getters have changed values | ||
| * | ||
| * @remarks | ||
| * Re-computes all getters that were accessed during the last render and | ||
| * compares their new values with stored values using Object.is() (reference | ||
| * equality). | ||
| * | ||
| * OPTIMIZATION: Render cache population | ||
| * - Computes ALL tracked getters (no early exit) to populate the render cache | ||
| * - This ensures each getter is computed only once per render cycle | ||
| * - If we're going to re-render, getters accessed during render will use cached values | ||
| * - Cache is populated even if no changes detected (useful for parent-triggered re-renders) | ||
| * - Trade-off: Give up early exit to ensure full cache population | ||
| * | ||
| * Error handling: If a getter throws during re-computation, we log a warning, | ||
| * stop tracking that specific getter, and treat it as "changed" to trigger | ||
| * a re-render. This prevents the tracking system from breaking while still | ||
| * allowing React's error boundary to handle the error on the next render. | ||
| * | ||
| * @param bloc - The bloc instance | ||
| * @param tracker - The getter tracking state | ||
| * @returns True if any tracked getter value changed, false otherwise | ||
| */ | ||
| function hasGetterChanges(bloc, tracker) { | ||
| if (!tracker || tracker.trackedGetters.size === 0) return false; | ||
| tracker.renderCache.clear(); | ||
| let hasAnyChange = false; | ||
| for (const prop of tracker.trackedGetters) try { | ||
| const descriptor = getDescriptor(bloc, prop); | ||
| if (!descriptor?.get) continue; | ||
| const newValue = descriptor.get.call(bloc); | ||
| const oldValue = tracker.trackedValues.get(prop); | ||
| tracker.renderCache.set(prop, newValue); | ||
| tracker.trackedValues.set(prop, newValue); | ||
| if (!Object.is(newValue, oldValue)) hasAnyChange = true; | ||
| } catch (error) { | ||
| console.warn(`[useBloc] Getter "${String(prop)}" threw error during change detection. Stopping tracking for this getter.`, error); | ||
| tracker.trackedGetters.delete(prop); | ||
| tracker.trackedValues.delete(prop); | ||
| tracker.cacheValid = false; | ||
| return true; | ||
| } | ||
| tracker.cacheValid = true; | ||
| return hasAnyChange; | ||
| } | ||
| /** | ||
| * Generates instance ID for isolated blocs | ||
| */ | ||
| function generateInstanceId$1(componentRef, isIsolated, providedId) { | ||
| if (providedId) return providedId; | ||
| function generateInstanceKey(componentRef, isIsolated, providedId) { | ||
| if (providedId !== void 0) return typeof providedId === "number" ? String(providedId) : providedId; | ||
| if (isIsolated) { | ||
| if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId = `isolated-${Math.random().toString(36).slice(2, 11)}`; | ||
| if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId = (0, __blac_core.generateIsolatedKey)(); | ||
| return componentRef.__blocInstanceId; | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/useBloc.ts | ||
| function determineTrackingMode(options) { | ||
@@ -202,71 +38,2 @@ return { | ||
| /** | ||
| * Factory: Creates subscribe function for automatic proxy tracking mode | ||
| */ | ||
| function createAutoTrackSubscribe(instance, hookState) { | ||
| return (callback) => { | ||
| return instance.subscribe(() => { | ||
| const tracker = hookState.tracker || (hookState.tracker = (0, __blac_core.createTrackerState)()); | ||
| let stateChanged = (0, __blac_core.hasChanges)(tracker, instance.state); | ||
| if (tracker.pathCache.size === 0 && hookState.getterTracker && hookState.getterTracker.trackedGetters.size > 0) stateChanged = false; | ||
| if (stateChanged) { | ||
| callback(); | ||
| return; | ||
| } | ||
| if (hasGetterChanges(instance, hookState.getterTracker)) callback(); | ||
| }); | ||
| }; | ||
| } | ||
| /** | ||
| * Factory: Creates subscribe function for manual dependencies mode | ||
| */ | ||
| function createManualDepsSubscribe(instance, hookState, options) { | ||
| return (callback) => { | ||
| return instance.subscribe(() => { | ||
| const newDeps = options.dependencies(instance.state, instance); | ||
| if (!hookState.manualDepsCache || !(0, __blac_core.shallowEqual)(hookState.manualDepsCache, newDeps)) { | ||
| hookState.manualDepsCache = newDeps; | ||
| callback(); | ||
| } | ||
| }); | ||
| }; | ||
| } | ||
| /** | ||
| * Factory: Creates subscribe function for no-tracking mode | ||
| */ | ||
| function createNoTrackSubscribe(instance) { | ||
| return (callback) => instance.subscribe(callback); | ||
| } | ||
| /** | ||
| * Factory: Creates getSnapshot function for automatic proxy tracking mode | ||
| */ | ||
| function createAutoTrackSnapshot(instance, hookState) { | ||
| return () => { | ||
| const tracker = hookState.tracker || (hookState.tracker = (0, __blac_core.createTrackerState)()); | ||
| if ((0, __blac_core.hasTrackedData)(tracker)) (0, __blac_core.captureTrackedPaths)(tracker, instance.state); | ||
| if (hookState.getterTracker) { | ||
| if (hookState.getterTracker.currentlyAccessing.size > 0) hookState.getterTracker.trackedGetters = new Set(hookState.getterTracker.currentlyAccessing); | ||
| hookState.getterTracker.currentlyAccessing.clear(); | ||
| hookState.getterTracker.isTracking = true; | ||
| activeTrackerMap.set(instance, hookState.getterTracker); | ||
| } | ||
| (0, __blac_core.startTracking)(tracker); | ||
| return (0, __blac_core.createProxy)(tracker, instance.state); | ||
| }; | ||
| } | ||
| /** | ||
| * Factory: Creates getSnapshot function for manual dependencies mode | ||
| */ | ||
| function createManualDepsSnapshot(instance, hookState, options) { | ||
| return () => { | ||
| hookState.manualDepsCache = options.dependencies(instance.state, instance); | ||
| return instance.state; | ||
| }; | ||
| } | ||
| /** | ||
| * Factory: Creates getSnapshot function for no-tracking mode | ||
| */ | ||
| function createNoTrackSnapshot(instance) { | ||
| return () => instance.state; | ||
| } | ||
| /** | ||
| * Lifecycle: INITIAL MOUNT | ||
@@ -290,46 +57,39 @@ * 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn | ||
| const componentRef = (0, react.useRef)({}); | ||
| const [bloc, subscribe, getSnapshot, instanceKey, hookState, rawInstance] = (0, react.useMemo)(() => { | ||
| const isIsolated = BlocClass.isolated === true; | ||
| const Constructor = BlocClass; | ||
| const instanceId = generateInstanceId$1(componentRef.current, isIsolated, options?.instanceId); | ||
| const instance = Constructor.getOrCreate(instanceId, options?.staticProps); | ||
| const Constructor = BlocClass; | ||
| const isIsolated = (0, __blac_core.isIsolatedClass)(BlocClass); | ||
| const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] = (0, react.useMemo)(() => { | ||
| const instanceKey$1 = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId); | ||
| const instance = BlocClass.resolve(instanceKey$1, options?.staticProps); | ||
| const { useManualDeps, autoTrackEnabled } = determineTrackingMode(options); | ||
| const hookState$1 = { | ||
| tracker: null, | ||
| manualDepsCache: null, | ||
| getterTracker: null, | ||
| proxiedBloc: null | ||
| }; | ||
| let subscribeFn; | ||
| let getSnapshotFn; | ||
| if (useManualDeps) { | ||
| subscribeFn = createManualDepsSubscribe(instance, hookState$1, options); | ||
| getSnapshotFn = createManualDepsSnapshot(instance, hookState$1, options); | ||
| hookState$1.proxiedBloc = instance; | ||
| let adapterState$1; | ||
| if (useManualDeps && options?.dependencies) { | ||
| adapterState$1 = (0, __blac_core.initManualDepsState)(instance); | ||
| subscribeFn = (0, __blac_core.createManualDepsSubscribe)(instance, adapterState$1, { dependencies: options.dependencies }); | ||
| getSnapshotFn = (0, __blac_core.createManualDepsSnapshot)(instance, adapterState$1, { dependencies: options.dependencies }); | ||
| } else if (!autoTrackEnabled) { | ||
| subscribeFn = createNoTrackSubscribe(instance); | ||
| getSnapshotFn = createNoTrackSnapshot(instance); | ||
| hookState$1.proxiedBloc = instance; | ||
| adapterState$1 = (0, __blac_core.initNoTrackState)(instance); | ||
| subscribeFn = (0, __blac_core.createNoTrackSubscribe)(instance); | ||
| getSnapshotFn = (0, __blac_core.createNoTrackSnapshot)(instance); | ||
| } else { | ||
| subscribeFn = createAutoTrackSubscribe(instance, hookState$1); | ||
| getSnapshotFn = createAutoTrackSnapshot(instance, hookState$1); | ||
| hookState$1.getterTracker = createGetterTracker(); | ||
| hookState$1.proxiedBloc = createBlocProxy(instance); | ||
| adapterState$1 = (0, __blac_core.initAutoTrackState)(instance); | ||
| subscribeFn = (0, __blac_core.createAutoTrackSubscribe)(instance, adapterState$1); | ||
| getSnapshotFn = (0, __blac_core.createAutoTrackSnapshot)(instance, adapterState$1); | ||
| } | ||
| return [ | ||
| hookState$1.proxiedBloc, | ||
| adapterState$1.proxiedBloc, | ||
| subscribeFn, | ||
| getSnapshotFn, | ||
| instanceId, | ||
| hookState$1, | ||
| instanceKey$1, | ||
| adapterState$1, | ||
| instance | ||
| ]; | ||
| }, [BlocClass]); | ||
| }, [BlocClass, options?.instanceId]); | ||
| const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot); | ||
| const [, forceUpdate] = (0, react.useReducer)((x) => x + 1, 0); | ||
| const externalDepsManager = (0, react.useRef)(new __blac_core.ExternalDependencyManager()); | ||
| (0, react.useEffect)(() => { | ||
| if (hookState.getterTracker) { | ||
| hookState.getterTracker.isTracking = false; | ||
| hookState.getterTracker.cacheValid = false; | ||
| activeTrackerMap.delete(rawInstance); | ||
| } | ||
| (0, __blac_core.disableGetterTracking)(adapterState, rawInstance); | ||
| externalDepsManager.current.updateSubscriptions(adapterState.getterTracker, rawInstance, forceUpdate); | ||
| }); | ||
@@ -339,5 +99,6 @@ (0, react.useEffect)(() => { | ||
| return () => { | ||
| externalDepsManager.current.cleanup(); | ||
| if (options?.onUnmount) options.onUnmount(bloc); | ||
| BlocClass.release(instanceKey); | ||
| if (BlocClass.isolated === true && !rawInstance.isDisposed) rawInstance.dispose(); | ||
| Constructor.release(instanceKey); | ||
| if (isIsolated && !rawInstance.isDisposed) rawInstance.dispose(); | ||
| }; | ||
@@ -354,34 +115,9 @@ }, []); | ||
| //#region src/useBlocActions.ts | ||
| /** | ||
| * Generates instance ID for isolated blocs | ||
| */ | ||
| function generateInstanceId(componentRef, isIsolated, providedId) { | ||
| if (providedId) return providedId; | ||
| if (isIsolated) { | ||
| if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId = `isolated-${Math.random().toString(36).slice(2, 11)}`; | ||
| return componentRef.__blocInstanceId; | ||
| } | ||
| } | ||
| /** | ||
| * React hook for accessing bloc instance without state subscription. | ||
| * Use this when you only need to call bloc methods/actions without reading state. | ||
| * | ||
| * Benefits over useBloc: | ||
| * - No state subscription overhead | ||
| * - No proxy tracking | ||
| * - Component never re-renders due to bloc state changes | ||
| * - Lighter weight for action-only components | ||
| * | ||
| * @template TBloc - The StateContainer type | ||
| * @param BlocClass - The bloc class constructor | ||
| * @param options - Optional configuration | ||
| * @returns The bloc instance | ||
| */ | ||
| function useBlocActions(BlocClass, options) { | ||
| const componentRef = (0, react.useRef)({}); | ||
| const [bloc, instanceKey] = (0, react.useMemo)(() => { | ||
| const isIsolated = BlocClass.isolated === true; | ||
| const isIsolated = (0, __blac_core.isIsolatedClass)(BlocClass); | ||
| const Constructor = BlocClass; | ||
| const instanceId = generateInstanceId(componentRef.current, isIsolated, options?.instanceId); | ||
| return [Constructor.getOrCreate(instanceId, options?.staticProps), instanceId]; | ||
| const instanceKey$1 = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId); | ||
| return [options?.staticProps ? Constructor.resolve(instanceKey$1, options.staticProps) : Constructor.resolve(instanceKey$1), instanceKey$1]; | ||
| }, [BlocClass]); | ||
@@ -393,3 +129,3 @@ (0, react.useEffect)(() => { | ||
| BlocClass.release(instanceKey); | ||
| if (BlocClass.isolated === true && !bloc.isDisposed) bloc.dispose(); | ||
| if ((0, __blac_core.isIsolatedClass)(BlocClass) && !bloc.isDisposed) bloc.dispose(); | ||
| }; | ||
@@ -396,0 +132,0 @@ }, []); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.cjs","names":["descriptor: PropertyDescriptor | undefined","generateInstanceId","hookState: HookState<TBloc>","subscribeFn: (callback: () => void) => () => void","getSnapshotFn: () => ExtractState<TBloc>","hookState"],"sources":["../src/useBloc.ts","../src/useBlocActions.ts"],"sourcesContent":["/**\n * useBloc - hook for BlaC state management in React with automatic proxy tracking\n *\n * @example\n * ```tsx\n * // Basic usage - automatic tracking of accessed properties\n * function Counter() {\n * const [state, bloc] = useBloc(CounterBloc);\n * return (\n * <div>\n * <p>Count: {state.count}</p> // Only re-renders when count changes\n * <button onClick={bloc.increment}>+</button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { useMemo, useSyncExternalStore, useEffect, useRef } from 'react';\nimport {\n type AnyObject,\n type BlocConstructor,\n StateContainer,\n type ExtractState,\n // Import tracking utilities from core\n type TrackerState,\n createTrackerState,\n startTracking,\n createProxy,\n captureTrackedPaths,\n hasChanges,\n hasTrackedData,\n shallowEqual,\n} from '@blac/core';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\n\n/**\n * StateContainer constructor with required static methods\n */\ntype StateContainerConstructor<TBloc extends StateContainer<any>> =\n BlocConstructor<TBloc> & {\n getOrCreate(instanceKey?: string, ...args: any[]): TBloc;\n release(instanceKey?: string): void;\n };\n\n/**\n * State for tracking getter access and values during render\n *\n * @remarks\n * This tracks which getters are accessed during component render and stores\n * their computed values for comparison on subsequent state changes.\n * Only getters (properties with a getter descriptor) are tracked automatically.\n *\n * Similar to state tracking, we use two sets:\n * - `currentlyAccessing`: Temporary set for getters accessed during current render\n * - `trackedGetters`: Committed set of getters from last completed render\n *\n * PERFORMANCE: Render cache optimization\n * - When checking if we should re-render (hasGetterChanges), we compute all getters\n * - Store these computed values in `renderCache` for the upcoming render\n * - During render, if we access a cached getter, use the cached value instead of recomputing\n * - This ensures each getter is computed at most once per render cycle\n * - Cache is invalidated after render completes or when state changes again\n */\ninterface GetterTrackingState {\n /** Map of getter names to their last computed values (for comparison) */\n trackedValues: Map<string | symbol, unknown>;\n /** Temporary set of getters being accessed during current render */\n currentlyAccessing: Set<string | symbol>;\n /** Committed set of getters from last completed render (used for change detection) */\n trackedGetters: Set<string | symbol>;\n /** Flag to enable/disable tracking (only enabled during render phase) */\n isTracking: boolean;\n /** Cache of getter values computed during hasGetterChanges (valid for current render cycle) */\n renderCache: Map<string | symbol, unknown>;\n /** Flag indicating render cache is valid and can be used */\n cacheValid: boolean;\n}\n\n/**\n * Cache for property descriptors to avoid repeated prototype chain walks\n * Maps from object constructor to a map of property name to descriptor\n */\nconst descriptorCache = new WeakMap<\n Function,\n Map<string | symbol, PropertyDescriptor | undefined>\n>();\n\n/**\n * Cache for proxied blocs to ensure same proxy is returned for same bloc instance\n * This is important for identity checks (e.g., bloc1 === bloc2) across components\n * using the same shared bloc instance.\n */\nconst blocProxyCache = new WeakMap<StateContainer<any>, any>();\n\n/**\n * Map to store the currently active tracker during render.\n * This allows the cached proxy to know which component's tracker to use.\n * Set before render, cleared after render.\n */\nconst activeTrackerMap = new WeakMap<\n StateContainer<any>,\n GetterTrackingState\n>();\n\n/**\n * Get property descriptor for a given property, with caching\n *\n * @remarks\n * Walks up the prototype chain once per class to find the descriptor,\n * then caches the result for performance. This is critical because\n * we need to distinguish getters from methods and properties.\n *\n * @param obj - The object to get the descriptor from\n * @param prop - The property name or symbol\n * @returns The property descriptor if found, undefined otherwise\n */\nfunction getDescriptor(\n obj: any,\n prop: string | symbol,\n): PropertyDescriptor | undefined {\n const constructor = obj.constructor;\n\n // Try to get from cache\n let constructorCache = descriptorCache.get(constructor);\n if (constructorCache?.has(prop)) {\n return constructorCache.get(prop);\n }\n\n // Walk prototype chain to find descriptor\n let current = obj;\n let descriptor: PropertyDescriptor | undefined;\n\n while (current && current !== Object.prototype) {\n descriptor = Object.getOwnPropertyDescriptor(current, prop);\n if (descriptor) {\n break;\n }\n current = Object.getPrototypeOf(current);\n }\n\n // Cache the result\n if (!constructorCache) {\n constructorCache = new Map();\n descriptorCache.set(constructor, constructorCache);\n }\n constructorCache.set(prop, descriptor);\n\n return descriptor;\n}\n\n/**\n * Check if a property is a getter (has a getter descriptor)\n *\n * @param obj - The object to check\n * @param prop - The property name or symbol\n * @returns True if the property is a getter, false otherwise\n */\nfunction isGetter(obj: any, prop: string | symbol): boolean {\n const descriptor = getDescriptor(obj, prop);\n return descriptor?.get !== undefined;\n}\n\n/**\n * Create a new getter tracking state\n *\n * @returns A new GetterTrackingState initialized with empty collections\n */\nfunction createGetterTracker(): GetterTrackingState {\n return {\n trackedValues: new Map(),\n currentlyAccessing: new Set(),\n trackedGetters: new Set(),\n isTracking: false,\n renderCache: new Map(),\n cacheValid: false,\n };\n}\n\n/**\n * Create a proxy that intercepts getter access on a bloc instance\n *\n * @remarks\n * This proxy wraps the bloc instance to track which getters are accessed\n * during component render. When tracking is enabled (during render phase),\n * it records accessed getters and stores their computed values for later\n * comparison.\n *\n * IMPORTANT: This function caches proxies per bloc instance. Multiple components\n * sharing the same bloc will get the same proxy instance. Each component sets\n * its tracker in activeTrackerMap before render, and the proxy looks it up.\n *\n * @param bloc - The bloc instance to wrap\n * @returns A proxied bloc that tracks getter access\n */\nfunction createBlocProxy<TBloc extends StateContainer<AnyObject>>(\n bloc: TBloc,\n): TBloc {\n // Check cache first - return existing proxy if available\n const cached = blocProxyCache.get(bloc);\n if (cached) {\n return cached;\n }\n\n const proxy = new Proxy(bloc, {\n get(target, prop, receiver) {\n // Get the active tracker for this bloc (set by getSnapshot)\n const tracker = activeTrackerMap.get(target);\n\n // Only track during render phase (when tracker is active and tracking enabled)\n if (tracker?.isTracking && isGetter(target, prop)) {\n // Record that this getter was accessed during current render\n tracker.currentlyAccessing.add(prop);\n\n // Use cached value if available from previous change detection\n if (tracker.cacheValid && tracker.renderCache.has(prop)) {\n const cachedValue = tracker.renderCache.get(prop);\n // Also store in trackedValues for consistency\n tracker.trackedValues.set(prop, cachedValue);\n return cachedValue;\n }\n\n // Compute getter if no cache available (first access or cache invalidated)\n const descriptor = getDescriptor(target, prop);\n const value = descriptor!.get!.call(target);\n tracker.trackedValues.set(prop, value);\n return value;\n }\n\n // Default behavior for non-getters or when tracking disabled\n return Reflect.get(target, prop, receiver);\n },\n });\n\n blocProxyCache.set(bloc, proxy);\n return proxy;\n}\n\n/**\n * Check if any tracked getters have changed values\n *\n * @remarks\n * Re-computes all getters that were accessed during the last render and\n * compares their new values with stored values using Object.is() (reference\n * equality).\n *\n * OPTIMIZATION: Render cache population\n * - Computes ALL tracked getters (no early exit) to populate the render cache\n * - This ensures each getter is computed only once per render cycle\n * - If we're going to re-render, getters accessed during render will use cached values\n * - Cache is populated even if no changes detected (useful for parent-triggered re-renders)\n * - Trade-off: Give up early exit to ensure full cache population\n *\n * Error handling: If a getter throws during re-computation, we log a warning,\n * stop tracking that specific getter, and treat it as \"changed\" to trigger\n * a re-render. This prevents the tracking system from breaking while still\n * allowing React's error boundary to handle the error on the next render.\n *\n * @param bloc - The bloc instance\n * @param tracker - The getter tracking state\n * @returns True if any tracked getter value changed, false otherwise\n */\nfunction hasGetterChanges<TBloc extends StateContainer<AnyObject>>(\n bloc: TBloc,\n tracker: GetterTrackingState | null,\n): boolean {\n // Early return if no tracker or no getters tracked\n if (!tracker || tracker.trackedGetters.size === 0) {\n return false;\n }\n\n // Clear previous render cache\n tracker.renderCache.clear();\n\n let hasAnyChange = false;\n\n // Compute all getters to populate render cache (no early exit)\n for (const prop of tracker.trackedGetters) {\n try {\n const descriptor = getDescriptor(bloc, prop);\n if (!descriptor?.get) {\n // Getter no longer exists (shouldn't happen, but be defensive)\n continue;\n }\n\n const newValue = descriptor.get.call(bloc);\n const oldValue = tracker.trackedValues.get(prop);\n\n // Store in render cache for upcoming render (even if unchanged)\n tracker.renderCache.set(prop, newValue);\n\n // Update tracked values for next comparison\n tracker.trackedValues.set(prop, newValue);\n\n // Use Object.is for reference equality comparison\n if (!Object.is(newValue, oldValue)) {\n hasAnyChange = true;\n // Don't return early - continue computing and caching remaining getters\n }\n } catch (error) {\n // Getter threw an error during comparison\n console.warn(\n `[useBloc] Getter \"${String(prop)}\" threw error during change detection. Stopping tracking for this getter.`,\n error,\n );\n\n // Stop tracking this getter\n tracker.trackedGetters.delete(prop);\n tracker.trackedValues.delete(prop);\n\n // Treat as \"changed\" to trigger re-render\n // Still return early on error to avoid cascading failures\n tracker.cacheValid = false; // Invalidate cache due to error\n return true;\n }\n }\n\n // Mark cache as valid for the upcoming render\n tracker.cacheValid = true;\n\n return hasAnyChange;\n}\n\n/**\n * Generates instance ID for isolated blocs\n */\nfunction generateInstanceId(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string,\n): string | undefined {\n if (providedId) return providedId;\n\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = `isolated-${Math.random().toString(36).slice(2, 11)}`;\n }\n return componentRef.__blocInstanceId;\n }\n\n return undefined;\n}\n\n/**\n * Determines tracking mode from options\n */\ninterface TrackingMode {\n useManualDeps: boolean;\n autoTrackEnabled: boolean;\n}\n\nfunction determineTrackingMode<TBloc extends StateContainer<AnyObject>>(\n options?: UseBlocOptions<TBloc>,\n): TrackingMode {\n return {\n useManualDeps: options?.dependencies !== undefined,\n autoTrackEnabled: options?.autoTrack !== false,\n };\n}\n\n/**\n * Internal state for subscription and snapshot functions\n */\ninterface HookState<TBloc extends StateContainer<AnyObject>> {\n /** State property tracker (existing) */\n tracker: TrackerState<ExtractState<TBloc>> | null;\n /** Manual dependencies cache (existing) */\n manualDepsCache: unknown[] | null;\n /** Getter tracking state (new) */\n getterTracker: GetterTrackingState | null;\n /** Cached proxied bloc instance (new) */\n proxiedBloc: TBloc | null;\n}\n\n/**\n * Factory: Creates subscribe function for automatic proxy tracking mode\n */\nfunction createAutoTrackSubscribe<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n hookState: HookState<TBloc>,\n): (callback: () => void) => () => void {\n return (callback: () => void) => {\n return instance.subscribe(() => {\n const tracker =\n hookState.tracker ||\n (hookState.tracker = createTrackerState<ExtractState<TBloc>>());\n\n let stateChanged = hasChanges(tracker, instance.state);\n\n // Special case: if NO state properties were tracked (pathCache.size === 0)\n // but getters WERE tracked, then don't treat \"no state tracking\" as \"track everything\".\n // Only rely on getter changes in this case.\n if (\n tracker.pathCache.size === 0 &&\n hookState.getterTracker &&\n hookState.getterTracker.trackedGetters.size > 0\n ) {\n stateChanged = false; // Override - only getters are relevant\n }\n\n // EARLY EXIT: If state already changed, skip getter checks entirely\n if (stateChanged) {\n callback();\n return;\n }\n\n // Only check getters if state didn't change\n const getterChanged = hasGetterChanges(instance, hookState.getterTracker);\n\n if (getterChanged) {\n callback();\n }\n });\n };\n}\n\n/**\n * Factory: Creates subscribe function for manual dependencies mode\n */\nfunction createManualDepsSubscribe<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n hookState: HookState<TBloc>,\n options: UseBlocOptions<TBloc>,\n): (callback: () => void) => () => void {\n return (callback: () => void) => {\n return instance.subscribe(() => {\n const newDeps = options.dependencies!(instance.state, instance);\n if (\n !hookState.manualDepsCache ||\n !shallowEqual(hookState.manualDepsCache, newDeps)\n ) {\n hookState.manualDepsCache = newDeps;\n callback();\n }\n });\n };\n}\n\n/**\n * Factory: Creates subscribe function for no-tracking mode\n */\nfunction createNoTrackSubscribe<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n): (callback: () => void) => () => void {\n return (callback: () => void) => instance.subscribe(callback);\n}\n\n/**\n * Factory: Creates getSnapshot function for automatic proxy tracking mode\n */\nfunction createAutoTrackSnapshot<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n hookState: HookState<TBloc>,\n): () => ExtractState<TBloc> {\n return () => {\n const tracker =\n hookState.tracker ||\n (hookState.tracker = createTrackerState<ExtractState<TBloc>>());\n\n if (hasTrackedData(tracker)) {\n captureTrackedPaths(tracker, instance.state);\n }\n\n // Enable getter tracking during render and set as active tracker\n if (hookState.getterTracker) {\n // Capture getters from previous render (commit currentlyAccessing to trackedGetters)\n if (hookState.getterTracker.currentlyAccessing.size > 0) {\n hookState.getterTracker.trackedGetters = new Set(\n hookState.getterTracker.currentlyAccessing,\n );\n }\n\n // Clear and enable tracking for this render\n hookState.getterTracker.currentlyAccessing.clear();\n hookState.getterTracker.isTracking = true;\n\n // Set this component's tracker as the active one for this bloc\n activeTrackerMap.set(instance, hookState.getterTracker);\n }\n\n startTracking(tracker);\n return createProxy(tracker, instance.state);\n };\n}\n\n/**\n * Factory: Creates getSnapshot function for manual dependencies mode\n */\nfunction createManualDepsSnapshot<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n hookState: HookState<TBloc>,\n options: UseBlocOptions<TBloc>,\n): () => ExtractState<TBloc> {\n return () => {\n hookState.manualDepsCache = options.dependencies!(instance.state, instance);\n return instance.state;\n };\n}\n\n/**\n * Factory: Creates getSnapshot function for no-tracking mode\n */\nfunction createNoTrackSnapshot<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n): () => ExtractState<TBloc> {\n return () => instance.state;\n}\n\n/**\n * Lifecycle: INITIAL MOUNT\n * 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn\n * 2. useSyncExternalStore calls getSnapshotFn (1st time) - lazy creates tracker, starts tracking, returns proxy\n * 3. Component renders - proxy tracks property accesses\n * 4. useSyncExternalStore calls getSnapshotFn (2nd time) - captures tracked paths, starts new tracking, returns proxy\n * 5. useSyncExternalStore calls subscribeFn - sets up state change listener\n *\n * Lifecycle: STATE CHANGE\n * 1. Bloc state changes\n * 2. subscribeFn callback checks hasChanges() - only re-renders if tracked paths changed\n * 3. If re-render: getSnapshotFn captures previous paths, starts tracking, returns proxy\n *\n * Lifecycle: RE-RENDER (parent re-render)\n * 1. useMemo returns cached values (same bloc, subscribeFn, getSnapshotFn)\n * 2. useSyncExternalStore calls getSnapshotFn - captures paths, starts tracking, returns proxy\n */\nexport function useBloc<TBloc extends StateContainer<AnyObject>>(\n BlocClass: BlocConstructor<TBloc>,\n options?: UseBlocOptions<TBloc>,\n): UseBlocReturn<TBloc> {\n // Component reference that persists across React Strict Mode remounts\n const componentRef = useRef<ComponentRef>({});\n\n const [bloc, subscribe, getSnapshot, instanceKey, hookState, rawInstance] =\n useMemo(() => {\n const isIsolated =\n (BlocClass as { isolated?: boolean }).isolated === true;\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n\n // Generate instance key\n const instanceId = generateInstanceId(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n // Get or create bloc instance\n const instance = Constructor.getOrCreate(\n instanceId,\n options?.staticProps,\n );\n\n // Determine tracking mode\n const { useManualDeps, autoTrackEnabled } =\n determineTrackingMode(options);\n\n // Mutable state shared between subscribe and getSnapshot\n const hookState: HookState<TBloc> = {\n tracker: null,\n manualDepsCache: null,\n getterTracker: null,\n proxiedBloc: null,\n };\n\n // Create subscribe and getSnapshot functions based on tracking mode\n let subscribeFn: (callback: () => void) => () => void;\n let getSnapshotFn: () => ExtractState<TBloc>;\n\n if (useManualDeps) {\n // Manual dependencies mode - no automatic tracking\n subscribeFn = createManualDepsSubscribe(instance, hookState, options!);\n getSnapshotFn = createManualDepsSnapshot(instance, hookState, options!);\n hookState.proxiedBloc = instance; // Use raw instance\n } else if (!autoTrackEnabled) {\n // No tracking mode\n subscribeFn = createNoTrackSubscribe(instance);\n getSnapshotFn = createNoTrackSnapshot(instance);\n hookState.proxiedBloc = instance; // Use raw instance\n } else {\n // Auto-tracking mode - enable both state and getter tracking\n subscribeFn = createAutoTrackSubscribe(instance, hookState);\n getSnapshotFn = createAutoTrackSnapshot(instance, hookState);\n\n // Initialize getter tracker and create proxied bloc\n hookState.getterTracker = createGetterTracker();\n hookState.proxiedBloc = createBlocProxy(instance);\n }\n\n return [\n hookState.proxiedBloc!,\n subscribeFn,\n getSnapshotFn,\n instanceId,\n hookState,\n instance,\n ] as const;\n }, [BlocClass]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot);\n\n // Disable getter tracking after each render and clear active tracker\n // Also invalidate render cache since this render cycle is complete\n useEffect(() => {\n if (hookState.getterTracker) {\n hookState.getterTracker.isTracking = false;\n hookState.getterTracker.cacheValid = false; // Invalidate cache after render\n activeTrackerMap.delete(rawInstance);\n }\n });\n\n // Mount/unmount lifecycle\n useEffect(() => {\n // Call onMount callback if provided\n if (options?.onMount) {\n options.onMount(bloc);\n }\n\n return () => {\n // Call onUnmount callback if provided\n if (options?.onUnmount) {\n options.onUnmount(bloc);\n }\n\n // Release bloc reference\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n Constructor.release(instanceKey);\n\n // For isolated instances, dispose manually since registry doesn't track them\n const isIsolated =\n (BlocClass as { isolated?: boolean }).isolated === true;\n if (isIsolated && !rawInstance.isDisposed) {\n rawInstance.dispose();\n }\n };\n }, []);\n\n return [state, bloc, componentRef] as UseBlocReturn<TBloc>;\n}\n","/**\n * useBlocActions - hook for accessing bloc instance without state subscription\n *\n * @example\n * ```tsx\n * // Use when you only need to call actions, not read state\n * function ActionsOnly() {\n * const bloc = useBlocActions(CounterBloc);\n * return (\n * <div>\n * <button onClick={bloc.increment}>+</button>\n * <button onClick={bloc.decrement}>-</button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { useMemo, useEffect, useRef } from 'react';\nimport type { AnyObject, BlocConstructor, StateContainer } from '@blac/core';\nimport type { ComponentRef } from './types';\n\n/**\n * StateContainer constructor with required static methods\n */\ntype StateContainerConstructor<TBloc extends StateContainer<any>> =\n BlocConstructor<TBloc> & {\n getOrCreate(instanceKey?: string, ...args: any[]): TBloc;\n release(instanceKey?: string): void;\n };\n\n/**\n * Configuration options for the useBlocActions hook\n *\n * @template TBloc - The StateContainer type\n */\nexport interface UseBlocActionsOptions<\n TBloc extends StateContainer<AnyObject>,\n> {\n /**\n * Static props to pass to the Bloc constructor\n * Type should match the constructor's first parameter\n */\n staticProps?: AnyObject;\n\n /**\n * Custom instance ID for shared blocs\n * - For isolated blocs, each useBlocActions call gets its own instance\n * - For shared blocs, the same instanceId will share the same bloc instance\n */\n instanceId?: string;\n\n /**\n * Callback invoked when the component mounts\n *\n * @param bloc - The bloc instance\n */\n onMount?: (bloc: TBloc) => void;\n\n /**\n * Callback invoked when the component unmounts\n *\n * @param bloc - The bloc instance\n */\n onUnmount?: (bloc: TBloc) => void;\n}\n\n/**\n * Generates instance ID for isolated blocs\n */\nfunction generateInstanceId(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string,\n): string | undefined {\n if (providedId) return providedId;\n\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = `isolated-${Math.random().toString(36).slice(2, 11)}`;\n }\n return componentRef.__blocInstanceId;\n }\n\n return undefined;\n}\n\n/**\n * React hook for accessing bloc instance without state subscription.\n * Use this when you only need to call bloc methods/actions without reading state.\n *\n * Benefits over useBloc:\n * - No state subscription overhead\n * - No proxy tracking\n * - Component never re-renders due to bloc state changes\n * - Lighter weight for action-only components\n *\n * @template TBloc - The StateContainer type\n * @param BlocClass - The bloc class constructor\n * @param options - Optional configuration\n * @returns The bloc instance\n */\nexport function useBlocActions<TBloc extends StateContainer<AnyObject>>(\n BlocClass: BlocConstructor<TBloc>,\n options?: UseBlocActionsOptions<TBloc>,\n): TBloc {\n // Component reference that persists across React Strict Mode remounts\n const componentRef = useRef<ComponentRef>({});\n\n const [bloc, instanceKey] = useMemo(() => {\n const isIsolated = (BlocClass as { isolated?: boolean }).isolated === true;\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n\n // Generate instance key\n const instanceId = generateInstanceId(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n // Get or create bloc instance\n const instance = Constructor.getOrCreate(instanceId, options?.staticProps);\n\n return [instance, instanceId] as const;\n }, [BlocClass]);\n\n // Mount/unmount lifecycle\n useEffect(() => {\n // Call onMount callback if provided\n if (options?.onMount) {\n options.onMount(bloc);\n }\n\n return () => {\n // Call onUnmount callback if provided\n if (options?.onUnmount) {\n options.onUnmount(bloc);\n }\n\n // Release bloc reference\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n Constructor.release(instanceKey);\n\n // For isolated instances, dispose manually since registry doesn't track them\n const isIsolated =\n (BlocClass as { isolated?: boolean }).isolated === true;\n if (isIsolated && !bloc.isDisposed) {\n bloc.dispose();\n }\n };\n }, []);\n\n return bloc;\n}\n"],"mappings":"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"} | ||
| {"version":3,"file":"index.cjs","names":["instanceKey","subscribeFn: (callback: () => void) => () => void","getSnapshotFn: () => ExtractState<TBloc>","adapterState: AdapterState<TBloc>","adapterState","ExternalDependencyManager","instanceKey"],"sources":["../src/utils/instance-keys.ts","../src/useBloc.ts","../src/useBlocActions.ts"],"sourcesContent":["/**\n * Instance key generation utilities for React integration\n */\n\nimport { generateIsolatedKey } from '@blac/core';\nimport type { ComponentRef } from '../types';\n\n/**\n * Generate an instance key for a bloc\n *\n * Logic:\n * - If user provides instanceId, use it (convert number to string)\n * - If isolated, generate or reuse a unique key for this component\n * - Otherwise, return undefined (use default key)\n *\n * @param componentRef - React component reference (persists across remounts)\n * @param isIsolated - Whether the bloc is isolated\n * @param providedId - User-provided instance ID (from options)\n * @returns Instance key string or undefined for default\n */\nexport function generateInstanceKey(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string | number,\n): string | undefined {\n // User explicitly provided an ID - use it\n if (providedId !== undefined) {\n return typeof providedId === 'number' ? String(providedId) : providedId;\n }\n\n // Isolated bloc - generate unique key per component\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = generateIsolatedKey();\n }\n return componentRef.__blocInstanceId;\n }\n\n // Shared bloc - use default key (undefined)\n return undefined;\n}\n","import {\n useMemo,\n useSyncExternalStore,\n useEffect,\n useRef,\n useReducer,\n} from 'react';\nimport {\n type BlocConstructor,\n StateContainer,\n type ExtractState,\n type AdapterState,\n ExternalDependencyManager,\n createAutoTrackSubscribe,\n createManualDepsSubscribe,\n createNoTrackSubscribe,\n createAutoTrackSnapshot,\n createManualDepsSnapshot,\n createNoTrackSnapshot,\n initAutoTrackState,\n initManualDepsState,\n initNoTrackState,\n disableGetterTracking,\n isIsolatedClass,\n} from '@blac/core';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\n\ntype StateContainerConstructor<TBloc extends StateContainer<any>> =\n BlocConstructor<TBloc> & {\n resolve(instanceKey?: string, ...args: any[]): TBloc;\n release(instanceKey?: string): void;\n };\n\ninterface TrackingMode {\n useManualDeps: boolean;\n autoTrackEnabled: boolean;\n}\n\nfunction determineTrackingMode<TBloc extends StateContainer<any>>(\n options?: UseBlocOptions<TBloc>,\n): TrackingMode {\n return {\n useManualDeps: options?.dependencies !== undefined,\n autoTrackEnabled: options?.autoTrack !== false,\n };\n}\n\n/**\n * Lifecycle: INITIAL MOUNT\n * 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn\n * 2. useSyncExternalStore calls getSnapshotFn (1st time) - lazy creates tracker, starts tracking, returns proxy\n * 3. Component renders - proxy tracks property accesses\n * 4. useSyncExternalStore calls getSnapshotFn (2nd time) - captures tracked paths, starts new tracking, returns proxy\n * 5. useSyncExternalStore calls subscribeFn - sets up state change listener\n *\n * Lifecycle: STATE CHANGE\n * 1. Bloc state changes\n * 2. subscribeFn callback checks hasChanges() - only re-renders if tracked paths changed\n * 3. If re-render: getSnapshotFn captures previous paths, starts tracking, returns proxy\n *\n * Lifecycle: RE-RENDER (parent re-render)\n * 1. useMemo returns cached values (same bloc, subscribeFn, getSnapshotFn)\n * 2. useSyncExternalStore calls getSnapshotFn - captures paths, starts tracking, returns proxy\n */\nexport function useBloc<T extends new (...args: any[]) => StateContainer<any>>(\n BlocClass: T & BlocConstructor<InstanceType<T>>,\n options?: UseBlocOptions<InstanceType<T>>,\n): UseBlocReturn<InstanceType<T>> {\n // Component reference that persists across React Strict Mode remounts\n type TBloc = InstanceType<T>;\n const componentRef = useRef<ComponentRef>({});\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n const isIsolated = isIsolatedClass(BlocClass);\n\n const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] =\n useMemo<\n readonly [\n TBloc,\n (callback: () => void) => () => void,\n () => ExtractState<TBloc>,\n string | undefined,\n AdapterState<TBloc>,\n TBloc,\n ]\n >(() => {\n // Generate instance key\n const instanceKey = generateInstanceKey(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n // Get or create bloc instance with ownership (increments ref count)\n const instance = BlocClass.resolve(instanceKey, options?.staticProps);\n\n // Determine tracking mode\n const { useManualDeps, autoTrackEnabled } =\n determineTrackingMode(options);\n\n // Create subscribe and getSnapshot functions based on tracking mode\n let subscribeFn: (callback: () => void) => () => void;\n let getSnapshotFn: () => ExtractState<TBloc>;\n let adapterState: AdapterState<TBloc>;\n\n if (useManualDeps && options?.dependencies) {\n // Manual dependencies mode - no automatic tracking\n adapterState = initManualDepsState(instance);\n subscribeFn = createManualDepsSubscribe(instance, adapterState, {\n dependencies: options.dependencies,\n });\n getSnapshotFn = createManualDepsSnapshot(instance, adapterState, {\n dependencies: options.dependencies,\n });\n } else if (!autoTrackEnabled) {\n // No tracking mode\n adapterState = initNoTrackState(instance);\n subscribeFn = createNoTrackSubscribe(instance);\n getSnapshotFn = createNoTrackSnapshot(instance);\n } else {\n // Auto-tracking mode - enable both state and getter tracking\n adapterState = initAutoTrackState(instance);\n subscribeFn = createAutoTrackSubscribe(instance, adapterState);\n getSnapshotFn = createAutoTrackSnapshot(instance, adapterState);\n }\n\n return [\n adapterState.proxiedBloc!,\n subscribeFn,\n getSnapshotFn,\n instanceKey,\n adapterState,\n instance,\n ];\n }, [BlocClass, options?.instanceId]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot);\n\n // Force re-render mechanism for external bloc changes\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n // External dependency manager (persists across renders)\n const externalDepsManager = useRef(new ExternalDependencyManager());\n\n // Disable getter tracking and manage external bloc subscriptions after each render\n useEffect(() => {\n disableGetterTracking(adapterState, rawInstance);\n externalDepsManager.current.updateSubscriptions(\n adapterState.getterTracker,\n rawInstance,\n forceUpdate,\n );\n }); // Run on every render to pick up new dependencies and disable tracking\n\n // Mount/unmount lifecycle\n useEffect(() => {\n // Call onMount callback if provided\n if (options?.onMount) {\n options.onMount(bloc);\n }\n\n return () => {\n // Cleanup in proper order: subscriptions -> callbacks -> disposal\n\n // 1. Clean up external subscriptions FIRST (before bloc is disposed)\n externalDepsManager.current.cleanup();\n\n // 2. Call onUnmount callback if provided\n if (options?.onUnmount) {\n options.onUnmount(bloc);\n }\n\n // 3. Release bloc reference\n Constructor.release(instanceKey);\n\n // 4. For isolated instances, dispose manually since registry doesn't track them\n if (isIsolated && !rawInstance.isDisposed) {\n rawInstance.dispose();\n }\n };\n }, []);\n\n return [state, bloc, componentRef] as UseBlocReturn<TBloc>;\n}\n","import { useMemo, useEffect, useRef } from 'react';\nimport {\n type BlocConstructor,\n StateContainer,\n isIsolatedClass,\n} from '@blac/core';\nimport type { ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\n\ntype StateContainerConstructor<TBloc extends StateContainer<any>> =\n BlocConstructor<TBloc> & {\n resolve(instanceKey?: string, ...args: any[]): TBloc;\n release(instanceKey?: string): void;\n };\n\nexport interface UseBlocActionsOptions<TBloc> {\n staticProps?: any;\n instanceId?: string | number;\n onMount?: (bloc: TBloc) => void;\n onUnmount?: (bloc: TBloc) => void;\n}\n\nexport function useBlocActions<\n T extends new (...args: any[]) => StateContainer<any>,\n>(\n BlocClass: T & BlocConstructor<InstanceType<T>>,\n options?: UseBlocActionsOptions<InstanceType<T>>,\n): InstanceType<T> {\n // Component reference that persists across React Strict Mode remounts\n type TBloc = InstanceType<T>;\n const componentRef = useRef<ComponentRef>({});\n\n const [bloc, instanceKey] = useMemo(() => {\n const isIsolated = isIsolatedClass(BlocClass);\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n\n // Generate instance key\n const instanceKey = generateInstanceKey(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n // Get or create bloc instance with ownership (increments ref count)\n const instance = options?.staticProps\n ? Constructor.resolve(instanceKey, options.staticProps)\n : Constructor.resolve(instanceKey);\n\n return [instance, instanceKey] as const;\n }, [BlocClass]);\n\n // Mount/unmount lifecycle\n useEffect(() => {\n // Call onMount callback if provided\n if (options?.onMount) {\n options.onMount(bloc);\n }\n\n return () => {\n // Call onUnmount callback if provided\n if (options?.onUnmount) {\n options.onUnmount(bloc);\n }\n\n // Release bloc reference\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n Constructor.release(instanceKey);\n\n // For isolated instances, dispose manually since registry doesn't track them\n if (isIsolatedClass(BlocClass) && !bloc.isDisposed) {\n bloc.dispose();\n }\n };\n }, []);\n\n return bloc;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,oBACd,cACA,YACA,YACoB;AAEpB,KAAI,eAAe,OACjB,QAAO,OAAO,eAAe,WAAW,OAAO,WAAW,GAAG;AAI/D,KAAI,YAAY;AACd,MAAI,CAAC,aAAa,iBAChB,cAAa,yDAAwC;AAEvD,SAAO,aAAa;;;;;;ACIxB,SAAS,sBACP,SACc;AACd,QAAO;EACL,eAAe,SAAS,iBAAiB;EACzC,kBAAkB,SAAS,cAAc;EAC1C;;;;;;;;;;;;;;;;;;;AAoBH,SAAgB,QACd,WACA,SACgC;CAGhC,MAAM,iCAAoC,EAAE,CAAC;CAC7C,MAAM,cAAc;CACpB,MAAM,8CAA6B,UAAU;CAE7C,MAAM,CAAC,MAAM,WAAW,aAAa,aAAa,cAAc,wCAUtD;EAEN,MAAMA,gBAAc,oBAClB,aAAa,SACb,YACA,SAAS,WACV;EAGD,MAAM,WAAW,UAAU,QAAQA,eAAa,SAAS,YAAY;EAGrE,MAAM,EAAE,eAAe,qBACrB,sBAAsB,QAAQ;EAGhC,IAAIC;EACJ,IAAIC;EACJ,IAAIC;AAEJ,MAAI,iBAAiB,SAAS,cAAc;AAE1C,yDAAmC,SAAS;AAC5C,4DAAwC,UAAUC,gBAAc,EAC9D,cAAc,QAAQ,cACvB,CAAC;AACF,6DAAyC,UAAUA,gBAAc,EAC/D,cAAc,QAAQ,cACvB,CAAC;aACO,CAAC,kBAAkB;AAE5B,sDAAgC,SAAS;AACzC,yDAAqC,SAAS;AAC9C,0DAAsC,SAAS;SAC1C;AAEL,wDAAkC,SAAS;AAC3C,2DAAuC,UAAUA,eAAa;AAC9D,4DAAwC,UAAUA,eAAa;;AAGjE,SAAO;GACLA,eAAa;GACb;GACA;GACAJ;GACAI;GACA;GACD;IACA,CAAC,WAAW,SAAS,WAAW,CAAC;CAEtC,MAAM,wCAA6B,WAAW,YAAY;CAG1D,MAAM,GAAG,sCAA2B,MAAc,IAAI,GAAG,EAAE;CAG3D,MAAM,wCAA6B,IAAIC,uCAA2B,CAAC;AAGnE,4BAAgB;AACd,yCAAsB,cAAc,YAAY;AAChD,sBAAoB,QAAQ,oBAC1B,aAAa,eACb,aACA,YACD;GACD;AAGF,4BAAgB;AAEd,MAAI,SAAS,QACX,SAAQ,QAAQ,KAAK;AAGvB,eAAa;AAIX,uBAAoB,QAAQ,SAAS;AAGrC,OAAI,SAAS,UACX,SAAQ,UAAU,KAAK;AAIzB,eAAY,QAAQ,YAAY;AAGhC,OAAI,cAAc,CAAC,YAAY,WAC7B,aAAY,SAAS;;IAGxB,EAAE,CAAC;AAEN,QAAO;EAAC;EAAO;EAAM;EAAa;;;;;AChKpC,SAAgB,eAGd,WACA,SACiB;CAGjB,MAAM,iCAAoC,EAAE,CAAC;CAE7C,MAAM,CAAC,MAAM,wCAA6B;EACxC,MAAM,8CAA6B,UAAU;EAC7C,MAAM,cAAc;EAGpB,MAAMC,gBAAc,oBAClB,aAAa,SACb,YACA,SAAS,WACV;AAOD,SAAO,CAJU,SAAS,cACtB,YAAY,QAAQA,eAAa,QAAQ,YAAY,GACrD,YAAY,QAAQA,cAAY,EAElBA,cAAY;IAC7B,CAAC,UAAU,CAAC;AAGf,4BAAgB;AAEd,MAAI,SAAS,QACX,SAAQ,QAAQ,KAAK;AAGvB,eAAa;AAEX,OAAI,SAAS,UACX,SAAQ,UAAU,KAAK;AAKzB,GADoB,UACR,QAAQ,YAAY;AAGhC,wCAAoB,UAAU,IAAI,CAAC,KAAK,WACtC,MAAK,SAAS;;IAGjB,EAAE,CAAC;AAEN,QAAO"} |
+9
-151
@@ -1,154 +0,12 @@ | ||
| import { AnyObject, BlocConstructor, ExtractState, StateContainer } from "@blac/core"; | ||
| import { RefObject } from "react"; | ||
| //#region src/types.d.ts | ||
| /** | ||
| * Configuration options for the useBloc hook | ||
| * React Integration | ||
| * | ||
| * @template TBloc - The StateContainer type | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * const [state, bloc] = useBloc(CounterBloc, { | ||
| * staticProps: { initialCount: 0 }, | ||
| * dependencies: (state) => [state.count], | ||
| * onMount: (bloc) => console.log('Mounted'), | ||
| * }); | ||
| * ``` | ||
| * Clean integration between React and StateContainer architecture. | ||
| * Constructor-based API with automatic type inference. | ||
| * Supports concurrent (useSyncExternalStore) mode for optimal performance. | ||
| */ | ||
| interface UseBlocOptions<TBloc extends StateContainer<AnyObject>> { | ||
| /** | ||
| * Static props to pass to the Bloc constructor | ||
| * Type should match the constructor's first parameter | ||
| */ | ||
| staticProps?: AnyObject; | ||
| /** | ||
| * Custom instance ID for shared blocs | ||
| * - For isolated blocs, each useBloc call gets its own instance | ||
| * - For shared blocs, the same instanceId will share the same bloc instance | ||
| */ | ||
| instanceId?: string; | ||
| /** | ||
| * Manual dependency tracking function | ||
| * - When provided, automatic proxy tracking is disabled | ||
| * - Component only re-renders when the returned values change (shallow equality) | ||
| * | ||
| * @param state - Current state of the bloc | ||
| * @param bloc - The bloc instance | ||
| * @returns Array of values to track | ||
| */ | ||
| dependencies?: (state: ExtractState<TBloc>, bloc: TBloc) => unknown[]; | ||
| /** | ||
| * Control automatic dependency tracking (default: true) | ||
| * - true: Automatically track accessed properties via Proxy | ||
| * - false: Disable tracking, all state changes trigger re-render | ||
| * - Ignored when `dependencies` option is provided | ||
| * | ||
| * @default true | ||
| */ | ||
| autoTrack?: boolean; | ||
| /** | ||
| * Callback invoked when the component mounts | ||
| * | ||
| * @param bloc - The bloc instance | ||
| */ | ||
| onMount?: (bloc: TBloc) => void; | ||
| /** | ||
| * Callback invoked when the component unmounts | ||
| * | ||
| * @param bloc - The bloc instance | ||
| */ | ||
| onUnmount?: (bloc: TBloc) => void; | ||
| } | ||
| /** | ||
| * Return type of the useBloc hook | ||
| * | ||
| * @template TBloc - The StateContainer type | ||
| * | ||
| * @returns A tuple containing: | ||
| * - [0]: The current state (may be a Proxy for tracking) | ||
| * - [1]: The bloc instance with methods | ||
| * - [2]: Component ref (internal use only) | ||
| */ | ||
| type UseBlocReturn<TBloc extends StateContainer<AnyObject>> = [ExtractState<TBloc>, TBloc, RefObject<ComponentRef>]; | ||
| /** | ||
| * @internal | ||
| * Component reference object for internal tracking | ||
| * Used to persist instance IDs across React Strict Mode remounts | ||
| */ | ||
| type ComponentRef = { | ||
| __blocInstanceId?: string; | ||
| __bridge?: any; | ||
| }; | ||
| //#endregion | ||
| //#region src/useBloc.d.ts | ||
| /** | ||
| * Lifecycle: INITIAL MOUNT | ||
| * 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn | ||
| * 2. useSyncExternalStore calls getSnapshotFn (1st time) - lazy creates tracker, starts tracking, returns proxy | ||
| * 3. Component renders - proxy tracks property accesses | ||
| * 4. useSyncExternalStore calls getSnapshotFn (2nd time) - captures tracked paths, starts new tracking, returns proxy | ||
| * 5. useSyncExternalStore calls subscribeFn - sets up state change listener | ||
| * | ||
| * Lifecycle: STATE CHANGE | ||
| * 1. Bloc state changes | ||
| * 2. subscribeFn callback checks hasChanges() - only re-renders if tracked paths changed | ||
| * 3. If re-render: getSnapshotFn captures previous paths, starts tracking, returns proxy | ||
| * | ||
| * Lifecycle: RE-RENDER (parent re-render) | ||
| * 1. useMemo returns cached values (same bloc, subscribeFn, getSnapshotFn) | ||
| * 2. useSyncExternalStore calls getSnapshotFn - captures paths, starts tracking, returns proxy | ||
| */ | ||
| declare function useBloc<TBloc extends StateContainer<AnyObject>>(BlocClass: BlocConstructor<TBloc>, options?: UseBlocOptions<TBloc>): UseBlocReturn<TBloc>; | ||
| //#endregion | ||
| //#region src/useBlocActions.d.ts | ||
| /** | ||
| * Configuration options for the useBlocActions hook | ||
| * | ||
| * @template TBloc - The StateContainer type | ||
| */ | ||
| interface UseBlocActionsOptions<TBloc extends StateContainer<AnyObject>> { | ||
| /** | ||
| * Static props to pass to the Bloc constructor | ||
| * Type should match the constructor's first parameter | ||
| */ | ||
| staticProps?: AnyObject; | ||
| /** | ||
| * Custom instance ID for shared blocs | ||
| * - For isolated blocs, each useBlocActions call gets its own instance | ||
| * - For shared blocs, the same instanceId will share the same bloc instance | ||
| */ | ||
| instanceId?: string; | ||
| /** | ||
| * Callback invoked when the component mounts | ||
| * | ||
| * @param bloc - The bloc instance | ||
| */ | ||
| onMount?: (bloc: TBloc) => void; | ||
| /** | ||
| * Callback invoked when the component unmounts | ||
| * | ||
| * @param bloc - The bloc instance | ||
| */ | ||
| onUnmount?: (bloc: TBloc) => void; | ||
| } | ||
| /** | ||
| * React hook for accessing bloc instance without state subscription. | ||
| * Use this when you only need to call bloc methods/actions without reading state. | ||
| * | ||
| * Benefits over useBloc: | ||
| * - No state subscription overhead | ||
| * - No proxy tracking | ||
| * - Component never re-renders due to bloc state changes | ||
| * - Lighter weight for action-only components | ||
| * | ||
| * @template TBloc - The StateContainer type | ||
| * @param BlocClass - The bloc class constructor | ||
| * @param options - Optional configuration | ||
| * @returns The bloc instance | ||
| */ | ||
| declare function useBlocActions<TBloc extends StateContainer<AnyObject>>(BlocClass: BlocConstructor<TBloc>, options?: UseBlocActionsOptions<TBloc>): TBloc; | ||
| //#endregion | ||
| export { type UseBlocActionsOptions, type UseBlocOptions, type UseBlocReturn, useBloc, useBlocActions }; | ||
| //# sourceMappingURL=index.d.cts.map | ||
| export { useBloc } from './useBloc'; | ||
| export { useBlocActions } from './useBlocActions'; | ||
| export type { UseBlocOptions, UseBlocReturn } from './types'; | ||
| export type { UseBlocActionsOptions } from './useBlocActions'; | ||
| //# sourceMappingURL=index.d.ts.map |
+8
-150
@@ -1,154 +0,12 @@ | ||
| import { RefObject } from "react"; | ||
| import { AnyObject, BlocConstructor, ExtractState, StateContainer } from "@blac/core"; | ||
| //#region src/types.d.ts | ||
| /** | ||
| * Configuration options for the useBloc hook | ||
| * React Integration | ||
| * | ||
| * @template TBloc - The StateContainer type | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * const [state, bloc] = useBloc(CounterBloc, { | ||
| * staticProps: { initialCount: 0 }, | ||
| * dependencies: (state) => [state.count], | ||
| * onMount: (bloc) => console.log('Mounted'), | ||
| * }); | ||
| * ``` | ||
| * Clean integration between React and StateContainer architecture. | ||
| * Constructor-based API with automatic type inference. | ||
| * Supports concurrent (useSyncExternalStore) mode for optimal performance. | ||
| */ | ||
| interface UseBlocOptions<TBloc extends StateContainer<AnyObject>> { | ||
| /** | ||
| * Static props to pass to the Bloc constructor | ||
| * Type should match the constructor's first parameter | ||
| */ | ||
| staticProps?: AnyObject; | ||
| /** | ||
| * Custom instance ID for shared blocs | ||
| * - For isolated blocs, each useBloc call gets its own instance | ||
| * - For shared blocs, the same instanceId will share the same bloc instance | ||
| */ | ||
| instanceId?: string; | ||
| /** | ||
| * Manual dependency tracking function | ||
| * - When provided, automatic proxy tracking is disabled | ||
| * - Component only re-renders when the returned values change (shallow equality) | ||
| * | ||
| * @param state - Current state of the bloc | ||
| * @param bloc - The bloc instance | ||
| * @returns Array of values to track | ||
| */ | ||
| dependencies?: (state: ExtractState<TBloc>, bloc: TBloc) => unknown[]; | ||
| /** | ||
| * Control automatic dependency tracking (default: true) | ||
| * - true: Automatically track accessed properties via Proxy | ||
| * - false: Disable tracking, all state changes trigger re-render | ||
| * - Ignored when `dependencies` option is provided | ||
| * | ||
| * @default true | ||
| */ | ||
| autoTrack?: boolean; | ||
| /** | ||
| * Callback invoked when the component mounts | ||
| * | ||
| * @param bloc - The bloc instance | ||
| */ | ||
| onMount?: (bloc: TBloc) => void; | ||
| /** | ||
| * Callback invoked when the component unmounts | ||
| * | ||
| * @param bloc - The bloc instance | ||
| */ | ||
| onUnmount?: (bloc: TBloc) => void; | ||
| } | ||
| /** | ||
| * Return type of the useBloc hook | ||
| * | ||
| * @template TBloc - The StateContainer type | ||
| * | ||
| * @returns A tuple containing: | ||
| * - [0]: The current state (may be a Proxy for tracking) | ||
| * - [1]: The bloc instance with methods | ||
| * - [2]: Component ref (internal use only) | ||
| */ | ||
| type UseBlocReturn<TBloc extends StateContainer<AnyObject>> = [ExtractState<TBloc>, TBloc, RefObject<ComponentRef>]; | ||
| /** | ||
| * @internal | ||
| * Component reference object for internal tracking | ||
| * Used to persist instance IDs across React Strict Mode remounts | ||
| */ | ||
| type ComponentRef = { | ||
| __blocInstanceId?: string; | ||
| __bridge?: any; | ||
| }; | ||
| //#endregion | ||
| //#region src/useBloc.d.ts | ||
| /** | ||
| * Lifecycle: INITIAL MOUNT | ||
| * 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn | ||
| * 2. useSyncExternalStore calls getSnapshotFn (1st time) - lazy creates tracker, starts tracking, returns proxy | ||
| * 3. Component renders - proxy tracks property accesses | ||
| * 4. useSyncExternalStore calls getSnapshotFn (2nd time) - captures tracked paths, starts new tracking, returns proxy | ||
| * 5. useSyncExternalStore calls subscribeFn - sets up state change listener | ||
| * | ||
| * Lifecycle: STATE CHANGE | ||
| * 1. Bloc state changes | ||
| * 2. subscribeFn callback checks hasChanges() - only re-renders if tracked paths changed | ||
| * 3. If re-render: getSnapshotFn captures previous paths, starts tracking, returns proxy | ||
| * | ||
| * Lifecycle: RE-RENDER (parent re-render) | ||
| * 1. useMemo returns cached values (same bloc, subscribeFn, getSnapshotFn) | ||
| * 2. useSyncExternalStore calls getSnapshotFn - captures paths, starts tracking, returns proxy | ||
| */ | ||
| declare function useBloc<TBloc extends StateContainer<AnyObject>>(BlocClass: BlocConstructor<TBloc>, options?: UseBlocOptions<TBloc>): UseBlocReturn<TBloc>; | ||
| //#endregion | ||
| //#region src/useBlocActions.d.ts | ||
| /** | ||
| * Configuration options for the useBlocActions hook | ||
| * | ||
| * @template TBloc - The StateContainer type | ||
| */ | ||
| interface UseBlocActionsOptions<TBloc extends StateContainer<AnyObject>> { | ||
| /** | ||
| * Static props to pass to the Bloc constructor | ||
| * Type should match the constructor's first parameter | ||
| */ | ||
| staticProps?: AnyObject; | ||
| /** | ||
| * Custom instance ID for shared blocs | ||
| * - For isolated blocs, each useBlocActions call gets its own instance | ||
| * - For shared blocs, the same instanceId will share the same bloc instance | ||
| */ | ||
| instanceId?: string; | ||
| /** | ||
| * Callback invoked when the component mounts | ||
| * | ||
| * @param bloc - The bloc instance | ||
| */ | ||
| onMount?: (bloc: TBloc) => void; | ||
| /** | ||
| * Callback invoked when the component unmounts | ||
| * | ||
| * @param bloc - The bloc instance | ||
| */ | ||
| onUnmount?: (bloc: TBloc) => void; | ||
| } | ||
| /** | ||
| * React hook for accessing bloc instance without state subscription. | ||
| * Use this when you only need to call bloc methods/actions without reading state. | ||
| * | ||
| * Benefits over useBloc: | ||
| * - No state subscription overhead | ||
| * - No proxy tracking | ||
| * - Component never re-renders due to bloc state changes | ||
| * - Lighter weight for action-only components | ||
| * | ||
| * @template TBloc - The StateContainer type | ||
| * @param BlocClass - The bloc class constructor | ||
| * @param options - Optional configuration | ||
| * @returns The bloc instance | ||
| */ | ||
| declare function useBlocActions<TBloc extends StateContainer<AnyObject>>(BlocClass: BlocConstructor<TBloc>, options?: UseBlocActionsOptions<TBloc>): TBloc; | ||
| //#endregion | ||
| export { type UseBlocActionsOptions, type UseBlocOptions, type UseBlocReturn, useBloc, useBlocActions }; | ||
| export { useBloc } from './useBloc'; | ||
| export { useBlocActions } from './useBlocActions'; | ||
| export type { UseBlocOptions, UseBlocReturn } from './types'; | ||
| export type { UseBlocActionsOptions } from './useBlocActions'; | ||
| //# sourceMappingURL=index.d.ts.map |
+48
-287
@@ -1,170 +0,31 @@ | ||
| import { useEffect, useMemo, useRef, useSyncExternalStore } from "react"; | ||
| import { captureTrackedPaths, createProxy, createTrackerState, hasChanges, hasTrackedData, shallowEqual, startTracking } from "@blac/core"; | ||
| import { useEffect, useMemo, useReducer, useRef, useSyncExternalStore } from "react"; | ||
| import { ExternalDependencyManager, createAutoTrackSnapshot, createAutoTrackSubscribe, createManualDepsSnapshot, createManualDepsSubscribe, createNoTrackSnapshot, createNoTrackSubscribe, disableGetterTracking, generateIsolatedKey, initAutoTrackState, initManualDepsState, initNoTrackState, isIsolatedClass } from "@blac/core"; | ||
| //#region src/useBloc.ts | ||
| //#region src/utils/instance-keys.ts | ||
| /** | ||
| * Cache for property descriptors to avoid repeated prototype chain walks | ||
| * Maps from object constructor to a map of property name to descriptor | ||
| * Instance key generation utilities for React integration | ||
| */ | ||
| const descriptorCache = /* @__PURE__ */ new WeakMap(); | ||
| /** | ||
| * Cache for proxied blocs to ensure same proxy is returned for same bloc instance | ||
| * This is important for identity checks (e.g., bloc1 === bloc2) across components | ||
| * using the same shared bloc instance. | ||
| */ | ||
| const blocProxyCache = /* @__PURE__ */ new WeakMap(); | ||
| /** | ||
| * Map to store the currently active tracker during render. | ||
| * This allows the cached proxy to know which component's tracker to use. | ||
| * Set before render, cleared after render. | ||
| */ | ||
| const activeTrackerMap = /* @__PURE__ */ new WeakMap(); | ||
| /** | ||
| * Get property descriptor for a given property, with caching | ||
| * Generate an instance key for a bloc | ||
| * | ||
| * @remarks | ||
| * Walks up the prototype chain once per class to find the descriptor, | ||
| * then caches the result for performance. This is critical because | ||
| * we need to distinguish getters from methods and properties. | ||
| * Logic: | ||
| * - If user provides instanceId, use it (convert number to string) | ||
| * - If isolated, generate or reuse a unique key for this component | ||
| * - Otherwise, return undefined (use default key) | ||
| * | ||
| * @param obj - The object to get the descriptor from | ||
| * @param prop - The property name or symbol | ||
| * @returns The property descriptor if found, undefined otherwise | ||
| * @param componentRef - React component reference (persists across remounts) | ||
| * @param isIsolated - Whether the bloc is isolated | ||
| * @param providedId - User-provided instance ID (from options) | ||
| * @returns Instance key string or undefined for default | ||
| */ | ||
| function getDescriptor(obj, prop) { | ||
| const constructor = obj.constructor; | ||
| let constructorCache = descriptorCache.get(constructor); | ||
| if (constructorCache?.has(prop)) return constructorCache.get(prop); | ||
| let current = obj; | ||
| let descriptor; | ||
| while (current && current !== Object.prototype) { | ||
| descriptor = Object.getOwnPropertyDescriptor(current, prop); | ||
| if (descriptor) break; | ||
| current = Object.getPrototypeOf(current); | ||
| } | ||
| if (!constructorCache) { | ||
| constructorCache = /* @__PURE__ */ new Map(); | ||
| descriptorCache.set(constructor, constructorCache); | ||
| } | ||
| constructorCache.set(prop, descriptor); | ||
| return descriptor; | ||
| } | ||
| /** | ||
| * Check if a property is a getter (has a getter descriptor) | ||
| * | ||
| * @param obj - The object to check | ||
| * @param prop - The property name or symbol | ||
| * @returns True if the property is a getter, false otherwise | ||
| */ | ||
| function isGetter(obj, prop) { | ||
| return getDescriptor(obj, prop)?.get !== void 0; | ||
| } | ||
| /** | ||
| * Create a new getter tracking state | ||
| * | ||
| * @returns A new GetterTrackingState initialized with empty collections | ||
| */ | ||
| function createGetterTracker() { | ||
| return { | ||
| trackedValues: /* @__PURE__ */ new Map(), | ||
| currentlyAccessing: /* @__PURE__ */ new Set(), | ||
| trackedGetters: /* @__PURE__ */ new Set(), | ||
| isTracking: false, | ||
| renderCache: /* @__PURE__ */ new Map(), | ||
| cacheValid: false | ||
| }; | ||
| } | ||
| /** | ||
| * Create a proxy that intercepts getter access on a bloc instance | ||
| * | ||
| * @remarks | ||
| * This proxy wraps the bloc instance to track which getters are accessed | ||
| * during component render. When tracking is enabled (during render phase), | ||
| * it records accessed getters and stores their computed values for later | ||
| * comparison. | ||
| * | ||
| * IMPORTANT: This function caches proxies per bloc instance. Multiple components | ||
| * sharing the same bloc will get the same proxy instance. Each component sets | ||
| * its tracker in activeTrackerMap before render, and the proxy looks it up. | ||
| * | ||
| * @param bloc - The bloc instance to wrap | ||
| * @returns A proxied bloc that tracks getter access | ||
| */ | ||
| function createBlocProxy(bloc) { | ||
| const cached = blocProxyCache.get(bloc); | ||
| if (cached) return cached; | ||
| const proxy = new Proxy(bloc, { get(target, prop, receiver) { | ||
| const tracker = activeTrackerMap.get(target); | ||
| if (tracker?.isTracking && isGetter(target, prop)) { | ||
| tracker.currentlyAccessing.add(prop); | ||
| if (tracker.cacheValid && tracker.renderCache.has(prop)) { | ||
| const cachedValue = tracker.renderCache.get(prop); | ||
| tracker.trackedValues.set(prop, cachedValue); | ||
| return cachedValue; | ||
| } | ||
| const value = getDescriptor(target, prop).get.call(target); | ||
| tracker.trackedValues.set(prop, value); | ||
| return value; | ||
| } | ||
| return Reflect.get(target, prop, receiver); | ||
| } }); | ||
| blocProxyCache.set(bloc, proxy); | ||
| return proxy; | ||
| } | ||
| /** | ||
| * Check if any tracked getters have changed values | ||
| * | ||
| * @remarks | ||
| * Re-computes all getters that were accessed during the last render and | ||
| * compares their new values with stored values using Object.is() (reference | ||
| * equality). | ||
| * | ||
| * OPTIMIZATION: Render cache population | ||
| * - Computes ALL tracked getters (no early exit) to populate the render cache | ||
| * - This ensures each getter is computed only once per render cycle | ||
| * - If we're going to re-render, getters accessed during render will use cached values | ||
| * - Cache is populated even if no changes detected (useful for parent-triggered re-renders) | ||
| * - Trade-off: Give up early exit to ensure full cache population | ||
| * | ||
| * Error handling: If a getter throws during re-computation, we log a warning, | ||
| * stop tracking that specific getter, and treat it as "changed" to trigger | ||
| * a re-render. This prevents the tracking system from breaking while still | ||
| * allowing React's error boundary to handle the error on the next render. | ||
| * | ||
| * @param bloc - The bloc instance | ||
| * @param tracker - The getter tracking state | ||
| * @returns True if any tracked getter value changed, false otherwise | ||
| */ | ||
| function hasGetterChanges(bloc, tracker) { | ||
| if (!tracker || tracker.trackedGetters.size === 0) return false; | ||
| tracker.renderCache.clear(); | ||
| let hasAnyChange = false; | ||
| for (const prop of tracker.trackedGetters) try { | ||
| const descriptor = getDescriptor(bloc, prop); | ||
| if (!descriptor?.get) continue; | ||
| const newValue = descriptor.get.call(bloc); | ||
| const oldValue = tracker.trackedValues.get(prop); | ||
| tracker.renderCache.set(prop, newValue); | ||
| tracker.trackedValues.set(prop, newValue); | ||
| if (!Object.is(newValue, oldValue)) hasAnyChange = true; | ||
| } catch (error) { | ||
| console.warn(`[useBloc] Getter "${String(prop)}" threw error during change detection. Stopping tracking for this getter.`, error); | ||
| tracker.trackedGetters.delete(prop); | ||
| tracker.trackedValues.delete(prop); | ||
| tracker.cacheValid = false; | ||
| return true; | ||
| } | ||
| tracker.cacheValid = true; | ||
| return hasAnyChange; | ||
| } | ||
| /** | ||
| * Generates instance ID for isolated blocs | ||
| */ | ||
| function generateInstanceId$1(componentRef, isIsolated, providedId) { | ||
| if (providedId) return providedId; | ||
| function generateInstanceKey(componentRef, isIsolated, providedId) { | ||
| if (providedId !== void 0) return typeof providedId === "number" ? String(providedId) : providedId; | ||
| if (isIsolated) { | ||
| if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId = `isolated-${Math.random().toString(36).slice(2, 11)}`; | ||
| if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId = generateIsolatedKey(); | ||
| return componentRef.__blocInstanceId; | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/useBloc.ts | ||
| function determineTrackingMode(options) { | ||
@@ -177,71 +38,2 @@ return { | ||
| /** | ||
| * Factory: Creates subscribe function for automatic proxy tracking mode | ||
| */ | ||
| function createAutoTrackSubscribe(instance, hookState) { | ||
| return (callback) => { | ||
| return instance.subscribe(() => { | ||
| const tracker = hookState.tracker || (hookState.tracker = createTrackerState()); | ||
| let stateChanged = hasChanges(tracker, instance.state); | ||
| if (tracker.pathCache.size === 0 && hookState.getterTracker && hookState.getterTracker.trackedGetters.size > 0) stateChanged = false; | ||
| if (stateChanged) { | ||
| callback(); | ||
| return; | ||
| } | ||
| if (hasGetterChanges(instance, hookState.getterTracker)) callback(); | ||
| }); | ||
| }; | ||
| } | ||
| /** | ||
| * Factory: Creates subscribe function for manual dependencies mode | ||
| */ | ||
| function createManualDepsSubscribe(instance, hookState, options) { | ||
| return (callback) => { | ||
| return instance.subscribe(() => { | ||
| const newDeps = options.dependencies(instance.state, instance); | ||
| if (!hookState.manualDepsCache || !shallowEqual(hookState.manualDepsCache, newDeps)) { | ||
| hookState.manualDepsCache = newDeps; | ||
| callback(); | ||
| } | ||
| }); | ||
| }; | ||
| } | ||
| /** | ||
| * Factory: Creates subscribe function for no-tracking mode | ||
| */ | ||
| function createNoTrackSubscribe(instance) { | ||
| return (callback) => instance.subscribe(callback); | ||
| } | ||
| /** | ||
| * Factory: Creates getSnapshot function for automatic proxy tracking mode | ||
| */ | ||
| function createAutoTrackSnapshot(instance, hookState) { | ||
| return () => { | ||
| const tracker = hookState.tracker || (hookState.tracker = createTrackerState()); | ||
| if (hasTrackedData(tracker)) captureTrackedPaths(tracker, instance.state); | ||
| if (hookState.getterTracker) { | ||
| if (hookState.getterTracker.currentlyAccessing.size > 0) hookState.getterTracker.trackedGetters = new Set(hookState.getterTracker.currentlyAccessing); | ||
| hookState.getterTracker.currentlyAccessing.clear(); | ||
| hookState.getterTracker.isTracking = true; | ||
| activeTrackerMap.set(instance, hookState.getterTracker); | ||
| } | ||
| startTracking(tracker); | ||
| return createProxy(tracker, instance.state); | ||
| }; | ||
| } | ||
| /** | ||
| * Factory: Creates getSnapshot function for manual dependencies mode | ||
| */ | ||
| function createManualDepsSnapshot(instance, hookState, options) { | ||
| return () => { | ||
| hookState.manualDepsCache = options.dependencies(instance.state, instance); | ||
| return instance.state; | ||
| }; | ||
| } | ||
| /** | ||
| * Factory: Creates getSnapshot function for no-tracking mode | ||
| */ | ||
| function createNoTrackSnapshot(instance) { | ||
| return () => instance.state; | ||
| } | ||
| /** | ||
| * Lifecycle: INITIAL MOUNT | ||
@@ -265,46 +57,39 @@ * 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn | ||
| const componentRef = useRef({}); | ||
| const [bloc, subscribe, getSnapshot, instanceKey, hookState, rawInstance] = useMemo(() => { | ||
| const isIsolated = BlocClass.isolated === true; | ||
| const Constructor = BlocClass; | ||
| const instanceId = generateInstanceId$1(componentRef.current, isIsolated, options?.instanceId); | ||
| const instance = Constructor.getOrCreate(instanceId, options?.staticProps); | ||
| const Constructor = BlocClass; | ||
| const isIsolated = isIsolatedClass(BlocClass); | ||
| const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] = useMemo(() => { | ||
| const instanceKey$1 = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId); | ||
| const instance = BlocClass.resolve(instanceKey$1, options?.staticProps); | ||
| const { useManualDeps, autoTrackEnabled } = determineTrackingMode(options); | ||
| const hookState$1 = { | ||
| tracker: null, | ||
| manualDepsCache: null, | ||
| getterTracker: null, | ||
| proxiedBloc: null | ||
| }; | ||
| let subscribeFn; | ||
| let getSnapshotFn; | ||
| if (useManualDeps) { | ||
| subscribeFn = createManualDepsSubscribe(instance, hookState$1, options); | ||
| getSnapshotFn = createManualDepsSnapshot(instance, hookState$1, options); | ||
| hookState$1.proxiedBloc = instance; | ||
| let adapterState$1; | ||
| if (useManualDeps && options?.dependencies) { | ||
| adapterState$1 = initManualDepsState(instance); | ||
| subscribeFn = createManualDepsSubscribe(instance, adapterState$1, { dependencies: options.dependencies }); | ||
| getSnapshotFn = createManualDepsSnapshot(instance, adapterState$1, { dependencies: options.dependencies }); | ||
| } else if (!autoTrackEnabled) { | ||
| adapterState$1 = initNoTrackState(instance); | ||
| subscribeFn = createNoTrackSubscribe(instance); | ||
| getSnapshotFn = createNoTrackSnapshot(instance); | ||
| hookState$1.proxiedBloc = instance; | ||
| } else { | ||
| subscribeFn = createAutoTrackSubscribe(instance, hookState$1); | ||
| getSnapshotFn = createAutoTrackSnapshot(instance, hookState$1); | ||
| hookState$1.getterTracker = createGetterTracker(); | ||
| hookState$1.proxiedBloc = createBlocProxy(instance); | ||
| adapterState$1 = initAutoTrackState(instance); | ||
| subscribeFn = createAutoTrackSubscribe(instance, adapterState$1); | ||
| getSnapshotFn = createAutoTrackSnapshot(instance, adapterState$1); | ||
| } | ||
| return [ | ||
| hookState$1.proxiedBloc, | ||
| adapterState$1.proxiedBloc, | ||
| subscribeFn, | ||
| getSnapshotFn, | ||
| instanceId, | ||
| hookState$1, | ||
| instanceKey$1, | ||
| adapterState$1, | ||
| instance | ||
| ]; | ||
| }, [BlocClass]); | ||
| }, [BlocClass, options?.instanceId]); | ||
| const state = useSyncExternalStore(subscribe, getSnapshot); | ||
| const [, forceUpdate] = useReducer((x) => x + 1, 0); | ||
| const externalDepsManager = useRef(new ExternalDependencyManager()); | ||
| useEffect(() => { | ||
| if (hookState.getterTracker) { | ||
| hookState.getterTracker.isTracking = false; | ||
| hookState.getterTracker.cacheValid = false; | ||
| activeTrackerMap.delete(rawInstance); | ||
| } | ||
| disableGetterTracking(adapterState, rawInstance); | ||
| externalDepsManager.current.updateSubscriptions(adapterState.getterTracker, rawInstance, forceUpdate); | ||
| }); | ||
@@ -314,5 +99,6 @@ useEffect(() => { | ||
| return () => { | ||
| externalDepsManager.current.cleanup(); | ||
| if (options?.onUnmount) options.onUnmount(bloc); | ||
| BlocClass.release(instanceKey); | ||
| if (BlocClass.isolated === true && !rawInstance.isDisposed) rawInstance.dispose(); | ||
| Constructor.release(instanceKey); | ||
| if (isIsolated && !rawInstance.isDisposed) rawInstance.dispose(); | ||
| }; | ||
@@ -329,34 +115,9 @@ }, []); | ||
| //#region src/useBlocActions.ts | ||
| /** | ||
| * Generates instance ID for isolated blocs | ||
| */ | ||
| function generateInstanceId(componentRef, isIsolated, providedId) { | ||
| if (providedId) return providedId; | ||
| if (isIsolated) { | ||
| if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId = `isolated-${Math.random().toString(36).slice(2, 11)}`; | ||
| return componentRef.__blocInstanceId; | ||
| } | ||
| } | ||
| /** | ||
| * React hook for accessing bloc instance without state subscription. | ||
| * Use this when you only need to call bloc methods/actions without reading state. | ||
| * | ||
| * Benefits over useBloc: | ||
| * - No state subscription overhead | ||
| * - No proxy tracking | ||
| * - Component never re-renders due to bloc state changes | ||
| * - Lighter weight for action-only components | ||
| * | ||
| * @template TBloc - The StateContainer type | ||
| * @param BlocClass - The bloc class constructor | ||
| * @param options - Optional configuration | ||
| * @returns The bloc instance | ||
| */ | ||
| function useBlocActions(BlocClass, options) { | ||
| const componentRef = useRef({}); | ||
| const [bloc, instanceKey] = useMemo(() => { | ||
| const isIsolated = BlocClass.isolated === true; | ||
| const isIsolated = isIsolatedClass(BlocClass); | ||
| const Constructor = BlocClass; | ||
| const instanceId = generateInstanceId(componentRef.current, isIsolated, options?.instanceId); | ||
| return [Constructor.getOrCreate(instanceId, options?.staticProps), instanceId]; | ||
| const instanceKey$1 = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId); | ||
| return [options?.staticProps ? Constructor.resolve(instanceKey$1, options.staticProps) : Constructor.resolve(instanceKey$1), instanceKey$1]; | ||
| }, [BlocClass]); | ||
@@ -368,3 +129,3 @@ useEffect(() => { | ||
| BlocClass.release(instanceKey); | ||
| if (BlocClass.isolated === true && !bloc.isDisposed) bloc.dispose(); | ||
| if (isIsolatedClass(BlocClass) && !bloc.isDisposed) bloc.dispose(); | ||
| }; | ||
@@ -371,0 +132,0 @@ }, []); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.js","names":["descriptor: PropertyDescriptor | undefined","generateInstanceId","hookState: HookState<TBloc>","subscribeFn: (callback: () => void) => () => void","getSnapshotFn: () => ExtractState<TBloc>","hookState"],"sources":["../src/useBloc.ts","../src/useBlocActions.ts"],"sourcesContent":["/**\n * useBloc - hook for BlaC state management in React with automatic proxy tracking\n *\n * @example\n * ```tsx\n * // Basic usage - automatic tracking of accessed properties\n * function Counter() {\n * const [state, bloc] = useBloc(CounterBloc);\n * return (\n * <div>\n * <p>Count: {state.count}</p> // Only re-renders when count changes\n * <button onClick={bloc.increment}>+</button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { useMemo, useSyncExternalStore, useEffect, useRef } from 'react';\nimport {\n type AnyObject,\n type BlocConstructor,\n StateContainer,\n type ExtractState,\n // Import tracking utilities from core\n type TrackerState,\n createTrackerState,\n startTracking,\n createProxy,\n captureTrackedPaths,\n hasChanges,\n hasTrackedData,\n shallowEqual,\n} from '@blac/core';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\n\n/**\n * StateContainer constructor with required static methods\n */\ntype StateContainerConstructor<TBloc extends StateContainer<any>> =\n BlocConstructor<TBloc> & {\n getOrCreate(instanceKey?: string, ...args: any[]): TBloc;\n release(instanceKey?: string): void;\n };\n\n/**\n * State for tracking getter access and values during render\n *\n * @remarks\n * This tracks which getters are accessed during component render and stores\n * their computed values for comparison on subsequent state changes.\n * Only getters (properties with a getter descriptor) are tracked automatically.\n *\n * Similar to state tracking, we use two sets:\n * - `currentlyAccessing`: Temporary set for getters accessed during current render\n * - `trackedGetters`: Committed set of getters from last completed render\n *\n * PERFORMANCE: Render cache optimization\n * - When checking if we should re-render (hasGetterChanges), we compute all getters\n * - Store these computed values in `renderCache` for the upcoming render\n * - During render, if we access a cached getter, use the cached value instead of recomputing\n * - This ensures each getter is computed at most once per render cycle\n * - Cache is invalidated after render completes or when state changes again\n */\ninterface GetterTrackingState {\n /** Map of getter names to their last computed values (for comparison) */\n trackedValues: Map<string | symbol, unknown>;\n /** Temporary set of getters being accessed during current render */\n currentlyAccessing: Set<string | symbol>;\n /** Committed set of getters from last completed render (used for change detection) */\n trackedGetters: Set<string | symbol>;\n /** Flag to enable/disable tracking (only enabled during render phase) */\n isTracking: boolean;\n /** Cache of getter values computed during hasGetterChanges (valid for current render cycle) */\n renderCache: Map<string | symbol, unknown>;\n /** Flag indicating render cache is valid and can be used */\n cacheValid: boolean;\n}\n\n/**\n * Cache for property descriptors to avoid repeated prototype chain walks\n * Maps from object constructor to a map of property name to descriptor\n */\nconst descriptorCache = new WeakMap<\n Function,\n Map<string | symbol, PropertyDescriptor | undefined>\n>();\n\n/**\n * Cache for proxied blocs to ensure same proxy is returned for same bloc instance\n * This is important for identity checks (e.g., bloc1 === bloc2) across components\n * using the same shared bloc instance.\n */\nconst blocProxyCache = new WeakMap<StateContainer<any>, any>();\n\n/**\n * Map to store the currently active tracker during render.\n * This allows the cached proxy to know which component's tracker to use.\n * Set before render, cleared after render.\n */\nconst activeTrackerMap = new WeakMap<\n StateContainer<any>,\n GetterTrackingState\n>();\n\n/**\n * Get property descriptor for a given property, with caching\n *\n * @remarks\n * Walks up the prototype chain once per class to find the descriptor,\n * then caches the result for performance. This is critical because\n * we need to distinguish getters from methods and properties.\n *\n * @param obj - The object to get the descriptor from\n * @param prop - The property name or symbol\n * @returns The property descriptor if found, undefined otherwise\n */\nfunction getDescriptor(\n obj: any,\n prop: string | symbol,\n): PropertyDescriptor | undefined {\n const constructor = obj.constructor;\n\n // Try to get from cache\n let constructorCache = descriptorCache.get(constructor);\n if (constructorCache?.has(prop)) {\n return constructorCache.get(prop);\n }\n\n // Walk prototype chain to find descriptor\n let current = obj;\n let descriptor: PropertyDescriptor | undefined;\n\n while (current && current !== Object.prototype) {\n descriptor = Object.getOwnPropertyDescriptor(current, prop);\n if (descriptor) {\n break;\n }\n current = Object.getPrototypeOf(current);\n }\n\n // Cache the result\n if (!constructorCache) {\n constructorCache = new Map();\n descriptorCache.set(constructor, constructorCache);\n }\n constructorCache.set(prop, descriptor);\n\n return descriptor;\n}\n\n/**\n * Check if a property is a getter (has a getter descriptor)\n *\n * @param obj - The object to check\n * @param prop - The property name or symbol\n * @returns True if the property is a getter, false otherwise\n */\nfunction isGetter(obj: any, prop: string | symbol): boolean {\n const descriptor = getDescriptor(obj, prop);\n return descriptor?.get !== undefined;\n}\n\n/**\n * Create a new getter tracking state\n *\n * @returns A new GetterTrackingState initialized with empty collections\n */\nfunction createGetterTracker(): GetterTrackingState {\n return {\n trackedValues: new Map(),\n currentlyAccessing: new Set(),\n trackedGetters: new Set(),\n isTracking: false,\n renderCache: new Map(),\n cacheValid: false,\n };\n}\n\n/**\n * Create a proxy that intercepts getter access on a bloc instance\n *\n * @remarks\n * This proxy wraps the bloc instance to track which getters are accessed\n * during component render. When tracking is enabled (during render phase),\n * it records accessed getters and stores their computed values for later\n * comparison.\n *\n * IMPORTANT: This function caches proxies per bloc instance. Multiple components\n * sharing the same bloc will get the same proxy instance. Each component sets\n * its tracker in activeTrackerMap before render, and the proxy looks it up.\n *\n * @param bloc - The bloc instance to wrap\n * @returns A proxied bloc that tracks getter access\n */\nfunction createBlocProxy<TBloc extends StateContainer<AnyObject>>(\n bloc: TBloc,\n): TBloc {\n // Check cache first - return existing proxy if available\n const cached = blocProxyCache.get(bloc);\n if (cached) {\n return cached;\n }\n\n const proxy = new Proxy(bloc, {\n get(target, prop, receiver) {\n // Get the active tracker for this bloc (set by getSnapshot)\n const tracker = activeTrackerMap.get(target);\n\n // Only track during render phase (when tracker is active and tracking enabled)\n if (tracker?.isTracking && isGetter(target, prop)) {\n // Record that this getter was accessed during current render\n tracker.currentlyAccessing.add(prop);\n\n // Use cached value if available from previous change detection\n if (tracker.cacheValid && tracker.renderCache.has(prop)) {\n const cachedValue = tracker.renderCache.get(prop);\n // Also store in trackedValues for consistency\n tracker.trackedValues.set(prop, cachedValue);\n return cachedValue;\n }\n\n // Compute getter if no cache available (first access or cache invalidated)\n const descriptor = getDescriptor(target, prop);\n const value = descriptor!.get!.call(target);\n tracker.trackedValues.set(prop, value);\n return value;\n }\n\n // Default behavior for non-getters or when tracking disabled\n return Reflect.get(target, prop, receiver);\n },\n });\n\n blocProxyCache.set(bloc, proxy);\n return proxy;\n}\n\n/**\n * Check if any tracked getters have changed values\n *\n * @remarks\n * Re-computes all getters that were accessed during the last render and\n * compares their new values with stored values using Object.is() (reference\n * equality).\n *\n * OPTIMIZATION: Render cache population\n * - Computes ALL tracked getters (no early exit) to populate the render cache\n * - This ensures each getter is computed only once per render cycle\n * - If we're going to re-render, getters accessed during render will use cached values\n * - Cache is populated even if no changes detected (useful for parent-triggered re-renders)\n * - Trade-off: Give up early exit to ensure full cache population\n *\n * Error handling: If a getter throws during re-computation, we log a warning,\n * stop tracking that specific getter, and treat it as \"changed\" to trigger\n * a re-render. This prevents the tracking system from breaking while still\n * allowing React's error boundary to handle the error on the next render.\n *\n * @param bloc - The bloc instance\n * @param tracker - The getter tracking state\n * @returns True if any tracked getter value changed, false otherwise\n */\nfunction hasGetterChanges<TBloc extends StateContainer<AnyObject>>(\n bloc: TBloc,\n tracker: GetterTrackingState | null,\n): boolean {\n // Early return if no tracker or no getters tracked\n if (!tracker || tracker.trackedGetters.size === 0) {\n return false;\n }\n\n // Clear previous render cache\n tracker.renderCache.clear();\n\n let hasAnyChange = false;\n\n // Compute all getters to populate render cache (no early exit)\n for (const prop of tracker.trackedGetters) {\n try {\n const descriptor = getDescriptor(bloc, prop);\n if (!descriptor?.get) {\n // Getter no longer exists (shouldn't happen, but be defensive)\n continue;\n }\n\n const newValue = descriptor.get.call(bloc);\n const oldValue = tracker.trackedValues.get(prop);\n\n // Store in render cache for upcoming render (even if unchanged)\n tracker.renderCache.set(prop, newValue);\n\n // Update tracked values for next comparison\n tracker.trackedValues.set(prop, newValue);\n\n // Use Object.is for reference equality comparison\n if (!Object.is(newValue, oldValue)) {\n hasAnyChange = true;\n // Don't return early - continue computing and caching remaining getters\n }\n } catch (error) {\n // Getter threw an error during comparison\n console.warn(\n `[useBloc] Getter \"${String(prop)}\" threw error during change detection. Stopping tracking for this getter.`,\n error,\n );\n\n // Stop tracking this getter\n tracker.trackedGetters.delete(prop);\n tracker.trackedValues.delete(prop);\n\n // Treat as \"changed\" to trigger re-render\n // Still return early on error to avoid cascading failures\n tracker.cacheValid = false; // Invalidate cache due to error\n return true;\n }\n }\n\n // Mark cache as valid for the upcoming render\n tracker.cacheValid = true;\n\n return hasAnyChange;\n}\n\n/**\n * Generates instance ID for isolated blocs\n */\nfunction generateInstanceId(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string,\n): string | undefined {\n if (providedId) return providedId;\n\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = `isolated-${Math.random().toString(36).slice(2, 11)}`;\n }\n return componentRef.__blocInstanceId;\n }\n\n return undefined;\n}\n\n/**\n * Determines tracking mode from options\n */\ninterface TrackingMode {\n useManualDeps: boolean;\n autoTrackEnabled: boolean;\n}\n\nfunction determineTrackingMode<TBloc extends StateContainer<AnyObject>>(\n options?: UseBlocOptions<TBloc>,\n): TrackingMode {\n return {\n useManualDeps: options?.dependencies !== undefined,\n autoTrackEnabled: options?.autoTrack !== false,\n };\n}\n\n/**\n * Internal state for subscription and snapshot functions\n */\ninterface HookState<TBloc extends StateContainer<AnyObject>> {\n /** State property tracker (existing) */\n tracker: TrackerState<ExtractState<TBloc>> | null;\n /** Manual dependencies cache (existing) */\n manualDepsCache: unknown[] | null;\n /** Getter tracking state (new) */\n getterTracker: GetterTrackingState | null;\n /** Cached proxied bloc instance (new) */\n proxiedBloc: TBloc | null;\n}\n\n/**\n * Factory: Creates subscribe function for automatic proxy tracking mode\n */\nfunction createAutoTrackSubscribe<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n hookState: HookState<TBloc>,\n): (callback: () => void) => () => void {\n return (callback: () => void) => {\n return instance.subscribe(() => {\n const tracker =\n hookState.tracker ||\n (hookState.tracker = createTrackerState<ExtractState<TBloc>>());\n\n let stateChanged = hasChanges(tracker, instance.state);\n\n // Special case: if NO state properties were tracked (pathCache.size === 0)\n // but getters WERE tracked, then don't treat \"no state tracking\" as \"track everything\".\n // Only rely on getter changes in this case.\n if (\n tracker.pathCache.size === 0 &&\n hookState.getterTracker &&\n hookState.getterTracker.trackedGetters.size > 0\n ) {\n stateChanged = false; // Override - only getters are relevant\n }\n\n // EARLY EXIT: If state already changed, skip getter checks entirely\n if (stateChanged) {\n callback();\n return;\n }\n\n // Only check getters if state didn't change\n const getterChanged = hasGetterChanges(instance, hookState.getterTracker);\n\n if (getterChanged) {\n callback();\n }\n });\n };\n}\n\n/**\n * Factory: Creates subscribe function for manual dependencies mode\n */\nfunction createManualDepsSubscribe<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n hookState: HookState<TBloc>,\n options: UseBlocOptions<TBloc>,\n): (callback: () => void) => () => void {\n return (callback: () => void) => {\n return instance.subscribe(() => {\n const newDeps = options.dependencies!(instance.state, instance);\n if (\n !hookState.manualDepsCache ||\n !shallowEqual(hookState.manualDepsCache, newDeps)\n ) {\n hookState.manualDepsCache = newDeps;\n callback();\n }\n });\n };\n}\n\n/**\n * Factory: Creates subscribe function for no-tracking mode\n */\nfunction createNoTrackSubscribe<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n): (callback: () => void) => () => void {\n return (callback: () => void) => instance.subscribe(callback);\n}\n\n/**\n * Factory: Creates getSnapshot function for automatic proxy tracking mode\n */\nfunction createAutoTrackSnapshot<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n hookState: HookState<TBloc>,\n): () => ExtractState<TBloc> {\n return () => {\n const tracker =\n hookState.tracker ||\n (hookState.tracker = createTrackerState<ExtractState<TBloc>>());\n\n if (hasTrackedData(tracker)) {\n captureTrackedPaths(tracker, instance.state);\n }\n\n // Enable getter tracking during render and set as active tracker\n if (hookState.getterTracker) {\n // Capture getters from previous render (commit currentlyAccessing to trackedGetters)\n if (hookState.getterTracker.currentlyAccessing.size > 0) {\n hookState.getterTracker.trackedGetters = new Set(\n hookState.getterTracker.currentlyAccessing,\n );\n }\n\n // Clear and enable tracking for this render\n hookState.getterTracker.currentlyAccessing.clear();\n hookState.getterTracker.isTracking = true;\n\n // Set this component's tracker as the active one for this bloc\n activeTrackerMap.set(instance, hookState.getterTracker);\n }\n\n startTracking(tracker);\n return createProxy(tracker, instance.state);\n };\n}\n\n/**\n * Factory: Creates getSnapshot function for manual dependencies mode\n */\nfunction createManualDepsSnapshot<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n hookState: HookState<TBloc>,\n options: UseBlocOptions<TBloc>,\n): () => ExtractState<TBloc> {\n return () => {\n hookState.manualDepsCache = options.dependencies!(instance.state, instance);\n return instance.state;\n };\n}\n\n/**\n * Factory: Creates getSnapshot function for no-tracking mode\n */\nfunction createNoTrackSnapshot<TBloc extends StateContainer<AnyObject>>(\n instance: TBloc,\n): () => ExtractState<TBloc> {\n return () => instance.state;\n}\n\n/**\n * Lifecycle: INITIAL MOUNT\n * 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn\n * 2. useSyncExternalStore calls getSnapshotFn (1st time) - lazy creates tracker, starts tracking, returns proxy\n * 3. Component renders - proxy tracks property accesses\n * 4. useSyncExternalStore calls getSnapshotFn (2nd time) - captures tracked paths, starts new tracking, returns proxy\n * 5. useSyncExternalStore calls subscribeFn - sets up state change listener\n *\n * Lifecycle: STATE CHANGE\n * 1. Bloc state changes\n * 2. subscribeFn callback checks hasChanges() - only re-renders if tracked paths changed\n * 3. If re-render: getSnapshotFn captures previous paths, starts tracking, returns proxy\n *\n * Lifecycle: RE-RENDER (parent re-render)\n * 1. useMemo returns cached values (same bloc, subscribeFn, getSnapshotFn)\n * 2. useSyncExternalStore calls getSnapshotFn - captures paths, starts tracking, returns proxy\n */\nexport function useBloc<TBloc extends StateContainer<AnyObject>>(\n BlocClass: BlocConstructor<TBloc>,\n options?: UseBlocOptions<TBloc>,\n): UseBlocReturn<TBloc> {\n // Component reference that persists across React Strict Mode remounts\n const componentRef = useRef<ComponentRef>({});\n\n const [bloc, subscribe, getSnapshot, instanceKey, hookState, rawInstance] =\n useMemo(() => {\n const isIsolated =\n (BlocClass as { isolated?: boolean }).isolated === true;\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n\n // Generate instance key\n const instanceId = generateInstanceId(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n // Get or create bloc instance\n const instance = Constructor.getOrCreate(\n instanceId,\n options?.staticProps,\n );\n\n // Determine tracking mode\n const { useManualDeps, autoTrackEnabled } =\n determineTrackingMode(options);\n\n // Mutable state shared between subscribe and getSnapshot\n const hookState: HookState<TBloc> = {\n tracker: null,\n manualDepsCache: null,\n getterTracker: null,\n proxiedBloc: null,\n };\n\n // Create subscribe and getSnapshot functions based on tracking mode\n let subscribeFn: (callback: () => void) => () => void;\n let getSnapshotFn: () => ExtractState<TBloc>;\n\n if (useManualDeps) {\n // Manual dependencies mode - no automatic tracking\n subscribeFn = createManualDepsSubscribe(instance, hookState, options!);\n getSnapshotFn = createManualDepsSnapshot(instance, hookState, options!);\n hookState.proxiedBloc = instance; // Use raw instance\n } else if (!autoTrackEnabled) {\n // No tracking mode\n subscribeFn = createNoTrackSubscribe(instance);\n getSnapshotFn = createNoTrackSnapshot(instance);\n hookState.proxiedBloc = instance; // Use raw instance\n } else {\n // Auto-tracking mode - enable both state and getter tracking\n subscribeFn = createAutoTrackSubscribe(instance, hookState);\n getSnapshotFn = createAutoTrackSnapshot(instance, hookState);\n\n // Initialize getter tracker and create proxied bloc\n hookState.getterTracker = createGetterTracker();\n hookState.proxiedBloc = createBlocProxy(instance);\n }\n\n return [\n hookState.proxiedBloc!,\n subscribeFn,\n getSnapshotFn,\n instanceId,\n hookState,\n instance,\n ] as const;\n }, [BlocClass]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot);\n\n // Disable getter tracking after each render and clear active tracker\n // Also invalidate render cache since this render cycle is complete\n useEffect(() => {\n if (hookState.getterTracker) {\n hookState.getterTracker.isTracking = false;\n hookState.getterTracker.cacheValid = false; // Invalidate cache after render\n activeTrackerMap.delete(rawInstance);\n }\n });\n\n // Mount/unmount lifecycle\n useEffect(() => {\n // Call onMount callback if provided\n if (options?.onMount) {\n options.onMount(bloc);\n }\n\n return () => {\n // Call onUnmount callback if provided\n if (options?.onUnmount) {\n options.onUnmount(bloc);\n }\n\n // Release bloc reference\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n Constructor.release(instanceKey);\n\n // For isolated instances, dispose manually since registry doesn't track them\n const isIsolated =\n (BlocClass as { isolated?: boolean }).isolated === true;\n if (isIsolated && !rawInstance.isDisposed) {\n rawInstance.dispose();\n }\n };\n }, []);\n\n return [state, bloc, componentRef] as UseBlocReturn<TBloc>;\n}\n","/**\n * useBlocActions - hook for accessing bloc instance without state subscription\n *\n * @example\n * ```tsx\n * // Use when you only need to call actions, not read state\n * function ActionsOnly() {\n * const bloc = useBlocActions(CounterBloc);\n * return (\n * <div>\n * <button onClick={bloc.increment}>+</button>\n * <button onClick={bloc.decrement}>-</button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { useMemo, useEffect, useRef } from 'react';\nimport type { AnyObject, BlocConstructor, StateContainer } from '@blac/core';\nimport type { ComponentRef } from './types';\n\n/**\n * StateContainer constructor with required static methods\n */\ntype StateContainerConstructor<TBloc extends StateContainer<any>> =\n BlocConstructor<TBloc> & {\n getOrCreate(instanceKey?: string, ...args: any[]): TBloc;\n release(instanceKey?: string): void;\n };\n\n/**\n * Configuration options for the useBlocActions hook\n *\n * @template TBloc - The StateContainer type\n */\nexport interface UseBlocActionsOptions<\n TBloc extends StateContainer<AnyObject>,\n> {\n /**\n * Static props to pass to the Bloc constructor\n * Type should match the constructor's first parameter\n */\n staticProps?: AnyObject;\n\n /**\n * Custom instance ID for shared blocs\n * - For isolated blocs, each useBlocActions call gets its own instance\n * - For shared blocs, the same instanceId will share the same bloc instance\n */\n instanceId?: string;\n\n /**\n * Callback invoked when the component mounts\n *\n * @param bloc - The bloc instance\n */\n onMount?: (bloc: TBloc) => void;\n\n /**\n * Callback invoked when the component unmounts\n *\n * @param bloc - The bloc instance\n */\n onUnmount?: (bloc: TBloc) => void;\n}\n\n/**\n * Generates instance ID for isolated blocs\n */\nfunction generateInstanceId(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string,\n): string | undefined {\n if (providedId) return providedId;\n\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = `isolated-${Math.random().toString(36).slice(2, 11)}`;\n }\n return componentRef.__blocInstanceId;\n }\n\n return undefined;\n}\n\n/**\n * React hook for accessing bloc instance without state subscription.\n * Use this when you only need to call bloc methods/actions without reading state.\n *\n * Benefits over useBloc:\n * - No state subscription overhead\n * - No proxy tracking\n * - Component never re-renders due to bloc state changes\n * - Lighter weight for action-only components\n *\n * @template TBloc - The StateContainer type\n * @param BlocClass - The bloc class constructor\n * @param options - Optional configuration\n * @returns The bloc instance\n */\nexport function useBlocActions<TBloc extends StateContainer<AnyObject>>(\n BlocClass: BlocConstructor<TBloc>,\n options?: UseBlocActionsOptions<TBloc>,\n): TBloc {\n // Component reference that persists across React Strict Mode remounts\n const componentRef = useRef<ComponentRef>({});\n\n const [bloc, instanceKey] = useMemo(() => {\n const isIsolated = (BlocClass as { isolated?: boolean }).isolated === true;\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n\n // Generate instance key\n const instanceId = generateInstanceId(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n // Get or create bloc instance\n const instance = Constructor.getOrCreate(instanceId, options?.staticProps);\n\n return [instance, instanceId] as const;\n }, [BlocClass]);\n\n // Mount/unmount lifecycle\n useEffect(() => {\n // Call onMount callback if provided\n if (options?.onMount) {\n options.onMount(bloc);\n }\n\n return () => {\n // Call onUnmount callback if provided\n if (options?.onUnmount) {\n options.onUnmount(bloc);\n }\n\n // Release bloc reference\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n Constructor.release(instanceKey);\n\n // For isolated instances, dispose manually since registry doesn't track them\n const isIsolated =\n (BlocClass as { isolated?: boolean }).isolated === true;\n if (isIsolated && !bloc.isDisposed) {\n bloc.dispose();\n }\n };\n }, []);\n\n return bloc;\n}\n"],"mappings":"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"} | ||
| {"version":3,"file":"index.js","names":["instanceKey","subscribeFn: (callback: () => void) => () => void","getSnapshotFn: () => ExtractState<TBloc>","adapterState: AdapterState<TBloc>","adapterState","instanceKey"],"sources":["../src/utils/instance-keys.ts","../src/useBloc.ts","../src/useBlocActions.ts"],"sourcesContent":["/**\n * Instance key generation utilities for React integration\n */\n\nimport { generateIsolatedKey } from '@blac/core';\nimport type { ComponentRef } from '../types';\n\n/**\n * Generate an instance key for a bloc\n *\n * Logic:\n * - If user provides instanceId, use it (convert number to string)\n * - If isolated, generate or reuse a unique key for this component\n * - Otherwise, return undefined (use default key)\n *\n * @param componentRef - React component reference (persists across remounts)\n * @param isIsolated - Whether the bloc is isolated\n * @param providedId - User-provided instance ID (from options)\n * @returns Instance key string or undefined for default\n */\nexport function generateInstanceKey(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string | number,\n): string | undefined {\n // User explicitly provided an ID - use it\n if (providedId !== undefined) {\n return typeof providedId === 'number' ? String(providedId) : providedId;\n }\n\n // Isolated bloc - generate unique key per component\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = generateIsolatedKey();\n }\n return componentRef.__blocInstanceId;\n }\n\n // Shared bloc - use default key (undefined)\n return undefined;\n}\n","import {\n useMemo,\n useSyncExternalStore,\n useEffect,\n useRef,\n useReducer,\n} from 'react';\nimport {\n type BlocConstructor,\n StateContainer,\n type ExtractState,\n type AdapterState,\n ExternalDependencyManager,\n createAutoTrackSubscribe,\n createManualDepsSubscribe,\n createNoTrackSubscribe,\n createAutoTrackSnapshot,\n createManualDepsSnapshot,\n createNoTrackSnapshot,\n initAutoTrackState,\n initManualDepsState,\n initNoTrackState,\n disableGetterTracking,\n isIsolatedClass,\n} from '@blac/core';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\n\ntype StateContainerConstructor<TBloc extends StateContainer<any>> =\n BlocConstructor<TBloc> & {\n resolve(instanceKey?: string, ...args: any[]): TBloc;\n release(instanceKey?: string): void;\n };\n\ninterface TrackingMode {\n useManualDeps: boolean;\n autoTrackEnabled: boolean;\n}\n\nfunction determineTrackingMode<TBloc extends StateContainer<any>>(\n options?: UseBlocOptions<TBloc>,\n): TrackingMode {\n return {\n useManualDeps: options?.dependencies !== undefined,\n autoTrackEnabled: options?.autoTrack !== false,\n };\n}\n\n/**\n * Lifecycle: INITIAL MOUNT\n * 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn\n * 2. useSyncExternalStore calls getSnapshotFn (1st time) - lazy creates tracker, starts tracking, returns proxy\n * 3. Component renders - proxy tracks property accesses\n * 4. useSyncExternalStore calls getSnapshotFn (2nd time) - captures tracked paths, starts new tracking, returns proxy\n * 5. useSyncExternalStore calls subscribeFn - sets up state change listener\n *\n * Lifecycle: STATE CHANGE\n * 1. Bloc state changes\n * 2. subscribeFn callback checks hasChanges() - only re-renders if tracked paths changed\n * 3. If re-render: getSnapshotFn captures previous paths, starts tracking, returns proxy\n *\n * Lifecycle: RE-RENDER (parent re-render)\n * 1. useMemo returns cached values (same bloc, subscribeFn, getSnapshotFn)\n * 2. useSyncExternalStore calls getSnapshotFn - captures paths, starts tracking, returns proxy\n */\nexport function useBloc<T extends new (...args: any[]) => StateContainer<any>>(\n BlocClass: T & BlocConstructor<InstanceType<T>>,\n options?: UseBlocOptions<InstanceType<T>>,\n): UseBlocReturn<InstanceType<T>> {\n // Component reference that persists across React Strict Mode remounts\n type TBloc = InstanceType<T>;\n const componentRef = useRef<ComponentRef>({});\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n const isIsolated = isIsolatedClass(BlocClass);\n\n const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] =\n useMemo<\n readonly [\n TBloc,\n (callback: () => void) => () => void,\n () => ExtractState<TBloc>,\n string | undefined,\n AdapterState<TBloc>,\n TBloc,\n ]\n >(() => {\n // Generate instance key\n const instanceKey = generateInstanceKey(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n // Get or create bloc instance with ownership (increments ref count)\n const instance = BlocClass.resolve(instanceKey, options?.staticProps);\n\n // Determine tracking mode\n const { useManualDeps, autoTrackEnabled } =\n determineTrackingMode(options);\n\n // Create subscribe and getSnapshot functions based on tracking mode\n let subscribeFn: (callback: () => void) => () => void;\n let getSnapshotFn: () => ExtractState<TBloc>;\n let adapterState: AdapterState<TBloc>;\n\n if (useManualDeps && options?.dependencies) {\n // Manual dependencies mode - no automatic tracking\n adapterState = initManualDepsState(instance);\n subscribeFn = createManualDepsSubscribe(instance, adapterState, {\n dependencies: options.dependencies,\n });\n getSnapshotFn = createManualDepsSnapshot(instance, adapterState, {\n dependencies: options.dependencies,\n });\n } else if (!autoTrackEnabled) {\n // No tracking mode\n adapterState = initNoTrackState(instance);\n subscribeFn = createNoTrackSubscribe(instance);\n getSnapshotFn = createNoTrackSnapshot(instance);\n } else {\n // Auto-tracking mode - enable both state and getter tracking\n adapterState = initAutoTrackState(instance);\n subscribeFn = createAutoTrackSubscribe(instance, adapterState);\n getSnapshotFn = createAutoTrackSnapshot(instance, adapterState);\n }\n\n return [\n adapterState.proxiedBloc!,\n subscribeFn,\n getSnapshotFn,\n instanceKey,\n adapterState,\n instance,\n ];\n }, [BlocClass, options?.instanceId]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot);\n\n // Force re-render mechanism for external bloc changes\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n // External dependency manager (persists across renders)\n const externalDepsManager = useRef(new ExternalDependencyManager());\n\n // Disable getter tracking and manage external bloc subscriptions after each render\n useEffect(() => {\n disableGetterTracking(adapterState, rawInstance);\n externalDepsManager.current.updateSubscriptions(\n adapterState.getterTracker,\n rawInstance,\n forceUpdate,\n );\n }); // Run on every render to pick up new dependencies and disable tracking\n\n // Mount/unmount lifecycle\n useEffect(() => {\n // Call onMount callback if provided\n if (options?.onMount) {\n options.onMount(bloc);\n }\n\n return () => {\n // Cleanup in proper order: subscriptions -> callbacks -> disposal\n\n // 1. Clean up external subscriptions FIRST (before bloc is disposed)\n externalDepsManager.current.cleanup();\n\n // 2. Call onUnmount callback if provided\n if (options?.onUnmount) {\n options.onUnmount(bloc);\n }\n\n // 3. Release bloc reference\n Constructor.release(instanceKey);\n\n // 4. For isolated instances, dispose manually since registry doesn't track them\n if (isIsolated && !rawInstance.isDisposed) {\n rawInstance.dispose();\n }\n };\n }, []);\n\n return [state, bloc, componentRef] as UseBlocReturn<TBloc>;\n}\n","import { useMemo, useEffect, useRef } from 'react';\nimport {\n type BlocConstructor,\n StateContainer,\n isIsolatedClass,\n} from '@blac/core';\nimport type { ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\n\ntype StateContainerConstructor<TBloc extends StateContainer<any>> =\n BlocConstructor<TBloc> & {\n resolve(instanceKey?: string, ...args: any[]): TBloc;\n release(instanceKey?: string): void;\n };\n\nexport interface UseBlocActionsOptions<TBloc> {\n staticProps?: any;\n instanceId?: string | number;\n onMount?: (bloc: TBloc) => void;\n onUnmount?: (bloc: TBloc) => void;\n}\n\nexport function useBlocActions<\n T extends new (...args: any[]) => StateContainer<any>,\n>(\n BlocClass: T & BlocConstructor<InstanceType<T>>,\n options?: UseBlocActionsOptions<InstanceType<T>>,\n): InstanceType<T> {\n // Component reference that persists across React Strict Mode remounts\n type TBloc = InstanceType<T>;\n const componentRef = useRef<ComponentRef>({});\n\n const [bloc, instanceKey] = useMemo(() => {\n const isIsolated = isIsolatedClass(BlocClass);\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n\n // Generate instance key\n const instanceKey = generateInstanceKey(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n // Get or create bloc instance with ownership (increments ref count)\n const instance = options?.staticProps\n ? Constructor.resolve(instanceKey, options.staticProps)\n : Constructor.resolve(instanceKey);\n\n return [instance, instanceKey] as const;\n }, [BlocClass]);\n\n // Mount/unmount lifecycle\n useEffect(() => {\n // Call onMount callback if provided\n if (options?.onMount) {\n options.onMount(bloc);\n }\n\n return () => {\n // Call onUnmount callback if provided\n if (options?.onUnmount) {\n options.onUnmount(bloc);\n }\n\n // Release bloc reference\n const Constructor = BlocClass as StateContainerConstructor<TBloc>;\n Constructor.release(instanceKey);\n\n // For isolated instances, dispose manually since registry doesn't track them\n if (isIsolatedClass(BlocClass) && !bloc.isDisposed) {\n bloc.dispose();\n }\n };\n }, []);\n\n return bloc;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,oBACd,cACA,YACA,YACoB;AAEpB,KAAI,eAAe,OACjB,QAAO,OAAO,eAAe,WAAW,OAAO,WAAW,GAAG;AAI/D,KAAI,YAAY;AACd,MAAI,CAAC,aAAa,iBAChB,cAAa,mBAAmB,qBAAqB;AAEvD,SAAO,aAAa;;;;;;ACIxB,SAAS,sBACP,SACc;AACd,QAAO;EACL,eAAe,SAAS,iBAAiB;EACzC,kBAAkB,SAAS,cAAc;EAC1C;;;;;;;;;;;;;;;;;;;AAoBH,SAAgB,QACd,WACA,SACgC;CAGhC,MAAM,eAAe,OAAqB,EAAE,CAAC;CAC7C,MAAM,cAAc;CACpB,MAAM,aAAa,gBAAgB,UAAU;CAE7C,MAAM,CAAC,MAAM,WAAW,aAAa,aAAa,cAAc,eAC9D,cASQ;EAEN,MAAMA,gBAAc,oBAClB,aAAa,SACb,YACA,SAAS,WACV;EAGD,MAAM,WAAW,UAAU,QAAQA,eAAa,SAAS,YAAY;EAGrE,MAAM,EAAE,eAAe,qBACrB,sBAAsB,QAAQ;EAGhC,IAAIC;EACJ,IAAIC;EACJ,IAAIC;AAEJ,MAAI,iBAAiB,SAAS,cAAc;AAE1C,oBAAe,oBAAoB,SAAS;AAC5C,iBAAc,0BAA0B,UAAUC,gBAAc,EAC9D,cAAc,QAAQ,cACvB,CAAC;AACF,mBAAgB,yBAAyB,UAAUA,gBAAc,EAC/D,cAAc,QAAQ,cACvB,CAAC;aACO,CAAC,kBAAkB;AAE5B,oBAAe,iBAAiB,SAAS;AACzC,iBAAc,uBAAuB,SAAS;AAC9C,mBAAgB,sBAAsB,SAAS;SAC1C;AAEL,oBAAe,mBAAmB,SAAS;AAC3C,iBAAc,yBAAyB,UAAUA,eAAa;AAC9D,mBAAgB,wBAAwB,UAAUA,eAAa;;AAGjE,SAAO;GACLA,eAAa;GACb;GACA;GACAJ;GACAI;GACA;GACD;IACA,CAAC,WAAW,SAAS,WAAW,CAAC;CAEtC,MAAM,QAAQ,qBAAqB,WAAW,YAAY;CAG1D,MAAM,GAAG,eAAe,YAAY,MAAc,IAAI,GAAG,EAAE;CAG3D,MAAM,sBAAsB,OAAO,IAAI,2BAA2B,CAAC;AAGnE,iBAAgB;AACd,wBAAsB,cAAc,YAAY;AAChD,sBAAoB,QAAQ,oBAC1B,aAAa,eACb,aACA,YACD;GACD;AAGF,iBAAgB;AAEd,MAAI,SAAS,QACX,SAAQ,QAAQ,KAAK;AAGvB,eAAa;AAIX,uBAAoB,QAAQ,SAAS;AAGrC,OAAI,SAAS,UACX,SAAQ,UAAU,KAAK;AAIzB,eAAY,QAAQ,YAAY;AAGhC,OAAI,cAAc,CAAC,YAAY,WAC7B,aAAY,SAAS;;IAGxB,EAAE,CAAC;AAEN,QAAO;EAAC;EAAO;EAAM;EAAa;;;;;AChKpC,SAAgB,eAGd,WACA,SACiB;CAGjB,MAAM,eAAe,OAAqB,EAAE,CAAC;CAE7C,MAAM,CAAC,MAAM,eAAe,cAAc;EACxC,MAAM,aAAa,gBAAgB,UAAU;EAC7C,MAAM,cAAc;EAGpB,MAAMC,gBAAc,oBAClB,aAAa,SACb,YACA,SAAS,WACV;AAOD,SAAO,CAJU,SAAS,cACtB,YAAY,QAAQA,eAAa,QAAQ,YAAY,GACrD,YAAY,QAAQA,cAAY,EAElBA,cAAY;IAC7B,CAAC,UAAU,CAAC;AAGf,iBAAgB;AAEd,MAAI,SAAS,QACX,SAAQ,QAAQ,KAAK;AAGvB,eAAa;AAEX,OAAI,SAAS,UACX,SAAQ,UAAU,KAAK;AAKzB,GADoB,UACR,QAAQ,YAAY;AAGhC,OAAI,gBAAgB,UAAU,IAAI,CAAC,KAAK,WACtC,MAAK,SAAS;;IAGjB,EAAE,CAAC;AAEN,QAAO"} |
+19
-6
| { | ||
| "name": "@blac/react", | ||
| "version": "2.0.0-rc.6", | ||
| "version": "2.0.0-rc.8", | ||
| "license": "MIT", | ||
| "author": "Brendan Mullins <jsnanigans@gmail.com>", | ||
| "main": "./dist/index.js", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/jsnanigans/blac.git", | ||
| "directory": "packages/blac-react" | ||
| }, | ||
| "homepage": "https://github.com/jsnanigans/blac#readme", | ||
| "bugs": { | ||
| "url": "https://github.com/jsnanigans/blac/issues" | ||
| }, | ||
| "main": "./dist/index.cjs", | ||
| "module": "./dist/index.js", | ||
@@ -22,3 +31,7 @@ "types": "./dist/index.d.ts", | ||
| "files": [ | ||
| "dist", | ||
| "dist/**/*.js", | ||
| "dist/**/*.cjs", | ||
| "dist/**/*.d.ts", | ||
| "dist/**/*.d.cts", | ||
| "dist/**/*.map", | ||
| "README.md", | ||
@@ -40,3 +53,3 @@ "LICENSE" | ||
| "react": "^18.0.0 || ^19.0.0", | ||
| "@blac/core": "2.0.0-rc.6" | ||
| "@blac/core": "2.0.0-rc.8" | ||
| }, | ||
@@ -70,7 +83,7 @@ "peerDependenciesMeta": { | ||
| "vitest": "^3.2.4", | ||
| "@blac/core": "2.0.0-rc.6" | ||
| "@blac/core": "2.0.0-rc.8" | ||
| }, | ||
| "scripts": { | ||
| "dev": "tsdown --watch", | ||
| "build": "tsdown", | ||
| "build": "tsdown && tsc -p tsconfig.build.json && cp dist/index.d.ts dist/index.d.cts", | ||
| "clean": "rm -rf dist", | ||
@@ -77,0 +90,0 @@ "format": "prettier --write \".\"", |
Sorry, the diff of this file is too big to display
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
17
88.89%0
-100%1
-50%0
-100%46806
-78.34%339
-62.83%1
Infinity%