Sub/unsub cleanups (#1535)

* Sub/unsub cleanups

* drr full idAndIndex

* Memo on nick call

* refCountDelay

* Remove scheduler param, always asap

* Flatten

* Test cleanups

* Make tests pass

* Remove logs

* Drop nick memo

* sourceConnection.unsubscribe

* Small cleanups
This commit is contained in:
Jaco Greeff
2019-11-01 14:17:47 +01:00
committed by GitHub
parent f41ad2eee7
commit dc105e6b31
42 changed files with 209 additions and 110 deletions
+38 -32
View File
@@ -5,17 +5,47 @@
import { AccountId, AccountIndex, Address } from '@polkadot/types/interfaces';
import { Observable, of } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { map } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { isU8a } from '@polkadot/util';
import { decodeAddress } from '@polkadot/util-crypto';
import { createType } from '@polkadot/types';
import { idToIndex } from './idToIndex';
import { indexToId } from './indexToId';
import { drr } from '../util/drr';
import { drr } from '../util';
interface Calls {
idToIndexCall (accountId: string | AccountId): Observable<AccountIndex | undefined>;
indexToIdCall (accountIndex: string | AccountIndex): Observable<AccountId | undefined>;
}
export type AccountIdAndIndex = [AccountId?, AccountIndex?];
function retrieve (address: Address | AccountId | AccountIndex | string | null | undefined, { idToIndexCall, indexToIdCall }: Calls): Observable<AccountIdAndIndex> {
try {
// yes, this can fail, don't care too much, catch will catch it
const decoded = isU8a(address)
? address
: decodeAddress((address || '').toString());
if (decoded.length === 32) {
const accountId = createType('AccountId', decoded);
return idToIndexCall(accountId).pipe(
map((accountIndex): AccountIdAndIndex => [accountId, accountIndex] as AccountIdAndIndex)
);
}
const accountIndex = createType('AccountIndex', decoded);
return indexToIdCall(accountIndex).pipe(
map((accountId): AccountIdAndIndex => [accountId, accountIndex] as AccountIdAndIndex)
);
} catch (error) {
return of([undefined, undefined] as AccountIdAndIndex);
}
}
/**
* @name idAndIndex
* @param {(Address | AccountId | AccountIndex | string | null)} address - An accounts address in various formats.
@@ -30,35 +60,11 @@ export type AccountIdAndIndex = [AccountId?, AccountIndex?];
* ```
*/
export function idAndIndex (api: ApiInterfaceRx): (address?: Address | AccountId | AccountIndex | string | null) => Observable<AccountIdAndIndex> {
const idToIndexCall = idToIndex(api);
const indexToIdCall = indexToId(api);
return (address?: Address | AccountId | AccountIndex | string | null): Observable<AccountIdAndIndex> => {
try {
// yes, this can fail, don't care too much, catch will catch it
const decoded = isU8a(address)
? address
: decodeAddress((address || '').toString());
if (decoded.length === 32) {
const accountId = createType('AccountId', decoded);
return idToIndexCall(accountId).pipe(
startWith(undefined),
map((accountIndex): AccountIdAndIndex => [accountId, accountIndex] as AccountIdAndIndex),
drr()
);
}
const accountIndex = createType('AccountIndex', decoded);
return indexToIdCall(accountIndex).pipe(
startWith(undefined),
map((accountId): AccountIdAndIndex => [accountId, accountIndex] as AccountIdAndIndex),
drr()
);
} catch (error) {
return of([undefined, undefined] as AccountIdAndIndex).pipe(drr());
}
const calls = {
idToIndexCall: idToIndex(api),
indexToIdCall: indexToId(api)
};
return (address?: Address | AccountId | AccountIndex | string | null): Observable<AccountIdAndIndex> =>
retrieve(address, calls).pipe(drr());
}
@@ -9,7 +9,7 @@ import { map, startWith } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { indexes, AccountIndexes } from './indexes';
import { drr } from '../util/drr';
import { drr } from '../util';
/**
* @name idToIndex
@@ -10,7 +10,7 @@ import { ApiInterfaceRx } from '@polkadot/api/types';
import { ENUMSET_SIZE } from '@polkadot/types/primitive/Generic/AccountIndex';
import { createType, ClassOf, Vec } from '@polkadot/types';
import { drr } from '../util/drr';
import { drr } from '../util';
/**
* @name indexToId
+1 -1
View File
@@ -10,7 +10,7 @@ import { map, switchMap } from 'rxjs/operators';
import { ENUMSET_SIZE } from '@polkadot/types/primitive/Generic/AccountIndex';
import { createType } from '@polkadot/types';
import { drr } from '../util/drr';
import { drr } from '../util';
export type AccountIndexes = Record<string, AccountIndex>;
+23 -18
View File
@@ -7,39 +7,44 @@ import { AccountId, AccountIndex, Address, Balance } from '@polkadot/types/inter
import { Codec } from '@polkadot/types/types';
import { DeriveAccountInfo } from '../types';
import { Observable, of } from 'rxjs';
import { Observable, of, combineLatest } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { Bytes, Option, u32 } from '@polkadot/types';
import { u8aToString } from '@polkadot/util';
import { drr } from '../util/drr';
import { drr } from '../util';
import { idAndIndex } from './idAndIndex';
function retrieveNick (api: ApiInterfaceRx): (accountId?: AccountId) => Observable<Option<[Bytes, Balance] & Codec> | undefined> {
return (accountId?: AccountId): Observable<Option<[Bytes, Balance] & Codec> | undefined> =>
accountId && api.query.nicks
? api.query.nicks.nameOf<Option<[Bytes, Balance] & Codec>>(accountId)
: of(undefined);
}
/**
* @name info
* @description Returns aux. info with regards to an account, current that includes the accountId, accountIndex and nickname
*/
export function info (api: ApiInterfaceRx): (address?: AccountIndex | AccountId | Address | string | null) => Observable<DeriveAccountInfo> {
const idAndIndexCall = idAndIndex(api);
const nickCall = retrieveNick(api);
// TODO We would really like to pass in an Address or AccountIndex here as well
return (address?: AccountIndex | AccountId | Address | string | null): Observable<DeriveAccountInfo> =>
idAndIndexCall(address).pipe(
switchMap(([accountId, accountIndex]): Observable<DeriveAccountInfo> =>
accountId && api.query.nicks
? api.query.nicks
.nameOf<Option<[Bytes, Balance] & Codec>>(accountId)
.pipe(
map((result): DeriveAccountInfo => ({
accountId,
accountIndex,
nickname: result.isSome
? u8aToString(result.unwrap()[0]).substr(0, (api.consts.nicks.maxLength as u32).toNumber())
: undefined
}))
)
: of({ accountId, accountIndex })
),
switchMap(([accountId, accountIndex]): Observable<[DeriveAccountInfo, Option<[Bytes, Balance] & Codec>?]> => {
return combineLatest([
of({ accountId, accountIndex }),
nickCall(accountId)
]);
}),
map(([{ accountId, accountIndex }, nameOf]): DeriveAccountInfo => ({
accountId,
accountIndex,
nickname: nameOf && nameOf.isSome
? u8aToString(nameOf.unwrap()[0]).substr(0, (api.consts.nicks.maxLength as u32).toNumber())
: undefined
})),
drr()
);
}
+1 -1
View File
@@ -14,7 +14,7 @@ import { bnMax } from '@polkadot/util';
import { info } from '../accounts/info';
import { bestNumber } from '../chain/bestNumber';
import { DerivedBalances } from '../types';
import { drr } from '../util/drr';
import { drr } from '../util';
type ResultBalance = [Balance, Balance, BalanceLock[], Option<VestingSchedule>];
type Result = [AccountId, BlockNumber, ResultBalance, Index];
+1 -1
View File
@@ -9,7 +9,7 @@ import { DerivedFees } from '../types';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { drr } from '../util/drr';
import { drr } from '../util';
type Result = [Balance, Balance, Balance, Balance, Balance];
@@ -8,7 +8,7 @@ import { combineLatest, Observable, of } from 'rxjs';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { DerivedBalances } from '../types';
import { drr } from '../util/drr';
import { drr } from '../util';
import { all } from './all';
export function votingBalances (api: ApiInterfaceRx): (addresses?: (AccountId | AccountIndex | Address | string)[]) => Observable<DerivedBalances[]> {
@@ -11,7 +11,7 @@ import { Vec } from '@polkadot/types';
import { info } from '../accounts/info';
import { DerivedBalances } from '../types';
import { drr } from '../util/drr';
import { drr } from '../util';
import { votingBalances } from './votingBalances';
export function votingBalancesNominatorsFor (api: ApiInterfaceRx): (address: AccountId | AccountIndex | Address | string) => Observable<DerivedBalances[]> {
+1 -1
View File
@@ -8,7 +8,7 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { drr } from '../util/drr';
import { drr } from '../util';
/**
* @name bestNumber
@@ -8,7 +8,7 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { drr } from '../util/drr';
import { drr } from '../util';
/**
* @name bestNumberFinalized
@@ -9,7 +9,7 @@ import { map } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { createType } from '@polkadot/types';
import { drr } from '../util/drr';
import { drr } from '../util';
import { bestNumber } from './bestNumber';
import { bestNumberFinalized } from './bestNumberFinalized';
+1 -1
View File
@@ -7,7 +7,7 @@ import { catchError, map } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { HeaderExtended } from '../type';
import { drr } from '../util/drr';
import { drr } from '../util';
import { HeaderAndValidators } from './subscribeNewHeads';
/**
@@ -9,7 +9,7 @@ import { Observable, combineLatest, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { HeaderExtended } from '../type';
import { drr } from '../util/drr';
import { drr } from '../util';
export type HeaderAndValidators = [Header, AccountId[]];
+1 -1
View File
@@ -9,7 +9,7 @@ import BN from 'bn.js';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { drr } from '../util/drr';
import { drr } from '../util';
type ResultV2 = [BN, BN, BN, BN, BN, BN, BN, BN, BN, BN];
@@ -12,7 +12,7 @@ import { Option } from '@polkadot/types';
import { isNull } from '@polkadot/util';
import { ReferendumInfoExtended } from '../type';
import { drr } from '../util/drr';
import { drr } from '../util';
export function constructInfo (index: BN | number, optionInfo?: Option<ReferendumInfo>): Option<ReferendumInfoExtended> {
const info = optionInfo
@@ -11,7 +11,7 @@ import { ApiInterfaceRx } from '@polkadot/api/types';
import { Option, Vec } from '@polkadot/types';
import { ReferendumInfoExtended } from '../type';
import { drr } from '../util/drr';
import { drr } from '../util';
import { constructInfo } from './referendumInfo';
export function referendumInfos (api: ApiInterfaceRx): (ids?: (BN | number)[]) => Observable<Option<ReferendumInfoExtended>[]> {
@@ -11,7 +11,7 @@ import { ApiInterfaceRx } from '@polkadot/api/types';
import { Vec, createType } from '@polkadot/types';
import { DerivedBalances, DerivedReferendumVote } from '../types';
import { drr } from '../util/drr';
import { drr } from '../util';
import { votes } from './votes';
import { votingBalances } from '../balances/votingBalances';
@@ -11,7 +11,7 @@ import { ApiInterfaceRx } from '@polkadot/api/types';
import { Option } from '@polkadot/types';
import { ReferendumInfoExtended } from '../type';
import { drr } from '../util/drr';
import { drr } from '../util';
import { referendumInfos } from './referendumInfos';
export function referendums (api: ApiInterfaceRx): () => Observable<Option<ReferendumInfoExtended>[]> {
+1 -1
View File
@@ -9,7 +9,7 @@ import { Observable, of } from 'rxjs';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { Vec } from '@polkadot/types';
import { drr } from '../util/drr';
import { drr } from '../util';
export function votes (api: ApiInterfaceRx): (referendumId: BN, accountIds?: AccountId[]) => Observable<Vote[]> {
return (referendumId: BN, accountIds: AccountId[] = []): Observable<Vote[]> => {
@@ -9,7 +9,7 @@ import { Observable } from 'rxjs';
import { Vec } from '@polkadot/types';
import { switchMap, map } from 'rxjs/operators';
import { approvalFlagsToBools } from '../util/approvalFlagsToBools';
import { drr } from '../util/drr';
import { drr } from '../util';
/**
* @name approvalsOf
@@ -11,7 +11,7 @@ import { ApiInterfaceRx } from '@polkadot/api/types';
import { Vec } from '@polkadot/types';
import { approvalFlagsToBools } from '../util/approvalFlagsToBools';
import { drr } from '../util/drr';
import { drr } from '../util';
/**
* @name approvalsOfAt
+1 -1
View File
@@ -11,7 +11,7 @@ import { ApiInterfaceRx } from '@polkadot/api/types';
import { createType, Vec, u32 } from '@polkadot/types';
import { DerivedElectionsInfo } from '../types';
import { drr } from '../util/drr';
import { drr } from '../util';
type ResultElectionsInner = [u32, u32, Vec<[AccountId, BlockNumber] & Codec>, SetIndex, BlockNumber, VoteIndex, SetIndex];
type ResultElections = [Vec<AccountId>, ResultElectionsInner];
@@ -10,7 +10,7 @@ import { DerivedVoterPositions } from '../types';
import BN from 'bn.js';
import { of, combineLatest, Observable } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';
import { drr } from '../util/drr';
import { drr } from '../util';
/**
* @name voterPositions
+1 -1
View File
@@ -10,7 +10,7 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { createType, Vec } from '@polkadot/types';
import { drr } from '../util/drr';
import { drr } from '../util';
import { voterPositions } from './voterPositions';
/**
@@ -10,7 +10,7 @@ import { map, switchMap } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { Bytes, Vec, u32 } from '@polkadot/types';
import { drr } from '../util/drr';
import { drr } from '../util';
/**
* @description Return a boolean array indicating whether the passed accounts had received heartbeats in the current session
+1 -1
View File
@@ -8,7 +8,7 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { drr } from '../util/drr';
import { drr } from '../util';
import { info } from './info';
export function eraLength (api: ApiInterfaceRx): () => Observable<BlockNumber> {
@@ -7,7 +7,7 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { drr } from '../util/drr';
import { drr } from '../util';
import { info } from './info';
export function eraProgress (api: ApiInterfaceRx): () => Observable<BN> {
+1 -1
View File
@@ -10,7 +10,7 @@ import { map } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { Option, u64, createType } from '@polkadot/types';
import { drr } from '../util/drr';
import { drr } from '../util';
import { bestNumber } from '../chain';
type ResultV1Session = [SessionIndex, Option<BlockNumber>, BlockNumber, BlockNumber, SessionIndex];
@@ -8,7 +8,7 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { drr } from '../util/drr';
import { drr } from '../util';
import { info } from './info';
export function sessionProgress (api: ApiInterfaceRx): () => Observable<BlockNumber> {
+1 -1
View File
@@ -8,7 +8,7 @@ import { Observable, combineLatest } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { drr } from '../util/drr';
import { drr } from '../util';
import { controllers } from './controllers';
import { info } from './info';
@@ -10,7 +10,7 @@ import { switchMap } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { Option } from '@polkadot/types';
import { drr } from '../util/drr';
import { drr } from '../util';
/**
* @description From the list of stash accounts, retrieve the list of controllers
+1 -1
View File
@@ -20,7 +20,7 @@ import { isUndefined } from '@polkadot/util';
import { bestNumber } from '../chain/bestNumber';
import { eraLength } from '../session/eraLength';
import { recentlyOffline } from './recentlyOffline';
import { drr } from '../util/drr';
import { drr } from '../util';
interface Calls {
bestNumberCall (): Observable<BlockNumber>;
@@ -11,7 +11,7 @@ import { map } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { DerivedRecentlyOffline } from '../types';
import { drr } from '../util/drr';
import { drr } from '../util';
type OfflineResult = [AccountId, BlockNumber, BN][] & Codec;
+2
View File
@@ -2,4 +2,6 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export { drr } from '@polkadot/rpc-core/rxjs';
export * from './approvalFlagsToBools';
+26 -7
View File
@@ -44,22 +44,41 @@ describe('Cached Observables', (): void => {
expect(observable2).not.toBe(observable1);
});
it('clears cache if there are no more subscribers', (): void => {
it('subscribes to the same one if within the period (unbsub delay)', (done): void => {
const observable1 = rpc.chain.subscribeNewHeads();
const sub1 = observable1.subscribe();
sub1.unsubscribe();
setTimeout((): void => {
const observable2 = rpc.chain.subscribeNewHeads();
const sub2 = observable2.subscribe();
expect(observable1).toBe(observable2);
sub2.unsubscribe();
done();
}, 500);
});
it('clears cache if there are no more subscribers', (done): void => {
const observable1 = rpc.chain.subscribeNewHeads();
const observable2 = rpc.chain.subscribeNewHeads();
expect(observable1).toBe(observable2);
const sub1 = observable1.subscribe();
const sub2 = observable2.subscribe();
expect(observable1).toBe(observable2);
sub1.unsubscribe();
sub2.unsubscribe();
// No more subscribers, now create a new observable
const observable3 = rpc.chain.subscribeNewHeads();
setTimeout((): void => {
// No more subscribers, now create a new observable
const observable3 = rpc.chain.subscribeNewHeads();
expect(observable3).not.toBe(observable1);
expect(observable3).not.toBe(observable1);
done();
}, 3500);
});
it('creates different observables for different methods but same arguments', (): void => {
+19 -15
View File
@@ -10,7 +10,7 @@ import { RpcInterfaceMethod, UserRpc } from './types';
import memoizee from 'memoizee';
import { combineLatest, from, Observable, Observer, of, throwError } from 'rxjs';
import { catchError, distinctUntilChanged, map, publishReplay, refCount, switchMap } from 'rxjs/operators';
import { catchError, map, publishReplay, refCount, switchMap } from 'rxjs/operators';
import jsonrpc from '@polkadot/jsonrpc';
import jsonrpcMethod from '@polkadot/jsonrpc/create/method';
import jsonrpcParam from '@polkadot/jsonrpc/create/param';
@@ -18,6 +18,8 @@ import { Option, StorageKey, Vec, createClass } from '@polkadot/types';
import { createTypeUnsafe } from '@polkadot/types/codec';
import { assert, isFunction, isNull, isNumber, logger, u8aToU8a } from '@polkadot/util';
import { drr } from './rxjs';
type UserRpcConverted = Record<string, Record<string, RpcMethod>>;
const l = logger('rpc-core');
@@ -223,6 +225,19 @@ export default class Rpc implements RpcInterface {
return call as RpcInterfaceMethod;
}
// create a subscriptor, it subscribes once and resolves with the id as subscribe
private createSubscriber ({ subType, subName, paramsJson, update }: { subType: string; subName: string; paramsJson: AnyJson[]; update: (error?: Error, result?: any) => void }, errorHandler: (error: Error) => void): Promise<number> {
return new Promise((resolve, reject): void => {
this.provider
.subscribe(subType, subName, paramsJson, update)
.then(resolve)
.catch((error): void => {
errorHandler(error);
reject(error);
});
});
}
private createMethodSubscribe (method: RpcMethod): RpcInterfaceMethod {
const [updateType, subMethod, unsubMethod] = method.pubsub;
const subName = `${method.section}_${subMethod}`;
@@ -253,9 +268,7 @@ export default class Rpc implements RpcInterface {
observer.next(this.formatOutput(method, params, result));
};
subscriptionPromise = this.provider
.subscribe(subType, subName, paramsJson, update)
.catch(errorHandler);
subscriptionPromise = this.createSubscriber({ subType, subName, paramsJson, update }, errorHandler);
} catch (error) {
errorHandler(error);
}
@@ -281,18 +294,9 @@ export default class Rpc implements RpcInterface {
? this.provider.unsubscribe(subType, unsubName, subscriptionId)
: Promise.resolve(false)
)
.catch((error: Error): void =>
l.error(this.createErrorMessage(method, error))
);
.catch((error: Error): void => l.error(this.createErrorMessage(method, error)));
};
}).pipe(
// Duplicated in api-derive/util/drr
distinctUntilChanged((a: any, b: any): boolean =>
JSON.stringify({ value: a }) === JSON.stringify({ value: b })
),
publishReplay(1),
refCount()
);
}).pipe(drr());
};
const memoized = memoizee(call, {
+2 -2
View File
@@ -57,12 +57,12 @@ describe('replay', (): void => {
const subscription = rpc.chain.subscribeNewHeads().subscribe((): void => {
subscription.unsubscribe();
// There's a promise inside .unsubscribe(), wait a bit
// There's a promise inside .unsubscribe(), wait a bit (> 2s)
setTimeout((): void => {
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(rpc.provider.unsubscribe).toHaveBeenCalled();
done();
}, 200);
}, 3500);
});
});
});
@@ -1,11 +1,13 @@
// Copyright 2017-2019 @polkadot/api-derive authors & contributors
// Copyright 2017-2019 @polkadot/rpc-core authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { catchError, distinctUntilChanged, publishReplay, refCount } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { catchError, distinctUntilChanged, publishReplay } from 'rxjs/operators';
import { logger } from '@polkadot/util';
import { refCountDelay } from './refCountDelay';
type DrrResult = <T> (source$: Observable<T>) => Observable<T>;
const l = logger('drr');
@@ -23,8 +25,8 @@ export const drr = (): DrrResult => <T> (source$: Observable<T>): Observable<T>
throw error;
}),
distinctUntilChanged((a: any, b: any): boolean =>
JSON.stringify({ value: a }) === JSON.stringify({ value: b })
JSON.stringify({ t: a }) === JSON.stringify({ t: b })
),
publishReplay(1),
refCount()
refCountDelay()
);
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2019 @polkadot/rpc-core authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export * from './drr';
export * from './refCountDelay';
@@ -0,0 +1,55 @@
// Copyright 2017-2019 @polkadot/rpc-core authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
// From https://medium.com/@volkeron/rxjs-unsubscribe-delay-218a9ab2672e (adapted for rxjs 6)
// v6 inspiration from https://github.com/cartant/rxjs-etc/blob/7d7cd21233f67c364fd246ef72077bbe494dd5c2/source/operators/refCountDelay.ts
import { asapScheduler, ConnectableObservable, MonoTypeOperatorFunction, NEVER, Observable, Subject, Subscription, timer, using } from 'rxjs';
import { scan, switchMap, tap } from 'rxjs/operators';
const DELAY = 1750;
// subscribe to a source, tracker and increment the counter
function subscribe <T> (source: Observable<T>, counter: Subject<number>, tracker: Observable<number>): [Subscription, Subscription] {
const sourceConn = (source as ConnectableObservable<T>).connect();
const trackerConn = tracker.subscribe();
counter.next(1);
return [sourceConn, trackerConn];
}
// unsubscribe from both the tracker and the source
function unsubscribe (sourceConn: Subscription | undefined, trackerConn: Subscription | undefined): void {
sourceConn && sourceConn.unsubscribe();
trackerConn && trackerConn.unsubscribe();
}
// keep track of references, but only unsubscribe after the configured delay
export function refCountDelay <T> (): MonoTypeOperatorFunction<T> {
return (source: Observable<T>): Observable<T> => {
let sourceConn: Subscription | undefined;
let trackerConn: Subscription | undefined;
const counter = new Subject<number>();
const tracker = counter.pipe(
scan((total, change) => change + total, 0),
switchMap((count) =>
count === 0 // when we have a zero count, schedule a unsubscribe
? timer(DELAY, asapScheduler).pipe(tap((): void => unsubscribe(sourceConn, trackerConn)))
: NEVER
)
);
return using(
() => {
[sourceConn, trackerConn] = subscribe(source, counter, tracker);
return {
unsubscribe: (): void => counter.next(-1)
};
},
(): Observable<T> => source
);
};
}