Your methods definitely shouldn't be coupled to the context as well as not to think about mapping result to the specific shape.
RxJS is all about functional programming. And in functional programming there is a pattern like Adapting Arguments to Parametersref
It allows us to decouple methods signature from context.
In order to achieve this you can write context depending version of map
, contentMap
, mergMap
operators so that the final solution looks like:
this.startUploadEvent$.pipe(
map(withKey('event')),
concatMap_(({event}) => this.getAuthenticationHeaders(event), 'headers'),
map_(({ headers }) => this.generateUploadId(headers), 'id'),
tap(({ event, id }) => this.emitUploadStartEvent(id, event)),
concatMap_(({ id }) => this.createPdfDocument(id), 'pdfId'),
concatMap_(({ pdfId }) => this.uploadBuilderForPdf(pdfId), 'cloudId'),
mergeMap_(({ cloudId }) => this.closePdf(cloudId)),
tap(({id, event, cloudId}) => this.emitUploadDoneEvent(id, event, cloudId)),
).subscribe(console.log);
Note _
after those operators.
Stackblitz Example
The goal of those custom operators if to take parameters object go through projection function and add result of projection to the original parameters object.
function map_<K extends string, P, V>(project: (params: P) => V): OperatorFunction<P, P>;
function map_<K extends string, P, V>(project: (params: P) => V, key: K): OperatorFunction<P, P & Record<K, V>>;
function map_<K extends string, P, V>(project: (params: P) => V, key?: K): OperatorFunction<P, P> {
return map(gatherParams(project, key));
}
function concatMap_<K extends string, P, V>(projection: (params: P) => Observable<V>): OperatorFunction<P, P>;
function concatMap_<K extends string, P, V>(projection: (params: P) => Observable<V>, key: K): OperatorFunction<P, P & Record<K, V>>;
function concatMap_<K extends string, P, V>(projection: (params: P) => Observable<V>, key?: K): OperatorFunction<P, P> {
return concatMap(gatherParamsOperator(projection, key));
}
function mergeMap_<K extends string, P, V>(projection: (params: P) => Observable<V>): OperatorFunction<P, P>;
function mergeMap_<K extends string, P, V>(projection: (params: P) => Observable<V>, key: K): OperatorFunction<P, P & Record<K, V>>;
function mergeMap_<K extends string, P, V>(projection: (params: P) => Observable<V>, key?: K): OperatorFunction<P, P> {
return mergeMap(gatherParamsOperator(projection, key));
}
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-am-i-getting-supplied-parameters-do-not-match-any-signature-error
function gatherParams<K extends string, P, V>(fn: (params: P) => V): (params: P) => P;
function gatherParams<K extends string, P, V>(fn: (params: P) => V, key: K): (params: P) => P & Record<K, V>;
function gatherParams<K extends string, P, V>(fn: (params: P) => V, key?: K): (params: P) => P {
return (params: P) => {
if (typeof key === 'string') {
return Object.assign({}, params, { [key]: fn(params) } as Record<K, V>);
}
return params;
};
}
function gatherParamsOperator<K extends string, P, V>(fn: (params: P) => Observable<V>): (params: P) => Observable<P>;
function gatherParamsOperator<K extends string, P, V>(fn: (params: P) => Observable<V>, key: K): (params: P) => Observable<P & Record<K, V>>;
function gatherParamsOperator<K extends string, P, V>(fn: (params: P) => Observable<V>, key?: K): (params: P) => Observable<P> {
return (params: P) => {
return fn(params).pipe(map(value => gatherParams((_: P) => value, key)(params)));
};
}
function withKey<K extends string, V>(key: K): (value: V) => Record<K, V> {
return (value: V) => ({ [key]: value } as Record<K, V>);
}
I used function overloads here because somethimes we don't need to add additional key to parameters. Parameters should only pass through it in case of this.closePdf(...)
method.
As a result you're getting decoupled version of the same you had before with type safety:
Doesn't it look like over-engineering?
In most cases you should follow YAGNI(You aren't gonna need it) principle. And it would be better not to add more complexity to existing code. For such scenario you should stick to some simple implementation of sharing parameters between operators as follows:
ngOnInit() {
const params: Partial<Params> = {};
this.startUploadEvent$.pipe(
concatMap(event => (params.event = event) && this.getAuthenticationHeaders(event)),
map(headers => (params.headers = headers) && this.generateUploadId(headers)),
tap(id => (params.uploadId = id) && this.emitUploadStartEvent(id, event)),
concatMap(id => this.createPdfDocument(id)),
concatMap(pdfId => (params.pdfId = pdfId) && this.uploadBuilderForPdf(pdfId)),
mergeMap(cloudId => (params.cloudId = cloudId) && this.closePdf(cloudId)),
tap(() => this.emitUploadDoneEvent(params.pdfId, params.cloudId, params.event)),
).subscribe(() => {
console.log(params)
});
where Params
type is:
interface Params {
event: any;
headers: any;
uploadId: any;
pdfId: any;
cloudId: any;
}
Please do note parentheses I used in assignments (params.cloudId = cloudId)
.
Stackblitz Example
There are also lots of other methods but they require to change your flow of using rxjs operators:
map()
results into arrays or object and then desctruct them. Another option could be usingtoPromise()
for each Observable and thenawait
each of them – Baguio