Skip to content

Commit 58073ab

Browse files
perf(component-store): push updates to queueScheduler and single selectors to asapSchedulers (#2586)
1 parent 4796c97 commit 58073ab

File tree

4 files changed

+171
-64
lines changed

4 files changed

+171
-64
lines changed

modules/component-store/spec/component-store.spec.ts

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
interval,
88
timer,
99
Observable,
10+
asapScheduler,
1011
} from 'rxjs';
1112
import {
1213
delayWhen,
@@ -15,6 +16,7 @@ import {
1516
map,
1617
tap,
1718
finalize,
19+
observeOn,
1820
} from 'rxjs/operators';
1921

2022
describe('Component Store', () => {
@@ -140,14 +142,17 @@ describe('Component Store', () => {
140142
it(
141143
'does not throws an Error when updater is called with async Observable' +
142144
' before initialization, that emits the value after initialization',
143-
() => {
145+
marbles((m) => {
144146
const componentStore = new ComponentStore();
145147
const INIT_STATE = { initState: 'passed' };
146148
const UPDATED_STATE = { updatedState: 'proccessed' };
147149

148-
// Record all the values that go through state$ into an array
149-
const results: object[] = [];
150-
componentStore.state$.subscribe((state) => results.push(state));
150+
// Record all the values that go through state$.
151+
const recordedStateValues$ = componentStore.state$.pipe(
152+
publishReplay()
153+
);
154+
// Need to "connect" to start getting notifications.
155+
(recordedStateValues$ as ConnectableObservable<object>).connect();
151156

152157
const asyncronousObservable$ = of(UPDATED_STATE).pipe(
153158
// Delays until the state gets the init value.
@@ -163,8 +168,10 @@ describe('Component Store', () => {
163168
// Trigger initial state.
164169
componentStore.setState(INIT_STATE);
165170

166-
expect(results).toEqual([INIT_STATE, UPDATED_STATE]);
167-
}
171+
m.expect(recordedStateValues$).toBeObservable(
172+
m.hot('(iu)', { i: INIT_STATE, u: UPDATED_STATE })
173+
);
174+
})
168175
);
169176
});
170177

@@ -228,7 +235,9 @@ describe('Component Store', () => {
228235

229236
// Update twice with different values
230237
updater(UPDATED);
238+
m.flush(); // flushed after each update
231239
updater(UPDATE_VALUE);
240+
m.flush(); // flushed after each update
232241

233242
expect(results).toEqual([
234243
{ value: 'init' },
@@ -254,6 +263,52 @@ describe('Component Store', () => {
254263
})
255264
);
256265

266+
it(
267+
'with latest value within the same microtask',
268+
marbles((m) => {
269+
const UPDATED: Partial<State> = { updated: true };
270+
const UPDATE_VALUE: Partial<State> = { value: 'updated' };
271+
const updater = componentStore.updater(
272+
(state, value: Partial<State>) => ({
273+
...state,
274+
...value,
275+
})
276+
);
277+
278+
// Record all the values that go through state$ into an array
279+
const results: object[] = [];
280+
componentStore.state$.subscribe((state) => results.push(state));
281+
282+
// Update twice with different values
283+
updater(UPDATED);
284+
updater(UPDATE_VALUE);
285+
286+
// 👆👆👆
287+
// Notice there is no "flush" until this point - all synchronous
288+
m.flush();
289+
290+
expect(results).toEqual([
291+
{ value: 'init' },
292+
// Notice there is no "intermediary" value of
293+
// {value: 'init', updated: true,},
294+
{
295+
value: 'updated',
296+
updated: true,
297+
},
298+
]);
299+
300+
// New subsriber gets the latest value only.
301+
m.expect(componentStore.state$).toBeObservable(
302+
m.hot('s', {
303+
s: {
304+
value: 'updated',
305+
updated: true,
306+
},
307+
})
308+
);
309+
})
310+
);
311+
257312
it(
258313
'with updater to a value based on the previous state and passed' +
259314
' Observable',
@@ -497,10 +552,48 @@ describe('Component Store', () => {
497552

498553
const observable$ = m.hot(' 1-2---3', observableValues);
499554
const updater$ = m.cold(' a--b--c|');
555+
const expectedSelector$ = m.hot('v-wx--(yz)-', {
556+
v: 'one a',
557+
w: 'two a',
558+
x: 'two b',
559+
y: 'three b', // 👈 different scheduler then 'select'
560+
z: 'three c',
561+
});
562+
563+
const selectorValue$ = componentStore.select((s) => s.value);
564+
const selector$ = componentStore.select(
565+
selectorValue$,
566+
observable$,
567+
(s1, o) => o + ' ' + s1
568+
);
569+
570+
componentStore.updater((state, newValue: string) => ({
571+
value: newValue,
572+
}))(updater$);
573+
574+
m.expect(selector$).toBeObservable(expectedSelector$);
575+
})
576+
);
577+
578+
it(
579+
'can combine with other Observables and skip intermediary values on' +
580+
' asapScheduler',
581+
marbles((m) => {
582+
const observableValues = {
583+
'1': 'one',
584+
'2': 'two',
585+
'3': 'three',
586+
};
587+
588+
const observable$ = m
589+
.hot(' 1-2---3', observableValues)
590+
.pipe(observeOn(asapScheduler));
591+
const updater$ = m.cold(' a--b--c|');
500592
const expectedSelector$ = m.hot('w-xy--z-', {
501593
w: 'one a',
502594
x: 'two a',
503595
y: 'two b',
596+
// 'three b', // 👈 same scheduler as 'select'
504597
z: 'three c',
505598
});
506599

@@ -559,12 +652,13 @@ describe('Component Store', () => {
559652
'3': 'three',
560653
};
561654

562-
const observable$ = m.cold(' 1-2---3|', observableValues);
655+
const observable$ = m.hot(' 1-2---3', observableValues);
563656
const updater$ = m.cold(' a--b--c|');
564-
const expectedSelector$ = m.hot('w-xy--z-', {
565-
w: 'one a',
566-
x: 'two a',
567-
y: 'two b',
657+
const expectedSelector$ = m.hot('v-wx--(yz)-', {
658+
v: 'one a',
659+
w: 'two a',
660+
x: 'two b',
661+
y: 'three b', // 👈 different scheduler then 'select'
568662
z: 'three c',
569663
});
570664

modules/component-store/spec/integration.spec.ts

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,91 @@
11
import { Component, Type, Injectable } from '@angular/core';
22
import { ComponentStore } from '../src';
3-
import { TestBed, ComponentFixture } from '@angular/core/testing';
3+
import {
4+
TestBed,
5+
ComponentFixture,
6+
flushMicrotasks,
7+
fakeAsync,
8+
tick,
9+
} from '@angular/core/testing';
410
import { CommonModule } from '@angular/common';
511
import { interval, Observable } from 'rxjs';
6-
import { fakeSchedulers } from 'rxjs-marbles/jest';
712
import { tap } from 'rxjs/operators';
813
import { By } from '@angular/platform-browser';
914

1015
describe('ComponentStore integration', () => {
11-
jest.useFakeTimers();
12-
1316
// The same set of tests is run against different versions of how
1417
// ComponentStore may be used - making sure all of them work.
1518
function testWith(setup: () => Promise<SetupData<Child>>) {
16-
it('does not emit until state is initialized', async () => {
17-
const state = await setup();
19+
let state: SetupData<Child>;
20+
beforeEach(async () => {
21+
state = await setup();
22+
});
1823

24+
it('does not emit until state is initialized', fakeAsync(() => {
1925
expect(state.parent.isChildVisible).toBe(true);
2026
expect(state.hasChild()).toBe(true);
2127

2228
state.fixture.detectChanges();
29+
flushMicrotasks();
30+
2331
// No values emitted, 👇 no initial state
2432
expect(state.propChanges).toEqual([]);
2533
expect(state.prop2Changes).toEqual([]);
26-
});
27-
28-
it('gets initial value when state is initialized', async () => {
29-
const state = await setup();
34+
}));
3035

36+
it('gets initial value when state is initialized', fakeAsync(() => {
3137
state.child.init();
38+
flushMicrotasks();
3239
// init state👇
3340
expect(state.propChanges).toEqual(['initial Value']);
3441
expect(state.prop2Changes).toEqual([undefined]);
35-
});
3642

37-
it(
38-
'effect updates values',
39-
fakeSchedulers(async (advance) => {
40-
const state = await setup();
43+
// clear "Periodic timers in queue"
44+
state.destroy();
45+
}));
4146

42-
state.child.init();
43-
44-
advance(40);
45-
// New value pushed every 10 ms.
46-
expect(state.prop2Changes).toEqual([undefined, 0, 1, 2, 3]);
47-
})
48-
);
47+
it('effect updates values', fakeAsync(() => {
48+
state.child.init();
4949

50-
it('updates values imperatively', async () => {
51-
const state = await setup();
50+
tick(40);
51+
// New value pushed every 10 ms.
52+
expect(state.prop2Changes).toEqual([undefined, 0, 1, 2, 3]);
5253

53-
state.child.init();
54+
// clear "Periodic timers in queue"
55+
state.destroy();
56+
}));
5457

55-
state.child.updateProp('new value');
56-
state.child.updateProp('yay!!!');
58+
it('updates values imperatively', fakeAsync(() => {
59+
state!.child.init();
5760

58-
expect(state.propChanges).toContain('new value');
59-
expect(state.propChanges).toContain('yay!!!');
60-
});
61+
state!.child.updateProp('new value');
62+
flushMicrotasks();
63+
state!.child.updateProp('yay!!!');
64+
flushMicrotasks();
6165

62-
it(
63-
'stops observables when destroyed',
64-
fakeSchedulers(async (advance) => {
65-
const state = await setup();
66+
expect(state!.propChanges).toContain('new value');
67+
expect(state!.propChanges).toContain('yay!!!');
6668

67-
state.child.init();
69+
// clear "Periodic timers in queue"
70+
state.destroy();
71+
}));
6872

69-
advance(40);
70-
// New value pushed every 10 ms.
71-
expect(state.prop2Changes).toEqual([undefined, 0, 1, 2, 3]);
73+
it('stops observables when destroyed', fakeAsync(() => {
74+
state.child.init();
7275

73-
state.parent.isChildVisible = false;
74-
state.fixture.detectChanges();
76+
tick(40);
77+
// New value pushed every 10 ms.
78+
expect(state.prop2Changes).toEqual([undefined, 0, 1, 2, 3]);
7579

76-
advance(20);
77-
// Still at the same values, so effect stopped running
78-
expect(state.prop2Changes).toEqual([undefined, 0, 1, 2, 3]);
79-
})
80-
);
80+
state.parent.isChildVisible = false;
81+
state.fixture.detectChanges();
8182

82-
it('ComponentStore is destroyed', async () => {
83-
const state = await setup();
83+
tick(20);
84+
// Still at the same values, so effect stopped running
85+
expect(state.prop2Changes).toEqual([undefined, 0, 1, 2, 3]);
86+
}));
8487

88+
it('ComponentStore is destroyed', () => {
8589
state.child.init();
8690

8791
state.parent.isChildVisible = false;
@@ -130,6 +134,7 @@ describe('ComponentStore integration', () => {
130134
hasChild: () => boolean;
131135
propChanges: string[];
132136
prop2Changes: Array<number | undefined>;
137+
destroy: () => void;
133138
componentStoreDestroySpy: jest.SpyInstance;
134139
}
135140

@@ -143,7 +148,7 @@ describe('ComponentStore integration', () => {
143148

144149
async function setupTestBed<T extends Child>(
145150
childClass: Type<T>
146-
): Promise<Omit<SetupData<T>, 'componentStoreDestroySpy'>> {
151+
): Promise<Omit<SetupData<T>, 'componentStoreDestroySpy' | 'destroy'>> {
147152
await TestBed.configureTestingModule({
148153
declarations: [ParentComponent, childClass],
149154
imports: [CommonModule],
@@ -218,6 +223,7 @@ describe('ComponentStore integration', () => {
218223
);
219224
return {
220225
...setup,
226+
destroy: () => setup.child.componentStore.ngOnDestroy(),
221227
componentStoreDestroySpy,
222228
};
223229
}
@@ -257,6 +263,7 @@ describe('ComponentStore integration', () => {
257263
const componentStoreDestroySpy = jest.spyOn(setup.child, 'ngOnDestroy');
258264
return {
259265
...setup,
266+
destroy: () => setup.child.ngOnDestroy(),
260267
componentStoreDestroySpy,
261268
};
262269
}
@@ -314,6 +321,7 @@ describe('ComponentStore integration', () => {
314321
);
315322
return {
316323
...setup,
324+
destroy: () => setup.child.propsStore.ngOnDestroy(),
317325
componentStoreDestroySpy,
318326
};
319327
}
@@ -363,6 +371,7 @@ describe('ComponentStore integration', () => {
363371
const componentStoreDestroySpy = jest.spyOn(setup.child, 'ngOnDestroy');
364372
return {
365373
...setup,
374+
destroy: () => setup.child.ngOnDestroy(),
366375
componentStoreDestroySpy,
367376
};
368377
}

0 commit comments

Comments
 (0)