如何监视RXJS订阅数?

时间:2019-05-18 04:22:09

标签: node.js typescript rxjs

我正在使用Observable为来自全局资源的客户端提供事件订阅接口,并且我需要根据活动订阅的数量来管理该资源:

  • 订阅数大于0时分配全局资源
  • 订阅数变为0时释放全局资源
  • 根据订阅数调整资源使用策略

RXJS中监视活动订阅数的正确方法是什么?


如何在RXJS语法中实现以下内容? -

const myEvent: Observable<any> = new Observable();

myEvent.onSubscription((newCount: number, prevCount: number) => {
   if(newCount === 0) {
      // release global resource
   } else {
      // allocate global resource, if not yet allocated
   }
   // for a scalable resource usage / load,
   // re-configure it, based on newCount
});

我不希望每次更改都得到保证的通知,因此newCount + prevCount参数。

UPDATE-1

这不是对this的重复,因为预订数量发生变化时,我需要得到通知,而不仅仅是在某个时候获得计数器。

UPDATE-2

到目前为止,没有任何答案,我通过完全封装(特别是类型Subject)提出了very ugly and limited work-around。希望能找到合适的解决方案。

UPDATE-3

在回答了几个问题之后,我仍然不确定如何实现我正在尝试的内容,如下所示:

class CustomType {

}

class CountedObservable<T> extends Observable<T> {

    private message: string; // random property

    public onCount; // magical Observable that needs to be implemented

    constructor(message: string) {
        // super(); ???
        this.message = message;
    }

    // random method
    public getMessage() {
        return this.message;
    }
}

const a = new CountedObservable<CustomType>('hello'); // can create directly

const msg = a.getMessage(); // can call methods

a.subscribe((data: CustomType) => {
    // handle subscriptions here;
});

// need that magic onCount implemented, so I can do this:
a.onCount.subscribe((newCount: number, prevCont: number) => {
    // manage some external resources
});

如何实现上面的类CountedObservable,让我订阅自己的类及其onCount属性,以监视其客户/订阅的数量?

UPDATE-4

所有建议的解决方案似乎都过于复杂,尽管我接受了其中一个答案,但最终还是得到了completely custom solution one of my own

4 个答案:

答案 0 :(得分:5)

您可以使用defer来跟踪订阅,使用finalize来跟踪完成,例如作为操作员:

// a custom operator that will count number of subscribers
function customOperator(onCountUpdate = noop) {
  return function refCountOperatorFunction(source$) {
    let counter = 0;

    return defer(()=>{
      counter++;
      onCountUpdate(counter);
      return source$;
    })
    .pipe(
      finalize(()=>{
        counter--;
        onCountUpdate(counter);
      })
    );
  };
}

// just a stub for `onCountUpdate`
function noop(){}

然后像这样使用它:

const source$ = new Subject();

const result$ = source$.pipe(
  customOperator( n => console.log('Count updated: ', n) )
);

这里有一个代码片段说明了这一点:

const { Subject, of, timer, pipe, defer } = rxjs;
const { finalize, takeUntil } = rxjs.operators;


const source$ = new Subject();

const result$ = source$.pipe(
  customOperator( n => console.log('Count updated: ', n) )
);

// emit events
setTimeout(()=>{
  source$.next('one');
}, 250);

setTimeout(()=>{
  source$.next('two');
}, 1000);

setTimeout(()=>{
  source$.next('three');
}, 1250);

setTimeout(()=>{
  source$.next('four');
}, 1750);


// subscribe and unsubscribe
const subscriptionA = result$
  .subscribe(value => console.log('A', value));

setTimeout(()=>{
  result$.subscribe(value => console.log('B', value));
}, 500);


setTimeout(()=>{
  result$.subscribe(value => console.log('C', value));
}, 1000);

setTimeout(()=>{
  subscriptionA.unsubscribe();
}, 1500);


// complete source
setTimeout(()=>{
  source$.complete();
}, 2000);


function customOperator(onCountUpdate = noop) {
  return function refCountOperatorFunction(source$) {
    let counter = 0;

    return defer(()=>{
      counter++;
      onCountUpdate(counter);
      return source$;
    })
    .pipe(
      finalize(()=>{
        counter--;
        onCountUpdate(counter);
      })
    );
  };
}

function noop(){}
<script src="https://unpkg.com/rxjs@6.4.0/bundles/rxjs.umd.min.js"></script>

*注意:如果您的货源价格很冷,则可能需要share

希望有帮助

答案 1 :(得分:5)

您实际上在这里问三个独立的问题,我质疑您是否真的需要您提到的全部功能。由于您要求的大多数资源管理内容已由库提供,因此执行自定义跟踪代码似乎是多余的。前两个问题:

  • 订阅数大于0时分配全局资源
  • 订阅数变为0时释放全局资源

可以使用using + share运算符来完成:

class ExpensiveResource {
  constructor () {
    // Do construction
  }
  unsubscribe () {
   // Do Tear down
  }
}

// Creates a resource and ties its lifecycle with that of the created `Observable`
// generated by the second factory function
// Using will accept anything that is "Subscription-like" meaning it has a unsubscribe function.
const sharedStream$ = using(
  // Creates an expensive resource
  () => new ExpensiveResource(), 
  // Passes that expensive resource to an Observable factory function
  er => timer(1000)
)
// Share the underlying source so that global creation and deletion are only
// processed when the subscriber count changes between 0 and 1 (or visa versa)
.pipe(share())

之后,sharedStream$可以作为基本流传递,该基本流将管理基础资源(假设您正确实现了unsubscribe),以便随着订户在0到1之间转换。

  • 根据订阅数调整资源使用策略

    我最怀疑的第三个问题是,为了完整起见,我会回答这个问题,前提是您比我更了解您的应用程序(因为我无法想到为什么您需要在不同使用级别下进行特定处理的原因)而不是介于0和1之间。)

基本上,我将使用与上述类似的方法,但是在转换逻辑上的封装会略有不同。

// Same as above
class ExpensiveResource {
  unsubscribe() {  console.log('Tear down this resource!')}
}

const usingReferenceTracking = 
  (onUp, onDown) => (resourceFactory, streamFactory) => {
    let instance, refCount = 0
    // Again manage the global resource state with using
    const r$ = using(
      // Unfortunately the using pattern doesn't let the resource escape the closure
      // so we need to cache it for ourselves to use later
      () => instance || (instance = resourceFactory()),
      // Forward stream creation as normal
      streamFactory
      )
    ).pipe(
      // Don't forget to clean up the stream after all is said and done
      // Because its behind a share this should only happen when all subscribers unsubscribe
      finalize(() => instance = null)
      share()
    )
    // Use defer to trigger "onSubscribe" side-effects
    // Note as well that these side-effects could be merged with the above for improved performance
    // But I prefer them separate for easier maintenance.
    return defer(() => onUp(instance, refCount += 1) || r$)
      // Use finalize to handle the "onFinish" side-effects
      .pipe(finalize(() => onDown(instance, refCount -= 1)))

}

const referenceTracked$ = usingReferenceTracking(
  (ref, count) => console.log('Ref count increased to ' + count),
  (ref, count) => console.log('Ref count decreased to ' + count)
)(
  () => new ExpensiveResource(),
  ref => timer(1000)
)

referenceTracked$.take(1).subscribe(x => console.log('Sub1 ' +x))
referenceTracked$.take(1).subscribe(x => console.log('Sub2 ' +x))


// Ref count increased to 1
// Ref count increased to 2
// Sub1 0
// Ref count decreased to 1
// Sub2 0
// Ref count decreased to 0
// Tear down this resource!

警告:这样做的一个副作用是,根据定义,流离开usingReferenceTracking函数后将变热,并且在首次订阅时将变热。确保在订阅阶段考虑到这一点。

答案 2 :(得分:3)

多么有趣的问题!如果我了解您的要求,这是我的解决方案:围绕Observable创建一个包装器类,该类通过截获[Unit] Description=uWSGI instance to serve itinapinch_rep After=network.target [Service] User=reports Group=nginx WorkingDirectory=/home/reports/itinapinch_rep Environment="PATH=/home/reports/itinapinch_rep/it_venv/bin" ExecStart=/home/reports/itinapinch_rep/it_venv/bin/uwsgi --ini itinapinch_rep.ini [Install] WantedBy=multi-user.target subscribe()来跟踪订阅。这是包装器类:

unsubscribe()

此包装器创建了一个可观察的辅助观察对象export class CountSubsObservable<T> extends Observable<T>{ private _subCount = 0; private _subCount$: BehaviorSubject<number> = new BehaviorSubject(0); public subCount$ = this._subCount$.asObservable(); constructor(public source: Observable<T>) { super(); } subscribe( observerOrNext?: PartialObserver<T> | ((value: T) => void), error?: (error: any) => void, complete?: () => void ): Subscription { this._subCount++; this._subCount$.next(this._subCount); let subscription = super.subscribe(observerOrNext as any, error, complete); const newUnsub: () => void = () => { if (this._subCount > 0) { this._subCount--; this._subCount$.next(this._subCount); subscription.unsubscribe(); } } subscription.unsubscribe = newUnsub; return subscription; } } ,它在每次对源可观察的更改进行订阅时都会发出。它将发出与当前订户数量相对应的号码。

要使用它,您将创建一个可观察的源,然后使用此类调用new来创建包装器。例如:

.subCount$

要查看其使用情况,请查看此Stackblitz

更新:

好吧,正如评论中提到的那样,我在努力了解数据流的来源。回顾您的问题,我看到您正在提供“事件订阅界面”。如果数据流是const source$ = interval(1000).pipe(take(10)); const myEvent$: CountSubsObservable<number> = new CountSubsObservable(source$); myEvent$.subCount$.subscribe(numSubs => { console.log('subCount$ notification! Number of subscriptions is now', numSubs); if(numSubs === 0) { // release global resource } else { // allocate global resource, if not yet allocated } // for a scalable resource usage / load, // re-configure it, based on numSubs }); source$.subscribe(result => console.log('result is ', result)); 的流(如您在上面的第三次更新中所详述),那么您可能希望使用CustomType中的fromEvent()来创建可观察的源我提供的包装器类。

为此,我创建了一个新的Stackblitz。来自rxjs的信息流,以及如何使用CountedObservable类实现您正在寻找的东西。

CustomType

我希望这会有所帮助。

答案 3 :(得分:1)

首先,我非常感谢人们花了很多时间和精力来回答我的问题!而且我敢肯定,这些答案将对其他开发人员使用RXJS解决类似情况提供有用的指导。

但是,特别是对于我试图摆脱RXJS的东西,我最终发现最好完全不使用它。我特别想要以下内容:

一个通用,易于使用的界面,用于订阅通知以及监视订阅-全部合而为一。对于RXJS,我最终会得到的最好的解决方法是,对于不是RXJS专家的开发人员来说,这些解决方案似乎不必要地令人费解,甚至难以理解。那不是我认为友好的界面,更像是过度设计的东西。

最后我得到了一个自定义,简单得多的界面,该界面可以完成我一直在寻找的一切:

export class Subscription {
    private unsub: () => void;

    constructor(unsub: () => void) {
        this.unsub = unsub;
    }

    public unsubscribe(): void {
        if (this.unsub) {
            this.unsub();
            this.unsub = null; // to prevent repeated calls
        }
    }
}

export class Observable<T = any> {
    protected subs: ((data: T) => void)[] = [];

    public subscribe(cb: (data: T) => void): Subscription {
        this.subs.push(cb);
        return new Subscription(this.createUnsub(cb));
    }

    public next(data: T): void {
        // we iterate through a safe clone, in case an un-subscribe occurs;
        // and since Node.js is the target, we are using process.nextTick:
        [...this.subs].forEach(cb => process.nextTick(() => cb(data)));
    }

    protected createUnsub(cb) {
        return () => {
            this.subs.splice(this.subs.indexOf(cb), 1);
        };
    }
}

export interface ISubCounts {
    newCount: number;
    prevCount: number;
}

export class CountedObservable<T = any> extends Observable<T> {    
    readonly onCount: Observable<ISubCounts> = new Observable();

    protected createUnsub(cb) {
        const s = this.subs;
        this.onCount.next({newCount: s.length, prevCount: s.length - 1});
        return () => {
            s.splice(s.indexOf(cb), 1);
            this.onCount.next({newCount: s.length, prevCount: s.length + 1});
        };
    }
}

它既小巧又优雅,可以让我以安全和友好的方式做开始时需要做的所有事情。我可以执行相同的subscribeonCount.subscribe,并获得所有相同的通知:

const a = new CountedObservable<string>();

const countSub = a.onCount.subscribe(({newCount, prevCount}) => {
    console.log('COUNTS:', newCount, prevCount);
});

const sub1 = a.subscribe(data => {
    console.log('SUB-1:', data);
});

const sub2 = a.subscribe(data => {
    console.log('SUB-2:', data);
});

a.next('hello');

sub1.unsubscribe();
sub2.unsubscribe();
countSub.unsubscribe();

我希望这也会对其他人有所帮助。

P.S。我将其进一步改进为an independent module