@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.
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +522 -297
- package/dist/index.es.js +1833 -1550
- package/dist/index.es.js.map +1 -1
- package/package.json +6 -7
- 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/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
|
@@ -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
|
-
});
|