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

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/dist/index.cjs.js +2 -2
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.d.ts +522 -297
  4. package/dist/index.es.js +1833 -1550
  5. package/dist/index.es.js.map +1 -1
  6. package/package.json +6 -7
  7. package/src/Blac.ts +38 -71
  8. package/src/Bloc.ts +8 -9
  9. package/src/BlocBase.ts +246 -540
  10. package/src/Cubit.ts +1 -16
  11. package/src/__tests__/Blac.config.test.ts +10 -17
  12. package/src/__tests__/Bloc.event.test.ts +27 -52
  13. package/src/__tests__/BlocBase.subscription.test.ts +349 -0
  14. package/src/__tests__/Cubit.test.ts +39 -50
  15. package/src/__tests__/memory-leaks.test.ts +383 -0
  16. package/src/__tests__/plugins.test.ts +107 -97
  17. package/src/adapter/BlacAdapter.ts +347 -207
  18. package/src/adapter/ProxyFactory.ts +196 -255
  19. package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
  20. package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
  21. package/src/adapter/index.ts +0 -1
  22. package/src/errors/BlacError.ts +69 -0
  23. package/src/errors/ErrorManager.ts +101 -0
  24. package/src/errors/handleError.ts +114 -0
  25. package/src/errors/index.ts +13 -0
  26. package/src/events.ts +7 -1
  27. package/src/index.ts +7 -1
  28. package/src/lifecycle/BlocLifecycle.ts +156 -0
  29. package/src/lifecycle/index.ts +1 -0
  30. package/src/plugins/BlocPluginRegistry.ts +71 -51
  31. package/src/plugins/SystemPluginRegistry.ts +63 -13
  32. package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
  33. package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
  34. package/src/plugins/types.ts +32 -3
  35. package/src/subscription/README.md +132 -0
  36. package/src/subscription/SubscriptionManager.ts +395 -0
  37. package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
  38. package/src/subscription/index.ts +2 -0
  39. package/src/subscription/types.ts +90 -0
  40. package/src/testing.ts +31 -37
  41. package/src/types.ts +4 -21
  42. package/src/utils/BatchingManager.ts +59 -0
  43. package/src/utils/RerenderLogger.ts +265 -0
  44. package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
  45. package/src/utils/__tests__/shallowEqual.test.ts +171 -0
  46. package/src/utils/__tests__/uuid.test.ts +144 -0
  47. package/src/utils/generateInstanceId.ts +54 -0
  48. package/src/utils/shallowEqual.ts +1 -1
  49. package/README.md +0 -329
  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
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, P = null> extends BlocBase<S, P> {
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
- Blac.setConfig({ exposeBlacInstance: true });
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._observer.subscribe({ id: 'test', fn: observer });
107
+ bloc.subscribe(observer);
108
108
 
109
109
  await bloc.add(new IncrementEvent(5));
110
110
 
111
- expect(observer).toHaveBeenCalledWith(5, 0, expect.any(IncrementEvent));
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._observer.subscribe({ id: 'test', fn: observer });
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
- 1,
134
- 1,
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._observer.subscribe({ id: 'test', fn: observer });
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
- 1,
170
- 1,
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._observer.subscribe({ id: 'test', fn: observer });
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._observer.subscribe({ id: 'test', fn: observer });
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._observer.subscribe({ id: 'test', fn: observer });
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, 0, event);
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._observer.subscribe({ id: 'test', fn: observer });
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._observer.subscribe({ id: 'test', fn: chainObserver });
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._observer.subscribe({ id: 'test', fn: observer });
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 maintain event processing integrity during disposal', async () => {
338
+ it('should prevent state updates after disposal is initiated', async () => {
369
339
  const observer = vi.fn();
370
- bloc._observer.subscribe({ id: 'test', fn: observer });
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._dispose(), 25);
347
+ setTimeout(() => bloc.dispose(), 25);
377
348
 
378
349
  await promise;
379
350
 
380
- // Event should complete processing despite disposal attempt
381
- expect(bloc.state).toBe(5);
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
+ });