Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@ngrx/component implement strategies #2458

Closed
9 of 15 tasks
BioPhoton opened this issue Mar 28, 2020 · 1 comment
Closed
9 of 15 tasks

@ngrx/component implement strategies #2458

BioPhoton opened this issue Mar 28, 2020 · 1 comment

Comments

@BioPhoton
Copy link
Contributor

BioPhoton commented Mar 28, 2020

UPDATE the coalescing stuff is irrelevant

Problem

In the alpha release, the optimization should be opt-in.
Also, the implementation needs a way to change the strategy in CdAware.

Actual

export function createCdAware<U>(cfg: {
  work: () => void;
  resetContextObserver: NextObserver<unknown>;
  configurableBehaviour: (
    o: Observable<Observable<U | null | undefined>>
  ) => Observable<Observable<U | null | undefined>>;
  updateViewContextObserver: PartialObserver<U | null | undefined>;
}): CdAware<U | undefined | null>

Usage

constructor(cdRef: ChangeDetectorRef, ngZone: NgZone) {
    this.cdAware = createCdAware<S>({
      work: setUpWork({
        ngZone,
        cdRef,
        context: (cdRef as EmbeddedViewRef<Type<any>>).context,
      }),
      updateViewContextObserver: this.updateViewContextObserver,
      resetContextObserver: this.resetContextObserver,
      configurableBehaviour: this.configurableBehaviour,
    });
    this.subscription = this.cdAware.subscribe();
  }

The problem here is work and configurableBehaviour depend on each other, the compositin of work is done outside they rely on the environment.
resetContextObserver and updateViewContextObserver is related to the class it is used in.

Based on this implementation configurable strategies are not possible.

Solution

Strategy Utils

export const DEFAULT_STRATEGY_NAME = 'idle';

export interface CdStrategy<T> {
    behaviour: (cfg?: any) => MonoTypeOperatorFunction<Observable<T>>;
    render: () => void;
    name: string;
}

export interface StrategySelection<U> {
    idle: CdStrategy<U>;
    [key: string]: CdStrategy<U>;
}

export function getStrategies<T>(cfg: StrategyFactoryConfig): StrategySelection<T> {
    return {
        idle: createIdleStrategy<T>(cfg)
    };
}

Idle Strategy

This strategy is the drop-in replacement for Angular's built-in async pipe.
This is the only strategy that does not also work in zone-less environments.

  • >=ViewEngine
Render Method Coalescing Coalesce Scope
ZoneFull cdRef.markForCheck None
ZoneLess cdRef.markForCheck None
  • Ivy
Render Method Coalescing Coalesce Scope
ZoneFull cdRef.markForCheck None
ZoneLess cdRef.markForCheck None
export function createIdleStrategy<T>(cfg: StrategyFactoryConfig): CdStrategy<T> {
    return {
        render: (): void => {
            cfg.cdRef.markForCheck();
        },
        behaviour: () => o => o,
        name: 'idle'
    };
}

Optimistic1 Strategy (Just an example)

  • >=ViewEngine
Render Method Coalescing Coalesce Scope
ZoneFull cdRef.markForCheck None
ZoneLess cdRef.detectChange ✔️ Component
  • Ivy
Render Method Coalescing Coalesce Scope
ZoneFull ɵmarkDirty None
ZoneLess ɵdetectChanges ✔️ LView
export function createOptimistic1Strategy<T>(cfg: StrategyFactoryConfig): CdStrategy<T> {
    const inIvy = isIvy();
    const inZone = hasZone(cfg.ngZone);
    const durationSelector = getSaveDurationSelector(cfg.ngZone);

    function render() {
        if (inIvy) {
            if (inZone) {
                markDirty(cfg.component);
            } else {
                detectChanges(cfg.component);
            }
        } else {
            if (inZone) {
                cfg.cdRef.markForCheck();
            } else {
                cfg.cdRef.detectChanges();
            }
        }
    }

    const coalesceConfig = { context: inIvy ? (cfg.cdRef as any)._lView : ((cfg.cdRef as any).context) as any};

    const behaviour = (o$: Observable<Observable<T>>): Observable<Observable<T>> => {
        return inZone ? o$.pipe(coalesce(durationSelector, coalesceConfig)) : o$;
    };

    return {
        behaviour: () => behaviour,
        render,
        name: 'optimistic1'
    };
}

New CdAware

export function createCdAware<U>(cfg: {
    strategies: StrategySelection<U>;
    resetContextObserver: NextObserver<unknown>;
    updateViewContextObserver: PartialObserver<any>;
}): CdAware<U | undefined | null>

New Usage

constructor(cdRef: ChangeDetectorRef, ngZone: NgZone) {
        this.cdAware = createCdAware<S>({
            strategies: getStrategies<S>({component: (cdRef as any).context, ngZone, cdRef}),
            updateViewContextObserver: this.updateViewContextObserver,
            resetContextObserver: this.resetContextObserver,
        });
        this.subscription = this.cdAware.subscribe();
    }

New Internals

export interface CdAware<U> extends Subscribable<U> {
    nextVale: (value: any) => void;
    nextConfig: (config: string) => void;
}

export function createCdAware<U>(cfg: {
    strategies: StrategySelection<U>;
    resetContextObserver: NextObserver<unknown>;
    updateViewContextObserver: PartialObserver<any>;
}): CdAware<U | undefined | null> {

    const configSubject = new Subject<string>();
    const config$: Observable<CdStrategy<U>> = configSubject.pipe(
        filter(v => !!v),
        distinctUntilChanged(),
        startWith(DEFAULT_STRATEGY_NAME),
        map((strategy: string): CdStrategy<U> => cfg.strategies[strategy] ? cfg.strategies[strategy] : cfg.strategies.idle)
    );

    const observablesSubject = new Subject<Observable<U>>();
    const observables$ = observablesSubject.pipe(
        distinctUntilChanged()
    );

    let prevObservable: Observable<U>;
    const renderSideEffect$ = combineLatest(observables$, config$).pipe(
        switchMap(([observable$, strategy]) => {
            if (prevObservable === observable$) {
                return NEVER;
            }
            prevObservable = observable$;

            if (observable$ === undefined || observable$ === null) {
                cfg.resetContextObserver.next(undefined);
                strategy.render();
                return NEVER;
            }

            return observable$.pipe(
                distinctUntilChanged(),
                tap(cfg.updateViewContextObserver),
                strategy.behaviour(),
                tap(() => strategy.render())
            );
        })
    );

    return {
        nextVale(value: any): void {
            observablesSubject.next(value);
        },
        nextConfig(nextConfig: string): void {
            configSubject.next(nextConfig);
        },
        subscribe(): Subscription {
            return renderSideEffect$.subscribe();
        }
    };
}

This enables us to have configurable strategies and also move the code related to configuration into CdAware.

Todos

  • Implement Strategies
    • Setup strategies
    • Refactore CdAware to take strategies
    • Refactore PushPipe and LetDirective` to initialize and provide strategies
  • Implement renderChanges operator
  • make hasZone lookup directly the window
  • Tests
    • update tests for hasZone
    • Implement tests for strategies
    • Refactore tests for CdAware
    • Add tests for switching/reassigning observables
    • Implement tests for strategies in PushPipe
    • Implement tests for strategies in LetDirective
  • Documentation
    • Document strategies

Related issues #2441

PR without tests is ready and will be pushed after #2456 is merged and #2442 are merged

If accepted, I would be willing to submit a PR for this feature

[x] Yes (Assistance is provided if you need help submitting a pull request)
[ ] No

@BioPhoton
Copy link
Contributor Author

Closed due to decision against strategies

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants