确保在进行网络请求之前首先发出另一个 Observable,但将响应作为冷可观察(带重播)提供给消费者

Ensure another Observable emitted first, before doing an network request but provide the response as cold observable (with replay) to the consumers

我想达到的目标:
如果通过提供不同的 Promises.
启用了某个功能,我有一个 FeaturesService 应该提供信息 启用的功能列表通过 HTTP API 作为 json 对象提供(参见 DataService)。
在发送获取功能列表的 HTTP 请求之前,我们需要确保首先对用户进行身份验证(参见 AuthService)。
此外:

到目前为止我想到了什么:

import { Subject, Observable, ReplaySubject } from "rxjs";
import { map, shareReplay, switchMap, take } from "rxjs/operators";
import { Container, injectable } from "inversify";
import "reflect-metadata";

interface DtoFeatures {
  supportedFeatures: string[];
}

@injectable()
class AuthService {
  private authenticated$: ReplaySubject<boolean> = new ReplaySubject<boolean>(1);

  public authenticate(): void {
    // Mock code
    console.log("Authenticate Request (Mock) started");
    setTimeout(() => {
      console.log("Authenticate Request (Mock) finished");
      this.authenticated$.next(true);
    }, 100);
  }

  public isAuthenticated(): Observable<boolean> {
    return this.authenticated$.asObservable();
  }
}

@injectable()
class DataService {
  public getFeatures(): Observable<DtoFeatures> {
    // Mock Code, real HTTP Request happening instead that needs to be authorized
    console.log("Network Request (Mock) started");
    const features$ = new Subject<DtoFeatures>();
    setTimeout(() => {
      console.log("Network Request (Mock) finished");
      features$.next({ supportedFeatures: ["featureX"] });
      features$.complete();
    }, 150);
    return features$;
  }
}

@injectable()
class FeaturesService {

  // private readonly dtoFeatures: Observable<DtoFeatures> = this.authService.isAuthenticated().pipe(
  //   take(1),
  //   switchMap(() => this.dataService.getFeatures()),
  //   shareReplay(1)
  // );

  private readonly dtoFeatures: Observable<DtoFeatures> = this.dataService.getFeatures().pipe(
    shareReplay(1)
  );

  constructor(
    private authService: AuthService,
    private dataService: DataService
  ) {}

  public supportedFeatures: Promise<string[]> = this.dtoFeatures.pipe(
    map((features) => features.supportedFeatures)
  ).toPromise();

  public isFeatureXSupported: Promise<boolean> = this.supportedFeatures.then(
    (features) => features.some((x) => x === "featureX")
  );

  public isFeatureYSupported: Promise<boolean> = this.supportedFeatures.then(
    (features) => features.some((x) => x === "featureY")
  );
}

@injectable()
class App {
  constructor(
    private authService: AuthService,
    private featureService: FeaturesService
  ) {
    console.log("App initialized");
    this.authService.authenticate();
    this.featureService.isFeatureXSupported.then((x) => console.log(`FeatureX enabled: ${x}`));
  }
}

// Configure DI
var container = new Container();
container.bind<AuthService>(AuthService).toSelf();
container.bind<DataService>(DataService).toSelf();
container.bind<FeaturesService>(FeaturesService).toSelf();
container.bind<App>(App).toSelf();

// Initialize App
container.resolve(App);

Link 到codesandbox: https://codesandbox.io/s/youthful-cerf-t43mi?file=/src/index.ts

给定的代码涵盖了大部分要求,除了功能请求发生在用户通过身份验证之前的问题。

控制台日志:

>Network Request (Mock) started <-- Problem!
>App initialized 
>Authenticate Request (Mock) started 
>Authenticate Request (Mock) finished 
>Network Request (Mock) finished 
>FeatureX enabled: true 

所以我尝试了这个版本,但是根本没有发生任何功能请求:

class FeaturesService {

  private readonly dtoFeatures: Observable<DtoFeatures> = this.authService.isAuthenticated().pipe(
     take(1),
     switchMap(() => this.dataService.getFeatures()),
     shareReplay(1)
  );

  // private readonly dtoFeatures: Observable<DtoFeatures> = this.dataService.getFeatures().pipe(
  //   shareReplay(1)
  // );

  ...
}

控制台日志:

>App initialized 
>Authenticate Request (Mock) started 
>Authenticate Request (Mock) finished 

我想我在这里混淆了与冷/热可观察量相关的东西,但我无法弄清楚问题所在。我真的很感激任何帮助,我认为这是 rxjs/reactive programming 的一个很好的现实世界用例,所以也许其他人也想知道如何实现它。

谢谢fridoo的解答!

第一个问题是DI配置错误,需要使用.inRequestScope().inSingletonScope() 注册服务时,例如:

container.bind<AuthService>(AuthService).toSelf().inRequestScope();

第二个问题是在 FeaturesService 中使用 .toPromise()。这导致即使没有功能服务的消费者也能完成网络请求。
通过一直使用 Observables,它会按预期工作。消费者也可以在他想使用 async / await 时使用 .toPromise()

完整代码示例:

import { Subject, Observable, ReplaySubject } from "rxjs";
import { map, shareReplay, switchMap, take } from "rxjs/operators";
import { Container, injectable } from "inversify";
import "reflect-metadata";

interface DtoFeatures {
  supportedFeatures: string[];
}

@injectable()
class AuthService {
  private authenticated$: ReplaySubject<boolean> = new ReplaySubject<boolean>(1);

  public authenticate(): void {
    // Mock code
    console.log("Authenticate Request (Mock) started");
    setTimeout(() => {
      console.log("Authenticate Request (Mock) finished");
      this.authenticated$.next(true);
    }, 100);
  }

  public isAuthenticated(): Observable<boolean> {
    return this.authenticated$.asObservable();
  }
}

@injectable()
class DataService {
  public getFeatures(): Observable<DtoFeatures> {
    // Mock Code, real HTTP Request happening instead that needs to be authorized
    console.log("Network Request (Mock) started");
    const features$ = new Subject<DtoFeatures>();
    setTimeout(() => {
      console.log("Network Request (Mock) finished");
      features$.next({ supportedFeatures: ["featureX"] });
      features$.complete();
    }, 150);
    return features$;
  }
}

@injectable()
class FeaturesService {

  private readonly dtoFeatures: Observable<DtoFeatures> = this.authService.isAuthenticated().pipe(
    take(1),
    switchMap(() => this.dataService.getFeatures()),
    shareReplay(1)
  );

  constructor(
    private authService: AuthService,
    private dataService: DataService
  ) {}

  public supportedFeatures: Observable<string[]> = this.dtoFeatures.pipe(
    map((features) => features.supportedFeatures)
  );

  public isFeatureXSupported: Observable<boolean> = this.supportedFeatures.pipe(
    map((features) => features.some((x) => x === "featureX"))
  );

  public isFeatureYSupported: Observable<boolean> = this.supportedFeatures.pipe(
    map((features) => features.some((x) => x === "featureY"))
  );
}

@injectable()
class App {
  constructor(
    private authService: AuthService,
    private featureService: FeaturesService
  ) {
    console.log("App initialized");
    this.authService.authenticate();

    // Comment this line out, to see that there is no (mocked) network request for features
    this.featureService.isFeatureXSupported.subscribe((x) => console.log(`FeatureX enabled: ${x}`));
    
    // Build promise at consumer level:
    // this.featureService.isFeatureXSupported.toPromise().then((x) => console.log(`FeatureX enabled: ${x}`));
  }
}

// Configure DI
var container = new Container();
container.bind<AuthService>(AuthService).toSelf().inRequestScope();
container.bind<DataService>(DataService).toSelf().inRequestScope();
container.bind<FeaturesService>(FeaturesService).toSelf().inRequestScope();
container.bind<App>(App).toSelf();

// Initialize App
container.resolve(App);

Codesandbox:https://codesandbox.io/s/confident-sammet-z1x2r?file=/src/index.ts

控制台日志(与消费者):

>App initialized 
>Authenticate Request (Mock) started 
>Authenticate Request (Mock) finished 
>Network Request (Mock) started 
>Network Request (Mock) finished 
>FeatureX enabled: true 

控制台日志(没有消费者):

>App initialized 
>Authenticate Request (Mock) started 
>Authenticate Request (Mock) finished