@blac/core 2.0.0-rc-13 → 2.0.0-rc-16

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 (54) hide show
  1. package/package.json +10 -15
  2. package/src/Blac.ts +38 -71
  3. package/src/Bloc.ts +8 -9
  4. package/src/BlocBase.ts +246 -540
  5. package/src/Cubit.ts +1 -16
  6. package/src/__tests__/Blac.config.test.ts +10 -17
  7. package/src/__tests__/Bloc.event.test.ts +27 -52
  8. package/src/__tests__/BlocBase.subscription.test.ts +349 -0
  9. package/src/__tests__/Cubit.test.ts +39 -50
  10. package/src/__tests__/memory-leaks.test.ts +383 -0
  11. package/src/__tests__/plugins.test.ts +107 -97
  12. package/src/adapter/BlacAdapter.ts +347 -207
  13. package/src/adapter/ProxyFactory.ts +196 -255
  14. package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
  15. package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
  16. package/src/adapter/index.ts +0 -1
  17. package/src/errors/BlacError.ts +69 -0
  18. package/src/errors/ErrorManager.ts +101 -0
  19. package/src/errors/handleError.ts +114 -0
  20. package/src/errors/index.ts +13 -0
  21. package/src/events.ts +7 -1
  22. package/src/index.ts +7 -1
  23. package/src/lifecycle/BlocLifecycle.ts +156 -0
  24. package/src/lifecycle/index.ts +1 -0
  25. package/src/plugins/BlocPluginRegistry.ts +71 -51
  26. package/src/plugins/SystemPluginRegistry.ts +63 -13
  27. package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
  28. package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
  29. package/src/plugins/types.ts +32 -3
  30. package/src/subscription/README.md +132 -0
  31. package/src/subscription/SubscriptionManager.ts +395 -0
  32. package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
  33. package/src/subscription/index.ts +2 -0
  34. package/src/subscription/types.ts +90 -0
  35. package/src/testing.ts +31 -37
  36. package/src/types.ts +4 -21
  37. package/src/utils/BatchingManager.ts +59 -0
  38. package/src/utils/RerenderLogger.ts +265 -0
  39. package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
  40. package/src/utils/__tests__/shallowEqual.test.ts +171 -0
  41. package/src/utils/__tests__/uuid.test.ts +144 -0
  42. package/src/utils/generateInstanceId.ts +54 -0
  43. package/src/utils/shallowEqual.ts +1 -1
  44. package/README.md +0 -329
  45. package/dist/index.cjs.js +0 -11
  46. package/dist/index.cjs.js.map +0 -1
  47. package/dist/index.d.ts +0 -1078
  48. package/dist/index.es.js +0 -1992
  49. package/dist/index.es.js.map +0 -1
  50. package/src/BlacObserver.ts +0 -189
  51. package/src/__tests__/BlocBase.lifecycle.test.ts +0 -441
  52. package/src/__tests__/props.test.ts +0 -256
  53. package/src/adapter/ConsumerTracker.ts +0 -241
  54. package/src/adapter/__tests__/ConsumerTracker.test.ts +0 -562
@@ -1,16 +1,14 @@
1
1
  import { Blac } from '../Blac';
2
- import { BlocBase, BlocLifecycleState } from '../BlocBase';
3
- import { BlocConstructor, BlocState, InferPropsFromGeneric } from '../types';
4
- import { PropsUpdated } from '../events';
2
+ import { BlocBase } from '../BlocBase';
3
+ import { BlocConstructor, BlocState } from '../types';
5
4
  import { generateUUID } from '../utils/uuid';
6
- import { shallowEqual } from '../utils/shallowEqual';
7
- import { ConsumerTracker, DependencyArray } from './ConsumerTracker';
5
+ import { generateInstanceIdFromProps } from '../utils/generateInstanceId';
8
6
  import { ProxyFactory } from './ProxyFactory';
9
7
 
10
8
  export interface AdapterOptions<B extends BlocBase<any>> {
11
- id?: string;
9
+ instanceId?: string;
12
10
  dependencies?: (bloc: B) => unknown[];
13
- props?: any;
11
+ staticProps?: any;
14
12
  onMount?: (bloc: B) => void;
15
13
  onUnmount?: (bloc: B) => void;
16
14
  }
@@ -20,29 +18,40 @@ export interface AdapterOptions<B extends BlocBase<any>> {
20
18
  * It manages dependency tracking, lifecycle hooks, and proxy creation.
21
19
  */
22
20
  export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
23
- public readonly id = `consumer-${generateUUID()}`;
21
+ public readonly id = `adapter-${generateUUID()}`;
24
22
  public readonly blocConstructor: B;
25
23
  public readonly componentRef: { current: object } = { current: {} };
26
- public blocInstance: InstanceType<B>;
27
-
28
- // Core components
29
- private consumerTracker: ConsumerTracker;
30
-
31
- // Props ownership tracking
32
- private static propsOwners = new WeakMap<BlocBase<any>, string>();
33
- private lastProps?: any;
24
+ public blocInstance!: InstanceType<B>;
34
25
 
35
26
  unmountTime: number = 0;
36
27
  mountTime: number = 0;
37
28
 
29
+ // Subscription management
30
+ private subscriptionId?: string;
31
+ private unsubscribe?: () => void;
32
+
38
33
  // Dependency tracking
39
34
  private dependencyValues?: unknown[];
40
35
  private isUsingDependencies: boolean = false;
36
+ private trackedPaths = new Set<string>();
37
+ private pendingTrackedPaths = new Set<string>(); // Paths tracked before subscription exists
38
+
39
+ // Proxy caching
40
+ private cachedStateProxy?: BlocState<InstanceType<B>>;
41
+ private cachedBlocProxy?: InstanceType<B>;
42
+ private lastProxiedState?: BlocState<InstanceType<B>>;
41
43
 
42
44
  // Lifecycle state
43
45
  private hasMounted = false;
44
46
  private mountCount = 0;
45
47
 
48
+ // Rerender tracking
49
+ private lastState?: BlocState<InstanceType<B>>;
50
+ private lastDependencyValues?: unknown[];
51
+ private componentName?: string;
52
+ private renderCount = 0;
53
+ private lastDependenciesFn?: (bloc: InstanceType<B>) => unknown[];
54
+
46
55
  options?: AdapterOptions<InstanceType<B>>;
47
56
 
48
57
  constructor(
@@ -54,22 +63,17 @@ export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
54
63
  this.componentRef = instanceProps.componentRef;
55
64
  this.isUsingDependencies = !!options?.dependencies;
56
65
 
57
- // Initialize consumer tracker
58
- this.consumerTracker = new ConsumerTracker();
59
-
60
- // Initialize bloc instance and register consumer
61
- this.blocInstance = this.updateBlocInstance();
62
- this.consumerTracker.register(instanceProps.componentRef.current, this.id);
66
+ // Initialize bloc instance
67
+ this.updateBlocInstance();
63
68
 
64
69
  // Initialize dependency values if using dependencies
65
70
  if (this.isUsingDependencies && options?.dependencies) {
66
71
  this.dependencyValues = options.dependencies(this.blocInstance);
67
72
  }
68
73
 
69
- // Handle initial props if provided
70
- if (options?.props !== undefined) {
71
- this.updateProps(options.props);
72
- }
74
+ // Notify plugins
75
+ const metadata = this.getAdapterMetadata();
76
+ Blac.getInstance().plugins.notifyAdapterCreated(this, metadata);
73
77
  }
74
78
 
75
79
  trackAccess(
@@ -78,39 +82,139 @@ export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
78
82
  path: string,
79
83
  value?: any,
80
84
  ): void {
81
- this.consumerTracker.trackAccess(consumerRef, type, path, value);
85
+ const fullPath = type === 'class' ? `_class.${path}` : path;
86
+ this.trackedPaths.add(fullPath);
87
+
88
+ if (this.subscriptionId) {
89
+ this.blocInstance.trackAccess(this.subscriptionId, fullPath, value);
90
+ } else {
91
+ // No subscription ID yet - store for later
92
+ this.pendingTrackedPaths.add(fullPath);
93
+ if ((Blac.config as any).logLevel === 'debug') {
94
+ Blac.log(`[BlacAdapter] trackAccess storing pending path: ${path}`);
95
+ }
96
+ }
82
97
  }
83
98
 
84
- getConsumerDependencies(consumerRef: object): DependencyArray | null {
85
- return this.consumerTracker.getDependencies(consumerRef);
99
+ updateBlocInstance(): InstanceType<B> {
100
+ // Determine the instance ID
101
+ let instanceId = this.options?.instanceId;
102
+
103
+ // If no explicit instanceId provided but staticProps exist, generate from them
104
+ if (!instanceId && this.options?.staticProps) {
105
+ const generatedId = generateInstanceIdFromProps(this.options.staticProps);
106
+ if (generatedId) {
107
+ instanceId = generatedId;
108
+ }
109
+ }
110
+
111
+ this.blocInstance = Blac.instance.getBloc<B>(this.blocConstructor, {
112
+ constructorParams: this.options?.staticProps,
113
+ id: instanceId,
114
+ instanceRef: this.id,
115
+ });
116
+ return this.blocInstance;
86
117
  }
87
118
 
88
- shouldNotifyConsumer(
89
- consumerRef: object,
90
- changedPaths: Set<string>,
91
- ): boolean {
92
- const consumerInfo = this.consumerTracker.getConsumerInfo(consumerRef);
93
- if (!consumerInfo) {
94
- return true; // If consumer not registered yet, notify by default
119
+ createSubscription = (options: { onChange: () => void }) => {
120
+ // If using manual dependencies, create a selector-based subscription
121
+ if (this.isUsingDependencies && this.options?.dependencies) {
122
+ this.unsubscribe = this.blocInstance.subscribeWithSelector(
123
+ (_state) => this.options!.dependencies!(this.blocInstance),
124
+ (newValues) => {
125
+ const hasChanged = this.hasDependencyValuesChanged(
126
+ this.dependencyValues,
127
+ newValues as unknown[],
128
+ );
129
+
130
+ if (hasChanged) {
131
+ this.dependencyValues = newValues as unknown[];
132
+ options.onChange();
133
+ }
134
+ },
135
+ );
136
+ } else {
137
+ // Create a component subscription with weak reference
138
+ const weakRef = new WeakRef(this.componentRef.current);
139
+ this.unsubscribe = this.blocInstance.subscribeComponent(
140
+ weakRef,
141
+ options.onChange,
142
+ );
143
+
144
+ // Get the subscription ID for tracking
145
+ const subscriptions = (this.blocInstance._subscriptionManager as any)
146
+ .subscriptions as Map<string, any>;
147
+ this.subscriptionId = Array.from(subscriptions.keys()).pop();
148
+
149
+ // Apply any pending tracked paths
150
+ if (this.subscriptionId && this.pendingTrackedPaths.size > 0) {
151
+ for (const path of this.pendingTrackedPaths) {
152
+ this.blocInstance.trackAccess(this.subscriptionId, path);
153
+ }
154
+ this.pendingTrackedPaths.clear();
155
+ }
156
+ }
157
+
158
+ // Don't call onChange initially - let React handle the initial render
159
+
160
+ return () => {
161
+ if (this.unsubscribe) {
162
+ this.unsubscribe();
163
+ this.unsubscribe = undefined;
164
+ this.subscriptionId = undefined;
165
+ }
166
+ };
167
+ };
168
+
169
+ hasDependencyValuesChanged = (
170
+ oldValues: unknown[] | undefined,
171
+ newValues: unknown[],
172
+ ): boolean => {
173
+ if (!oldValues) return true;
174
+ if (oldValues.length !== newValues.length) return true;
175
+
176
+ for (let i = 0; i < oldValues.length; i++) {
177
+ if (!Object.is(oldValues[i], newValues[i])) {
178
+ return true;
179
+ }
95
180
  }
96
181
 
97
- // First render - always notify to establish baseline
98
- if (!consumerInfo.hasRendered) {
99
- return true;
182
+ return false;
183
+ };
184
+
185
+ getStateProxy = (): BlocState<InstanceType<B>> => {
186
+ // If using manual dependencies or proxy tracking is disabled, return raw state
187
+ if (this.isUsingDependencies || !Blac.config.proxyDependencyTracking) {
188
+ return this.blocInstance.state;
100
189
  }
101
190
 
102
- // Use built-in method from ConsumerTracker
103
- return this.consumerTracker.shouldNotifyConsumer(consumerRef, changedPaths);
104
- }
191
+ // Return cached proxy if state hasn't changed
192
+ const currentState = this.blocInstance.state;
193
+ if (this.cachedStateProxy && this.lastProxiedState === currentState) {
194
+ return this.cachedStateProxy;
195
+ }
105
196
 
106
- updateLastNotified(consumerRef: object): void {
107
- this.consumerTracker.updateLastNotified(consumerRef);
108
- this.consumerTracker.setHasRendered(consumerRef, true);
109
- }
197
+ // Create new proxy for new state
198
+ this.lastProxiedState = currentState;
199
+ this.cachedStateProxy = this.createStateProxy({ target: currentState });
200
+ return this.cachedStateProxy!;
201
+ };
110
202
 
111
- resetConsumerTracking(): void {
112
- this.consumerTracker.resetTracking(this.componentRef.current);
113
- }
203
+ getBlocProxy = (): InstanceType<B> => {
204
+ // If using manual dependencies or proxy tracking is disabled, return raw bloc
205
+ if (this.isUsingDependencies || !Blac.config.proxyDependencyTracking) {
206
+ return this.blocInstance;
207
+ }
208
+
209
+ // Return cached proxy if bloc instance hasn't changed
210
+ if (this.cachedBlocProxy) {
211
+ return this.cachedBlocProxy;
212
+ }
213
+
214
+ // Create and cache proxy
215
+ this.cachedBlocProxy = this.createClassProxy({ target: this.blocInstance });
216
+ return this.cachedBlocProxy;
217
+ };
114
218
 
115
219
  createStateProxy = <T extends object>(props: { target: T }): T => {
116
220
  return ProxyFactory.createStateProxy({
@@ -128,200 +232,236 @@ export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
128
232
  });
129
233
  };
130
234
 
131
- updateBlocInstance(): InstanceType<B> {
132
- this.blocInstance = Blac.instance.getBloc<B>(this.blocConstructor, {
133
- props: this.options?.props,
134
- id: this.options?.id,
135
- instanceRef: this.id,
136
- });
137
- return this.blocInstance;
138
- }
139
-
140
- createSubscription = (options: { onChange: () => void }) => {
141
- const unsubscribe = this.blocInstance._observer.subscribe({
142
- id: this.id,
143
- fn: (
144
- newState: BlocState<InstanceType<B>>,
145
- oldState: BlocState<InstanceType<B>>,
146
- ) => {
147
- // Case 1: Manual dependencies provided
148
- if (this.isUsingDependencies && this.options?.dependencies) {
149
- const newValues = this.options.dependencies(this.blocInstance);
150
- const hasChanged = this.hasDependencyValuesChanged(
151
- this.dependencyValues,
152
- newValues,
153
- );
154
-
155
- if (!hasChanged) {
156
- return; // Don't trigger re-render
157
- }
158
-
159
- this.dependencyValues = newValues;
160
- }
161
- // Case 2: Proxy tracking disabled globally (and no manual dependencies)
162
- else if (!Blac.config.proxyDependencyTracking) {
163
- // Always trigger re-render when proxy tracking is disabled
164
- options.onChange();
165
- return;
166
- }
167
- // Case 3: Proxy tracking enabled (default behavior)
168
- else {
169
- // Check if any tracked values have changed (proxy-based tracking)
170
- const consumerInfo = this.consumerTracker.getConsumerInfo(
171
- this.componentRef.current,
172
- );
173
- if (consumerInfo && consumerInfo.hasRendered) {
174
- // Only check dependencies if component has rendered at least once
175
- const hasChanged = this.consumerTracker.hasValuesChanged(
176
- this.componentRef.current,
177
- newState,
178
- this.blocInstance,
179
- );
180
-
181
- if (!hasChanged) {
182
- return; // Don't trigger re-render
183
- }
184
- }
185
- }
186
-
187
- options.onChange();
188
- },
189
- });
235
+ // Lifecycle methods
236
+ mount = () => {
237
+ this.hasMounted = true;
238
+ this.mountCount++;
239
+ this.mountTime = Date.now();
190
240
 
191
- return unsubscribe;
192
- };
241
+ // Call onMount hook
242
+ if (this.options?.onMount) {
243
+ this.options.onMount(this.blocInstance);
244
+ }
193
245
 
194
- mount = (): void => {
195
- // Re-run dependencies on mount to ensure fresh values
246
+ // Refresh dependencies if using manual tracking
196
247
  if (this.isUsingDependencies && this.options?.dependencies) {
197
248
  this.dependencyValues = this.options.dependencies(this.blocInstance);
198
249
  }
199
250
 
200
- // Lifecycle management
201
- this.mountCount++;
202
- this.blocInstance._addConsumer(this.id, this.componentRef.current);
203
-
204
- // Call onMount callback if provided and not already called
205
- if (!this.hasMounted) {
206
- this.hasMounted = true;
207
- this.mountTime = Date.now();
208
-
209
- if (this.options?.onMount) {
210
- try {
211
- this.options.onMount(this.blocInstance);
212
- } catch (error) {
213
- throw error;
214
- }
215
- }
216
- }
251
+ // Notify plugins
252
+ const metadata = this.getAdapterMetadata();
253
+ Blac.getInstance().plugins.notifyAdapterMount(this, metadata);
217
254
  };
218
255
 
219
- unmount = (): void => {
256
+ unmount = () => {
220
257
  this.unmountTime = Date.now();
221
- this.consumerTracker.unregister(this.componentRef.current);
222
- this.blocInstance._removeConsumer(this.id);
223
258
 
224
- // Clear ownership if we own this bloc
225
- if (BlacAdapter.propsOwners.get(this.blocInstance) === this.id) {
226
- BlacAdapter.propsOwners.delete(this.blocInstance);
259
+ // Cancel subscription
260
+ if (this.unsubscribe) {
261
+ this.unsubscribe();
262
+ this.unsubscribe = undefined;
263
+ this.subscriptionId = undefined;
227
264
  }
228
265
 
229
- // Call onUnmount callback
266
+ // Call onUnmount hook
230
267
  if (this.options?.onUnmount) {
231
268
  try {
232
269
  this.options.onUnmount(this.blocInstance);
233
270
  } catch (error) {
234
- // Don't re-throw on unmount to allow cleanup to continue
271
+ console.error('Error in onUnmount hook:', error);
235
272
  }
236
273
  }
274
+
275
+ // Notify plugins
276
+ const metadata = this.getAdapterMetadata();
277
+ Blac.getInstance().plugins.notifyAdapterUnmount(this, metadata);
237
278
  };
238
279
 
239
- getProxyState = (
240
- state: BlocState<InstanceType<B>>,
241
- ): BlocState<InstanceType<B>> => {
242
- // Return raw state if proxy tracking is disabled globally or using manual dependencies
243
- if (this.isUsingDependencies || !Blac.config.proxyDependencyTracking) {
244
- return state;
280
+ // Reset tracking for next render
281
+ resetTracking(): void {
282
+ // Clear tracked paths from previous render
283
+ this.trackedPaths.clear();
284
+ this.pendingTrackedPaths.clear();
285
+
286
+ // Clear subscription dependencies to track only current render
287
+ if (this.subscriptionId) {
288
+ const subscription = (
289
+ this.blocInstance._subscriptionManager as any
290
+ ).subscriptions.get(this.subscriptionId);
291
+ if (subscription && subscription.dependencies) {
292
+ // Clear old dependencies - we'll track new ones in this render
293
+ subscription.dependencies.clear();
294
+ }
245
295
  }
296
+ }
246
297
 
247
- return ProxyFactory.getProxyState({
248
- state,
249
- consumerRef: this.componentRef.current,
250
- consumerTracker: this as any,
251
- });
252
- };
298
+ // Set component name for rerender logging
299
+ setComponentName(name: string): void {
300
+ this.componentName = name;
301
+ }
253
302
 
254
- getProxyBlocInstance = (): InstanceType<B> => {
255
- // Return raw instance if proxy tracking is disabled globally or using manual dependencies
256
- if (this.isUsingDependencies || !Blac.config.proxyDependencyTracking) {
257
- return this.blocInstance;
303
+ // Notify plugins about render
304
+ notifyRender(): void {
305
+ this.renderCount++;
306
+
307
+ // Update dependency values if using manual tracking
308
+ if (this.isUsingDependencies && this.options?.dependencies) {
309
+ this.lastDependencyValues = this.dependencyValues;
310
+ this.dependencyValues = this.options.dependencies(this.blocInstance);
258
311
  }
259
312
 
260
- return ProxyFactory.getProxyBlocInstance({
261
- blocInstance: this.blocInstance,
262
- consumerRef: this.componentRef.current,
263
- consumerTracker: this as any,
264
- });
265
- };
313
+ const metadata = this.getAdapterMetadata();
314
+ Blac.getInstance().plugins.notifyAdapterRender(this, metadata);
315
+ }
266
316
 
267
- // Expose calledOnMount for backward compatibility
268
- get calledOnMount(): boolean {
269
- return this.hasMounted;
317
+ // Get adapter metadata for plugins
318
+ private getAdapterMetadata(): any {
319
+ return {
320
+ componentName: this.componentName,
321
+ blocInstance: this.blocInstance,
322
+ renderCount: this.renderCount,
323
+ trackedPaths: Array.from(this.trackedPaths),
324
+ isUsingDependencies: this.isUsingDependencies,
325
+ lastState: this.lastState,
326
+ lastDependencyValues: this.lastDependencyValues,
327
+ currentDependencyValues: this.dependencyValues,
328
+ };
270
329
  }
271
330
 
272
- updateProps(props: any): void {
273
- const bloc = this.blocInstance;
274
-
275
- // Check ownership
276
- if (!BlacAdapter.propsOwners.has(bloc)) {
277
- // First adapter with props becomes the owner
278
- BlacAdapter.propsOwners.set(bloc, this.id);
279
- }
280
-
281
- if (BlacAdapter.propsOwners.get(bloc) !== this.id) {
282
- Blac.warn(
283
- `[BlacAdapter] Attempted to set props on ${bloc.constructor.name} from non-owner adapter`
284
- );
285
- return;
331
+ // Get value at a dot-notated path
332
+ private getValueAtPath(obj: any, path: string): any {
333
+ const parts = path.split('.');
334
+ let current = obj;
335
+
336
+ for (const part of parts) {
337
+ if (current == null) return undefined;
338
+ current = current[part];
286
339
  }
287
-
288
- // Disposal safety
289
- if ((bloc as any)._disposalState === BlocLifecycleState.DISPOSED ||
290
- (bloc as any)._disposalState === BlocLifecycleState.DISPOSING) {
291
- return;
340
+
341
+ return current;
342
+ }
343
+
344
+ // Get paths that changed between states
345
+ private getStateChangedPaths(oldState: any, newState: any): string[] {
346
+ const changedPaths: string[] = [];
347
+
348
+ // Only check tracked paths if we have any
349
+ if (this.trackedPaths.size > 0) {
350
+ for (const path of this.trackedPaths) {
351
+ // Skip class property paths
352
+ if (path.startsWith('_class.')) continue;
353
+
354
+ const oldValue = this.getValueAtPath(oldState, path);
355
+ const newValue = this.getValueAtPath(newState, path);
356
+
357
+ if (!Object.is(oldValue, newValue)) {
358
+ changedPaths.push(path);
359
+ }
360
+ }
361
+ } else {
362
+ // No tracked paths - do deep comparison to find what changed
363
+ if (oldState !== newState) {
364
+ const deepChanges = this.getDeepChangedPaths(oldState, newState);
365
+ if (deepChanges.length > 0) {
366
+ return deepChanges;
367
+ }
368
+ changedPaths.push('(entire state)');
369
+ }
292
370
  }
293
-
294
- // Check if props have changed
295
- if (shallowEqual(this.lastProps, props)) {
296
- return;
371
+
372
+ return changedPaths;
373
+ }
374
+
375
+ // Deep comparison to find all changed paths
376
+ private getDeepChangedPaths(
377
+ oldObj: any,
378
+ newObj: any,
379
+ basePath: string = '',
380
+ ): string[] {
381
+ const changes: string[] = [];
382
+
383
+ // Handle null/undefined
384
+ if (oldObj === newObj) return changes;
385
+ if (oldObj == null || newObj == null) {
386
+ changes.push(basePath || '(root)');
387
+ return changes;
297
388
  }
298
-
299
- // Update props based on bloc type
300
- if ('add' in bloc && typeof bloc.add === 'function') {
301
- // Bloc: dispatch PropsUpdated event
302
- (bloc as any).add(new PropsUpdated(props));
303
- } else if ('_updateProps' in bloc && typeof (bloc as any)._updateProps === 'function') {
304
- // Cubit: direct props update
305
- (bloc as any)._updateProps(props);
389
+
390
+ // Handle primitives
391
+ if (typeof oldObj !== 'object' || typeof newObj !== 'object') {
392
+ if (oldObj !== newObj) {
393
+ changes.push(basePath || '(root)');
394
+ }
395
+ return changes;
306
396
  }
307
-
308
- this.lastProps = props;
309
- }
310
397
 
311
- private hasDependencyValuesChanged(
312
- prev: unknown[] | undefined,
313
- next: unknown[],
314
- ): boolean {
315
- if (!prev) return true; // First run, always trigger
316
- if (prev.length !== next.length) return true;
398
+ // Handle arrays
399
+ if (Array.isArray(oldObj) || Array.isArray(newObj)) {
400
+ if (
401
+ !Array.isArray(oldObj) ||
402
+ !Array.isArray(newObj) ||
403
+ oldObj.length !== newObj.length
404
+ ) {
405
+ changes.push(basePath || '(root)');
406
+ return changes;
407
+ }
408
+ // Compare array elements
409
+ for (let i = 0; i < oldObj.length; i++) {
410
+ const elementPath = basePath ? `${basePath}[${i}]` : `[${i}]`;
411
+ changes.push(
412
+ ...this.getDeepChangedPaths(oldObj[i], newObj[i], elementPath),
413
+ );
414
+ }
415
+ return changes;
416
+ }
317
417
 
318
- // Use Object.is for comparison (handles NaN, +0/-0 correctly)
319
- for (let i = 0; i < prev.length; i++) {
320
- if (!Object.is(prev[i], next[i])) {
321
- return true;
418
+ // Handle objects
419
+ const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
420
+ for (const key of allKeys) {
421
+ const keyPath = basePath ? `${basePath}.${key}` : key;
422
+
423
+ if (!(key in oldObj)) {
424
+ changes.push(`${keyPath} (added)`);
425
+ } else if (!(key in newObj)) {
426
+ changes.push(`${keyPath} (removed)`);
427
+ } else {
428
+ const oldVal = oldObj[key];
429
+ const newVal = newObj[key];
430
+
431
+ // Use Object.is for comparison
432
+ if (!Object.is(oldVal, newVal)) {
433
+ // For primitives and different object references, record the change
434
+ if (
435
+ typeof oldVal !== 'object' ||
436
+ typeof newVal !== 'object' ||
437
+ oldVal === null ||
438
+ newVal === null
439
+ ) {
440
+ changes.push(keyPath);
441
+ } else {
442
+ // For objects/arrays, recurse deeper
443
+ const deeperChanges = this.getDeepChangedPaths(
444
+ oldVal,
445
+ newVal,
446
+ keyPath,
447
+ );
448
+ if (deeperChanges.length > 0) {
449
+ changes.push(...deeperChanges);
450
+ } else {
451
+ // Objects are different references but have same content
452
+ changes.push(keyPath);
453
+ }
454
+ }
455
+ }
322
456
  }
323
457
  }
324
458
 
325
- return false;
459
+ return changes;
460
+ }
461
+
462
+ // Get last rerender reason
463
+ getLastRerenderReason(): any {
464
+ // This method is deprecated - render reason is now handled by the RenderLoggingPlugin
465
+ return undefined;
326
466
  }
327
467
  }