@blac/core 2.0.0-rc-11 → 2.0.0-rc-14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/index.cjs.js +2 -2
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.d.ts +739 -260
  4. package/dist/index.es.js +1924 -1237
  5. package/dist/index.es.js.map +1 -1
  6. package/package.json +6 -7
  7. package/src/Blac.ts +82 -69
  8. package/src/Bloc.ts +45 -3
  9. package/src/BlocBase.ts +263 -454
  10. package/src/Cubit.ts +1 -16
  11. package/src/__tests__/Blac.config.test.ts +10 -17
  12. package/src/__tests__/Bloc.event.test.ts +27 -52
  13. package/src/__tests__/BlocBase.subscription.test.ts +349 -0
  14. package/src/__tests__/Cubit.test.ts +39 -50
  15. package/src/__tests__/memory-leaks.test.ts +383 -0
  16. package/src/__tests__/plugins.test.ts +464 -0
  17. package/src/adapter/BlacAdapter.ts +347 -207
  18. package/src/adapter/ProxyFactory.ts +196 -255
  19. package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
  20. package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
  21. package/src/adapter/index.ts +0 -1
  22. package/src/errors/BlacError.ts +69 -0
  23. package/src/errors/ErrorManager.ts +101 -0
  24. package/src/errors/handleError.ts +114 -0
  25. package/src/errors/index.ts +13 -0
  26. package/src/events.ts +7 -1
  27. package/src/index.ts +10 -1
  28. package/src/lifecycle/BlocLifecycle.ts +156 -0
  29. package/src/lifecycle/index.ts +1 -0
  30. package/src/plugins/BlocPluginRegistry.ts +240 -0
  31. package/src/plugins/SystemPluginRegistry.ts +242 -0
  32. package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
  33. package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
  34. package/src/plugins/index.ts +8 -0
  35. package/src/plugins/types.ts +129 -0
  36. package/src/subscription/README.md +132 -0
  37. package/src/subscription/SubscriptionManager.ts +395 -0
  38. package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
  39. package/src/subscription/index.ts +2 -0
  40. package/src/subscription/types.ts +90 -0
  41. package/src/testing.ts +31 -37
  42. package/src/types.ts +4 -21
  43. package/src/utils/BatchingManager.ts +59 -0
  44. package/src/utils/RerenderLogger.ts +265 -0
  45. package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
  46. package/src/utils/__tests__/shallowEqual.test.ts +171 -0
  47. package/src/utils/__tests__/uuid.test.ts +144 -0
  48. package/src/utils/generateInstanceId.ts +54 -0
  49. package/src/utils/shallowEqual.ts +1 -1
  50. package/README.md +0 -329
  51. package/src/BlacObserver.ts +0 -186
  52. package/src/__tests__/BlocBase.lifecycle.test.ts +0 -441
  53. package/src/__tests__/props.test.ts +0 -256
  54. package/src/adapter/ConsumerTracker.ts +0 -241
  55. package/src/adapter/__tests__/ConsumerTracker.test.ts +0 -562
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Generates a deterministic instance ID from static props by extracting primitive values
3
+ *
4
+ * Rules:
5
+ * 1. Only considers primitive types: string, number, boolean, null, undefined
6
+ * 2. Ignores complex types: objects, arrays, functions, symbols
7
+ * 3. Sorts keys alphabetically for deterministic output
8
+ * 4. Returns undefined if no primitives found (caller should use default ID)
9
+ */
10
+ export function generateInstanceIdFromProps(
11
+ props: unknown,
12
+ ): string | undefined {
13
+ if (!props || typeof props !== 'object') {
14
+ return undefined;
15
+ }
16
+
17
+ const primitives: Array<[string, unknown]> = [];
18
+
19
+ // Extract primitive values
20
+ for (const [key, value] of Object.entries(props)) {
21
+ const type = typeof value;
22
+ if (
23
+ type === 'string' ||
24
+ type === 'number' ||
25
+ type === 'boolean' ||
26
+ value === null ||
27
+ value === undefined
28
+ ) {
29
+ primitives.push([key, value]);
30
+ }
31
+ }
32
+
33
+ // No primitives found
34
+ if (primitives.length === 0) {
35
+ return undefined;
36
+ }
37
+
38
+ // Sort by key for deterministic output
39
+ primitives.sort(([a], [b]) => a.localeCompare(b));
40
+
41
+ // Create instance ID
42
+ const parts: string[] = [];
43
+ for (const [key, value] of primitives) {
44
+ if (value === null) {
45
+ parts.push(`${key}:null`);
46
+ } else if (value === undefined) {
47
+ parts.push(`${key}:undefined`);
48
+ } else {
49
+ parts.push(`${key}:${String(value)}`);
50
+ }
51
+ }
52
+
53
+ return parts.join('|');
54
+ }
@@ -37,4 +37,4 @@ export function shallowEqual(a: any, b: any): boolean {
37
37
  }
38
38
 
39
39
  return true;
40
- }
40
+ }
package/README.md DELETED
@@ -1,329 +0,0 @@
1
- # @blac/core
2
-
3
- A lightweight, flexible state management library for JavaScript/TypeScript applications focusing on predictable state transitions.
4
-
5
- ## Features
6
-
7
- - 🔄 Predictable unidirectional data flow
8
- - 🧩 Modular architecture with Blocs and Cubits
9
- - 🧪 Unit test friendly
10
- - 🔒 Isolated state instances when needed
11
- - 🔌 Plugin system for extensibility
12
-
13
- ## Installation
14
-
15
- Install `@blac/core` using your favorite package manager:
16
-
17
- ```bash
18
- # pnpm
19
- pnpm add @blac/core
20
-
21
- # yarn
22
- yarn add @blac/core
23
-
24
- # npm
25
- npm install @blac/core
26
- ```
27
-
28
- ## Configuration
29
-
30
- BlaC provides global configuration options to customize its behavior:
31
-
32
- ```typescript
33
- import { Blac } from '@blac/core';
34
-
35
- // Configure BlaC before using it
36
- Blac.setConfig({
37
- // Enable/disable automatic dependency tracking for optimized re-renders
38
- proxyDependencyTracking: true, // default: true
39
-
40
- // Expose Blac instance globally for debugging
41
- exposeBlacInstance: false // default: false
42
- });
43
-
44
- // Read current configuration
45
- const config = Blac.config;
46
- console.log(config.proxyDependencyTracking); // true
47
- ```
48
-
49
- ### Configuration Options
50
-
51
- - **`proxyDependencyTracking`**: When enabled (default), BlaC automatically tracks which state properties your components access and only triggers re-renders when those specific properties change. Disable this for simpler behavior where any state change triggers re-renders.
52
-
53
- - **`exposeBlacInstance`**: When enabled, exposes the BlaC instance globally (useful for debugging). Not recommended for production.
54
-
55
- ## Testing
56
-
57
- Blac provides comprehensive testing utilities to make testing your state management logic simple and powerful:
58
-
59
- ```typescript
60
- import { BlocTest, MockCubit, MemoryLeakDetector } from '@blac/core';
61
-
62
- describe('Counter Tests', () => {
63
- beforeEach(() => BlocTest.setUp());
64
- afterEach(() => BlocTest.tearDown());
65
-
66
- it('should increment counter', async () => {
67
- const counter = BlocTest.createBloc(CounterCubit);
68
-
69
- counter.increment();
70
-
71
- expect(counter.state.count).toBe(1);
72
- });
73
-
74
- it('should track state history', () => {
75
- const mockCubit = new MockCubit({ count: 0 });
76
-
77
- mockCubit.emit({ count: 1 });
78
- mockCubit.emit({ count: 2 });
79
-
80
- const history = mockCubit.getStateHistory();
81
- expect(history).toHaveLength(3); // Initial + 2 emissions
82
- });
83
-
84
- it('should detect memory leaks', () => {
85
- const detector = new MemoryLeakDetector();
86
-
87
- // Create and use blocs...
88
-
89
- const result = detector.checkForLeaks();
90
- expect(result.hasLeaks).toBe(false);
91
- });
92
- });
93
- ```
94
-
95
- **[📚 View Complete Testing Documentation](./docs/testing.md)**
96
-
97
- ## Core Concepts
98
-
99
- ### Blocs and Cubits
100
-
101
- **Cubit**: A simple state container with methods to emit new states.
102
-
103
- ```typescript
104
- class CounterCubit extends Cubit<number> {
105
- constructor() {
106
- super(0); // Initial state
107
- }
108
-
109
- increment = () => {
110
- this.emit(this.state + 1);
111
- }
112
-
113
- decrement = () => {
114
- this.emit(this.state - 1);
115
- }
116
- }
117
- ```
118
-
119
- **Bloc**: More powerful state container that uses an event-handler pattern for type-safe, event-driven state transitions. Events (instances of classes) are dispatched via `this.add()`, and handlers are registered using `this.on(EventClass, handler)`.
120
-
121
- ```typescript
122
- // Define event classes
123
- class IncrementEvent { constructor(public readonly amount: number = 1) {} }
124
- class DecrementEvent { constructor(public readonly amount: number = 1) {} }
125
-
126
- // Optional: Union type for all events
127
- type CounterEvent = IncrementEvent | DecrementEvent;
128
-
129
- class CounterBloc extends Bloc<number, CounterEvent> {
130
- constructor() {
131
- super(0); // Initial state
132
-
133
- // Register event handlers
134
- this.on(IncrementEvent, (event, emit) => {
135
- emit(this.state + event.amount);
136
- });
137
-
138
- this.on(DecrementEvent, (event, emit) => {
139
- emit(this.state - event.amount);
140
- });
141
- }
142
-
143
- // Helper methods to dispatch event instances (optional)
144
- increment = (amount = 1) => {
145
- this.add(new IncrementEvent(amount));
146
- }
147
-
148
- decrement = (amount = 1) => {
149
- this.add(new DecrementEvent(amount));
150
- }
151
- }
152
- ```
153
-
154
- ### Important: Arrow Functions Required
155
-
156
- All methods in Bloc or Cubit classes must use arrow function syntax (`method = () => {}`) instead of the traditional method syntax (`method() {}`). This is because arrow functions automatically bind `this` to the class instance. Without this binding, methods called from React components would lose their context and could not access instance properties like `this.state` or `this.emit()`.
157
-
158
- ### State Management Patterns
159
-
160
- #### Shared State (Default)
161
-
162
- By default, bloc instances are shared across all consumers:
163
-
164
- ```typescript
165
- class GlobalCounterCubit extends Cubit<number> {
166
- constructor() {
167
- super(0);
168
- }
169
-
170
- increment = () => {
171
- this.emit(this.state + 1);
172
- }
173
- }
174
- ```
175
-
176
- #### Isolated State
177
-
178
- When each consumer needs its own state instance:
179
-
180
- ```typescript
181
- class LocalCounterCubit extends Cubit<number> {
182
- static isolated = true; // Each consumer gets its own instance
183
-
184
- constructor() {
185
- super(0);
186
- }
187
-
188
- increment = () => {
189
- this.emit(this.state + 1);
190
- }
191
- }
192
- ```
193
-
194
- #### Persistent State
195
-
196
- Keep state alive even when no consumers are using it:
197
-
198
- ```typescript
199
- class PersistentCounterCubit extends Cubit<number> {
200
- static keepAlive = true; // State persists even when no consumers
201
-
202
- constructor() {
203
- super(0);
204
- }
205
-
206
- increment = () => {
207
- this.emit(this.state + 1);
208
- }
209
- }
210
- ```
211
-
212
- ## Advanced Usage
213
-
214
- ### Custom Plugins
215
-
216
- Create plugins to add functionality like logging, persistence, or analytics:
217
-
218
- ```typescript
219
- import { BlacPlugin, BlacLifecycleEvent, BlocBase } from '@blac/core';
220
-
221
- class LoggerPlugin implements BlacPlugin {
222
- name = 'LoggerPlugin';
223
-
224
- onEvent(event: BlacLifecycleEvent, bloc: BlocBase, params?: any) {
225
- if (event === BlacLifecycleEvent.STATE_CHANGED) {
226
- console.log(`[${bloc._name}] State changed:`, bloc.state);
227
- }
228
- }
229
- }
230
-
231
- // Add the plugin to Blac
232
- import { Blac } from '@blac/core';
233
- Blac.addPlugin(new LoggerPlugin());
234
- ```
235
-
236
- ### Using Props with Blocs
237
-
238
- Blocs can be designed to accept properties through their constructor, allowing for configurable instances. Here's an example of a `UserProfileBloc` that takes a `userId` prop:
239
-
240
- ```typescript
241
- import { Bloc } from '@blac/core'; // Or your specific import path
242
-
243
- // Define props interface (optional, but good practice)
244
- interface UserProfileProps {
245
- userId: string;
246
- }
247
-
248
- // Define state interface
249
- interface UserProfileState {
250
- loading: boolean;
251
- userData: { id: string; name: string; bio?: string } | null;
252
- error: string | null;
253
- }
254
-
255
- // Define Event Classes for UserProfileBloc
256
- class UserProfileFetchEvent {}
257
- class UserProfileDataLoadedEvent { constructor(public readonly data: any) {} }
258
- class UserProfileErrorEvent { constructor(public readonly error: string) {} }
259
-
260
- type UserProfileEvents = UserProfileFetchEvent | UserProfileDataLoadedEvent | UserProfileErrorEvent;
261
-
262
- class UserProfileBloc extends Bloc<UserProfileState, UserProfileEvents, UserProfileProps> {
263
- private userId: string;
264
-
265
- constructor(props: UserProfileProps) {
266
- super({ loading: true, userData: null, error: null }); // Initial state
267
- this.userId = props.userId;
268
- this._name = `UserProfileBloc_${this.userId}`;
269
-
270
- // Register event handlers
271
- this.on(UserProfileFetchEvent, this.handleFetchUserProfile);
272
- this.on(UserProfileDataLoadedEvent, (event, emit) => {
273
- emit({ ...this.state, loading: false, userData: event.data, error: null });
274
- });
275
- this.on(UserProfileErrorEvent, (event, emit) => {
276
- emit({ ...this.state, loading: false, error: event.error });
277
- });
278
-
279
- // Initial fetch
280
- this.add(new UserProfileFetchEvent());
281
- }
282
-
283
- private handleFetchUserProfile = async (_event: UserProfileFetchEvent, emit: (state: UserProfileState) => void) => {
284
- // Emit loading state directly if not already covered by initial state or another event
285
- // For this example, constructor sets loading: true, so an immediate emit here might be redundant
286
- // unless an event handler could set loading to false before this runs.
287
- // emit({ ...this.state, loading: true }); // Ensure loading is true
288
- try {
289
- await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
290
- const mockUserData = { id: this.userId, name: `User ${this.userId}`, bio: 'Loves Blac states!' };
291
- this.add(new UserProfileDataLoadedEvent(mockUserData));
292
- } catch (e:any) {
293
- this.add(new UserProfileErrorEvent(e.message || 'Failed to fetch user profile'));
294
- }
295
- }
296
-
297
- // Public method to re-trigger fetch if needed
298
- refetchUserProfile = () => {
299
- this.add(new UserProfileFetchEvent());
300
- }
301
- }
302
- ```
303
-
304
- ## API Reference
305
-
306
- ### Core Classes
307
-
308
- - `BlocBase<S, P>`: Base class for state containers
309
- - `Cubit<S, P>`: Simple state container with `emit()` and `patch()`
310
- - `Bloc<S, E, P>`: Event-driven state container with `on(EventClass, handler)` and `add(eventInstance)` methods.
311
- - `Blac`: Singleton manager for all Bloc instances
312
-
313
- ### React Hooks
314
-
315
- - `useBloc<B>(BlocClass, options?)`: Connect a component to a Bloc
316
-
317
- ### Lifecycle Events
318
-
319
- - `BLOC_CREATED`: When a new Bloc is instantiated
320
- - `BLOC_DISPOSED`: When a Bloc is disposed
321
- - `LISTENER_ADDED`: When a state listener is added
322
- - `LISTENER_REMOVED`: When a state listener is removed
323
- - `STATE_CHANGED`: When state is updated
324
- - `BLOC_CONSUMER_ADDED`: When a new consumer starts using a Bloc
325
- - `BLOC_CONSUMER_REMOVED`: When a consumer stops using a Bloc
326
-
327
- ## License
328
-
329
- This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
@@ -1,186 +0,0 @@
1
- import { Blac } from './Blac';
2
- import { BlocLifecycleState } from './BlocBase';
3
- import { BlocBase } from './BlocBase';
4
- import { BlocHookDependencyArrayFn } from './types';
5
-
6
- /**
7
- * Represents an observer that can subscribe to state changes in a Bloc
8
- * @template S - The type of state being observed
9
- */
10
- export type BlacObserver<S> = {
11
- /** Function to be called when state changes */
12
- fn: (newState: S, oldState: S, action?: unknown) => void | Promise<void>;
13
- /** Optional function to determine if the observer should be notified of state changes */
14
- dependencyArray?: BlocHookDependencyArrayFn<S>;
15
- /** Dispose function for the observer */
16
- dispose?: () => void;
17
- /** Cached state values used for dependency comparison */
18
- lastState?: unknown[];
19
- /** Unique identifier for the observer */
20
- id: string;
21
- };
22
-
23
- /**
24
- * A class that manages observers for a Bloc's state changes
25
- * @template S - The type of state being observed
26
- */
27
- export class BlacObservable<S = unknown> {
28
- /** The Bloc instance this observable is associated with */
29
- bloc: BlocBase<S>;
30
-
31
- /**
32
- * Creates a new BlacObservable instance
33
- * @param bloc - The Bloc instance to observe
34
- */
35
- constructor(bloc: BlocBase<S>) {
36
- this.bloc = bloc;
37
- }
38
-
39
- private _observers = new Set<BlacObserver<S>>();
40
-
41
- /**
42
- * Gets the number of active observers
43
- * @returns The number of observers currently subscribed
44
- */
45
- get size(): number {
46
- return this._observers.size;
47
- }
48
-
49
- /**
50
- * Gets the set of all observers
51
- * @returns The Set of all BlacObserver instances
52
- */
53
- get observers() {
54
- return this._observers;
55
- }
56
-
57
- /**
58
- * Subscribes an observer to state changes
59
- * @param observer - The observer to subscribe
60
- * @returns A function that can be called to unsubscribe the observer
61
- */
62
- subscribe(observer: BlacObserver<S>): () => void {
63
- // Check if bloc is disposed or in disposal process
64
- const disposalState = (this.bloc as any)._disposalState;
65
- if (
66
- disposalState === BlocLifecycleState.DISPOSED ||
67
- disposalState === BlocLifecycleState.DISPOSING
68
- ) {
69
- Blac.log(
70
- 'BlacObservable.subscribe: Cannot subscribe to disposed/disposing bloc.',
71
- this.bloc,
72
- observer,
73
- );
74
- return () => {}; // Return no-op unsubscribe
75
- }
76
-
77
- Blac.log(
78
- 'BlacObservable.subscribe: Subscribing observer.',
79
- this.bloc,
80
- observer,
81
- );
82
- this._observers.add(observer);
83
-
84
- // If we're in DISPOSAL_REQUESTED state, cancel the disposal since we have a new observer
85
- if (disposalState === BlocLifecycleState.DISPOSAL_REQUESTED) {
86
- Blac.log(
87
- 'BlacObservable.subscribe: Cancelling disposal due to new subscription.',
88
- this.bloc,
89
- );
90
- // Transition back to active state
91
- (this.bloc as any)._atomicStateTransition(
92
- BlocLifecycleState.DISPOSAL_REQUESTED,
93
- BlocLifecycleState.ACTIVE,
94
- );
95
- }
96
-
97
- // Don't initialize lastState here - let it remain undefined for first-time detection
98
- return () => {
99
- Blac.log(
100
- 'BlacObservable.subscribe: Unsubscribing observer.',
101
- this.bloc,
102
- observer,
103
- );
104
- this.unsubscribe(observer);
105
- };
106
- }
107
-
108
- /**
109
- * Unsubscribes an observer from state changes
110
- * @param observer - The observer to unsubscribe
111
- */
112
- unsubscribe(observer: BlacObserver<S>) {
113
- this._observers.delete(observer);
114
- // Blac.instance.dispatchEvent(BlacLifecycleEvent.LISTENER_REMOVED, this.bloc, { listenerId: observer.id });
115
-
116
- if (this.size === 0) {
117
- Blac.log('BlacObservable.unsubscribe: No observers left.', this.bloc);
118
- // Check if bloc should be disposed when both observers and consumers are gone
119
- if (
120
- this.bloc._consumers.size === 0 &&
121
- !this.bloc._keepAlive &&
122
- (this.bloc as any)._disposalState === BlocLifecycleState.ACTIVE
123
- ) {
124
- Blac.log(
125
- `[${this.bloc._name}:${this.bloc._id}] No observers or consumers left. Scheduling disposal.`,
126
- );
127
- (this.bloc as any)._scheduleDisposal();
128
- }
129
- }
130
- }
131
-
132
- /**
133
- * Notifies all observers of a state change
134
- * @param newState - The new state value
135
- * @param oldState - The previous state value
136
- * @param action - Optional action that triggered the state change
137
- */
138
- notify(newState: S, oldState: S, action?: unknown) {
139
- this._observers.forEach((observer) => {
140
- let shouldUpdate = false;
141
-
142
- if (observer.dependencyArray) {
143
- const lastDependencyCheck = observer.lastState;
144
- const newDependencyCheck = observer.dependencyArray(
145
- newState,
146
- oldState,
147
- this.bloc,
148
- );
149
-
150
- // If this is the first time (no lastState), trigger initial render
151
- if (!lastDependencyCheck) {
152
- shouldUpdate = true;
153
- } else {
154
- // Compare dependency arrays
155
- if (lastDependencyCheck.length !== newDependencyCheck.length) {
156
- shouldUpdate = true;
157
- } else {
158
- // Compare each dependency value using Object.is (same as React)
159
- for (let i = 0; i < newDependencyCheck.length; i++) {
160
- if (!Object.is(lastDependencyCheck[i], newDependencyCheck[i])) {
161
- shouldUpdate = true;
162
- break;
163
- }
164
- }
165
- }
166
- }
167
-
168
- observer.lastState = newDependencyCheck;
169
- } else {
170
- shouldUpdate = true;
171
- }
172
-
173
- if (shouldUpdate) {
174
- void observer.fn(newState, oldState, action);
175
- }
176
- });
177
- }
178
-
179
- /**
180
- * Clears the observer set
181
- */
182
- clear() {
183
- // Just clear the observers without calling unsubscribe to avoid circular disposal
184
- this._observers.clear();
185
- }
186
- }