diff --git a/apps/backend/src/modules/documents/document.controller.ts b/apps/backend/src/modules/documents/document.controller.ts index fbef2e13..0230e2af 100644 --- a/apps/backend/src/modules/documents/document.controller.ts +++ b/apps/backend/src/modules/documents/document.controller.ts @@ -621,7 +621,7 @@ class DocumentController extends BaseController { * @swagger * /documents/{id}/resume: * post: - * description: Back to processing, and re-enqueue what still needs work + * description: Back to processing, and re-enqueue what still needs work. Returns current document DTO * security: * - bearerAuth: [] * parameters: @@ -644,9 +644,12 @@ class DocumentController extends BaseController { private async resume( options: DocumentIdHandlerOptions, ): Promise { - await this.documentService.resume(options.params.id, options.user.userId); + const document = await this.documentService.resume( + options.params.id, + options.user.userId, + ); return { - payload: null, + payload: document, status: HTTPCode.OK, }; } diff --git a/apps/backend/src/modules/documents/document.repository.ts b/apps/backend/src/modules/documents/document.repository.ts index 9f3130e6..07b0b37d 100644 --- a/apps/backend/src/modules/documents/document.repository.ts +++ b/apps/backend/src/modules/documents/document.repository.ts @@ -167,6 +167,63 @@ class DocumentRepository { return document ? DocumentEntity.initialize(document) : null; } + public async findByIdAndOwnerIdForUpdateWithDetails( + id: number, + ownerId: number, + trx: Transaction, + ): Promise { + const knex = this.documentModel.knex(); + + const document = await knex + .select([ + "dp.documentId as id", + "d.error_message as errorMessage", + "dp.title", + "dp.status", + "dp.pageCount", + "dp.cursorPageNo", + knex.raw("round(dp.budget_usd, 2)::text as ??", ["budgetUsd"]), + knex.raw("round(dp.spent_usd, 2)::text as ??", ["spentUsd"]), + knex.raw( + "coalesce(round(dp.spent_usd / nullif(dp.budget_usd, 0) * 100, 1), 0)::float8 as ??", + ["usedPct"], + ), + "pr.id as presetId", + "pr.name as presetName", + "pr.version as presetVersion", + "dp.pagesTotal", + "dp.pagesVerified", + "dp.pagesReadyToCheck", + "dp.pagesInWork", + "dp.pagesPending", + "dp.pagesFailed", + "dp.pagesBlank", + "dp.pagesSkipped", + "dp.verifiedPct", + "dp.closedPct", + ]) + .from(`${DatabaseTableName.DOCUMENT} as d`) + .innerJoin( + `${DatabaseTableName.DOCUMENT_PROGRESS} as dp`, + "dp.documentId", + "d.id", + ) + .innerJoin(`${DatabaseTableName.PRESET} as pr`, "pr.id", "d.presetId") + .where({ + "d.id": id, + "d.ownerId": ownerId, + }) + .transacting(trx) + .forUpdate("d") + .first(); + + if (!document) { + return null; + } + + return DocumentDetailsEntity.initialize(document); + } + public async findDraftsOlderThan(date: string): Promise { const documents = await this.documentModel .query() diff --git a/apps/backend/src/modules/documents/document.service.ts b/apps/backend/src/modules/documents/document.service.ts index 17ea80c3..56797bad 100644 --- a/apps/backend/src/modules/documents/document.service.ts +++ b/apps/backend/src/modules/documents/document.service.ts @@ -460,13 +460,6 @@ class DocumentService { }); } - private throwInvalidStatusToResumeError(): never { - throw new HTTPError({ - message: DocumentValidationMessage.INVALID_STATUS_TO_RESUME, - status: HTTPCode.CONFLICT, - }); - } - public async create({ fileBytes, fileName, @@ -849,39 +842,62 @@ class DocumentService { this.throwInvalidStatusToPauseError(); } } - public async resume(documentId: number, userId: number): Promise { - const pages = await DocumentModel.transaction(async (trx) => { - const document = - await this.documentRepository.findByIdAndOwnerIdForUpdate( + public async resume( + documentId: number, + userId: number, + ): Promise { + const { document, isPaused, pages } = await DocumentModel.transaction( + async (trx) => { + const document = + await this.documentRepository.findByIdAndOwnerIdForUpdateWithDetails( + documentId, + userId, + trx, + ); + + if (!document) { + this.throwDocumentNotFoundError(); + } + + const documentObject = document.toObject(); + + if (documentObject.status !== DocumentStatus.PAUSED) { + return { document: documentObject, isPaused: false, pages: [] }; + } + + await this.documentRepository.updateStatus( documentId, - userId, + DocumentStatus.PROCESSING, + trx, + ); + await this.pageRepository.updateFirstPendingPagesAsQueued( + documentId, + PAGES_TO_QUEUE, trx, ); - if (!document) { - this.throwDocumentNotFoundError(); - } - - if (document.toObject().status !== DocumentStatus.PAUSED) { - this.throwInvalidStatusToResumeError(); - } + const pages = await this.pageRepository.findQueuedPages( + documentId, + trx, + ); - await this.documentRepository.updateStatus( - documentId, - DocumentStatus.PROCESSING, - trx, - ); - await this.pageRepository.updateFirstPendingPagesAsQueued( - documentId, - PAGES_TO_QUEUE, - trx, - ); + return { + document: { + ...documentObject, + status: DocumentStatus.PROCESSING, + }, + isPaused: true, + pages, + }; + }, + ); - return await this.pageRepository.findQueuedPages(documentId, trx); - }); + if (!isPaused) { + return document; + } if (pages.length === EMPTY_COLLECTION_LENGTH) { - return; + return document; } try { @@ -896,6 +912,8 @@ class DocumentService { }); }), ); + + return document; } catch (error) { await this.documentRepository.updateOwnedStatusFrom({ currentStatus: DocumentStatus.PROCESSING, diff --git a/apps/frontend/src/modules/documents/documents-api.ts b/apps/frontend/src/modules/documents/documents-api.ts index 1df63851..bbf2798b 100644 --- a/apps/frontend/src/modules/documents/documents-api.ts +++ b/apps/frontend/src/modules/documents/documents-api.ts @@ -148,8 +148,8 @@ class DocumentApi extends BaseHTTPApi { ); } - public async resume(id: number): Promise { - await this.load( + public async resume(id: number): Promise { + const response = await this.load( this.getFullEndpoint(DocumentsApiPath.RESUME, { id: String(id), }), @@ -160,6 +160,8 @@ class DocumentApi extends BaseHTTPApi { payload: JSON.stringify({}), }, ); + + return await response.json(); } public async updateBudget( diff --git a/apps/frontend/src/modules/documents/slices/actions.ts b/apps/frontend/src/modules/documents/slices/actions.ts index 752d3946..a0d6be37 100644 --- a/apps/frontend/src/modules/documents/slices/actions.ts +++ b/apps/frontend/src/modules/documents/slices/actions.ts @@ -145,14 +145,18 @@ const pollDocumentById = createAsyncThunk< { serializeError }, ); -const resume = createAsyncThunk( +const resume = createAsyncThunk< + DocumentGetByIdResponseDto, + number, + AsyncThunkConfig +>( `${sliceName}/resume`, async (id, { extra }) => { const { documentApi } = extra; - await documentApi.resume(id); + const document = await documentApi.resume(id); - return id; + return document; }, { serializeError }, ); diff --git a/apps/frontend/src/modules/documents/slices/documents.slice.ts b/apps/frontend/src/modules/documents/slices/documents.slice.ts index 7655707a..7548a950 100644 --- a/apps/frontend/src/modules/documents/slices/documents.slice.ts +++ b/apps/frontend/src/modules/documents/slices/documents.slice.ts @@ -95,7 +95,18 @@ const { actions, name, reducer } = createSlice({ }); builder.addCase(pollDocumentById.fulfilled, (state, action) => { if (state.document && state.document.id === action.payload.id) { - state.document = action.payload; + const isProcessingOrPaused = + action.payload.status === DocumentStatus.PROCESSING || + action.payload.status === DocumentStatus.PAUSED; + + if ( + state.document.status === DocumentStatus.PAUSED && + isProcessingOrPaused + ) { + return; + } + + state.document = { ...action.payload }; } }); builder.addCase(remove.fulfilled, (state, action) => { @@ -109,16 +120,12 @@ const { actions, name, reducer } = createSlice({ }); builder.addCase(pause.fulfilled, (state, action) => { state.pauseResumeDataStatuses[action.meta.arg] = DataStatus.FULFILLED; - - if (state.document && state.document.id === action.meta.arg) { - state.document.status = DocumentStatus.PAUSED; - } }); builder.addCase(resume.fulfilled, (state, action) => { state.pauseResumeDataStatuses[action.meta.arg] = DataStatus.FULFILLED; if (state.document && state.document.id === action.meta.arg) { - state.document.status = DocumentStatus.PROCESSING; + state.document = action.payload; } }); builder.addCase(pause.pending, (state, action) => { @@ -138,7 +145,11 @@ const { actions, name, reducer } = createSlice({ builder.addCase(pause.rejected, (state, action) => { state.pauseResumeDataStatuses[action.meta.arg] = DataStatus.REJECTED; - if (state.document && state.document.id === action.meta.arg) { + if ( + state.document && + state.document.id === action.meta.arg && + state.document.status === DocumentStatus.PAUSED + ) { state.document.status = DocumentStatus.PROCESSING; } }); diff --git a/apps/frontend/src/pages/document/libs/components/document-status-block/document-status-block.tsx b/apps/frontend/src/pages/document/libs/components/document-status-block/document-status-block.tsx index e7eebcbb..cb077f0e 100644 --- a/apps/frontend/src/pages/document/libs/components/document-status-block/document-status-block.tsx +++ b/apps/frontend/src/pages/document/libs/components/document-status-block/document-status-block.tsx @@ -27,15 +27,15 @@ const DocumentStatusBlock: React.FC = ({ documentId, status }) => { const isPauseResumeLoading = pauseResumeDataStatus === DataStatus.PENDING; const handleToggleProcessing = useCallback(() => { - const action = isPaused ? documentActions.resume : documentActions.pause; - - void dispatch(action(documentId)) - .unwrap() - .then(() => { - if (isPaused) { - void dispatch(documentActions.startPolling(documentId)); - } - }); + const togglePromise = isPaused + ? dispatch(documentActions.resume(documentId)) + : dispatch(documentActions.pause(documentId)); + + void togglePromise.unwrap().then(() => { + if (isPaused) { + void dispatch(documentActions.startPolling(documentId)); + } + }); }, [dispatch, documentId, isPaused]); const showProcessingToggle =