@blac/core 2.0.0-rn-12 → 2.0.1

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,256 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { Bloc } from '../Bloc';
3
- import { Cubit } from '../Cubit';
4
- import { PropsUpdated } from '../events';
5
- import { BlacAdapter } from '../adapter/BlacAdapter';
6
- import { Blac } from '../Blac';
7
-
8
- // Test types
9
- interface TestProps {
10
- query: string;
11
- filters?: string[];
12
- }
13
-
14
- interface TestState {
15
- data: string;
16
- loading: boolean;
17
- }
18
-
19
- // Test Cubit with props
20
- class TestCubit extends Cubit<TestState, TestProps> {
21
- onPropsChangedMock = vi.fn();
22
-
23
- constructor() {
24
- super({ data: '', loading: false });
25
- }
26
-
27
- protected onPropsChanged(oldProps: TestProps | undefined, newProps: TestProps): void {
28
- this.onPropsChangedMock(oldProps, newProps);
29
- if (oldProps?.query !== newProps.query) {
30
- this.emit({ ...this.state, data: `Query: ${newProps.query}` });
31
- }
32
- }
33
-
34
- loadData = () => {
35
- const query = this.props?.query ?? 'default';
36
- this.emit({ data: `Loaded: ${query}`, loading: false });
37
- };
38
- }
39
-
40
- // Test Bloc with props
41
- class TestBloc extends Bloc<TestState, PropsUpdated<TestProps>> {
42
- constructor() {
43
- super({ data: '', loading: false });
44
-
45
- this.on(PropsUpdated<TestProps>, (event, emit) => {
46
- emit({ ...this.state, data: `Props: ${event.props.query}` });
47
- });
48
- }
49
- }
50
-
51
- describe('Props functionality', () => {
52
- beforeEach(() => {
53
- // Clear Blac instance between tests
54
- Blac.resetInstance();
55
- });
56
-
57
- describe('PropsUpdated event', () => {
58
- it('should create PropsUpdated event with correct props', () => {
59
- const props = { query: 'test', filters: ['a', 'b'] };
60
- const event = new PropsUpdated(props);
61
-
62
- expect(event.props).toEqual(props);
63
- expect(event.props).toBe(props); // Should be the same reference
64
- });
65
- });
66
-
67
- describe('Cubit props support', () => {
68
- it('should support props getter', () => {
69
- const cubit = new TestCubit();
70
- expect(cubit.props).toBeNull();
71
- });
72
-
73
- it('should update props via _updateProps', () => {
74
- const cubit = new TestCubit();
75
- const props = { query: 'test' };
76
-
77
- (cubit as any)._updateProps(props);
78
-
79
- expect(cubit.props).toEqual(props);
80
- expect(cubit.onPropsChangedMock).toHaveBeenCalledWith(null, props);
81
- });
82
-
83
- it('should emit state when props change', () => {
84
- const cubit = new TestCubit();
85
- const props1 = { query: 'test1' };
86
- const props2 = { query: 'test2' };
87
-
88
- (cubit as any)._updateProps(props1);
89
- expect(cubit.state.data).toBe('Query: test1');
90
-
91
- (cubit as any)._updateProps(props2);
92
- expect(cubit.state.data).toBe('Query: test2');
93
- });
94
-
95
- it('should access props in methods', () => {
96
- const cubit = new TestCubit();
97
-
98
- cubit.loadData();
99
- expect(cubit.state.data).toBe('Loaded: default');
100
-
101
- (cubit as any)._updateProps({ query: 'custom' });
102
- cubit.loadData();
103
- expect(cubit.state.data).toBe('Loaded: custom');
104
- });
105
- });
106
-
107
- describe('Bloc props support', () => {
108
- it('should handle PropsUpdated events', async () => {
109
- const bloc = new TestBloc();
110
- const props = { query: 'search' };
111
-
112
- await bloc.add(new PropsUpdated(props));
113
-
114
- expect(bloc.state.data).toBe('Props: search');
115
- });
116
-
117
- it('should queue PropsUpdated events like any other event', async () => {
118
- const bloc = new TestBloc();
119
-
120
- await bloc.add(new PropsUpdated({ query: 'first' }));
121
- await bloc.add(new PropsUpdated({ query: 'second' }));
122
-
123
- expect(bloc.state.data).toBe('Props: second');
124
- });
125
- });
126
-
127
- describe('BlacAdapter props ownership', () => {
128
- it('should allow first adapter to own props', () => {
129
- const Constructor = TestCubit as any;
130
- const adapter1 = new BlacAdapter(
131
- { componentRef: { current: {} }, blocConstructor: Constructor },
132
- { props: { query: 'test1' } }
133
- );
134
-
135
- expect(() => {
136
- adapter1.updateProps({ query: 'test2' });
137
- }).not.toThrow();
138
- });
139
-
140
- it('should prevent non-owner adapters from updating props', () => {
141
- const warnSpy = vi.spyOn(Blac, 'warn').mockImplementation(() => {});
142
- const Constructor = TestCubit as any;
143
-
144
- // First adapter becomes owner
145
- const adapter1 = new BlacAdapter(
146
- { componentRef: { current: {} }, blocConstructor: Constructor },
147
- { props: { query: 'test1' } }
148
- );
149
-
150
- // Second adapter tries to update props
151
- const adapter2 = new BlacAdapter(
152
- { componentRef: { current: {} }, blocConstructor: Constructor },
153
- {}
154
- );
155
-
156
- adapter2.updateProps({ query: 'hijack' });
157
-
158
- expect(warnSpy).toHaveBeenCalledWith(
159
- expect.stringContaining('non-owner adapter')
160
- );
161
-
162
- warnSpy.mockRestore();
163
- });
164
-
165
- it('should clear ownership on adapter unmount', () => {
166
- const Constructor = TestCubit as any;
167
- const adapter1 = new BlacAdapter(
168
- { componentRef: { current: {} }, blocConstructor: Constructor },
169
- { props: { query: 'test1' } }
170
- );
171
-
172
- adapter1.mount();
173
- adapter1.unmount();
174
-
175
- // New adapter should be able to take ownership
176
- const adapter2 = new BlacAdapter(
177
- { componentRef: { current: {} }, blocConstructor: Constructor },
178
- { props: { query: 'test2' } }
179
- );
180
-
181
- expect(() => {
182
- adapter2.updateProps({ query: 'test3' });
183
- }).not.toThrow();
184
- });
185
-
186
- it('should not update props if they are shallowly equal', () => {
187
- const cubit = new TestCubit();
188
- const Constructor = TestCubit as any;
189
- const adapter = new BlacAdapter(
190
- { componentRef: { current: {} }, blocConstructor: Constructor },
191
- {}
192
- );
193
-
194
- const props = { query: 'test' };
195
- adapter.updateProps(props);
196
-
197
- // Reset mock
198
- cubit.onPropsChangedMock.mockClear();
199
-
200
- // Update with same props
201
- adapter.updateProps({ query: 'test' });
202
-
203
- // onPropsChanged should not have been called
204
- expect(cubit.onPropsChangedMock).not.toHaveBeenCalled();
205
- });
206
-
207
- it('should ignore props updates during disposal', () => {
208
- const Constructor = TestCubit as any;
209
- const adapter = new BlacAdapter(
210
- { componentRef: { current: {} }, blocConstructor: Constructor },
211
- { props: { query: 'test1' } }
212
- );
213
-
214
- // Force disposal state
215
- (adapter.blocInstance as any)._disposalState = 'disposing';
216
-
217
- expect(() => {
218
- adapter.updateProps({ query: 'test2' });
219
- }).not.toThrow();
220
-
221
- // Props should not have been updated
222
- expect((adapter.blocInstance as TestCubit).props?.query).toBe('test1');
223
- });
224
- });
225
-
226
- describe('Props integration', () => {
227
- it('should dispatch PropsUpdated for Bloc instances', async () => {
228
- const Constructor = TestBloc as any;
229
- const adapter = new BlacAdapter(
230
- { componentRef: { current: {} }, blocConstructor: Constructor },
231
- {}
232
- );
233
-
234
- const bloc = adapter.blocInstance as TestBloc;
235
- adapter.updateProps({ query: 'adapter-test' });
236
-
237
- // Wait for event processing
238
- await new Promise(resolve => setTimeout(resolve, 50));
239
-
240
- expect(bloc.state.data).toBe('Props: adapter-test');
241
- });
242
-
243
- it('should call _updateProps for Cubit instances', () => {
244
- const Constructor = TestCubit as any;
245
- const adapter = new BlacAdapter(
246
- { componentRef: { current: {} }, blocConstructor: Constructor },
247
- {}
248
- );
249
-
250
- adapter.updateProps({ query: 'adapter-test' });
251
-
252
- expect((adapter.blocInstance as TestCubit).props?.query).toBe('adapter-test');
253
- expect((adapter.blocInstance as TestCubit).state.data).toBe('Query: adapter-test');
254
- });
255
- });
256
- });
@@ -1,327 +0,0 @@
1
- import { Blac } from '../Blac';
2
- import { BlocBase, BlocLifecycleState } from '../BlocBase';
3
- import { BlocConstructor, BlocState, InferPropsFromGeneric } from '../types';
4
- import { PropsUpdated } from '../events';
5
- import { generateUUID } from '../utils/uuid';
6
- import { shallowEqual } from '../utils/shallowEqual';
7
- import { ConsumerTracker, DependencyArray } from './ConsumerTracker';
8
- import { ProxyFactory } from './ProxyFactory';
9
-
10
- export interface AdapterOptions<B extends BlocBase<any>> {
11
- id?: string;
12
- dependencies?: (bloc: B) => unknown[];
13
- props?: any;
14
- onMount?: (bloc: B) => void;
15
- onUnmount?: (bloc: B) => void;
16
- }
17
-
18
- /**
19
- * BlacAdapter orchestrates the connection between Bloc instances and React components.
20
- * It manages dependency tracking, lifecycle hooks, and proxy creation.
21
- */
22
- export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
23
- public readonly id = `consumer-${generateUUID()}`;
24
- public readonly blocConstructor: B;
25
- 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;
34
-
35
- unmountTime: number = 0;
36
- mountTime: number = 0;
37
-
38
- // Dependency tracking
39
- private dependencyValues?: unknown[];
40
- private isUsingDependencies: boolean = false;
41
-
42
- // Lifecycle state
43
- private hasMounted = false;
44
- private mountCount = 0;
45
-
46
- options?: AdapterOptions<InstanceType<B>>;
47
-
48
- constructor(
49
- instanceProps: { componentRef: { current: object }; blocConstructor: B },
50
- options?: typeof this.options,
51
- ) {
52
- this.options = options;
53
- this.blocConstructor = instanceProps.blocConstructor;
54
- this.componentRef = instanceProps.componentRef;
55
- this.isUsingDependencies = !!options?.dependencies;
56
-
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);
63
-
64
- // Initialize dependency values if using dependencies
65
- if (this.isUsingDependencies && options?.dependencies) {
66
- this.dependencyValues = options.dependencies(this.blocInstance);
67
- }
68
-
69
- // Handle initial props if provided
70
- if (options?.props !== undefined) {
71
- this.updateProps(options.props);
72
- }
73
- }
74
-
75
- trackAccess(
76
- consumerRef: object,
77
- type: 'state' | 'class',
78
- path: string,
79
- value?: any,
80
- ): void {
81
- this.consumerTracker.trackAccess(consumerRef, type, path, value);
82
- }
83
-
84
- getConsumerDependencies(consumerRef: object): DependencyArray | null {
85
- return this.consumerTracker.getDependencies(consumerRef);
86
- }
87
-
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
95
- }
96
-
97
- // First render - always notify to establish baseline
98
- if (!consumerInfo.hasRendered) {
99
- return true;
100
- }
101
-
102
- // Use built-in method from ConsumerTracker
103
- return this.consumerTracker.shouldNotifyConsumer(consumerRef, changedPaths);
104
- }
105
-
106
- updateLastNotified(consumerRef: object): void {
107
- this.consumerTracker.updateLastNotified(consumerRef);
108
- this.consumerTracker.setHasRendered(consumerRef, true);
109
- }
110
-
111
- resetConsumerTracking(): void {
112
- this.consumerTracker.resetTracking(this.componentRef.current);
113
- }
114
-
115
- createStateProxy = <T extends object>(props: { target: T }): T => {
116
- return ProxyFactory.createStateProxy({
117
- target: props.target,
118
- consumerRef: this.componentRef.current,
119
- consumerTracker: this as any,
120
- });
121
- };
122
-
123
- createClassProxy = <T extends object>(props: { target: T }): T => {
124
- return ProxyFactory.createClassProxy({
125
- target: props.target,
126
- consumerRef: this.componentRef.current,
127
- consumerTracker: this as any,
128
- });
129
- };
130
-
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
- });
190
-
191
- return unsubscribe;
192
- };
193
-
194
- mount = (): void => {
195
- // Re-run dependencies on mount to ensure fresh values
196
- if (this.isUsingDependencies && this.options?.dependencies) {
197
- this.dependencyValues = this.options.dependencies(this.blocInstance);
198
- }
199
-
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
- }
217
- };
218
-
219
- unmount = (): void => {
220
- this.unmountTime = Date.now();
221
- this.consumerTracker.unregister(this.componentRef.current);
222
- this.blocInstance._removeConsumer(this.id);
223
-
224
- // Clear ownership if we own this bloc
225
- if (BlacAdapter.propsOwners.get(this.blocInstance) === this.id) {
226
- BlacAdapter.propsOwners.delete(this.blocInstance);
227
- }
228
-
229
- // Call onUnmount callback
230
- if (this.options?.onUnmount) {
231
- try {
232
- this.options.onUnmount(this.blocInstance);
233
- } catch (error) {
234
- // Don't re-throw on unmount to allow cleanup to continue
235
- }
236
- }
237
- };
238
-
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;
245
- }
246
-
247
- return ProxyFactory.getProxyState({
248
- state,
249
- consumerRef: this.componentRef.current,
250
- consumerTracker: this as any,
251
- });
252
- };
253
-
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;
258
- }
259
-
260
- return ProxyFactory.getProxyBlocInstance({
261
- blocInstance: this.blocInstance,
262
- consumerRef: this.componentRef.current,
263
- consumerTracker: this as any,
264
- });
265
- };
266
-
267
- // Expose calledOnMount for backward compatibility
268
- get calledOnMount(): boolean {
269
- return this.hasMounted;
270
- }
271
-
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;
286
- }
287
-
288
- // Disposal safety
289
- if ((bloc as any)._disposalState === BlocLifecycleState.DISPOSED ||
290
- (bloc as any)._disposalState === BlocLifecycleState.DISPOSING) {
291
- return;
292
- }
293
-
294
- // Check if props have changed
295
- if (shallowEqual(this.lastProps, props)) {
296
- return;
297
- }
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);
306
- }
307
-
308
- this.lastProps = props;
309
- }
310
-
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;
317
-
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;
322
- }
323
- }
324
-
325
- return false;
326
- }
327
- }