@blac/core 2.0.0-rc-8 → 2.0.0-rc-10
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 +185 -4
- package/dist/index.es.js +1099 -601
- package/dist/index.es.js.map +1 -1
- package/package.json +5 -4
- package/src/Blac.ts +63 -40
- package/src/BlacObserver.ts +58 -24
- package/src/Bloc.ts +166 -157
- package/src/BlocBase.ts +266 -90
- package/src/Cubit.ts +1 -1
- package/src/__tests__/Bloc.event.test.ts +419 -0
- package/src/__tests__/BlocBase.lifecycle.test.ts +441 -0
- package/src/__tests__/Cubit.test.ts +473 -0
- package/src/adapter/BlacAdapter.ts +262 -0
- package/src/adapter/ConsumerTracker.ts +241 -0
- package/src/adapter/ProxyFactory.ts +308 -0
- package/src/adapter/__tests__/BlacAdapter.test.ts +561 -0
- package/src/adapter/__tests__/ConsumerTracker.test.ts +562 -0
- package/src/adapter/__tests__/ProxyFactory.test.ts +655 -0
- package/src/adapter/index.ts +3 -0
- package/src/index.ts +2 -1
- package/src/testing.ts +60 -47
- package/src/types.ts +11 -3
- package/src/utils/uuid.ts +4 -4
- package/src/BlacEvent.ts +0 -5
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { Cubit } from '../Cubit';
|
|
3
|
+
import { Blac } from '../Blac';
|
|
4
|
+
|
|
5
|
+
// Test Cubit implementations
|
|
6
|
+
class CounterCubit extends Cubit<number> {
|
|
7
|
+
constructor(initialValue = 0) {
|
|
8
|
+
super(initialValue);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
increment = () => this.emit(this.state + 1);
|
|
12
|
+
decrement = () => this.emit(this.state - 1);
|
|
13
|
+
set = (value: number) => this.emit(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UserState {
|
|
17
|
+
name: string;
|
|
18
|
+
age: number;
|
|
19
|
+
email: string;
|
|
20
|
+
preferences?: {
|
|
21
|
+
theme: string;
|
|
22
|
+
notifications: boolean;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class UserCubit extends Cubit<UserState> {
|
|
27
|
+
constructor() {
|
|
28
|
+
super({
|
|
29
|
+
name: 'John Doe',
|
|
30
|
+
age: 30,
|
|
31
|
+
email: 'john@example.com',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
updateName = (name: string) => {
|
|
36
|
+
this.patch({ name });
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
updateMultiple = (updates: Partial<UserState>) => {
|
|
40
|
+
this.patch(updates);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
setPreferences = (theme: string, notifications: boolean) => {
|
|
44
|
+
this.patch({
|
|
45
|
+
preferences: { theme, notifications },
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class PrimitiveCubit extends Cubit<string> {
|
|
51
|
+
constructor() {
|
|
52
|
+
super('initial');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
update = (value: string) => this.emit(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('Cubit State Emissions', () => {
|
|
59
|
+
let blacInstance: Blac;
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
blacInstance = new Blac({ __unsafe_ignore_singleton: true });
|
|
63
|
+
Blac.enableLog = false;
|
|
64
|
+
vi.clearAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('Basic emit() functionality', () => {
|
|
68
|
+
it('should emit new state and notify observers', () => {
|
|
69
|
+
const cubit = new CounterCubit();
|
|
70
|
+
const observer = vi.fn();
|
|
71
|
+
|
|
72
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
73
|
+
|
|
74
|
+
cubit.increment();
|
|
75
|
+
|
|
76
|
+
expect(observer).toHaveBeenCalledWith(1, 0, undefined);
|
|
77
|
+
expect(cubit.state).toBe(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should not emit when new state is identical (Object.is comparison)', () => {
|
|
81
|
+
const cubit = new CounterCubit(5);
|
|
82
|
+
const observer = vi.fn();
|
|
83
|
+
|
|
84
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
85
|
+
|
|
86
|
+
// Try to emit same value
|
|
87
|
+
cubit.set(5);
|
|
88
|
+
|
|
89
|
+
expect(observer).not.toHaveBeenCalled();
|
|
90
|
+
expect(cubit.state).toBe(5);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle NaN correctly with Object.is', () => {
|
|
94
|
+
const cubit = new CounterCubit(NaN);
|
|
95
|
+
const observer = vi.fn();
|
|
96
|
+
|
|
97
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
98
|
+
|
|
99
|
+
// NaN === NaN is false, but Object.is(NaN, NaN) is true
|
|
100
|
+
cubit.set(NaN);
|
|
101
|
+
|
|
102
|
+
expect(observer).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should distinguish between +0 and -0', () => {
|
|
106
|
+
const cubit = new CounterCubit(0);
|
|
107
|
+
const observer = vi.fn();
|
|
108
|
+
|
|
109
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
110
|
+
|
|
111
|
+
// Object.is can distinguish +0 and -0
|
|
112
|
+
cubit.set(-0);
|
|
113
|
+
|
|
114
|
+
expect(observer).toHaveBeenCalledWith(-0, 0, undefined);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should emit multiple state changes sequentially', () => {
|
|
118
|
+
const cubit = new CounterCubit();
|
|
119
|
+
const observer = vi.fn();
|
|
120
|
+
|
|
121
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
122
|
+
|
|
123
|
+
cubit.increment();
|
|
124
|
+
cubit.increment();
|
|
125
|
+
cubit.decrement();
|
|
126
|
+
|
|
127
|
+
expect(observer).toHaveBeenCalledTimes(3);
|
|
128
|
+
expect(observer).toHaveBeenNthCalledWith(1, 1, 0, undefined);
|
|
129
|
+
expect(observer).toHaveBeenNthCalledWith(2, 2, 1, undefined);
|
|
130
|
+
expect(observer).toHaveBeenNthCalledWith(3, 1, 2, undefined);
|
|
131
|
+
expect(cubit.state).toBe(1);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('patch() functionality for object states', () => {
|
|
136
|
+
it('should partially update object state', () => {
|
|
137
|
+
const cubit = new UserCubit();
|
|
138
|
+
const observer = vi.fn();
|
|
139
|
+
|
|
140
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
141
|
+
|
|
142
|
+
cubit.updateName('Jane Doe');
|
|
143
|
+
|
|
144
|
+
expect(observer).toHaveBeenCalledWith(
|
|
145
|
+
{
|
|
146
|
+
name: 'Jane Doe',
|
|
147
|
+
age: 30,
|
|
148
|
+
email: 'john@example.com',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'John Doe',
|
|
152
|
+
age: 30,
|
|
153
|
+
email: 'john@example.com',
|
|
154
|
+
},
|
|
155
|
+
undefined,
|
|
156
|
+
);
|
|
157
|
+
expect(cubit.state.name).toBe('Jane Doe');
|
|
158
|
+
expect(cubit.state.age).toBe(30); // Unchanged
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should update multiple properties at once', () => {
|
|
162
|
+
const cubit = new UserCubit();
|
|
163
|
+
const observer = vi.fn();
|
|
164
|
+
|
|
165
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
166
|
+
|
|
167
|
+
cubit.updateMultiple({ name: 'Jane Smith', age: 25 });
|
|
168
|
+
|
|
169
|
+
expect(observer).toHaveBeenCalledTimes(1);
|
|
170
|
+
expect(cubit.state).toEqual({
|
|
171
|
+
name: 'Jane Smith',
|
|
172
|
+
age: 25,
|
|
173
|
+
email: 'john@example.com',
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should not emit when patched values are identical', () => {
|
|
178
|
+
const cubit = new UserCubit();
|
|
179
|
+
const observer = vi.fn();
|
|
180
|
+
|
|
181
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
182
|
+
|
|
183
|
+
// Patch with same values
|
|
184
|
+
cubit.patch({ name: 'John Doe', age: 30 });
|
|
185
|
+
|
|
186
|
+
expect(observer).not.toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should emit when at least one patched value differs', () => {
|
|
190
|
+
const cubit = new UserCubit();
|
|
191
|
+
const observer = vi.fn();
|
|
192
|
+
|
|
193
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
194
|
+
|
|
195
|
+
// One value different
|
|
196
|
+
cubit.patch({ name: 'John Doe', age: 31 });
|
|
197
|
+
|
|
198
|
+
expect(observer).toHaveBeenCalledTimes(1);
|
|
199
|
+
expect(cubit.state.age).toBe(31);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle nested object updates', () => {
|
|
203
|
+
const cubit = new UserCubit();
|
|
204
|
+
const observer = vi.fn();
|
|
205
|
+
|
|
206
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
207
|
+
|
|
208
|
+
cubit.setPreferences('dark', true);
|
|
209
|
+
|
|
210
|
+
expect(observer).toHaveBeenCalledTimes(1);
|
|
211
|
+
expect(cubit.state.preferences).toEqual({
|
|
212
|
+
theme: 'dark',
|
|
213
|
+
notifications: true,
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should handle patch with ignoreChangeCheck flag', () => {
|
|
218
|
+
const cubit = new UserCubit();
|
|
219
|
+
const observer = vi.fn();
|
|
220
|
+
|
|
221
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
222
|
+
|
|
223
|
+
// Force emit even with same values
|
|
224
|
+
cubit.patch({ name: 'John Doe' }, true);
|
|
225
|
+
|
|
226
|
+
expect(observer).toHaveBeenCalledTimes(1);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should warn when patch is called on non-object state', () => {
|
|
230
|
+
const cubit = new PrimitiveCubit();
|
|
231
|
+
const warnSpy = vi.spyOn(Blac, 'warn').mockImplementation(() => {});
|
|
232
|
+
|
|
233
|
+
cubit.patch('new value' as any);
|
|
234
|
+
|
|
235
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
236
|
+
expect.stringContaining(
|
|
237
|
+
'Cubit.patch: was called on a cubit where the state is not an object',
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
expect(cubit.state).toBe('initial'); // State unchanged
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should handle null state gracefully', () => {
|
|
244
|
+
class NullStateCubit extends Cubit<null> {
|
|
245
|
+
constructor() {
|
|
246
|
+
super(null);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const cubit = new NullStateCubit();
|
|
251
|
+
const warnSpy = vi.spyOn(Blac, 'warn').mockImplementation(() => {});
|
|
252
|
+
|
|
253
|
+
cubit.patch({} as any);
|
|
254
|
+
|
|
255
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
256
|
+
expect(cubit.state).toBe(null);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('State change detection edge cases', () => {
|
|
261
|
+
it('should detect changes in array states', () => {
|
|
262
|
+
class ArrayCubit extends Cubit<number[]> {
|
|
263
|
+
constructor() {
|
|
264
|
+
super([1, 2, 3]);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
add = (num: number) => {
|
|
268
|
+
this.emit([...this.state, num]);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
updateIndex = (index: number, value: number) => {
|
|
272
|
+
const newArray = [...this.state];
|
|
273
|
+
newArray[index] = value;
|
|
274
|
+
this.emit(newArray);
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const cubit = new ArrayCubit();
|
|
279
|
+
const observer = vi.fn();
|
|
280
|
+
|
|
281
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
282
|
+
|
|
283
|
+
// New array reference should trigger update
|
|
284
|
+
cubit.add(4);
|
|
285
|
+
expect(observer).toHaveBeenCalledTimes(1);
|
|
286
|
+
expect(cubit.state).toEqual([1, 2, 3, 4]);
|
|
287
|
+
|
|
288
|
+
// Same array reference should not trigger
|
|
289
|
+
observer.mockClear();
|
|
290
|
+
const currentState = cubit.state;
|
|
291
|
+
cubit.emit(currentState);
|
|
292
|
+
expect(observer).not.toHaveBeenCalled();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should handle undefined and null transitions', () => {
|
|
296
|
+
class NullableCubit extends Cubit<string | null | undefined> {
|
|
297
|
+
constructor() {
|
|
298
|
+
super('initial');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
setNull = () => this.emit(null);
|
|
302
|
+
setUndefined = () => this.emit(undefined);
|
|
303
|
+
setString = (s: string) => this.emit(s);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const cubit = new NullableCubit();
|
|
307
|
+
const observer = vi.fn();
|
|
308
|
+
|
|
309
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
310
|
+
|
|
311
|
+
// null and undefined are different according to Object.is
|
|
312
|
+
expect(Object.is(null, undefined)).toBe(false);
|
|
313
|
+
|
|
314
|
+
cubit.setNull();
|
|
315
|
+
expect(observer).toHaveBeenCalledWith(null, 'initial', undefined);
|
|
316
|
+
expect(cubit.state).toBe(null);
|
|
317
|
+
|
|
318
|
+
observer.mockClear();
|
|
319
|
+
|
|
320
|
+
// NOTE: There's a limitation in BlocBase._pushState that prevents undefined states
|
|
321
|
+
// It has a validation: if (newState === undefined) return;
|
|
322
|
+
// So undefined cannot be emitted as a state
|
|
323
|
+
cubit.setUndefined();
|
|
324
|
+
|
|
325
|
+
// State should remain null since undefined is rejected
|
|
326
|
+
expect(observer).not.toHaveBeenCalled();
|
|
327
|
+
expect(cubit.state).toBe(null);
|
|
328
|
+
|
|
329
|
+
observer.mockClear();
|
|
330
|
+
cubit.setString('value');
|
|
331
|
+
expect(observer).toHaveBeenCalledWith('value', null, undefined);
|
|
332
|
+
expect(cubit.state).toBe('value');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should handle complex object comparisons', () => {
|
|
336
|
+
interface ComplexState {
|
|
337
|
+
data: { id: number; values: number[] };
|
|
338
|
+
metadata: Map<string, any>;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
class ComplexCubit extends Cubit<ComplexState> {
|
|
342
|
+
constructor() {
|
|
343
|
+
super({
|
|
344
|
+
data: { id: 1, values: [1, 2, 3] },
|
|
345
|
+
metadata: new Map([['key', 'value']]),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
updateData = (data: ComplexState['data']) => {
|
|
350
|
+
this.patch({ data });
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const cubit = new ComplexCubit();
|
|
355
|
+
const observer = vi.fn();
|
|
356
|
+
|
|
357
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
358
|
+
|
|
359
|
+
// Different object reference with same values
|
|
360
|
+
cubit.updateData({ id: 1, values: [1, 2, 3] });
|
|
361
|
+
|
|
362
|
+
// Should emit because object reference is different
|
|
363
|
+
expect(observer).toHaveBeenCalledTimes(1);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('Integration with BlocBase', () => {
|
|
368
|
+
it('should properly inherit from BlocBase', () => {
|
|
369
|
+
const cubit = new CounterCubit();
|
|
370
|
+
|
|
371
|
+
// Should have BlocBase properties and methods
|
|
372
|
+
expect(cubit._state).toBe(0);
|
|
373
|
+
expect(cubit._observer).toBeDefined();
|
|
374
|
+
expect(cubit._consumers).toBeDefined();
|
|
375
|
+
expect(typeof cubit._addConsumer).toBe('function');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should work with state batching', () => {
|
|
379
|
+
const cubit = new CounterCubit();
|
|
380
|
+
const observer = vi.fn();
|
|
381
|
+
|
|
382
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
383
|
+
|
|
384
|
+
cubit.batch(() => {
|
|
385
|
+
cubit.increment(); // 0 -> 1
|
|
386
|
+
cubit.increment(); // 1 -> 2
|
|
387
|
+
cubit.increment(); // 2 -> 3
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Should only notify once with final state
|
|
391
|
+
// The oldState in the batch notification is from the last update (2 -> 3)
|
|
392
|
+
expect(observer).toHaveBeenCalledTimes(1);
|
|
393
|
+
expect(observer).toHaveBeenCalledWith(3, 2, undefined);
|
|
394
|
+
expect(cubit.state).toBe(3);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should maintain state history correctly', () => {
|
|
398
|
+
const cubit = new CounterCubit();
|
|
399
|
+
|
|
400
|
+
cubit.increment();
|
|
401
|
+
expect(cubit._oldState).toBe(0);
|
|
402
|
+
expect(cubit._state).toBe(1);
|
|
403
|
+
|
|
404
|
+
cubit.increment();
|
|
405
|
+
expect(cubit._oldState).toBe(1);
|
|
406
|
+
expect(cubit._state).toBe(2);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe('Memory and Performance', () => {
|
|
411
|
+
it('should not leak memory with rapid emissions', () => {
|
|
412
|
+
const cubit = new CounterCubit();
|
|
413
|
+
const observer = vi.fn();
|
|
414
|
+
|
|
415
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
416
|
+
|
|
417
|
+
// Rapid emissions
|
|
418
|
+
for (let i = 0; i < 1000; i++) {
|
|
419
|
+
cubit.increment();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
expect(cubit.state).toBe(1000);
|
|
423
|
+
expect(observer).toHaveBeenCalledTimes(1000);
|
|
424
|
+
|
|
425
|
+
// Check no accumulation of state
|
|
426
|
+
expect(cubit._state).toBe(1000);
|
|
427
|
+
expect(cubit._oldState).toBe(999);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should handle concurrent patch operations', () => {
|
|
431
|
+
const cubit = new UserCubit();
|
|
432
|
+
const observer = vi.fn();
|
|
433
|
+
|
|
434
|
+
cubit._observer.subscribe({ id: 'test', fn: observer });
|
|
435
|
+
|
|
436
|
+
// Multiple patches in quick succession
|
|
437
|
+
cubit.patch({ name: 'Name1' });
|
|
438
|
+
cubit.patch({ age: 25 });
|
|
439
|
+
cubit.patch({ email: 'new@example.com' });
|
|
440
|
+
|
|
441
|
+
expect(observer).toHaveBeenCalledTimes(3);
|
|
442
|
+
expect(cubit.state).toEqual({
|
|
443
|
+
name: 'Name1',
|
|
444
|
+
age: 25,
|
|
445
|
+
email: 'new@example.com',
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
describe('Type Safety', () => {
|
|
451
|
+
it('should maintain type safety with emit', () => {
|
|
452
|
+
const cubit = new CounterCubit();
|
|
453
|
+
|
|
454
|
+
// TypeScript should enforce number type
|
|
455
|
+
cubit.emit(42);
|
|
456
|
+
expect(cubit.state).toBe(42);
|
|
457
|
+
|
|
458
|
+
// Test that emit only accepts numbers
|
|
459
|
+
// cubit.emit('not a number'); // This would be a type error
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should maintain type safety with patch', () => {
|
|
463
|
+
const cubit = new UserCubit();
|
|
464
|
+
|
|
465
|
+
// Valid patch
|
|
466
|
+
cubit.patch({ name: 'New Name' });
|
|
467
|
+
|
|
468
|
+
// Test that patch only accepts valid properties and types
|
|
469
|
+
// cubit.patch({ invalidProp: 'value' }); // This would be a type error
|
|
470
|
+
// cubit.patch({ age: 'not a number' }); // This would be a type error
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
});
|