英文:
ConcatMap with CombineLatest creating more calls than expected
问题
我有一个ngrx
的action/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<FileList>) => {
return combineLatest([
files.pipe(
mergeMap(list => Array.from(list).map(i => ({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) =>
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) {
//calls the reducer with the ID of the current set of uploads
this.setUploadId(event.body.ID);
}
}
},
}),
catchError(() => EMPTY) //for now
)
)
)
});
Thanks
Here are my changes after @maxime1992 suggestion:
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) //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<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))
)
)
)
);
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) => {
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论