Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / docs-core / lib / UseCase / SquashDocument.ts
blobaae3d59dc6985225474a9f8ecb60bf5e9fc3a60f
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 '../Domain/Result/Result'
12 import type { DocsApi } from '../Api/DocsApi'
13 import type { EncryptMessage } from './EncryptMessage'
14 import type { DocumentKeys } from '@proton/drive-store'
15 import type { DocumentMetaInterface } from '@proton/docs-shared'
16 import type { DecryptCommit } from './DecryptCommit'
17 import metrics from '@proton/metrics'
18 import type { UpdatePair, SquashAlgorithm, SquashResult } from './SquashAlgorithm'
19 import { SQUASH_FACTOR, GetCommitDULimit } from '../Types/SquashingConstants'
20 import type { VerifyCommit } from './VerifyCommit'
21 import type { DecryptedCommit } from '../Models/DecryptedCommit'
22 import type { SquashVerificationObjectionCallback } from '../Types/SquashVerificationObjection'
23 import { SquashVerificationObjectionDecision } from '../Types/SquashVerificationObjection'
24 import { GenerateUUID } from '../Util/GenerateUuid'
25 import { metricsBucketNumberForUpdateCount } from '../Util/bucketNumberForUpdateCount'
27 export type SquashDocumentDTO = {
28   docMeta: DocumentMetaInterface
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   ) {}
46   async execute(dto: SquashDocumentDTO): Promise<Result<boolean>> {
47     const startTime = Date.now()
49     const { docMeta, commitId, keys } = dto
51     const lockResult = await this.docsApi.lockDocument(docMeta, commitId)
52     if (lockResult.isFailed()) {
53       return Result.fail(lockResult.getError())
54     }
56     const squashLock = SquashLock.deserializeBinary(lockResult.getValue())
58     const decryptionResult = await this.decryptCommit.execute({
59       commit: squashLock.commit,
60       keys: keys,
61       commitId: squashLock.commitId,
62     })
63     if (decryptionResult.isFailed()) {
64       metrics.docs_aborted_squashes_total.increment({ reason: 'decryption_error' })
65       return Result.fail(decryptionResult.getError())
66     }
68     const decryptedCommit = decryptionResult.getValue()
70     const verificationResult = await this.performVerification(decryptedCommit)
72     if (verificationResult.isFailed()) {
73       const objectionDecision = await dto.handleVerificationObjection()
74       if (objectionDecision === SquashVerificationObjectionDecision.AbortSquash) {
75         return Result.fail('Verification failed')
76       }
77     }
79     const squashCommitResult = await this.squashTheCommit(decryptedCommit, squashLock, keys)
80     if (squashCommitResult.isFailed()) {
81       return Result.fail(squashCommitResult.getError())
82     }
84     const squashCommit = squashCommitResult.getValue()
86     const commitResult = await this.docsApi.squashCommit(docMeta, decryptedCommit.commitId, squashCommit)
87     if (commitResult.isFailed()) {
88       return Result.fail(commitResult.getError())
89     }
91     const endTime = Date.now()
92     const timeToSquashInSeconds = Math.floor((endTime - startTime) / 1000)
94     metrics.docs_squashes_latency_histogram.observe({
95       Labels: {
96         updates: metricsBucketNumberForUpdateCount(decryptedCommit.updates.length),
97       },
98       Value: timeToSquashInSeconds,
99     })
101     metrics.docs_squashes_total.increment({})
103     return Result.ok(true)
104   }
106   async squashTheCommit(
107     decryptedCommit: DecryptedCommit,
108     squashLock: SquashLock,
109     keys: DocumentKeys,
110   ): Promise<Result<SquashCommit>> {
111     const updatePairs: UpdatePair[] = decryptedCommit.updates.map((update, index) => ({
112       encrypted: squashLock.commit.updates.documentUpdates[index],
113       decrypted: update,
114     }))
116     const squashResult = await this.squashAlgoritm.execute(updatePairs, {
117       limit: GetCommitDULimit(),
118       factor: SQUASH_FACTOR,
119     })
120     if (squashResult.isFailed()) {
121       return Result.fail(squashResult.getError())
122     }
124     const squashValue = squashResult.getValue()
126     if (!squashValue.updatesAsSquashed) {
127       return Result.fail('Squash failed; nothing to squash.')
128     }
130     const encryptedResult = await this.encryptSquashResult(squashValue, keys)
131     if (encryptedResult.isFailed()) {
132       metrics.docs_aborted_squashes_total.increment({ reason: 'encryption_error' })
133       return Result.fail(encryptedResult.getError())
134     }
136     const commit = CreateCommit({
137       updates: encryptedResult.getValue(),
138       version: CommitVersion.V1,
139       lockId: squashLock.lockId,
140     })
142     const squashCommit = CreateSquashCommit({
143       lockId: squashLock.lockId,
144       commitId: squashLock.commitId,
145       commit,
146     })
148     return Result.ok(squashCommit)
149   }
151   async performVerification(commit: DecryptedCommit): Promise<Result<true>> {
152     const verificationResult = await this.verifyCommit.execute({
153       commit,
154     })
156     if (verificationResult.isFailed()) {
157       return Result.fail(verificationResult.getError())
158     }
160     const verificationValue = verificationResult.getValue()
162     if (!verificationValue.allVerified) {
163       return Result.fail('Verification failed')
164     }
166     return Result.ok(true)
167   }
169   async encryptSquashResult(squashResult: SquashResult, keys: DocumentKeys): Promise<Result<DocumentUpdate[]>> {
170     const resultingUpdates: DocumentUpdate[] = []
171     resultingUpdates.push(...squashResult.unmodifiedUpdates.map((update) => update.encrypted))
173     if (squashResult.updatesAsSquashed) {
174       const metadata = {
175         version: DocumentUpdateVersion.V1,
176         authorAddress: keys.userOwnAddress,
177         timestamp: Date.now(),
178       }
180       const encryptedUpdate = await this.encryptMessage.execute(squashResult.updatesAsSquashed, metadata, keys)
181       if (encryptedUpdate.isFailed()) {
182         return Result.fail(encryptedUpdate.getError())
183       }
185       const update = CreateDocumentUpdate({
186         content: encryptedUpdate.getValue(),
187         authorAddress: metadata.authorAddress,
188         timestamp: metadata.timestamp,
189         version: metadata.version,
190         uuid: GenerateUUID(),
191       })
193       resultingUpdates.push(update)
194     }
196     return Result.ok(resultingUpdates)
197   }