Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / docs-core / lib / UseCase / SquashDocument.ts
blob4aee98a3ff083374b264cc2aff1903255c9e39b1
1 import type { DocumentUpdate, SquashCommit } from '@proton/docs-proto'
2 import {
3   CommitVersion,
4   CreateDocumentUpdate,
5   DocumentUpdateVersion,
6   SquashLock,
7   CreateCommit,
8   CreateSquashCommit,
9 } from '@proton/docs-proto'
10 import type { UseCaseInterface } from '../Domain/UseCase/UseCaseInterface'
11 import { Result } from '@proton/docs-shared'
12 import type { DocsApi } from '../Api/DocsApi'
13 import type { EncryptMessage } from './EncryptMessage'
14 import type { DocumentKeys, NodeMeta } from '@proton/drive-store'
15 import type { DecryptCommit } from './DecryptCommit'
16 import metrics from '@proton/metrics'
17 import type { UpdatePair, SquashAlgorithm, SquashResult } from './SquashAlgorithm'
18 import { SQUASH_FACTOR, GetCommitDULimit } from '../Types/SquashingConstants'
19 import type { VerifyCommit } from './VerifyCommit'
20 import type { DecryptedCommit } from '../Models/DecryptedCommit'
21 import type { SquashVerificationObjectionCallback } from '../Types/SquashVerificationObjection'
22 import { SquashVerificationObjectionDecision } from '../Types/SquashVerificationObjection'
23 import { GenerateUUID } from '../Util/GenerateUuid'
24 import { metricsBucketNumberForUpdateCount } from '../Util/bucketNumberForUpdateCount'
25 import type { LoggerInterface } from '@proton/utils/logs'
27 export type SquashDocumentDTO = {
28   nodeMeta: NodeMeta
29   commitId: string
30   keys: DocumentKeys
31   handleVerificationObjection: SquashVerificationObjectionCallback
34 /**
35  * Squashes a document's series of updates into one or more resulting updates.
36  */
37 export class SquashDocument implements UseCaseInterface<boolean> {
38   constructor(
39     private docsApi: DocsApi,
40     private encryptMessage: EncryptMessage,
41     private decryptCommit: DecryptCommit,
42     private verifyCommit: VerifyCommit,
43     private squashAlgoritm: SquashAlgorithm,
44     private logger: LoggerInterface,
45   ) {}
47   async execute(dto: SquashDocumentDTO): Promise<Result<boolean>> {
48     const startTime = Date.now()
50     const { nodeMeta, commitId, keys } = dto
52     this.logger.info('[Squash] Locking document...')
54     const lockResult = await this.docsApi.lockDocument(nodeMeta, commitId)
55     if (lockResult.isFailed()) {
56       return Result.fail(lockResult.getError().message)
57     }
59     const squashLock = SquashLock.deserializeBinary(lockResult.getValue())
61     this.logger.info('[Squash] Decrypting commit...')
63     const decryptionResult = await this.decryptCommit.execute({
64       commit: squashLock.commit,
65       documentContentKey: keys.documentContentKey,
66       commitId: squashLock.commitId,
67     })
68     if (decryptionResult.isFailed()) {
69       metrics.docs_aborted_squashes_total.increment({ reason: 'decryption_error' })
70       return Result.fail(decryptionResult.getError())
71     }
73     const decryptedCommit = decryptionResult.getValue()
75     const verificationResult = await this.performVerification(decryptedCommit)
77     if (verificationResult.isFailed()) {
78       const objectionDecision = await dto.handleVerificationObjection()
79       if (objectionDecision === SquashVerificationObjectionDecision.AbortSquash) {
80         return Result.fail('Verification failed')
81       }
82     }
84     const squashCommitResult = await this.squashTheCommit(decryptedCommit, squashLock, keys)
85     if (squashCommitResult.isFailed()) {
86       return Result.fail(squashCommitResult.getError())
87     }
89     const squashCommit = squashCommitResult.getValue()
91     this.logger.info('[Squash] Sending squash commit to API...')
93     const commitResult = await this.docsApi.squashCommit(nodeMeta, decryptedCommit.commitId, squashCommit)
94     if (commitResult.isFailed()) {
95       return Result.fail(commitResult.getError().message)
96     }
98     const endTime = Date.now()
99     const timeToSquashInSeconds = Math.floor((endTime - startTime) / 1000)
101     this.logger.info(`[Squash] Took ${timeToSquashInSeconds} seconds to complete successfully`)
103     metrics.docs_squashes_latency_histogram.observe({
104       Labels: {
105         updates: metricsBucketNumberForUpdateCount(decryptedCommit.updates.length),
106       },
107       Value: timeToSquashInSeconds,
108     })
110     metrics.docs_squashes_total.increment({})
112     return Result.ok(true)
113   }
115   async squashTheCommit(
116     decryptedCommit: DecryptedCommit,
117     squashLock: SquashLock,
118     keys: DocumentKeys,
119   ): Promise<Result<SquashCommit>> {
120     const updatePairs: UpdatePair[] = decryptedCommit.updates.map((update, index) => ({
121       encrypted: squashLock.commit.updates.documentUpdates[index],
122       decrypted: update,
123     }))
125     this.logger.info('[Squash] Executing squash algorithm...')
127     const squashResult = await this.squashAlgoritm.execute(updatePairs, {
128       limit: GetCommitDULimit(),
129       factor: SQUASH_FACTOR,
130     })
131     if (squashResult.isFailed()) {
132       return Result.fail(squashResult.getError())
133     }
135     const squashValue = squashResult.getValue()
137     if (!squashValue.updatesAsSquashed) {
138       return Result.fail('Squash failed; nothing to squash.')
139     }
141     this.logger.info('[Squash] Encrypting squash result...')
143     const encryptedResult = await this.encryptSquashResult(squashValue, keys)
144     if (encryptedResult.isFailed()) {
145       metrics.docs_aborted_squashes_total.increment({ reason: 'encryption_error' })
146       return Result.fail(encryptedResult.getError())
147     }
149     const commit = CreateCommit({
150       updates: encryptedResult.getValue(),
151       version: CommitVersion.V1,
152       lockId: squashLock.lockId,
153     })
155     const squashCommit = CreateSquashCommit({
156       lockId: squashLock.lockId,
157       commitId: squashLock.commitId,
158       commit,
159     })
161     return Result.ok(squashCommit)
162   }
164   async performVerification(commit: DecryptedCommit): Promise<Result<true>> {
165     this.logger.info('[Squash] Verifying commit...')
167     const verificationResult = await this.verifyCommit.execute({
168       commit,
169     })
171     if (verificationResult.isFailed()) {
172       return Result.fail(verificationResult.getError())
173     }
175     const verificationValue = verificationResult.getValue()
177     if (!verificationValue.allVerified) {
178       return Result.fail('Verification failed')
179     }
181     return Result.ok(true)
182   }
184   async encryptSquashResult(squashResult: SquashResult, keys: DocumentKeys): Promise<Result<DocumentUpdate[]>> {
185     const resultingUpdates: DocumentUpdate[] = []
186     resultingUpdates.push(...squashResult.unmodifiedUpdates.map((update) => update.encrypted))
188     if (squashResult.updatesAsSquashed) {
189       const metadata = {
190         version: DocumentUpdateVersion.V1,
191         authorAddress: keys.userOwnAddress,
192         timestamp: Date.now(),
193       }
195       const encryptedUpdate = await this.encryptMessage.execute(squashResult.updatesAsSquashed, metadata, keys)
196       if (encryptedUpdate.isFailed()) {
197         return Result.fail(encryptedUpdate.getError())
198       }
200       const update = CreateDocumentUpdate({
201         content: encryptedUpdate.getValue(),
202         authorAddress: metadata.authorAddress,
203         timestamp: metadata.timestamp,
204         version: metadata.version,
205         uuid: GenerateUUID(),
206       })
208       resultingUpdates.push(update)
209     }
211     return Result.ok(resultingUpdates)
212   }