@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.
- package/package.json +10 -15
- 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/dist/index.cjs.js +0 -11
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.d.ts +0 -1078
- package/dist/index.es.js +0 -1992
- package/dist/index.es.js.map +0 -1
- 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
package/src/Cubit.ts
CHANGED
|
@@ -5,23 +5,8 @@ import { BlocBase } from './BlocBase';
|
|
|
5
5
|
* A Cubit is a simpler version of a Bloc that doesn't handle events.
|
|
6
6
|
* It manages state and provides methods to update it.
|
|
7
7
|
* @template S - The type of state this Cubit manages
|
|
8
|
-
* @template P - The type of parameters (optional, defaults to null)
|
|
9
8
|
*/
|
|
10
|
-
export abstract class Cubit<S
|
|
11
|
-
/**
|
|
12
|
-
* @internal
|
|
13
|
-
* Protected method for useBloc to call
|
|
14
|
-
*/
|
|
15
|
-
protected _updateProps(props: P): void {
|
|
16
|
-
const oldProps = this.props;
|
|
17
|
-
this.props = props;
|
|
18
|
-
this.onPropsChanged?.(oldProps as P | undefined, props);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Optional override for props handling
|
|
23
|
-
*/
|
|
24
|
-
protected onPropsChanged?(oldProps: P | undefined, newProps: P): void;
|
|
9
|
+
export abstract class Cubit<S> extends BlocBase<S> {
|
|
25
10
|
/**
|
|
26
11
|
* Updates the current state and notifies all observers of the change.
|
|
27
12
|
* If the new state is identical to the current state (using Object.is),
|
|
@@ -7,7 +7,6 @@ describe('Blac.config', () => {
|
|
|
7
7
|
beforeEach(() => {
|
|
8
8
|
// Reset to default config before each test
|
|
9
9
|
Blac.setConfig({
|
|
10
|
-
exposeBlacInstance: false,
|
|
11
10
|
proxyDependencyTracking: true,
|
|
12
11
|
});
|
|
13
12
|
});
|
|
@@ -20,26 +19,26 @@ describe('Blac.config', () => {
|
|
|
20
19
|
describe('setConfig', () => {
|
|
21
20
|
it('should have default configuration', () => {
|
|
22
21
|
expect(Blac.config).toEqual({
|
|
23
|
-
exposeBlacInstance: false,
|
|
24
22
|
proxyDependencyTracking: true,
|
|
25
23
|
});
|
|
26
24
|
});
|
|
27
25
|
|
|
28
26
|
it('should update configuration with partial config', () => {
|
|
29
27
|
Blac.setConfig({ proxyDependencyTracking: false });
|
|
30
|
-
|
|
28
|
+
|
|
31
29
|
expect(Blac.config).toEqual({
|
|
32
|
-
exposeBlacInstance: false,
|
|
33
30
|
proxyDependencyTracking: false,
|
|
34
31
|
});
|
|
35
32
|
});
|
|
36
33
|
|
|
37
34
|
it('should merge configuration properly', () => {
|
|
38
|
-
|
|
35
|
+
// First set to false
|
|
39
36
|
Blac.setConfig({ proxyDependencyTracking: false });
|
|
40
|
-
|
|
37
|
+
|
|
38
|
+
// Then set empty config - should keep false
|
|
39
|
+
Blac.setConfig({});
|
|
40
|
+
|
|
41
41
|
expect(Blac.config).toEqual({
|
|
42
|
-
exposeBlacInstance: true,
|
|
43
42
|
proxyDependencyTracking: false,
|
|
44
43
|
});
|
|
45
44
|
});
|
|
@@ -50,16 +49,10 @@ describe('Blac.config', () => {
|
|
|
50
49
|
}).toThrow('BlacConfig.proxyDependencyTracking must be a boolean');
|
|
51
50
|
});
|
|
52
51
|
|
|
53
|
-
it('should throw error for invalid exposeBlacInstance type', () => {
|
|
54
|
-
expect(() => {
|
|
55
|
-
Blac.setConfig({ exposeBlacInstance: 1 as any });
|
|
56
|
-
}).toThrow('BlacConfig.exposeBlacInstance must be a boolean');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
52
|
it('should return a copy of config, not the original', () => {
|
|
60
53
|
const config1 = Blac.config;
|
|
61
54
|
const config2 = Blac.config;
|
|
62
|
-
|
|
55
|
+
|
|
63
56
|
expect(config1).not.toBe(config2);
|
|
64
57
|
expect(config1).toEqual(config2);
|
|
65
58
|
});
|
|
@@ -67,7 +60,7 @@ describe('Blac.config', () => {
|
|
|
67
60
|
it('should not allow direct modification of config', () => {
|
|
68
61
|
const config = Blac.config as any;
|
|
69
62
|
config.proxyDependencyTracking = false;
|
|
70
|
-
|
|
63
|
+
|
|
71
64
|
// Original config should remain unchanged
|
|
72
65
|
expect(Blac.config.proxyDependencyTracking).toBe(true);
|
|
73
66
|
});
|
|
@@ -78,9 +71,9 @@ describe('Blac.config', () => {
|
|
|
78
71
|
const testConfig: Partial<BlacConfig> = {
|
|
79
72
|
proxyDependencyTracking: false,
|
|
80
73
|
};
|
|
81
|
-
|
|
74
|
+
|
|
82
75
|
Blac.setConfig(testConfig);
|
|
83
76
|
expect(Blac.config.proxyDependencyTracking).toBe(false);
|
|
84
77
|
});
|
|
85
78
|
});
|
|
86
|
-
});
|
|
79
|
+
});
|
|
@@ -104,11 +104,11 @@ describe('Bloc Event Handling', () => {
|
|
|
104
104
|
|
|
105
105
|
it('should handle events with proper type inference', async () => {
|
|
106
106
|
const observer = vi.fn();
|
|
107
|
-
bloc.
|
|
107
|
+
bloc.subscribe(observer);
|
|
108
108
|
|
|
109
109
|
await bloc.add(new IncrementEvent(5));
|
|
110
110
|
|
|
111
|
-
expect(observer).toHaveBeenCalledWith(5
|
|
111
|
+
expect(observer).toHaveBeenCalledWith(5);
|
|
112
112
|
expect(bloc.state).toBe(5);
|
|
113
113
|
});
|
|
114
114
|
});
|
|
@@ -116,7 +116,7 @@ describe('Bloc Event Handling', () => {
|
|
|
116
116
|
describe('Event Queue and Sequential Processing', () => {
|
|
117
117
|
it('should queue events and process them sequentially', async () => {
|
|
118
118
|
const observer = vi.fn();
|
|
119
|
-
bloc.
|
|
119
|
+
bloc.subscribe(observer);
|
|
120
120
|
|
|
121
121
|
// Add multiple events rapidly
|
|
122
122
|
const promises = [
|
|
@@ -129,30 +129,15 @@ describe('Bloc Event Handling', () => {
|
|
|
129
129
|
|
|
130
130
|
// Should be called 3 times with sequential state updates
|
|
131
131
|
expect(observer).toHaveBeenCalledTimes(3);
|
|
132
|
-
expect(observer).toHaveBeenNthCalledWith(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
0,
|
|
136
|
-
expect.any(IncrementEvent),
|
|
137
|
-
);
|
|
138
|
-
expect(observer).toHaveBeenNthCalledWith(
|
|
139
|
-
2,
|
|
140
|
-
3,
|
|
141
|
-
1,
|
|
142
|
-
expect.any(IncrementEvent),
|
|
143
|
-
);
|
|
144
|
-
expect(observer).toHaveBeenNthCalledWith(
|
|
145
|
-
3,
|
|
146
|
-
6,
|
|
147
|
-
3,
|
|
148
|
-
expect.any(IncrementEvent),
|
|
149
|
-
);
|
|
132
|
+
expect(observer).toHaveBeenNthCalledWith(1, 1);
|
|
133
|
+
expect(observer).toHaveBeenNthCalledWith(2, 3);
|
|
134
|
+
expect(observer).toHaveBeenNthCalledWith(3, 6);
|
|
150
135
|
expect(bloc.state).toBe(6);
|
|
151
136
|
});
|
|
152
137
|
|
|
153
138
|
it('should process async events in order', async () => {
|
|
154
139
|
const observer = vi.fn();
|
|
155
|
-
bloc.
|
|
140
|
+
bloc.subscribe(observer);
|
|
156
141
|
|
|
157
142
|
// Add async events with different delays
|
|
158
143
|
const promises = [
|
|
@@ -165,29 +150,14 @@ describe('Bloc Event Handling', () => {
|
|
|
165
150
|
|
|
166
151
|
// Despite different delays, events should process in order
|
|
167
152
|
expect(observer).toHaveBeenCalledTimes(3);
|
|
168
|
-
expect(observer).toHaveBeenNthCalledWith(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
0,
|
|
172
|
-
expect.any(AsyncIncrementEvent),
|
|
173
|
-
);
|
|
174
|
-
expect(observer).toHaveBeenNthCalledWith(
|
|
175
|
-
2,
|
|
176
|
-
3,
|
|
177
|
-
1,
|
|
178
|
-
expect.any(AsyncIncrementEvent),
|
|
179
|
-
);
|
|
180
|
-
expect(observer).toHaveBeenNthCalledWith(
|
|
181
|
-
3,
|
|
182
|
-
6,
|
|
183
|
-
3,
|
|
184
|
-
expect.any(AsyncIncrementEvent),
|
|
185
|
-
);
|
|
153
|
+
expect(observer).toHaveBeenNthCalledWith(1, 1);
|
|
154
|
+
expect(observer).toHaveBeenNthCalledWith(2, 3);
|
|
155
|
+
expect(observer).toHaveBeenNthCalledWith(3, 6);
|
|
186
156
|
});
|
|
187
157
|
|
|
188
158
|
it('should handle mixed sync and async events', async () => {
|
|
189
159
|
const observer = vi.fn();
|
|
190
|
-
bloc.
|
|
160
|
+
bloc.subscribe(observer);
|
|
191
161
|
|
|
192
162
|
const promises = [
|
|
193
163
|
bloc.add(new IncrementEvent(1)),
|
|
@@ -207,7 +177,7 @@ describe('Bloc Event Handling', () => {
|
|
|
207
177
|
it('should handle errors in event handlers gracefully', async () => {
|
|
208
178
|
const errorSpy = vi.spyOn(Blac, 'error').mockImplementation(() => {});
|
|
209
179
|
const observer = vi.fn();
|
|
210
|
-
bloc.
|
|
180
|
+
bloc.subscribe(observer);
|
|
211
181
|
|
|
212
182
|
await bloc.add(new ErrorEvent('Test error'));
|
|
213
183
|
|
|
@@ -250,12 +220,12 @@ describe('Bloc Event Handling', () => {
|
|
|
250
220
|
describe('Event Context and Metadata', () => {
|
|
251
221
|
it('should pass event instance to state change notifications', async () => {
|
|
252
222
|
const observer = vi.fn();
|
|
253
|
-
bloc.
|
|
223
|
+
bloc.subscribe(observer);
|
|
254
224
|
|
|
255
225
|
const event = new IncrementEvent(10);
|
|
256
226
|
await bloc.add(event);
|
|
257
227
|
|
|
258
|
-
expect(observer).toHaveBeenCalledWith(10
|
|
228
|
+
expect(observer).toHaveBeenCalledWith(10);
|
|
259
229
|
});
|
|
260
230
|
|
|
261
231
|
it('should maintain correct state context during handler execution', async () => {
|
|
@@ -293,7 +263,7 @@ describe('Bloc Event Handling', () => {
|
|
|
293
263
|
|
|
294
264
|
it('should handle rapid event additions during processing', async () => {
|
|
295
265
|
const observer = vi.fn();
|
|
296
|
-
bloc.
|
|
266
|
+
bloc.subscribe(observer);
|
|
297
267
|
|
|
298
268
|
// Custom bloc that adds more events during processing
|
|
299
269
|
class ChainReactionBloc extends Bloc<number> {
|
|
@@ -313,7 +283,7 @@ describe('Bloc Event Handling', () => {
|
|
|
313
283
|
|
|
314
284
|
const chainBloc = new ChainReactionBloc();
|
|
315
285
|
const chainObserver = vi.fn();
|
|
316
|
-
chainBloc.
|
|
286
|
+
chainBloc.subscribe(chainObserver);
|
|
317
287
|
|
|
318
288
|
await chainBloc.add(new IncrementEvent(1));
|
|
319
289
|
|
|
@@ -338,7 +308,7 @@ describe('Bloc Event Handling', () => {
|
|
|
338
308
|
|
|
339
309
|
const multiBloc = new MultiEmitBloc();
|
|
340
310
|
const observer = vi.fn();
|
|
341
|
-
multiBloc.
|
|
311
|
+
multiBloc.subscribe(observer);
|
|
342
312
|
|
|
343
313
|
await multiBloc.add(new IncrementEvent(3));
|
|
344
314
|
|
|
@@ -365,20 +335,25 @@ describe('Bloc Event Handling', () => {
|
|
|
365
335
|
expect(simpleBloc.state).toBe('handled');
|
|
366
336
|
});
|
|
367
337
|
|
|
368
|
-
it('should
|
|
338
|
+
it('should prevent state updates after disposal is initiated', async () => {
|
|
369
339
|
const observer = vi.fn();
|
|
370
|
-
bloc.
|
|
340
|
+
bloc.subscribe(observer);
|
|
341
|
+
const errorSpy = vi.spyOn(Blac, 'error').mockImplementation(() => {});
|
|
371
342
|
|
|
372
343
|
// Start async event processing
|
|
373
344
|
const promise = bloc.add(new AsyncIncrementEvent(5, 50));
|
|
374
345
|
|
|
375
346
|
// Dispose bloc while event is processing
|
|
376
|
-
setTimeout(() => bloc.
|
|
347
|
+
setTimeout(() => bloc.dispose(), 25);
|
|
377
348
|
|
|
378
349
|
await promise;
|
|
379
350
|
|
|
380
|
-
//
|
|
381
|
-
expect(bloc.state).toBe(
|
|
351
|
+
// State should not be updated after disposal
|
|
352
|
+
expect(bloc.state).toBe(0);
|
|
353
|
+
// Error should be logged about attempted state update on disposed bloc
|
|
354
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
355
|
+
expect.stringContaining('Attempted state update on'),
|
|
356
|
+
);
|
|
382
357
|
});
|
|
383
358
|
});
|
|
384
359
|
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { BlocBase } from '../BlocBase';
|
|
3
|
+
import { Blac } from '../Blac';
|
|
4
|
+
|
|
5
|
+
// Test implementation of BlocBase
|
|
6
|
+
class TestBloc extends BlocBase<{ count: number; message: string }> {
|
|
7
|
+
constructor(initialState = { count: 0, message: 'initial' }) {
|
|
8
|
+
super(initialState);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
increment() {
|
|
12
|
+
this.emit({ ...this.state, count: this.state.count + 1 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
updateMessage(message: string) {
|
|
16
|
+
this.emit({ ...this.state, message });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
updateBoth(count: number, message: string) {
|
|
20
|
+
this.emit({ count, message });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Test bloc with static properties
|
|
25
|
+
class KeepAliveBloc extends BlocBase<string> {
|
|
26
|
+
static keepAlive = true;
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
super('initial');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
update(value: string) {
|
|
33
|
+
this.emit(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('BlocBase Subscription Model', () => {
|
|
38
|
+
let blac: Blac;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
blac = new Blac({ __unsafe_ignore_singleton: true });
|
|
42
|
+
vi.useFakeTimers();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
vi.useRealTimers();
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('Basic Subscriptions', () => {
|
|
51
|
+
it('should subscribe to all state changes', () => {
|
|
52
|
+
const bloc = new TestBloc();
|
|
53
|
+
const callback = vi.fn();
|
|
54
|
+
|
|
55
|
+
const unsubscribe = bloc.subscribe(callback);
|
|
56
|
+
|
|
57
|
+
bloc.increment();
|
|
58
|
+
expect(callback).toHaveBeenCalledWith({ count: 1, message: 'initial' });
|
|
59
|
+
|
|
60
|
+
bloc.updateMessage('updated');
|
|
61
|
+
expect(callback).toHaveBeenCalledWith({ count: 1, message: 'updated' });
|
|
62
|
+
|
|
63
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
64
|
+
|
|
65
|
+
unsubscribe();
|
|
66
|
+
bloc.increment();
|
|
67
|
+
expect(callback).toHaveBeenCalledTimes(2); // No new calls
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should track subscription count', () => {
|
|
71
|
+
const bloc = new TestBloc();
|
|
72
|
+
|
|
73
|
+
expect(bloc.subscriptionCount).toBe(0);
|
|
74
|
+
|
|
75
|
+
const unsub1 = bloc.subscribe(() => {});
|
|
76
|
+
expect(bloc.subscriptionCount).toBe(1);
|
|
77
|
+
|
|
78
|
+
const unsub2 = bloc.subscribe(() => {});
|
|
79
|
+
expect(bloc.subscriptionCount).toBe(2);
|
|
80
|
+
|
|
81
|
+
unsub1();
|
|
82
|
+
expect(bloc.subscriptionCount).toBe(1);
|
|
83
|
+
|
|
84
|
+
unsub2();
|
|
85
|
+
expect(bloc.subscriptionCount).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('Selector-based Subscriptions', () => {
|
|
90
|
+
it('should only notify when selected value changes', () => {
|
|
91
|
+
const bloc = new TestBloc();
|
|
92
|
+
const callback = vi.fn();
|
|
93
|
+
|
|
94
|
+
const unsubscribe = bloc.subscribeWithSelector(
|
|
95
|
+
(state) => state.count,
|
|
96
|
+
callback,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Should not be called on subscription
|
|
100
|
+
expect(callback).not.toHaveBeenCalled();
|
|
101
|
+
|
|
102
|
+
bloc.increment();
|
|
103
|
+
expect(callback).toHaveBeenCalledWith(1);
|
|
104
|
+
|
|
105
|
+
// Message update should not trigger callback
|
|
106
|
+
bloc.updateMessage('updated');
|
|
107
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
108
|
+
|
|
109
|
+
bloc.increment();
|
|
110
|
+
expect(callback).toHaveBeenCalledWith(2);
|
|
111
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
112
|
+
|
|
113
|
+
unsubscribe();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should support custom equality function', () => {
|
|
117
|
+
const bloc = new TestBloc();
|
|
118
|
+
const callback = vi.fn();
|
|
119
|
+
|
|
120
|
+
// Only notify when count changes by more than 2
|
|
121
|
+
const unsubscribe = bloc.subscribeWithSelector(
|
|
122
|
+
(state) => state.count,
|
|
123
|
+
callback,
|
|
124
|
+
(a, b) => Math.abs(a - b) <= 2,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
bloc.increment(); // 0 -> 1 (diff = 1, not notified)
|
|
128
|
+
expect(callback).not.toHaveBeenCalled();
|
|
129
|
+
|
|
130
|
+
bloc.increment(); // 1 -> 2 (diff = 1, not notified)
|
|
131
|
+
expect(callback).not.toHaveBeenCalled();
|
|
132
|
+
|
|
133
|
+
bloc.updateBoth(5, 'test'); // 2 -> 5 (diff = 3, notified)
|
|
134
|
+
expect(callback).toHaveBeenCalledWith(5);
|
|
135
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
136
|
+
|
|
137
|
+
unsubscribe();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should handle complex selectors', () => {
|
|
141
|
+
const bloc = new TestBloc();
|
|
142
|
+
const callback = vi.fn();
|
|
143
|
+
|
|
144
|
+
const unsubscribe = bloc.subscribeWithSelector(
|
|
145
|
+
(state) => ({
|
|
146
|
+
doubled: state.count * 2,
|
|
147
|
+
upper: state.message.toUpperCase(),
|
|
148
|
+
}),
|
|
149
|
+
callback,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
bloc.increment();
|
|
153
|
+
expect(callback).toHaveBeenCalledWith({ doubled: 2, upper: 'INITIAL' });
|
|
154
|
+
|
|
155
|
+
bloc.updateMessage('hello');
|
|
156
|
+
expect(callback).toHaveBeenCalledWith({ doubled: 2, upper: 'HELLO' });
|
|
157
|
+
|
|
158
|
+
unsubscribe();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('Component Subscriptions', () => {
|
|
163
|
+
it('should support weak reference subscriptions', () => {
|
|
164
|
+
const bloc = new TestBloc();
|
|
165
|
+
const callback = vi.fn();
|
|
166
|
+
let component: any = { id: 'test-component' };
|
|
167
|
+
const weakRef = new WeakRef(component);
|
|
168
|
+
|
|
169
|
+
const unsubscribe = bloc.subscribeComponent(weakRef, callback);
|
|
170
|
+
|
|
171
|
+
bloc.increment();
|
|
172
|
+
expect(callback).toHaveBeenCalled();
|
|
173
|
+
|
|
174
|
+
// Clear strong reference
|
|
175
|
+
component = null;
|
|
176
|
+
|
|
177
|
+
// Force garbage collection (platform-specific)
|
|
178
|
+
if ((global as any).gc) {
|
|
179
|
+
(global as any).gc();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
unsubscribe();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('Lifecycle and Disposal', () => {
|
|
187
|
+
it('should dispose when no subscriptions remain', async () => {
|
|
188
|
+
const bloc = new TestBloc();
|
|
189
|
+
const disposeSpy = vi.spyOn(bloc, 'dispose');
|
|
190
|
+
|
|
191
|
+
const unsub1 = bloc.subscribe(() => {});
|
|
192
|
+
const unsub2 = bloc.subscribe(() => {});
|
|
193
|
+
|
|
194
|
+
unsub1();
|
|
195
|
+
expect(disposeSpy).not.toHaveBeenCalled();
|
|
196
|
+
|
|
197
|
+
unsub2();
|
|
198
|
+
|
|
199
|
+
// Disposal is scheduled with timeout
|
|
200
|
+
await vi.runAllTimersAsync();
|
|
201
|
+
expect(disposeSpy).toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should not dispose keep-alive blocs', async () => {
|
|
205
|
+
const bloc = new KeepAliveBloc();
|
|
206
|
+
const disposeSpy = vi.spyOn(bloc, 'dispose');
|
|
207
|
+
|
|
208
|
+
const unsubscribe = bloc.subscribe(() => {});
|
|
209
|
+
unsubscribe();
|
|
210
|
+
|
|
211
|
+
await vi.runAllTimersAsync();
|
|
212
|
+
expect(disposeSpy).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should cancel disposal if new subscription added', async () => {
|
|
216
|
+
const bloc = new TestBloc();
|
|
217
|
+
const disposeSpy = vi.spyOn(bloc, 'dispose');
|
|
218
|
+
|
|
219
|
+
const unsub1 = bloc.subscribe(() => {});
|
|
220
|
+
unsub1();
|
|
221
|
+
|
|
222
|
+
// Disposal scheduled
|
|
223
|
+
expect(bloc.subscriptionCount).toBe(0);
|
|
224
|
+
|
|
225
|
+
// Add new subscription before disposal
|
|
226
|
+
const unsub2 = bloc.subscribe(() => {});
|
|
227
|
+
|
|
228
|
+
await vi.runAllTimersAsync();
|
|
229
|
+
expect(disposeSpy).not.toHaveBeenCalled();
|
|
230
|
+
|
|
231
|
+
unsub2();
|
|
232
|
+
await vi.runAllTimersAsync();
|
|
233
|
+
expect(disposeSpy).toHaveBeenCalled();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should not allow subscriptions to disposed bloc', async () => {
|
|
237
|
+
const bloc = new TestBloc();
|
|
238
|
+
await bloc.dispose();
|
|
239
|
+
|
|
240
|
+
const callback = vi.fn();
|
|
241
|
+
const unsubscribe = bloc.subscribe(callback);
|
|
242
|
+
|
|
243
|
+
bloc.increment();
|
|
244
|
+
expect(callback).not.toHaveBeenCalled();
|
|
245
|
+
|
|
246
|
+
unsubscribe();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('State Updates and Batching', () => {
|
|
251
|
+
it('should batch multiple state updates', () => {
|
|
252
|
+
const bloc = new TestBloc();
|
|
253
|
+
const callback = vi.fn();
|
|
254
|
+
|
|
255
|
+
bloc.subscribe(callback);
|
|
256
|
+
|
|
257
|
+
bloc._batchUpdates(() => {
|
|
258
|
+
bloc.increment();
|
|
259
|
+
bloc.increment();
|
|
260
|
+
bloc.updateMessage('batched');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Only one notification for the final state
|
|
264
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
265
|
+
expect(callback).toHaveBeenCalledWith({ count: 2, message: 'batched' });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should not emit undefined states', () => {
|
|
269
|
+
const bloc = new TestBloc();
|
|
270
|
+
const callback = vi.fn();
|
|
271
|
+
|
|
272
|
+
bloc.subscribe(callback);
|
|
273
|
+
|
|
274
|
+
// Try to emit undefined
|
|
275
|
+
(bloc as any).emit(undefined);
|
|
276
|
+
|
|
277
|
+
expect(callback).not.toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('Priority-based Notifications', () => {
|
|
282
|
+
it('should notify subscriptions in priority order', () => {
|
|
283
|
+
const bloc = new TestBloc();
|
|
284
|
+
const order: number[] = [];
|
|
285
|
+
|
|
286
|
+
// Direct access to subscription manager for priority testing
|
|
287
|
+
const manager = bloc._subscriptionManager;
|
|
288
|
+
|
|
289
|
+
manager.subscribe({
|
|
290
|
+
type: 'observer',
|
|
291
|
+
priority: 1,
|
|
292
|
+
notify: () => order.push(1),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
manager.subscribe({
|
|
296
|
+
type: 'observer',
|
|
297
|
+
priority: 10,
|
|
298
|
+
notify: () => order.push(10),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
manager.subscribe({
|
|
302
|
+
type: 'observer',
|
|
303
|
+
priority: 5,
|
|
304
|
+
notify: () => order.push(5),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
bloc.increment();
|
|
308
|
+
|
|
309
|
+
expect(order).toEqual([10, 5, 1]); // Highest priority first
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('Error Handling', () => {
|
|
314
|
+
it('should handle errors in subscription callbacks', () => {
|
|
315
|
+
const bloc = new TestBloc();
|
|
316
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
317
|
+
|
|
318
|
+
const goodCallback = vi.fn();
|
|
319
|
+
const badCallback = vi.fn(() => {
|
|
320
|
+
throw new Error('Subscription error');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
bloc.subscribe(badCallback);
|
|
324
|
+
bloc.subscribe(goodCallback);
|
|
325
|
+
|
|
326
|
+
bloc.increment();
|
|
327
|
+
|
|
328
|
+
expect(goodCallback).toHaveBeenCalled();
|
|
329
|
+
expect(badCallback).toHaveBeenCalled();
|
|
330
|
+
|
|
331
|
+
errorSpy.mockRestore();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should handle errors in selectors', () => {
|
|
335
|
+
const bloc = new TestBloc();
|
|
336
|
+
const callback = vi.fn();
|
|
337
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
338
|
+
|
|
339
|
+
bloc.subscribeWithSelector(() => {
|
|
340
|
+
throw new Error('Selector error');
|
|
341
|
+
}, callback);
|
|
342
|
+
|
|
343
|
+
bloc.increment();
|
|
344
|
+
expect(callback).not.toHaveBeenCalled();
|
|
345
|
+
|
|
346
|
+
errorSpy.mockRestore();
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|