ConcatMap 与 CombineLatest 创建的调用次数超出了预期。

huangapple go评论52阅读模式
英文:

ConcatMap with CombineLatest creating more calls than expected

问题

我有一个ngrxaction/effect,它接受一个FileList并按顺序将它们上传到服务器。在第一个File上传后,将生成一个需要作为第二个File参数使用的ID,以便后续与第一个相关联。我设置component-store的方式是,我有一个包含ID(最初为null)和文件的State

问题出现在我尝试在第一个文件上传后更新存储中的ID时。我使用的concatMap仍然在处理文件,只有在所有文件实际上传后,新的流才会带着在第一次调用后设置的ID出现。因此,最后我有比预期更多的https调用,而且没有一个文件使用最初创建的ID。

真正的问题是我不知道如何在concatMap发射文件的时候获取存储中ID的最新值。我猜测combineLatest可能不在正确的位置。

以下是我迄今为止尝试过的内容:

readonly uploadFiles = this.effect((files: Observable<FileList>) => {
    return combineLatest([
        files.pipe(
            mergeMap(list => Array.from(list).map(i => ({ file: i, length: list.length })))
        ),
        // 保存在第一个文件上传后将设置的ID的选择器
        this.uploadId$,
    ]).pipe(
        concatMap(([tuple, uploadId], index) => 
            this._dataSvc.upload(tuple.file, index, tuple.length, uploadId.ID).pipe(
                tap({
                    next: (event) => {
                        switch (event.type) {
                            case HttpEventType.UploadProgress:
                                this.uploadProgress([
                                    Math.round((event.loaded / event.total) * 100),
                                    index
                                ]);
                                break;
                            case HttpEventType.Response:
                                if (uploadId.ID === null) {
                                    // 使用当前上传集合的ID调用reducer
                                    this.setUploadId(event.body.ID);
                                }
                        }
                    },
                }),
                catchError(() => EMPTY) // 暂时使用
            )
        )
    );
});

以下是在@maxime1992建议后我所做的更改:

readonly uploadFiles = this.effect((files$: Observable<FileList>) => {
    return files$.pipe(
        concatMap((files) =>
            of({
                ID: null,
            }).pipe(
                expand((ids, index) => {
                    const idx = index / 5;
                    return files.length === idx || index % 5 !== 0 ? EMPTY :
                        this._dataSvc.upload(files[idx], idx, files.length, ids.ID).pipe(
                            tap({
                                next: (event) => {
                                    switch (event.type) {
                                        case HttpEventType.UploadProgress:
                                            this.uploadProgress([
                                                Math.round((event.loaded / event.total) * 100),
                                                idx
                                            ]);
                                            break;
                                        case HttpEventType.Response:
                                            this.uploadCompleted(idx);
                                    }
                                },
                            }),
                            map((event, index) => 
                                event.type === HttpEventType.Response ? 
                                ({ ID: event.body.ID }) : ({ ID: null })),
                            catchError(() => EMPTY) // 暂时使用
                        )   
                })
            )
        )
    );
});
英文:

I have an ngrx action/effect that takes a FileList and uploads them sequentially to the server. After the first File is uploaded, an ID will be generated that needs to be used by the second File as argument, so it can be later related to the first one in the back. The way I setup the component-store is that I have a State that holds the ID (initially null and the files).

The problem arises when I try to update the ID in the store after the first file is uploaded. The concatMap that I am using to keep the sequential order is still working on the files and its only after all the files are actually uploaded that a new stream comes along the way with the ID that I set after the first call. So, at the end I have more https calls than expected and none of the files are using the ID that was created initially.

The real problem is that I don't know how to get the latest value of the ID from the store at the moment that the concatMap is shooting the files. I am guessing that combineLatest is not in the right place.

Here's what I have tried so far:

readonly uploadFiles = this.effect((files: Observable&lt;FileList&gt;) =&gt; {
    return combineLatest([
        files.pipe(
            mergeMap(list =&gt; Array.from(list).map(i =&gt; ({file: i, length: list.length})) )
        ),
        //selector holding the ID that will be set after the first file is uploaded
        this.uploadId$,
    ]).pipe(
        concatMap(([tuple, uploadId], index) =&gt; 
            this._dataSvc.upload(tuple.file, index, tuple.length, uploadId.ID).pipe(
                tap({
                    next: (event) =&gt; {
                        switch (event.type) {
                          case HttpEventType.UploadProgress:
                            this.uploadProgress([
                                Math.round((event.loaded / event.total) * 100),
                                index
                            ]);
                            break;
                          case HttpEventType.Response:
                            if(uploadId.ID === null) {
                              //calls the reducer with the ID of the current set of uploads
                              this.setUploadId(event.body.ID);
                            }
                        }
                    },
                }),
                catchError(() =&gt; EMPTY) //for now
            )
        )
    )
  });

Thanks

Here are my changes after @maxime1992 suggestion:

readonly uploadFiles = this.effect((files$: Observable&lt;FileList&gt;) =&gt; {
    return files$.pipe(
        concatMap((files) =&gt;
            of({
                ID: null,
            }).pipe(
                expand((ids, index) =&gt;{
                    const idx = index/5;
                    return files.length === idx || index % 5 !== 0 ? EMPTY :
                    this._dataSvc.upload(files[idx], idx, files.length, ids.ID).pipe(
                        tap({
                            next: (event) =&gt; {
                                switch (event.type) {
                                  case HttpEventType.UploadProgress:
                                    this.uploadProgress([
                                        Math.round((event.loaded / event.total) * 100),
                                        idx
                                    ]);
                                    break;
                                  case HttpEventType.Response:
                                    this.uploadCompleted(idx);
                                }
                            },
                        }),
                        map((event, index) =&gt; 
                            event.type === HttpEventType.Response ? 
                            ({ ID: event.body.ID }) : ({ ID: null })),
                        catchError(() =&gt; EMPTY) //for now
                    )   
                })
            )
        )
    );

答案1

得分: 1

假设我正确理解了你的用例,你想要为每个文件上传提供前一个上传的ID。通常情况下,与这个最接近的用例是分页,当你切换页面时,你需要知道前一页的页码或页面ID。

对于这种用例,最合适的方法是 expand,正如其名称所示,它允许你 "展开" 并进行递归调用。

为了让演示工作,我已经调整了代码以在 ngrx/effects 上下文之外工作,并模拟了后端,但我的代码现在是这样的:

const uploadFiles = (files$: Observable<FileList>) =>
  files$.pipe(
    switchMap((files) =>
      of(null).pipe(
        expand((uploadId, index) =>
          files.length === index
            ? EMPTY
            : _mockDataSvc
                .upload(files[index], index, files.length, uploadId)
                .pipe(map((res) => res.ID))
        )
      )
    )
  );

我已经使用以下方式模拟了后端:

let mockResponseId = 0;

// 模拟 API 调用以创建一个可工作的演示
const _mockDataSvc = {
  upload: (file: File, index: number, length: number, uploadId: string) => {
    console.log(
      `[模拟后端]: 调用上传索引为 ${index} 的文件,长度为 ${length},以前的上传ID为 ${uploadId}`
    );
    return of({ ID: `模拟响应ID-${mockResponseId++}` }).pipe(delay(500));
  },
};

当我运行这个代码时,以下是输出结果:

[模拟后端]: 调用上传索引为 0 的文件,长度为 3,以前的上传ID为 null
[模拟后端]: 调用上传索引为 1 的文件,长度为 3,以前的上传ID为 模拟响应ID-0
[模拟后端]: 调用上传索引为 2 的文件,长度为 3,以前的上传ID为 模拟响应ID-1

你可以看到,第一个请求的前一个ID是 null(并且此请求返回ID 0,然后下一个请求传递了先前接收到的ID,依此类推)。

我认为你有一个良好的基础来将它适应回你的效果。

这里是完整的代码和实时演示

英文:

Assuming I understood your use case correctly, you want to provide for each file upload the previous upload ID. The closest use case for this in general terms is pagination, when you move on you need to know the previous page number or page ID.

For such use case, the most appropriate method is expand, which as the name suggests, let you "expand" and have recursive calls.

In order to get a demo working, I've tweaked the code to work outside of ngrx/effects context and mocked the backend but my code is down to this:

const uploadFiles = (files$: Observable&lt;FileList&gt;) =&gt;
  files$.pipe(
    switchMap((files) =&gt;
      of(null).pipe(
        expand((uploadId, index) =&gt;
          files.length === index
            ? EMPTY
            : _mockDataSvc
                .upload(files[index], index, files.length, uploadId)
                .pipe(map((res) =&gt; res.ID))
        )
      )
    )
  );

I've mocked the backend with the following:

let mockResponseId = 0;

// mocking the API call in order to have a working demo
const _mockDataSvc = {
  upload: (file: File, index: number, length: number, uploadId: string) =&gt; {
    console.log(
      `[Mocked backend]: Call for uploading a file that is the index ${index}, has length ${length} and with a previous upload ID of ${uploadId}`
    );
    return of({ ID: `mock-response-ID-${mockResponseId++}` }).pipe(delay(500));
  },
};

When I run this, here's the output:

[Mocked backend]: Call for uploading a file that is the index 0, has length 3 and with a previous upload ID of null
[Mocked backend]: Call for uploading a file that is the index 1, has length 3 and with a previous upload ID of mock-response-ID-0
[Mocked backend]: Call for uploading a file that is the index 2, has length 3 and with a previous upload ID of mock-response-ID-1

You can see that the previous ID for the first request is null (and this request returns ID 0, then next request passes the ID received previously and so on.

I think you've got a good base here to adapt that back to your effect.

Here's the full code with a live demo.

huangapple
  • 本文由 发表于 2023年5月26日 10:35:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/76337318.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定