Skip to content

Commit

Permalink
perf: use lazy properties on product context (#617)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhhyi authored Mar 16, 2021
1 parent 082e0d2 commit 1126496
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 165 deletions.
74 changes: 47 additions & 27 deletions src/app/core/facades/product-context.facade.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { omit, pick } from 'lodash-es';
import { BehaviorSubject, EMPTY, Observable, Subject, of } from 'rxjs';
import { map, mapTo, switchMapTo } from 'rxjs/operators';
import { anyString, anything, instance, mock, when } from 'ts-mockito';
import { anyString, anything, instance, mock, verify, when } from 'ts-mockito';

import { AttributeGroup } from 'ish-core/models/attribute-group/attribute-group.model';
import { AttributeGroupTypes } from 'ish-core/models/attribute-group/attribute-group.types';
Expand Down Expand Up @@ -33,6 +33,7 @@ describe('Product Context Facade', () => {

beforeEach(() => {
shoppingFacade = mock(ShoppingFacade);
when(shoppingFacade.productLinks$(anything())).thenReturn(of({}));
when(shoppingFacade.productParts$(anything())).thenReturn(EMPTY);
when(shoppingFacade.category$(anything())).thenReturn(of(undefined));

Expand All @@ -54,6 +55,8 @@ describe('Product Context Facade', () => {
"requiredCompletenessLevel": 2,
}
`);
verify(shoppingFacade.productLinks$(anything())).never();
verify(shoppingFacade.promotions$(anything())).never();
expect(context.get('loading')).toBeFalsy();
});

Expand Down Expand Up @@ -289,6 +292,17 @@ describe('Product Context Facade', () => {
});
});

describe('lazy property handling', () => {
it('should not load product links until subscription', done => {
verify(shoppingFacade.productLinks$(anything())).never();

context.select('links').subscribe(() => {
verify(shoppingFacade.productLinks$(anything())).once();
done();
});
});
});

it('should set correct display properties for product', () => {
expect(context.get('displayProperties')).toMatchInlineSnapshot(`
Object {
Expand Down Expand Up @@ -425,19 +439,22 @@ describe('Product Context Facade', () => {
context.set('sku', () => '123');
});

it('should set parts property for retail set', () => {
expect(context.get('parts')).toMatchInlineSnapshot(`
Array [
Object {
"quantity": 1,
"sku": "p1",
},
Object {
"quantity": 1,
"sku": "p2",
},
]
`);
it('should set parts property for retail set', done => {
context.select('parts').subscribe(parts => {
expect(parts).toMatchInlineSnapshot(`
Array [
Object {
"quantity": 1,
"sku": "p1",
},
Object {
"quantity": 1,
"sku": "p2",
},
]
`);
done();
});
});

it('should set correct display properties for retail set', () => {
Expand Down Expand Up @@ -492,19 +509,22 @@ describe('Product Context Facade', () => {
context.set('sku', () => '123');
});

it('should set parts property for bundle', () => {
expect(context.get('parts')).toMatchInlineSnapshot(`
Array [
Object {
"quantity": 1,
"sku": "p1",
},
Object {
"quantity": 2,
"sku": "p2",
},
]
`);
it('should set parts property for bundle', done => {
context.select('parts').subscribe(parts => {
expect(parts).toMatchInlineSnapshot(`
Array [
Object {
"quantity": 1,
"sku": "p1",
},
Object {
"quantity": 2,
"sku": "p2",
},
]
`);
done();
});
});

it('should set correct display properties for bundle', () => {
Expand Down
69 changes: 51 additions & 18 deletions src/app/core/facades/product-context.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import { debounceTime, distinctUntilChanged, filter, first, map, skip, startWith

import { AttributeGroupTypes } from 'ish-core/models/attribute-group/attribute-group.types';
import { Image } from 'ish-core/models/image/image.model';
import { ProductLinksDictionary } from 'ish-core/models/product-links/product-links.model';
import { ProductVariationHelper } from 'ish-core/models/product-variation/product-variation.helper';
import { ProductView } from 'ish-core/models/product-view/product-view.model';
import { ProductCompletenessLevel, ProductHelper, SkuQuantityType } from 'ish-core/models/product/product.model';
import { Promotion } from 'ish-core/models/promotion/promotion.model';
import { generateProductUrl } from 'ish-core/routing/product/product.route';
import { whenTruthy } from 'ish-core/utils/operators';

Expand Down Expand Up @@ -80,9 +82,13 @@ export interface ProductContext {
loading: boolean;
label: string;
categoryId: string;

displayProperties: Partial<ProductContextDisplayProperties>;

// lazy
links: ProductLinksDictionary;
promotions: Promotion[];
parts: SkuQuantityType[];

// variation handling
variationCount: number;

Expand All @@ -99,7 +105,6 @@ export interface ProductContext {
hasQuantityError: boolean;

// child contexts
parts: SkuQuantityType[];
propagateActive: boolean;
children: ProductContext[];
}
Expand All @@ -108,6 +113,7 @@ export interface ProductContext {
export class ProductContextFacade extends RxState<ProductContext> {
private privateConfig$ = new BehaviorSubject<Partial<ProductContextDisplayProperties>>({});
private loggingActive = false;
private lazyFieldsInitialized: string[] = [];

set config(config: Partial<ProductContextDisplayProperties>) {
this.privateConfig$.next(config);
Expand Down Expand Up @@ -231,14 +237,6 @@ export class ProductContextFacade extends RxState<ProductContext> {
)
);

this.connect(
'parts',
this.select('sku').pipe(
whenTruthy(),
switchMap(sku => this.shoppingFacade.productParts$(sku))
)
);

const externalDisplayPropertyProviders = injector
.get<ExternalDisplayPropertiesProvider[]>(EXTERNAL_DISPLAY_PROPERTY_PROVIDER, [])
.map(edp => edp.setup(this.select('product')));
Expand Down Expand Up @@ -275,6 +273,49 @@ export class ProductContextFacade extends RxState<ProductContext> {
}
}

select(): Observable<ProductContext>;
select<K1 extends keyof ProductContext>(k1: K1): Observable<ProductContext[K1]>;
select<K1 extends keyof ProductContext, K2 extends keyof ProductContext[K1]>(
k1: K1,
k2: K2
): Observable<ProductContext[K1][K2]>;

select<K1 extends keyof ProductContext, K2 extends keyof ProductContext[K1]>(k1?: K1, k2?: K2) {
const wrap = <K extends keyof ProductContext>(key: K, obs: Observable<ProductContext[K]>) => {
if (!this.lazyFieldsInitialized.includes(key)) {
this.connect(key, obs);
this.lazyFieldsInitialized.push(key);
}
};

switch (k1) {
case 'links':
wrap('links', this.shoppingFacade.productLinks$(this.select('sku')));
break;
case 'promotions':
wrap(
'promotions',
combineLatest([this.select('displayProperties', 'promotions'), this.select('product', 'promotionIds')]).pipe(
filter(([visible]) => !!visible),
switchMap(([, ids]) => this.shoppingFacade.promotions$(ids))
)
);
break;
case 'parts':
wrap(
'parts',
combineLatest([
this.select('displayProperties', 'bundleParts'),
this.select('displayProperties', 'retailSetParts'),
]).pipe(
filter(([a, b]) => a || b),
switchMap(() => this.shoppingFacade.productParts$(this.select('sku')))
)
);
}
return k2 ? super.select(k1, k2) : k1 ? super.select(k1) : super.select();
}

log(val: boolean) {
if (!this.loggingActive) {
this.hold(this.select().pipe(filter(() => !!val)), ctx => {
Expand Down Expand Up @@ -341,12 +382,4 @@ export class ProductContextFacade extends RxState<ProductContext> {
)
);
}

productLinks$() {
return this.shoppingFacade.productLinks$(this.select('sku'));
}

productPromotions$() {
return this.select('product', 'promotionIds').pipe(switchMap(ids => this.shoppingFacade.promotions$(ids)));
}
}
14 changes: 11 additions & 3 deletions src/app/core/facades/shopping.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
loadProduct,
loadProductIfNotLoaded,
loadProductLinks,
loadProductParts,
} from 'ish-core/store/shopping/products';
import { getPromotion, getPromotions, loadPromotion } from 'ish-core/store/shopping/promotions';
import {
Expand Down Expand Up @@ -119,17 +120,24 @@ export class ShoppingFacade {

productLinks$(sku: string | Observable<string>) {
return toObservable(sku).pipe(
whenTruthy(),
tap(plainSKU => {
this.store.dispatch(loadProductLinks({ sku: plainSKU }));
}),
switchMap(plainSKU => this.store.pipe(select(getProductLinks(plainSKU))))
);
}

// PRODUCT RETAILSET / BUNDLES
// PRODUCT RETAIL SET / BUNDLES

productParts$(sku: string) {
return this.store.pipe(select(getProductParts(sku)));
productParts$(sku: string | Observable<string>) {
return toObservable(sku).pipe(
whenTruthy(),
tap(plainSKU => {
this.store.dispatch(loadProductParts({ sku: plainSKU }));
}),
switchMap(plainSKU => this.store.pipe(select(getProductParts(plainSKU))))
);
}

// SEARCH
Expand Down
11 changes: 4 additions & 7 deletions src/app/core/store/shopping/products/products.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,11 @@ export const loadProductVariationsSuccess = createAction(
payload<{ sku: string; variations: string[]; defaultVariation: string }>()
);

export const loadProductBundlesSuccess = createAction(
'[Products API] Load Product Bundles Success',
payload<{ sku: string; bundledProducts: SkuQuantityType[] }>()
);
export const loadProductParts = createAction('[Products API] Load Product Parts', payload<{ sku: string }>());

export const loadRetailSetSuccess = createAction(
'[Products API] Load Retail Set Success',
payload<{ sku: string; parts: string[] }>()
export const loadProductPartsSuccess = createAction(
'[Products API] Load Product Parts Success',
payload<{ sku: string; parts: SkuQuantityType[] }>()
);

export const loadProductLinks = createAction('[Products Internal] Load Product Links', payload<{ sku: string }>());
Expand Down
Loading

0 comments on commit 1126496

Please sign in to comment.