@blac/core 2.0.0-rc-13 → 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.
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +522 -297
- package/dist/index.es.js +1833 -1550
- package/dist/index.es.js.map +1 -1
- package/package.json +6 -7
- package/src/Blac.ts +38 -71
- package/src/Bloc.ts +8 -9
- package/src/BlocBase.ts +246 -540
- package/src/Cubit.ts +1 -16
- package/src/__tests__/Blac.config.test.ts +10 -17
- package/src/__tests__/Bloc.event.test.ts +27 -52
- package/src/__tests__/BlocBase.subscription.test.ts +349 -0
- package/src/__tests__/Cubit.test.ts +39 -50
- package/src/__tests__/memory-leaks.test.ts +383 -0
- package/src/__tests__/plugins.test.ts +107 -97
- package/src/adapter/BlacAdapter.ts +347 -207
- package/src/adapter/ProxyFactory.ts +196 -255
- package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
- package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
- package/src/adapter/index.ts +0 -1
- package/src/errors/BlacError.ts +69 -0
- package/src/errors/ErrorManager.ts +101 -0
- package/src/errors/handleError.ts +114 -0
- package/src/errors/index.ts +13 -0
- package/src/events.ts +7 -1
- package/src/index.ts +7 -1
- package/src/lifecycle/BlocLifecycle.ts +156 -0
- package/src/lifecycle/index.ts +1 -0
- package/src/plugins/BlocPluginRegistry.ts +71 -51
- package/src/plugins/SystemPluginRegistry.ts +63 -13
- package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
- package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
- package/src/plugins/types.ts +32 -3
- package/src/subscription/README.md +132 -0
- package/src/subscription/SubscriptionManager.ts +395 -0
- package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
- package/src/subscription/index.ts +2 -0
- package/src/subscription/types.ts +90 -0
- package/src/testing.ts +31 -37
- package/src/types.ts +4 -21
- package/src/utils/BatchingManager.ts +59 -0
- package/src/utils/RerenderLogger.ts +265 -0
- package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
- package/src/utils/__tests__/shallowEqual.test.ts +171 -0
- package/src/utils/__tests__/uuid.test.ts +144 -0
- package/src/utils/generateInstanceId.ts +54 -0
- package/src/utils/shallowEqual.ts +1 -1
- package/README.md +0 -329
- package/src/BlacObserver.ts +0 -189
- package/src/__tests__/BlocBase.lifecycle.test.ts +0 -441
- package/src/__tests__/props.test.ts +0 -256
- package/src/adapter/ConsumerTracker.ts +0 -241
- 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
|
+
}
|
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.
|
package/src/BlacObserver.ts
DELETED
|
@@ -1,189 +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
|
-
// Notify system plugins of state change
|
|
179
|
-
Blac.instance.plugins.notifyStateChanged(this.bloc, oldState, newState);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Clears the observer set
|
|
184
|
-
*/
|
|
185
|
-
clear() {
|
|
186
|
-
// Just clear the observers without calling unsubscribe to avoid circular disposal
|
|
187
|
-
this._observers.clear();
|
|
188
|
-
}
|
|
189
|
-
}
|