diff --git a/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts b/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts index f047db97be..f9c3cbeb55 100644 --- a/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts @@ -1,20 +1,56 @@ import { Injectable } from '@angular/core'; -import { Observable, ReplaySubject, throwError } from 'rxjs'; +import { concat, Observable, of, ReplaySubject, throwError } from 'rxjs'; +import { delay, retryWhen, shareReplay, take, tap } from 'rxjs/operators'; +import { LoadingStrategy } from '../strategies'; import { uuid } from '../utils'; @Injectable({ providedIn: 'root', }) export class LazyLoadService { + readonly loaded = new Set(); + loadedLibraries: { [url: string]: ReplaySubject } = {}; + load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable; load( urlOrUrls: string | string[], type: 'script' | 'style', - content: string = '', + content?: string, + targetQuery?: string, + position?: InsertPosition, + ): Observable; + load( + strategyOrUrl: LoadingStrategy | string | string[], + retryTimesOrType?: number | 'script' | 'style', + retryDelayOrContent?: number | string, targetQuery: string = 'body', position: InsertPosition = 'beforeend', - ): Observable { + ): Observable { + if (strategyOrUrl instanceof LoadingStrategy) { + const strategy = strategyOrUrl; + const retryTimes = retryTimesOrType as number; + const retryDelay = retryDelayOrContent as number; + + if (this.loaded.has(strategy.path)) return of(new CustomEvent('load')); + + return strategy.createStream().pipe( + retryWhen(error$ => + concat( + error$.pipe(delay(retryDelay), take(retryTimes)), + throwError(new CustomEvent('error')), + ), + ), + tap(() => this.loaded.add(strategy.path)), + delay(100), + shareReplay({ bufferSize: 1, refCount: true }), + ); + } + + let urlOrUrls = strategyOrUrl; + const content = retryDelayOrContent as string; + const type = retryTimesOrType as 'script' | 'style'; + if (!urlOrUrls && !content) { return throwError('Should pass url or content'); } else if (!urlOrUrls && content) { diff --git a/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts index a29b60974e..3596da2591 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts @@ -1,9 +1,65 @@ import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { of, throwError } from 'rxjs'; +import { catchError, switchMap } from 'rxjs/operators'; import { LazyLoadService } from '../services/lazy-load.service'; -import { catchError } from 'rxjs/operators'; -import { of } from 'rxjs'; +import { ScriptLoadingStrategy } from '../strategies'; describe('LazyLoadService', () => { + describe('#load', () => { + const service = new LazyLoadService(); + const strategy = new ScriptLoadingStrategy('http://example.com/'); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should emit an error event if not loaded', done => { + const counter = jest.fn(); + jest.spyOn(strategy, 'createStream').mockReturnValueOnce( + of(null).pipe( + switchMap(() => { + counter(); + return throwError('THIS WILL NOT BE THE FINAL ERROR'); + }), + ), + ); + + service.load(strategy, 5, 0).subscribe({ + error: errorEvent => { + expect(errorEvent).toEqual(new CustomEvent('error')); + expect(counter).toHaveBeenCalledTimes(6); + expect(service.loaded.has(strategy.path)).toBe(false); + done(); + }, + }); + }); + + it('should emit a load event if loaded', done => { + const loadEvent = new CustomEvent('load'); + jest.spyOn(strategy, 'createStream').mockReturnValue(of(loadEvent)); + + service.load(strategy).subscribe({ + next: event => { + expect(event).toBe(loadEvent); + expect(service.loaded.has(strategy.path)).toBe(true); + done(); + }, + }); + }); + + it('should emit a custom load event if loaded if resource is loaded before', done => { + const loadEvent = new CustomEvent('load'); + service.loaded.add(strategy.path); + + service.load(strategy).subscribe(event => { + expect(event).toEqual(loadEvent); + done(); + }); + }); + }); +}); + +describe('LazyLoadService (Deprecated)', () => { let spectator: SpectatorService; let service: LazyLoadService; const scriptElement = document.createElement('script'); @@ -25,15 +81,17 @@ describe('LazyLoadService', () => { spy.mockReturnValue(scriptElement); service.load('https://abp.io', 'script', 'test').subscribe(res => { - expect(document.querySelector('script[src="https://abp.io"][type="text/javascript"]').textContent).toMatch( - 'test', - ); + expect( + document.querySelector('script[src="https://abp.io"][type="text/javascript"]').textContent, + ).toMatch('test'); }); scriptElement.onload(null); service.load('https://abp.io', 'script', 'test').subscribe(res => { - expect(document.querySelectorAll('script[src="https://abp.io"][type="text/javascript"]')).toHaveLength(1); + expect( + document.querySelectorAll('script[src="https://abp.io"][type="text/javascript"]'), + ).toHaveLength(1); done(); }); }); @@ -59,7 +117,9 @@ describe('LazyLoadService', () => { test('should load an link element', done => { service.load('https://abp.io', 'style').subscribe(res => { - expect(document.querySelector('link[type="text/css"][rel="stylesheet"][href="https://abp.io"]')).toBeTruthy(); + expect( + document.querySelector('link[type="text/css"][rel="stylesheet"][href="https://abp.io"]'), + ).toBeTruthy(); done(); });