@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,562 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { ConsumerTracker } from '../ConsumerTracker';
3
-
4
- describe('ConsumerTracker', () => {
5
- let tracker: ConsumerTracker;
6
- let consumerRef1: object;
7
- let consumerRef2: object;
8
-
9
- beforeEach(() => {
10
- tracker = new ConsumerTracker();
11
- consumerRef1 = { id: 'consumer-1' };
12
- consumerRef2 = { id: 'consumer-2' };
13
- });
14
-
15
- describe('Consumer Registration', () => {
16
- it('should register new consumers', () => {
17
- tracker.register(consumerRef1, 'consumer-id-1');
18
-
19
- const info = tracker.getConsumerInfo(consumerRef1);
20
- expect(info).toBeDefined();
21
- expect(info?.id).toBe('consumer-id-1');
22
- expect(info?.hasRendered).toBe(false);
23
- expect(info?.stateAccesses.size).toBe(0);
24
- expect(info?.classAccesses.size).toBe(0);
25
- });
26
-
27
- it('should track registration statistics', () => {
28
- tracker.register(consumerRef1, 'id-1');
29
- tracker.register(consumerRef2, 'id-2');
30
-
31
- const stats = tracker.getStats();
32
- expect(stats.totalRegistrations).toBe(2);
33
- expect(stats.activeConsumers).toBe(2);
34
- });
35
-
36
- it('should handle re-registration of same consumer', () => {
37
- tracker.register(consumerRef1, 'id-1');
38
- tracker.register(consumerRef1, 'id-1-updated');
39
-
40
- const info = tracker.getConsumerInfo(consumerRef1);
41
- expect(info?.id).toBe('id-1-updated'); // Should update
42
-
43
- const stats = tracker.getStats();
44
- expect(stats.totalRegistrations).toBe(2);
45
- expect(stats.activeConsumers).toBe(1); // Still only one active
46
- });
47
-
48
- it('should unregister consumers', () => {
49
- tracker.register(consumerRef1, 'id-1');
50
- tracker.register(consumerRef2, 'id-2');
51
-
52
- tracker.unregister(consumerRef1);
53
-
54
- expect(tracker.getConsumerInfo(consumerRef1)).toBeUndefined();
55
- expect(tracker.hasConsumer(consumerRef1)).toBe(false);
56
-
57
- const stats = tracker.getStats();
58
- expect(stats.activeConsumers).toBe(1);
59
- });
60
-
61
- it('should handle unregistering non-existent consumer gracefully', () => {
62
- expect(() => tracker.unregister(consumerRef1)).not.toThrow();
63
-
64
- const stats = tracker.getStats();
65
- expect(stats.activeConsumers).toBe(0);
66
- });
67
- });
68
-
69
- describe('Access Tracking', () => {
70
- beforeEach(() => {
71
- tracker.register(consumerRef1, 'id-1');
72
- });
73
-
74
- it('should track state property access', () => {
75
- tracker.trackAccess(consumerRef1, 'state', 'name', 'John');
76
- tracker.trackAccess(consumerRef1, 'state', 'age', 30);
77
-
78
- const deps = tracker.getDependencies(consumerRef1);
79
- expect(deps?.statePaths).toContain('name');
80
- expect(deps?.statePaths).toContain('age');
81
- expect(deps?.statePaths).toHaveLength(2);
82
- });
83
-
84
- it('should track class property access', () => {
85
- tracker.trackAccess(consumerRef1, 'class', 'isValid', true);
86
- tracker.trackAccess(consumerRef1, 'class', 'total', 100);
87
-
88
- const deps = tracker.getDependencies(consumerRef1);
89
- expect(deps?.classPaths).toContain('isValid');
90
- expect(deps?.classPaths).toContain('total');
91
- expect(deps?.classPaths).toHaveLength(2);
92
- });
93
-
94
- it('should store tracked values with timestamps', () => {
95
- const now = Date.now();
96
- tracker.trackAccess(consumerRef1, 'state', 'count', 5);
97
-
98
- const info = tracker.getConsumerInfo(consumerRef1);
99
- const trackedValue = info?.stateValues.get('count');
100
-
101
- expect(trackedValue?.value).toBe(5);
102
- expect(trackedValue?.lastAccessTime).toBeGreaterThanOrEqual(now);
103
- });
104
-
105
- it('should update access statistics', () => {
106
- const info = tracker.getConsumerInfo(consumerRef1);
107
- expect(info?.accessCount).toBe(0);
108
- expect(info?.firstAccessTime).toBe(0);
109
-
110
- tracker.trackAccess(consumerRef1, 'state', 'prop1', 'value1');
111
- const updatedInfo = tracker.getConsumerInfo(consumerRef1);
112
-
113
- expect(updatedInfo?.accessCount).toBe(1);
114
- expect(updatedInfo?.firstAccessTime).toBeGreaterThan(0);
115
- expect(updatedInfo?.lastAccessTime).toBeGreaterThan(0);
116
-
117
- // Track more accesses
118
- tracker.trackAccess(consumerRef1, 'state', 'prop2', 'value2');
119
- const finalInfo = tracker.getConsumerInfo(consumerRef1);
120
-
121
- expect(finalInfo?.accessCount).toBe(2);
122
- expect(finalInfo?.lastAccessTime).toBeGreaterThanOrEqual(
123
- updatedInfo?.lastAccessTime || 0,
124
- );
125
- });
126
-
127
- it('should not track access for unregistered consumers', () => {
128
- tracker.trackAccess(consumerRef2, 'state', 'test', 'value');
129
-
130
- const deps = tracker.getDependencies(consumerRef2);
131
- expect(deps).toBeNull();
132
- });
133
-
134
- it('should handle duplicate path tracking', () => {
135
- tracker.trackAccess(consumerRef1, 'state', 'name', 'John');
136
- tracker.trackAccess(consumerRef1, 'state', 'name', 'Jane'); // Same path, different value
137
-
138
- const deps = tracker.getDependencies(consumerRef1);
139
- expect(deps?.statePaths).toHaveLength(1); // No duplicates
140
-
141
- const info = tracker.getConsumerInfo(consumerRef1);
142
- const trackedValue = info?.stateValues.get('name');
143
- expect(trackedValue?.value).toBe('Jane'); // Latest value
144
- });
145
-
146
- it('should handle undefined values', () => {
147
- tracker.trackAccess(consumerRef1, 'state', 'optional', undefined);
148
-
149
- const info = tracker.getConsumerInfo(consumerRef1);
150
- // undefined values are not stored in stateValues map (only tracked in stateAccesses)
151
- expect(info?.stateAccesses.has('optional')).toBe(true);
152
- expect(info?.stateValues.has('optional')).toBe(false);
153
- });
154
- });
155
-
156
- describe('Dependency Change Detection', () => {
157
- beforeEach(() => {
158
- tracker.register(consumerRef1, 'id-1');
159
- });
160
-
161
- it('should detect value changes', () => {
162
- // Initial tracking
163
- tracker.trackAccess(consumerRef1, 'state', 'count', 5);
164
- tracker.trackAccess(consumerRef1, 'state', 'name', 'John');
165
-
166
- // No changes
167
- let hasChanged = tracker.hasValuesChanged(
168
- consumerRef1,
169
- { count: 5, name: 'John' },
170
- {},
171
- );
172
- expect(hasChanged).toBe(false);
173
-
174
- // Count changed
175
- hasChanged = tracker.hasValuesChanged(
176
- consumerRef1,
177
- { count: 6, name: 'John' },
178
- {},
179
- );
180
- expect(hasChanged).toBe(true);
181
- });
182
-
183
- it('should detect nested value changes', () => {
184
- // Track nested access
185
- tracker.trackAccess(consumerRef1, 'state', 'user.profile.theme', 'light');
186
-
187
- const hasChanged = tracker.hasValuesChanged(
188
- consumerRef1,
189
- { user: { profile: { theme: 'dark' } } },
190
- {},
191
- );
192
- expect(hasChanged).toBe(true);
193
- });
194
-
195
- it('should handle missing nested paths gracefully', () => {
196
- tracker.trackAccess(consumerRef1, 'state', 'a.b.c', 'value');
197
-
198
- // Path doesn't exist in new state
199
- const hasChanged = tracker.hasValuesChanged(
200
- consumerRef1,
201
- { a: {} }, // Missing b.c
202
- {},
203
- );
204
- expect(hasChanged).toBe(true); // Treated as change
205
- });
206
-
207
- it('should track class property changes', () => {
208
- const mockBloc = {
209
- get isValid() {
210
- return true;
211
- },
212
- get count() {
213
- return 10;
214
- },
215
- };
216
-
217
- tracker.trackAccess(consumerRef1, 'class', 'isValid', true);
218
- tracker.trackAccess(consumerRef1, 'class', 'count', 10);
219
-
220
- // No changes
221
- let hasChanged = tracker.hasValuesChanged(consumerRef1, {}, mockBloc);
222
- expect(hasChanged).toBe(false);
223
-
224
- // Create new bloc with different getter values
225
- const newMockBloc = {
226
- get isValid() {
227
- return false;
228
- },
229
- get count() {
230
- return 10;
231
- },
232
- };
233
-
234
- hasChanged = tracker.hasValuesChanged(consumerRef1, {}, newMockBloc);
235
- expect(hasChanged).toBe(true);
236
- });
237
-
238
- it('should return true when no values tracked but accesses exist', () => {
239
- // Track access without values (happens on first render)
240
- tracker.trackAccess(consumerRef1, 'state', 'prop', undefined);
241
-
242
- const info = tracker.getConsumerInfo(consumerRef1);
243
- info!.stateValues.clear(); // Simulate no tracked values
244
-
245
- const hasChanged = tracker.hasValuesChanged(
246
- consumerRef1,
247
- { prop: 'value' },
248
- {},
249
- );
250
- expect(hasChanged).toBe(true); // Establish baseline
251
- });
252
-
253
- it('should update tracked values after change detection', () => {
254
- tracker.trackAccess(consumerRef1, 'state', 'count', 1);
255
-
256
- tracker.hasValuesChanged(consumerRef1, { count: 2 }, {});
257
-
258
- const info = tracker.getConsumerInfo(consumerRef1);
259
- const trackedValue = info?.stateValues.get('count');
260
- expect(trackedValue?.value).toBe(2); // Updated
261
- });
262
- });
263
-
264
- describe('Consumer Notification Logic', () => {
265
- beforeEach(() => {
266
- tracker.register(consumerRef1, 'id-1');
267
- tracker.setHasRendered(consumerRef1, true);
268
- });
269
-
270
- it('should notify when tracked paths change', () => {
271
- tracker.trackAccess(consumerRef1, 'state', 'user.name', 'John');
272
- tracker.trackAccess(consumerRef1, 'state', 'user.age', 30);
273
-
274
- const changedPaths = new Set(['user.name']);
275
- expect(tracker.shouldNotifyConsumer(consumerRef1, changedPaths)).toBe(
276
- true,
277
- );
278
- });
279
-
280
- it('should not notify when no tracked paths change', () => {
281
- tracker.trackAccess(consumerRef1, 'state', 'user.name', 'John');
282
-
283
- const changedPaths = new Set(['user.email', 'settings.theme']);
284
- expect(tracker.shouldNotifyConsumer(consumerRef1, changedPaths)).toBe(
285
- false,
286
- );
287
- });
288
-
289
- it('should handle nested path matching', () => {
290
- tracker.trackAccess(consumerRef1, 'state', 'user.profile', undefined);
291
-
292
- // Child path changed
293
- let changedPaths = new Set(['user.profile.email']);
294
- expect(tracker.shouldNotifyConsumer(consumerRef1, changedPaths)).toBe(
295
- true,
296
- );
297
-
298
- // Parent path changed
299
- changedPaths = new Set(['user']);
300
- expect(tracker.shouldNotifyConsumer(consumerRef1, changedPaths)).toBe(
301
- true,
302
- );
303
- });
304
-
305
- it('should handle exact path matching', () => {
306
- tracker.trackAccess(consumerRef1, 'state', 'user.name', 'John');
307
-
308
- // Sibling path - should not notify
309
- const changedPaths = new Set(['user.email']);
310
- expect(tracker.shouldNotifyConsumer(consumerRef1, changedPaths)).toBe(
311
- false,
312
- );
313
- });
314
-
315
- it('should return false for unregistered consumers', () => {
316
- const changedPaths = new Set(['any.path']);
317
- expect(tracker.shouldNotifyConsumer(consumerRef2, changedPaths)).toBe(
318
- false,
319
- );
320
- });
321
- });
322
-
323
- describe('Reset and Cleanup', () => {
324
- it('should reset tracking for a consumer', () => {
325
- tracker.register(consumerRef1, 'id-1');
326
-
327
- // Add some tracking
328
- tracker.trackAccess(consumerRef1, 'state', 'prop1', 'value1');
329
- tracker.trackAccess(consumerRef1, 'class', 'getter1', 100);
330
-
331
- let info = tracker.getConsumerInfo(consumerRef1);
332
- expect(info?.stateAccesses.size).toBe(1);
333
- expect(info?.classAccesses.size).toBe(1);
334
- expect(info?.accessCount).toBe(2);
335
-
336
- // Reset
337
- tracker.resetTracking(consumerRef1);
338
-
339
- info = tracker.getConsumerInfo(consumerRef1);
340
- expect(info?.stateAccesses.size).toBe(0);
341
- expect(info?.classAccesses.size).toBe(0);
342
- expect(info?.stateValues.size).toBe(0);
343
- expect(info?.classValues.size).toBe(0);
344
- expect(info?.accessCount).toBe(0);
345
- expect(info?.firstAccessTime).toBe(0);
346
- });
347
-
348
- it('should handle reset for non-existent consumer', () => {
349
- expect(() => tracker.resetTracking(consumerRef1)).not.toThrow();
350
- });
351
- });
352
-
353
- describe('Metadata Management', () => {
354
- it('should update last notified timestamp', () => {
355
- tracker.register(consumerRef1, 'id-1');
356
-
357
- const initialInfo = tracker.getConsumerInfo(consumerRef1);
358
- const initialTime = initialInfo?.lastNotified || 0;
359
-
360
- // Wait a bit and update
361
- setTimeout(() => {
362
- tracker.updateLastNotified(consumerRef1);
363
-
364
- const updatedInfo = tracker.getConsumerInfo(consumerRef1);
365
- expect(updatedInfo?.lastNotified).toBeGreaterThan(initialTime);
366
- }, 10);
367
- });
368
-
369
- it('should set hasRendered flag', () => {
370
- tracker.register(consumerRef1, 'id-1');
371
-
372
- let info = tracker.getConsumerInfo(consumerRef1);
373
- expect(info?.hasRendered).toBe(false);
374
-
375
- tracker.setHasRendered(consumerRef1, true);
376
-
377
- info = tracker.getConsumerInfo(consumerRef1);
378
- expect(info?.hasRendered).toBe(true);
379
- });
380
-
381
- it('should handle metadata updates for non-existent consumer', () => {
382
- expect(() => {
383
- tracker.updateLastNotified(consumerRef1);
384
- tracker.setHasRendered(consumerRef1, true);
385
- }).not.toThrow();
386
- });
387
- });
388
-
389
- describe('Utility Methods', () => {
390
- it('should correctly parse nested paths', () => {
391
- tracker.register(consumerRef1, 'id-1');
392
- tracker.trackAccess(consumerRef1, 'state', 'a.b.c.d', 'deep');
393
-
394
- const state = {
395
- a: {
396
- b: {
397
- c: {
398
- d: 'deep',
399
- },
400
- },
401
- },
402
- };
403
-
404
- const hasChanged = tracker.hasValuesChanged(consumerRef1, state, {});
405
- expect(hasChanged).toBe(false);
406
- });
407
-
408
- it('should handle array indices in paths', () => {
409
- tracker.register(consumerRef1, 'id-1');
410
- tracker.trackAccess(consumerRef1, 'state', 'items.0.name', 'First');
411
- tracker.trackAccess(consumerRef1, 'state', 'items.1.name', 'Second');
412
-
413
- const state = {
414
- items: [{ name: 'First' }, { name: 'Second' }],
415
- };
416
-
417
- const hasChanged = tracker.hasValuesChanged(consumerRef1, state, {});
418
- expect(hasChanged).toBe(false);
419
- });
420
-
421
- it('should handle null/undefined in path traversal', () => {
422
- tracker.register(consumerRef1, 'id-1');
423
-
424
- // First establish baseline - mark as rendered so change detection works
425
- const info = tracker.getConsumerInfo(consumerRef1);
426
- info!.hasRendered = true;
427
-
428
- // Track a nested path with a value
429
- tracker.trackAccess(
430
- consumerRef1,
431
- 'state',
432
- 'maybe.nested.value',
433
- 'exists',
434
- );
435
-
436
- // Null in path - getValueAtPath returns undefined when encountering null
437
- let hasChanged = tracker.hasValuesChanged(
438
- consumerRef1,
439
- { maybe: null },
440
- {},
441
- );
442
- expect(hasChanged).toBe(true); // 'exists' !== undefined
443
-
444
- // Reset tracked value to test undefined case
445
- info!.stateValues.clear();
446
- tracker.trackAccess(
447
- consumerRef1,
448
- 'state',
449
- 'maybe.nested.value',
450
- 'exists',
451
- );
452
-
453
- // Undefined in path - getValueAtPath returns undefined
454
- hasChanged = tracker.hasValuesChanged(
455
- consumerRef1,
456
- { maybe: undefined },
457
- {},
458
- );
459
- expect(hasChanged).toBe(true); // 'exists' !== undefined
460
- });
461
- });
462
-
463
- describe('Memory Management', () => {
464
- it('should use WeakMap for automatic garbage collection', () => {
465
- // Register many consumers
466
- const consumers: any[] = [];
467
- for (let i = 0; i < 100; i++) {
468
- const ref = { id: `consumer-${i}` };
469
- consumers.push(ref);
470
- tracker.register(ref, `id-${i}`);
471
- }
472
-
473
- expect(tracker.getStats().activeConsumers).toBe(100);
474
-
475
- // Clear references
476
- consumers.length = 0;
477
-
478
- // Consumers should be eligible for GC
479
- // (Can't test actual GC in unit tests, but WeakMap enables it)
480
- });
481
- });
482
-
483
- describe('Edge Cases', () => {
484
- it('should handle consumers without proper getDependencies', () => {
485
- const deps = tracker.getDependencies(consumerRef1);
486
- expect(deps).toBeNull(); // Not registered
487
- });
488
-
489
- it('should handle error in value access gracefully', () => {
490
- tracker.register(consumerRef1, 'id-1');
491
- tracker.trackAccess(consumerRef1, 'state', 'getter', 5);
492
-
493
- // Create object that throws on property access
494
- const throwingState = {
495
- get getter() {
496
- throw new Error('Access error');
497
- },
498
- };
499
-
500
- // Should treat as changed when access fails
501
- const hasChanged = tracker.hasValuesChanged(
502
- consumerRef1,
503
- throwingState,
504
- {},
505
- );
506
- expect(hasChanged).toBe(true);
507
- });
508
- });
509
-
510
- describe('Dependency Tracking After Access Pattern Changes', () => {
511
- it('should not detect changes for properties no longer accessed', () => {
512
- tracker.register(consumerRef1, 'id-1');
513
-
514
- // First render: access both count and name
515
- tracker.trackAccess(consumerRef1, 'state', 'count', 10);
516
- tracker.trackAccess(consumerRef1, 'state', 'name', 'Alice');
517
-
518
- // Initial state
519
- const initialState = { count: 10, name: 'Alice' };
520
-
521
- // Verify no changes with same values
522
- let hasChanged = tracker.hasValuesChanged(consumerRef1, initialState, {});
523
- expect(hasChanged).toBe(false);
524
-
525
- // Change name - should detect change since we're still tracking it
526
- hasChanged = tracker.hasValuesChanged(
527
- consumerRef1,
528
- { count: 10, name: 'Bob' },
529
- {},
530
- );
531
- expect(hasChanged).toBe(true);
532
-
533
- tracker.setHasRendered(consumerRef1, true);
534
-
535
- // Simulate next render cycle where only count is accessed
536
- // Reset tracking to simulate a new render
537
- tracker.resetTracking(consumerRef1);
538
- tracker.trackAccess(consumerRef1, 'state', 'count', 10);
539
-
540
- // Now only 'count' is tracked, not 'name'
541
- const deps = tracker.getDependencies(consumerRef1);
542
- expect(deps?.statePaths).toEqual(['count']);
543
- expect(deps?.statePaths).not.toContain('name');
544
-
545
- // Change name - should NOT detect change since we're no longer tracking it
546
- hasChanged = tracker.hasValuesChanged(
547
- consumerRef1,
548
- { count: 10, name: 'Charlie' },
549
- {},
550
- );
551
- expect(hasChanged).toBe(false);
552
-
553
- // But changing count should still be detected
554
- hasChanged = tracker.hasValuesChanged(
555
- consumerRef1,
556
- { count: 11, name: 'Charlie' },
557
- {},
558
- );
559
- expect(hasChanged).toBe(true);
560
- });
561
- });
562
- });