@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,8 +1,8 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { Blac } from '../Blac';
3
3
  import { Cubit } from '../Cubit';
4
4
  import { Bloc } from '../Bloc';
5
- import { BlacPlugin, BlocPlugin, PluginCapabilities, ErrorContext } from '../plugins';
5
+ import { BlacPlugin, BlocPlugin, PluginCapabilities } from '../plugins';
6
6
  import { BlocBase } from '../BlocBase';
7
7
 
8
8
  // Test Cubit
@@ -10,7 +10,7 @@ class CounterCubit extends Cubit<number> {
10
10
  constructor() {
11
11
  super(0);
12
12
  }
13
-
13
+
14
14
  increment = () => this.emit(this.state + 1);
15
15
  decrement = () => this.emit(this.state - 1);
16
16
  setValue = (value: number) => this.emit(value);
@@ -29,7 +29,7 @@ class SetValue extends CounterEvent {
29
29
  class CounterBloc extends Bloc<number, CounterEvent> {
30
30
  constructor() {
31
31
  super(0);
32
-
32
+
33
33
  this.on(Increment, (event, emit) => emit(this.state + 1));
34
34
  this.on(Decrement, (event, emit) => emit(this.state - 1));
35
35
  this.on(SetValue, (event, emit) => emit(event.value));
@@ -41,31 +41,41 @@ class TestLoggingPlugin implements BlacPlugin {
41
41
  readonly name = 'logging';
42
42
  readonly version = '1.0.0';
43
43
  private logLevel: 'debug' | 'info' | 'warn' | 'error';
44
-
45
- constructor(options: { logLevel?: 'debug' | 'info' | 'warn' | 'error' } = {}) {
44
+
45
+ constructor(
46
+ options: { logLevel?: 'debug' | 'info' | 'warn' | 'error' } = {},
47
+ ) {
46
48
  this.logLevel = options.logLevel || 'info';
47
49
  }
48
-
50
+
49
51
  onBlocCreated(bloc: BlocBase<any>): void {
50
52
  this.log('debug', `Bloc created: ${bloc._name}:${bloc._id}`);
51
53
  }
52
-
53
- onStateChanged(bloc: BlocBase<any>, previousState: any, currentState: any): void {
54
+
55
+ onStateChanged(
56
+ bloc: BlocBase<any>,
57
+ previousState: any,
58
+ currentState: any,
59
+ ): void {
54
60
  this.log('debug', `State changed in ${bloc._name}:${bloc._id}`, {
55
61
  previousState,
56
- currentState
62
+ currentState,
57
63
  });
58
64
  }
59
-
60
- private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, data?: any): void {
65
+
66
+ private log(
67
+ level: 'debug' | 'info' | 'warn' | 'error',
68
+ message: string,
69
+ data?: any,
70
+ ): void {
61
71
  const levels = ['debug', 'info', 'warn', 'error'];
62
72
  const currentLevelIndex = levels.indexOf(this.logLevel);
63
73
  const messageLevelIndex = levels.indexOf(level);
64
-
74
+
65
75
  if (messageLevelIndex >= currentLevelIndex) {
66
76
  const timestamp = new Date().toISOString();
67
77
  const logMessage = `[BlaC] [${timestamp}] ${message}`;
68
-
78
+
69
79
  switch (level) {
70
80
  case 'debug':
71
81
  console.debug(logMessage, data);
@@ -98,14 +108,14 @@ class TestPersistencePlugin<TState> implements BlocPlugin<TState> {
98
108
  transformState: true,
99
109
  interceptEvents: false,
100
110
  persistData: true,
101
- accessMetadata: false
111
+ accessMetadata: false,
102
112
  };
103
-
113
+
104
114
  private storage: StorageAdapter;
105
115
  private key: string;
106
116
  private saveDebounceMs: number;
107
117
  private saveTimer?: any;
108
-
118
+
109
119
  constructor(options: {
110
120
  key: string;
111
121
  storage?: StorageAdapter;
@@ -115,20 +125,20 @@ class TestPersistencePlugin<TState> implements BlocPlugin<TState> {
115
125
  this.storage = options.storage || new MockStorage();
116
126
  this.saveDebounceMs = options.saveDebounceMs ?? 100;
117
127
  }
118
-
119
- onAttach(bloc: BlocBase<TState, any>): void {
128
+
129
+ onAttach(bloc: BlocBase<TState>): void {
120
130
  const savedData = this.storage.getItem(this.key);
121
131
  if (savedData) {
122
132
  const restoredState = JSON.parse(savedData);
123
133
  (bloc as any)._state = restoredState;
124
134
  }
125
135
  }
126
-
136
+
127
137
  onStateChange(previousState: TState, currentState: TState): void {
128
138
  if (this.saveTimer) {
129
139
  clearTimeout(this.saveTimer);
130
140
  }
131
-
141
+
132
142
  if (this.saveDebounceMs > 0) {
133
143
  this.saveTimer = setTimeout(() => {
134
144
  this.storage.setItem(this.key, JSON.stringify(currentState));
@@ -147,14 +157,14 @@ class TestValidationPlugin<T> implements BlocPlugin<T> {
147
157
  transformState: true,
148
158
  interceptEvents: false,
149
159
  persistData: false,
150
- accessMetadata: false
160
+ accessMetadata: false,
151
161
  };
152
-
162
+
153
163
  constructor(private validator: (state: T) => boolean | string) {}
154
-
164
+
155
165
  transformState(previousState: T, nextState: T): T {
156
166
  const result = this.validator(nextState);
157
-
167
+
158
168
  if (result === true) {
159
169
  return nextState;
160
170
  } else if (result === false) {
@@ -170,15 +180,15 @@ class TestValidationPlugin<T> implements BlocPlugin<T> {
170
180
  // Mock storage
171
181
  class MockStorage implements StorageAdapter {
172
182
  private store = new Map<string, string>();
173
-
183
+
174
184
  getItem(key: string): string | null {
175
185
  return this.store.get(key) || null;
176
186
  }
177
-
187
+
178
188
  setItem(key: string, value: string): void {
179
189
  this.store.set(key, value);
180
190
  }
181
-
191
+
182
192
  removeItem(key: string): void {
183
193
  this.store.delete(key);
184
194
  }
@@ -188,120 +198,120 @@ describe('New Plugin System', () => {
188
198
  beforeEach(() => {
189
199
  Blac.resetInstance();
190
200
  });
191
-
201
+
192
202
  describe('System Plugins (BlacPlugin)', () => {
193
203
  it('should register and execute system plugins', () => {
194
204
  const onBlocCreated = vi.fn();
195
205
  const onStateChanged = vi.fn();
196
-
206
+
197
207
  const plugin: BlacPlugin = {
198
208
  name: 'test-system-plugin',
199
209
  version: '1.0.0',
200
210
  onBlocCreated,
201
- onStateChanged
211
+ onStateChanged,
202
212
  };
203
-
213
+
204
214
  Blac.instance.plugins.add(plugin);
205
-
215
+
206
216
  const cubit = Blac.getBloc(CounterCubit);
207
217
  expect(onBlocCreated).toHaveBeenCalledWith(cubit);
208
-
218
+
209
219
  cubit.increment();
210
220
  expect(onStateChanged).toHaveBeenCalledWith(cubit, 0, 1);
211
221
  });
212
-
222
+
213
223
  it('should handle plugin errors gracefully', () => {
214
224
  const errorPlugin: BlacPlugin = {
215
225
  name: 'error-plugin',
216
226
  version: '1.0.0',
217
227
  onBlocCreated: () => {
218
228
  throw new Error('Plugin error');
219
- }
229
+ },
220
230
  };
221
-
231
+
222
232
  Blac.instance.plugins.add(errorPlugin);
223
-
233
+
224
234
  // Should not throw when creating bloc
225
235
  expect(() => Blac.getBloc(CounterCubit)).not.toThrow();
226
236
  });
227
-
237
+
228
238
  it('should execute bootstrap hooks', () => {
229
239
  const beforeBootstrap = vi.fn();
230
240
  const afterBootstrap = vi.fn();
231
-
241
+
232
242
  const plugin: BlacPlugin = {
233
243
  name: 'bootstrap-plugin',
234
244
  version: '1.0.0',
235
245
  beforeBootstrap,
236
- afterBootstrap
246
+ afterBootstrap,
237
247
  };
238
-
248
+
239
249
  Blac.resetInstance();
240
250
  Blac.instance.plugins.add(plugin);
241
251
  Blac.instance.bootstrap();
242
-
252
+
243
253
  expect(beforeBootstrap).toHaveBeenCalled();
244
254
  expect(afterBootstrap).toHaveBeenCalled();
245
255
  });
246
-
256
+
247
257
  it('should track metrics', () => {
248
258
  const plugin: BlacPlugin = {
249
259
  name: 'metrics-plugin',
250
260
  version: '1.0.0',
251
261
  onBlocCreated: () => {
252
262
  // Do something
253
- }
263
+ },
254
264
  };
255
-
265
+
256
266
  Blac.instance.plugins.add(plugin);
257
267
  Blac.getBloc(CounterCubit);
258
-
268
+
259
269
  const metrics = Blac.instance.plugins.getMetrics('metrics-plugin');
260
270
  expect(metrics).toBeDefined();
261
271
  expect(metrics?.get('onBlocCreated')).toBeDefined();
262
272
  expect(metrics?.get('onBlocCreated')?.executionCount).toBe(1);
263
273
  });
264
274
  });
265
-
275
+
266
276
  describe('Bloc Plugins (BlocPlugin)', () => {
267
277
  it('should attach plugins to specific blocs', () => {
268
278
  const onAttach = vi.fn();
269
279
  const onStateChange = vi.fn();
270
-
280
+
271
281
  const plugin: BlocPlugin<number> = {
272
282
  name: 'test-bloc-plugin',
273
283
  version: '1.0.0',
274
284
  onAttach,
275
- onStateChange
285
+ onStateChange,
276
286
  };
277
-
287
+
278
288
  const cubit = new CounterCubit();
279
289
  cubit.addPlugin(plugin);
280
-
290
+
281
291
  // Activate the bloc
282
292
  Blac.activateBloc(cubit as any);
283
-
293
+
284
294
  expect(onAttach).toHaveBeenCalledWith(cubit);
285
-
295
+
286
296
  cubit.increment();
287
297
  expect(onStateChange).toHaveBeenCalledWith(0, 1);
288
298
  });
289
-
299
+
290
300
  it('should transform state through plugins', () => {
291
301
  const transformPlugin: BlocPlugin<number> = {
292
302
  name: 'double-plugin',
293
303
  version: '1.0.0',
294
- transformState: (prev, next) => next * 2
304
+ transformState: (prev, next) => next * 2,
295
305
  };
296
-
306
+
297
307
  const cubit = new CounterCubit();
298
308
  cubit.addPlugin(transformPlugin);
299
309
  Blac.activateBloc(cubit as any);
300
-
310
+
301
311
  cubit.setValue(5);
302
312
  expect(cubit.state).toBe(10); // 5 * 2
303
313
  });
304
-
314
+
305
315
  it('should transform events through plugins', () => {
306
316
  const transformPlugin: BlocPlugin<number, CounterEvent> = {
307
317
  name: 'event-doubler',
@@ -311,21 +321,21 @@ describe('New Plugin System', () => {
311
321
  return new SetValue(event.value * 2);
312
322
  }
313
323
  return event;
314
- }
324
+ },
315
325
  };
316
-
326
+
317
327
  const bloc = new CounterBloc();
318
328
  bloc.addPlugin(transformPlugin);
319
329
  Blac.activateBloc(bloc as any);
320
-
330
+
321
331
  bloc.add(new SetValue(5));
322
-
332
+
323
333
  // Wait for event processing
324
334
  setTimeout(() => {
325
335
  expect(bloc.state).toBe(10); // 5 * 2
326
336
  }, 10);
327
337
  });
328
-
338
+
329
339
  it('should respect plugin capabilities', () => {
330
340
  const readOnlyPlugin: BlocPlugin<number> = {
331
341
  name: 'read-only',
@@ -335,120 +345,120 @@ describe('New Plugin System', () => {
335
345
  transformState: false,
336
346
  interceptEvents: false,
337
347
  persistData: false,
338
- accessMetadata: false
348
+ accessMetadata: false,
339
349
  },
340
350
  transformState: () => {
341
351
  throw new Error('Should not be called');
342
352
  },
343
- onStateChange: vi.fn()
353
+ onStateChange: vi.fn(),
344
354
  };
345
-
355
+
346
356
  const cubit = new CounterCubit();
347
357
  cubit.addPlugin(readOnlyPlugin);
348
358
  Blac.activateBloc(cubit as any);
349
-
359
+
350
360
  cubit.increment();
351
361
  expect(cubit.state).toBe(1); // Transform not applied
352
362
  expect(readOnlyPlugin.onStateChange).toHaveBeenCalled();
353
363
  });
354
364
  });
355
-
365
+
356
366
  describe('Example Plugins', () => {
357
367
  it('should log with TestLoggingPlugin', () => {
358
368
  const consoleSpy = vi.spyOn(console, 'debug');
359
-
369
+
360
370
  const plugin = new TestLoggingPlugin({ logLevel: 'debug' });
361
371
  Blac.instance.plugins.add(plugin);
362
-
372
+
363
373
  const cubit = Blac.getBloc(CounterCubit);
364
374
  cubit.increment();
365
-
375
+
366
376
  expect(consoleSpy).toHaveBeenCalledWith(
367
377
  expect.stringContaining('Bloc created'),
368
- undefined
378
+ undefined,
369
379
  );
370
-
380
+
371
381
  consoleSpy.mockRestore();
372
382
  });
373
-
383
+
374
384
  it('should persist state with TestPersistencePlugin', () => {
375
385
  const storage = new MockStorage();
376
386
  storage.setItem('counter', '42');
377
-
387
+
378
388
  const plugin = new TestPersistencePlugin<number>({
379
389
  key: 'counter',
380
390
  storage,
381
- saveDebounceMs: 0
391
+ saveDebounceMs: 0,
382
392
  });
383
-
393
+
384
394
  const cubit = new CounterCubit();
385
395
  cubit.addPlugin(plugin);
386
396
  Blac.activateBloc(cubit as any);
387
-
397
+
388
398
  // Should restore from storage
389
399
  expect(cubit.state).toBe(42);
390
-
400
+
391
401
  // Should save new state
392
402
  cubit.setValue(100);
393
-
403
+
394
404
  // Wait for debounced save
395
405
  setTimeout(() => {
396
406
  expect(storage.getItem('counter')).toBe('100');
397
407
  }, 10);
398
408
  });
399
-
409
+
400
410
  it('should validate state with TestValidationPlugin', () => {
401
411
  const validator = (state: number) => {
402
412
  if (state < 0) return 'Value must be non-negative';
403
413
  if (state > 100) return 'Value must not exceed 100';
404
414
  return true;
405
415
  };
406
-
416
+
407
417
  const plugin = new TestValidationPlugin(validator);
408
-
418
+
409
419
  const cubit = new CounterCubit();
410
420
  cubit.addPlugin(plugin);
411
421
  Blac.activateBloc(cubit as any);
412
-
422
+
413
423
  cubit.setValue(-5);
414
424
  expect(cubit.state).toBe(0); // Rejected
415
-
425
+
416
426
  cubit.setValue(50);
417
427
  expect(cubit.state).toBe(50); // Accepted
418
-
428
+
419
429
  cubit.setValue(150);
420
430
  expect(cubit.state).toBe(50); // Rejected
421
431
  });
422
432
  });
423
-
433
+
424
434
  describe('Plugin Composition', () => {
425
435
  it('should compose multiple bloc plugins', () => {
426
436
  const storage = new MockStorage();
427
-
437
+
428
438
  const persistencePlugin = new TestPersistencePlugin<number>({
429
439
  key: 'validated-counter',
430
440
  storage,
431
- saveDebounceMs: 0
441
+ saveDebounceMs: 0,
432
442
  });
433
-
443
+
434
444
  const validationPlugin = new TestValidationPlugin<number>(
435
- (state) => state >= 0 && state <= 10
445
+ (state) => state >= 0 && state <= 10,
436
446
  );
437
-
447
+
438
448
  const cubit = new CounterCubit();
439
449
  cubit.addPlugin(validationPlugin);
440
450
  cubit.addPlugin(persistencePlugin);
441
451
  Blac.activateBloc(cubit as any);
442
-
452
+
443
453
  cubit.setValue(5);
444
454
  expect(cubit.state).toBe(5);
445
-
455
+
446
456
  cubit.setValue(15);
447
457
  expect(cubit.state).toBe(5); // Validation rejected
448
-
458
+
449
459
  setTimeout(() => {
450
460
  expect(storage.getItem('validated-counter')).toBe('5');
451
461
  }, 10);
452
462
  });
453
463
  });
454
- });
464
+ });