@blac/core 2.0.0-rc-7 → 2.0.0-rc-10

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.
@@ -1,4 +1,5 @@
1
1
  import { Blac } from './Blac';
2
+ import { BlocLifecycleState } from './BlocBase';
2
3
  import { BlocBase } from './BlocBase';
3
4
  import { BlocHookDependencyArrayFn } from './types';
4
5
 
@@ -14,7 +15,7 @@ export type BlacObserver<S> = {
14
15
  /** Dispose function for the observer */
15
16
  dispose?: () => void;
16
17
  /** Cached state values used for dependency comparison */
17
- lastState?: unknown[][];
18
+ lastState?: unknown[];
18
19
  /** Unique identifier for the observer */
19
20
  id: string;
20
21
  };
@@ -59,13 +60,49 @@ export class BlacObservable<S = unknown> {
59
60
  * @returns A function that can be called to unsubscribe the observer
60
61
  */
61
62
  subscribe(observer: BlacObserver<S>): () => void {
62
- Blac.log('BlacObservable.subscribe: Subscribing observer.', this.bloc, observer);
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
+ );
63
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
+
64
97
  // Don't initialize lastState here - let it remain undefined for first-time detection
65
98
  return () => {
66
- Blac.log('BlacObservable.subscribe: Unsubscribing observer.', this.bloc, observer);
99
+ Blac.log(
100
+ 'BlacObservable.subscribe: Unsubscribing observer.',
101
+ this.bloc,
102
+ observer,
103
+ );
67
104
  this.unsubscribe(observer);
68
- }
105
+ };
69
106
  }
70
107
 
71
108
  /**
@@ -78,7 +115,17 @@ export class BlacObservable<S = unknown> {
78
115
 
79
116
  if (this.size === 0) {
80
117
  Blac.log('BlacObservable.unsubscribe: No observers left.', this.bloc);
81
- // The bloc will handle its own disposal through consumer management
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
+ }
82
129
  }
83
130
  }
84
131
 
@@ -94,36 +141,26 @@ export class BlacObservable<S = unknown> {
94
141
 
95
142
  if (observer.dependencyArray) {
96
143
  const lastDependencyCheck = observer.lastState;
97
- const newDependencyCheck = observer.dependencyArray(newState);
144
+ const newDependencyCheck = observer.dependencyArray(
145
+ newState,
146
+ oldState,
147
+ this.bloc,
148
+ );
98
149
 
99
- // If this is the first time (no lastState), always update
150
+ // If this is the first time (no lastState), trigger initial render
100
151
  if (!lastDependencyCheck) {
101
152
  shouldUpdate = true;
102
153
  } else {
103
- // Compare dependency arrays for changes
154
+ // Compare dependency arrays
104
155
  if (lastDependencyCheck.length !== newDependencyCheck.length) {
105
156
  shouldUpdate = true;
106
157
  } else {
107
- // Compare each part of the dependency arrays
108
- for (let o = 0; o < newDependencyCheck.length; o++) {
109
- const partNew = newDependencyCheck[o];
110
- const partOld = lastDependencyCheck[o] || [];
111
-
112
- // If the part lengths are different, definitely update
113
- if (partNew.length !== partOld.length) {
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])) {
114
161
  shouldUpdate = true;
115
162
  break;
116
163
  }
117
-
118
- // Compare each value in the parts
119
- for (let i = 0; i < partNew.length; i++) {
120
- if (!Object.is(partNew[i], partOld[i])) {
121
- shouldUpdate = true;
122
- break;
123
- }
124
- }
125
-
126
- if (shouldUpdate) break;
127
164
  }
128
165
  }
129
166
  }
package/src/Bloc.ts CHANGED
@@ -6,165 +6,182 @@ import { BlocEventConstraint } from './types';
6
6
  // to access action.constructor and ensure proper event structure.
7
7
  // P is for props, changed from any to unknown.
8
8
  export abstract class Bloc<
9
- S, // State type
10
- A extends BlocEventConstraint = BlocEventConstraint, // Base Action/Event type with proper constraints
11
- P = unknown // Props type
9
+ S, // State type
10
+ A extends BlocEventConstraint = BlocEventConstraint, // Base Action/Event type with proper constraints
11
+ P = unknown, // Props type
12
12
  > extends BlocBase<S, P> {
13
- // Stores handlers: Map<EventConstructor (subtype of A), HandlerFunction>
14
- // The handler's event parameter will be correctly typed to the specific EventConstructor
15
- // by the 'on' method's signature.
16
- readonly eventHandlers: Map<
17
- // Key: Constructor of a specific event E (where E extends A)
18
- // Using 'any[]' for constructor arguments for broader compatibility.
19
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
- new (...args: any[]) => A,
21
- // Value: Handler function. 'event: A' is used here for the stored function type.
22
- // The 'on' method ensures the specific handler (event: E) is correctly typed.
23
- (event: A, emit: (newState: S) => void) => void | Promise<void>
24
- > = new Map();
13
+ // Stores handlers: Map<EventConstructor (subtype of A), HandlerFunction>
14
+ // The handler's event parameter will be correctly typed to the specific EventConstructor
15
+ // by the 'on' method's signature.
16
+ readonly eventHandlers: Map<
17
+ // Key: Constructor of a specific event E (where E extends A)
18
+ // TODO: 'any[]' is required for constructor arguments to allow flexible event instantiation.
19
+ // Using specific parameter types would break type inference for events with different
20
+ // constructor signatures. The 'any[]' enables polymorphic event handling while
21
+ // maintaining type safety through the generic constraint 'E extends A'.
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ new (...args: any[]) => A,
24
+ // Value: Handler function. 'event: A' is used here for the stored function type.
25
+ // The 'on' method ensures the specific handler (event: E) is correctly typed.
26
+ (event: A, emit: (newState: S) => void) => void | Promise<void>
27
+ > = new Map();
25
28
 
26
- /**
27
- * @internal
28
- * Event queue to ensure sequential processing of async events
29
- */
30
- private _eventQueue: A[] = [];
31
-
32
- /**
33
- * @internal
34
- * Flag indicating if an event is currently being processed
35
- */
36
- private _isProcessingEvent = false;
29
+ /**
30
+ * @internal
31
+ * Event queue to ensure sequential processing of async events
32
+ */
33
+ private _eventQueue: A[] = [];
37
34
 
38
- /**
39
- * Registers an event handler for a specific event type.
40
- * This method is typically called in the constructor of a derived Bloc class.
41
- * @param eventConstructor The constructor of the event to handle (e.g., LoadDataEvent).
42
- * @param handler A function that processes the event and can emit new states.
43
- * The 'event' parameter in the handler will be typed to the specific eventConstructor.
44
- */
45
- protected on<E extends A>(
46
- // Using 'any[]' for constructor arguments for broader compatibility.
47
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
- eventConstructor: new (...args: any[]) => E,
49
- handler: (event: E, emit: (newState: S) => void) => void | Promise<void>
50
- ): void {
51
- if (this.eventHandlers.has(eventConstructor)) {
52
- // Using Blac.warn or a similar logging mechanism from BlocBase if available,
53
- // otherwise console.warn. Assuming this._name and this._id are available from BlocBase.
54
- Blac.warn(
55
- `[Bloc ${this._name}:${String(this._id)}] Handler for event '${eventConstructor.name}' already registered. It will be overwritten.`
56
- );
57
- }
58
- // Cast the specific handler (event: E) to a more general (event: A) for storage.
59
- // This is safe because E extends A. When the handler is called with an 'action' of type A,
60
- // if it was originally registered for type E, 'action' must be an instance of E.
61
- this.eventHandlers.set(
62
- eventConstructor,
63
- handler as (event: A, emit: (newState: S) => void) => void | Promise<void>
64
- );
35
+ /**
36
+ * @internal
37
+ * Flag indicating if an event is currently being processed
38
+ */
39
+ private _isProcessingEvent = false;
40
+
41
+ /**
42
+ * Registers an event handler for a specific event type.
43
+ * This method is typically called in the constructor of a derived Bloc class.
44
+ * @param eventConstructor The constructor of the event to handle (e.g., LoadDataEvent).
45
+ * @param handler A function that processes the event and can emit new states.
46
+ * The 'event' parameter in the handler will be typed to the specific eventConstructor.
47
+ */
48
+ protected on<E extends A>(
49
+ // TODO: 'any[]' is required for constructor arguments (see explanation above).
50
+ // This allows events with different constructor signatures to be handled uniformly.
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ eventConstructor: new (...args: any[]) => E,
53
+ handler: (event: E, emit: (newState: S) => void) => void | Promise<void>,
54
+ ): void {
55
+ if (this.eventHandlers.has(eventConstructor)) {
56
+ // Using Blac.warn or a similar logging mechanism from BlocBase if available,
57
+ // otherwise console.warn. Assuming this._name and this._id are available from BlocBase.
58
+ Blac.warn(
59
+ `[Bloc ${this._name}:${String(this._id)}] Handler for event '${eventConstructor.name}' already registered. It will be overwritten.`,
60
+ );
65
61
  }
62
+ // Cast the specific handler (event: E) to a more general (event: A) for storage.
63
+ // This is safe because E extends A. When the handler is called with an 'action' of type A,
64
+ // if it was originally registered for type E, 'action' must be an instance of E.
65
+ this.eventHandlers.set(
66
+ eventConstructor,
67
+ handler as (
68
+ event: A,
69
+ emit: (newState: S) => void,
70
+ ) => void | Promise<void>,
71
+ );
72
+ }
66
73
 
67
- /**
68
- * Dispatches an action/event to the Bloc.
69
- * Events are queued and processed sequentially to prevent race conditions.
70
- * @param action The action/event instance to be processed.
71
- */
72
- public add = async (action: A): Promise<void> => {
73
- // Add event to queue
74
- this._eventQueue.push(action);
75
-
76
- // If not already processing, start processing the queue
77
- if (!this._isProcessingEvent) {
78
- await this._processEventQueue();
79
- }
80
- };
74
+ /**
75
+ * Dispatches an action/event to the Bloc.
76
+ * Events are queued and processed sequentially to prevent race conditions.
77
+ * @param action The action/event instance to be processed.
78
+ */
79
+ public add = async (action: A): Promise<void> => {
80
+ // Add event to queue
81
+ this._eventQueue.push(action);
81
82
 
82
- /**
83
- * @internal
84
- * Processes events from the queue sequentially
85
- */
86
- private async _processEventQueue(): Promise<void> {
87
- // Prevent concurrent processing
88
- if (this._isProcessingEvent) {
89
- return;
90
- }
91
-
92
- this._isProcessingEvent = true;
93
-
94
- try {
95
- while (this._eventQueue.length > 0) {
96
- const action = this._eventQueue.shift()!;
97
- await this._processEvent(action);
98
- }
99
- } finally {
100
- this._isProcessingEvent = false;
101
- }
83
+ // If not already processing, start processing the queue
84
+ if (!this._isProcessingEvent) {
85
+ await this._processEventQueue();
86
+ }
87
+ };
88
+
89
+ /**
90
+ * @internal
91
+ * Processes events from the queue sequentially
92
+ */
93
+ private async _processEventQueue(): Promise<void> {
94
+ // Prevent concurrent processing
95
+ if (this._isProcessingEvent) {
96
+ return;
102
97
  }
103
98
 
104
- /**
105
- * @internal
106
- * Processes a single event
107
- */
108
- private async _processEvent(action: A): Promise<void> {
109
- // Using 'any[]' for constructor arguments for broader compatibility.
110
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
- const eventConstructor = action.constructor as new (...args: any[]) => A;
112
- const handler = this.eventHandlers.get(eventConstructor);
99
+ this._isProcessingEvent = true;
100
+
101
+ try {
102
+ while (this._eventQueue.length > 0) {
103
+ const action = this._eventQueue.shift()!;
104
+ await this._processEvent(action);
105
+ }
106
+ } finally {
107
+ this._isProcessingEvent = false;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * @internal
113
+ * Processes a single event
114
+ */
115
+ private async _processEvent(action: A): Promise<void> {
116
+ // Using 'any[]' for constructor arguments for broader compatibility.
117
+ // TODO: Type assertion required to cast action.constructor to proper event constructor type.
118
+ // JavaScript's constructor property returns 'Function', but we need the specific event
119
+ // constructor type to look up handlers. This is safe because we validate the action
120
+ // extends the BlocEventConstraint interface.
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ const eventConstructor = action.constructor as new (...args: any[]) => A;
123
+ const handler = this.eventHandlers.get(eventConstructor);
124
+
125
+ if (handler) {
126
+ // Define the 'emit' function that handlers will use to update state.
127
+ // It captures the current state ('this.state') right before each emission
128
+ // to provide the correct 'previousState' to _pushState.
129
+ const emit = (newState: S): void => {
130
+ const previousState = this.state; // State just before this specific emission
131
+ // The 'action' passed to _pushState is the original action that triggered the handler,
132
+ // providing context for the state change (e.g., for logging or plugins).
133
+ this._pushState(newState, previousState, action);
134
+ };
135
+
136
+ try {
137
+ // Await the handler in case it's an async function (e.g., performs API calls).
138
+ // The 'action' is passed to the handler, and due to the way 'on' is typed,
139
+ // the 'event' parameter within the handler function will be correctly
140
+ // typed to its specific class (e.g., LoadMyFeatureData).
141
+ await handler(action, emit);
142
+ } catch (error) {
143
+ // Enhanced error handling with better context
144
+ const constructorName =
145
+ (action.constructor as { name?: string }).name ||
146
+ 'UnnamedConstructor';
147
+ const errorContext = {
148
+ blocName: this._name,
149
+ blocId: String(this._id),
150
+ eventType: constructorName,
151
+ currentState: this.state,
152
+ action: action,
153
+ timestamp: new Date().toISOString(),
154
+ };
155
+
156
+ Blac.error(
157
+ `[Bloc ${this._name}:${String(this._id)}] Error in event handler for '${constructorName}':`,
158
+ error,
159
+ 'Context:',
160
+ errorContext,
161
+ );
113
162
 
114
- if (handler) {
115
- // Define the 'emit' function that handlers will use to update state.
116
- // It captures the current state ('this.state') right before each emission
117
- // to provide the correct 'previousState' to _pushState.
118
- const emit = (newState: S): void => {
119
- const previousState = this.state; // State just before this specific emission
120
- // The 'action' passed to _pushState is the original action that triggered the handler,
121
- // providing context for the state change (e.g., for logging or plugins).
122
- this._pushState(newState, previousState, action);
123
- };
163
+ // TODO: Consider implementing error boundary pattern
164
+ // For now, we log and continue, but applications may want to:
165
+ // 1. Emit an error state
166
+ // 2. Re-throw the error
167
+ // 3. Call an error handler callback
124
168
 
125
- try {
126
- // Await the handler in case it's an async function (e.g., performs API calls).
127
- // The 'action' is passed to the handler, and due to the way 'on' is typed,
128
- // the 'event' parameter within the handler function will be correctly
129
- // typed to its specific class (e.g., LoadMyFeatureData).
130
- await handler(action, emit);
131
- } catch (error) {
132
- // Enhanced error handling with better context
133
- const constructorName = (action.constructor as { name?: string }).name || 'UnnamedConstructor';
134
- const errorContext = {
135
- blocName: this._name,
136
- blocId: String(this._id),
137
- eventType: constructorName,
138
- currentState: this.state,
139
- action: action,
140
- timestamp: new Date().toISOString()
141
- };
142
-
143
- Blac.error(
144
- `[Bloc ${this._name}:${String(this._id)}] Error in event handler for '${constructorName}':`,
145
- error,
146
- "Context:", errorContext
147
- );
148
-
149
- // TODO: Consider implementing error boundary pattern
150
- // For now, we log and continue, but applications may want to:
151
- // 1. Emit an error state
152
- // 2. Re-throw the error
153
- // 3. Call an error handler callback
154
-
155
- // Optional: Re-throw for critical errors (can be configured)
156
- if (error instanceof Error && error.name === 'CriticalError') {
157
- throw error;
158
- }
159
- }
160
- } else {
161
- // Enhanced warning with more context
162
- const constructorName = (action.constructor as { name?: string }).name || 'UnnamedConstructor';
163
- Blac.warn(
164
- `[Bloc ${this._name}:${String(this._id)}] No handler registered for action type: '${constructorName}'.`,
165
- "Registered handlers:", Array.from(this.eventHandlers.keys()).map(k => k.name),
166
- "Action was:", action
167
- );
169
+ // Optional: Re-throw for critical errors (can be configured)
170
+ if (error instanceof Error && error.name === 'CriticalError') {
171
+ throw error;
168
172
  }
173
+ }
174
+ } else {
175
+ // Enhanced warning with more context
176
+ const constructorName =
177
+ (action.constructor as { name?: string }).name || 'UnnamedConstructor';
178
+ Blac.warn(
179
+ `[Bloc ${this._name}:${String(this._id)}] No handler registered for action type: '${constructorName}'.`,
180
+ 'Registered handlers:',
181
+ Array.from(this.eventHandlers.keys()).map((k) => k.name),
182
+ 'Action was:',
183
+ action,
184
+ );
169
185
  }
186
+ }
170
187
  }