@blac/core 2.0.0-rc-8 → 2.0.0-rc-11
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/README.md +27 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +224 -14
- package/dist/index.es.js +1122 -556
- package/dist/index.es.js.map +1 -1
- package/package.json +5 -4
- package/src/Blac.ts +109 -47
- package/src/BlacObserver.ts +58 -24
- package/src/Bloc.ts +120 -160
- package/src/BlocBase.ts +266 -90
- package/src/Cubit.ts +15 -1
- package/src/__tests__/Blac.config.test.ts +86 -0
- package/src/__tests__/Bloc.event.test.ts +419 -0
- package/src/__tests__/BlocBase.lifecycle.test.ts +441 -0
- package/src/__tests__/Cubit.test.ts +473 -0
- package/src/__tests__/props.test.ts +256 -0
- package/src/adapter/BlacAdapter.ts +327 -0
- package/src/adapter/ConsumerTracker.ts +241 -0
- package/src/adapter/ProxyFactory.ts +308 -0
- package/src/adapter/__tests__/BlacAdapter.test.ts +561 -0
- package/src/adapter/__tests__/ConsumerTracker.test.ts +562 -0
- package/src/adapter/__tests__/ProxyFactory.test.ts +655 -0
- package/src/adapter/index.ts +3 -0
- package/src/events.ts +3 -0
- package/src/index.ts +4 -1
- package/src/testing.ts +60 -47
- package/src/types.ts +12 -13
- package/src/utils/shallowEqual.ts +40 -0
- package/src/utils/uuid.ts +4 -4
- package/src/BlacEvent.ts +0 -5
|
@@ -0,0 +1,256 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,327 @@
|
|
|
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
|
+
}
|