Skip to content

Commit 15fbf4d

Browse files
authored
refactor: reduce RxJS overhead (#184)
1 parent 7048308 commit 15fbf4d

File tree

1 file changed

+33
-18
lines changed

1 file changed

+33
-18
lines changed

projects/ngneat/helipopper/src/lib/tippy.factory.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,55 @@
11
import type tippy from 'tippy.js';
2-
import { inject, Injectable, NgZone } from '@angular/core';
3-
import { defer, from, map, type Observable, of, shareReplay } from 'rxjs';
2+
import { inject, Injectable, NgZone, ɵisPromise as isPromise } from '@angular/core';
3+
import { defer, map, Observable, of, tap } from 'rxjs';
44
import { TIPPY_LOADER, type TippyProps } from '@ngneat/helipopper/config';
55

6-
// We need to use `isPromise` instead of checking whether
7-
// `value instanceof Promise`. In zone.js patched environments, `global.Promise`
8-
// is the `ZoneAwarePromise`.
9-
// `import(...)` returns a native promise (not a `ZoneAwarePromise`), causing
10-
// `instanceof` check to be falsy.
11-
function isPromise<T>(value: any): value is Promise<T> {
12-
return typeof value?.then === 'function';
13-
}
14-
156
@Injectable({ providedIn: 'root' })
167
export class TippyFactory {
178
private readonly _ngZone = inject(NgZone);
189

1910
private readonly _loader = inject(TIPPY_LOADER);
2011

2112
private _tippyImpl$: Observable<typeof tippy> | null = null;
13+
private _tippy: typeof tippy | null = null;
2214

2315
/**
2416
* This returns an observable because the user should provide a `loader`
2517
* function, which may return a promise if the tippy.js library is to be
2618
* loaded asynchronously.
2719
*/
2820
create(target: HTMLElement, props?: Partial<TippyProps>) {
29-
// We use `shareReplay` to ensure that subsequent emissions are
30-
// synchronous and to avoid triggering the `defer` callback repeatedly
31-
// when new subscribers arrive.
3221
this._tippyImpl$ ||= defer(() => {
22+
if (this._tippy) return of(this._tippy);
23+
24+
// Call the `loader` function lazily — only when a subscriber
25+
// arrives — to avoid importing `tippy.js` on the server.
3326
const maybeTippy = this._ngZone.runOutsideAngular(() => this._loader());
34-
return isPromise(maybeTippy)
35-
? from(maybeTippy).pipe(map((tippy) => tippy.default))
36-
: of(maybeTippy);
37-
}).pipe(shareReplay());
27+
28+
let tippy$: Observable<typeof tippy>;
29+
// We need to use `isPromise` instead of checking whether
30+
// `result instanceof Promise`. In zone.js patched environments, `global.Promise`
31+
// is the `ZoneAwarePromise`. Some APIs, which are likely not patched by zone.js
32+
// for certain reasons, might not work with `instanceof`. For instance, the dynamic
33+
// import `() => import('./chunk.js')` returns a native promise (not a `ZoneAwarePromise`),
34+
// causing this check to be falsy.
35+
if (isPromise(maybeTippy)) {
36+
// This pulls less RxJS symbols compared to using `from()` to resolve a promise value.
37+
tippy$ = new Observable((subscriber) => {
38+
maybeTippy.then((tippy) => {
39+
subscriber.next(tippy.default);
40+
subscriber.complete();
41+
});
42+
});
43+
} else {
44+
tippy$ = of(maybeTippy);
45+
}
46+
47+
return tippy$.pipe(
48+
tap((tippy) => {
49+
this._tippy = tippy;
50+
})
51+
);
52+
});
3853

3954
return this._tippyImpl$.pipe(
4055
map((tippy) => {

0 commit comments

Comments
 (0)