1 import type { DocumentUpdate, SquashCommit } from '@proton/docs-proto'
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
31 handleVerificationObjection: SquashVerificationObjectionCallback
35 * Squashes a document's series of updates into one or more resulting updates.
37 export class SquashDocument implements UseCaseInterface<boolean> {
39 private docsApi: DocsApi,
40 private encryptMessage: EncryptMessage,
41 private decryptCommit: DecryptCommit,
42 private verifyCommit: VerifyCommit,
43 private squashAlgoritm: SquashAlgorithm,
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())
56 const squashLock = SquashLock.deserializeBinary(lockResult.getValue())
58 const decryptionResult = await this.decryptCommit.execute({
59 commit: squashLock.commit,
61 commitId: squashLock.commitId,
63 if (decryptionResult.isFailed()) {
64 metrics.docs_aborted_squashes_total.increment({ reason: 'decryption_error' })
65 return Result.fail(decryptionResult.getError())
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')
79 const squashCommitResult = await this.squashTheCommit(decryptedCommit, squashLock, keys)
80 if (squashCommitResult.isFailed()) {
81 return Result.fail(squashCommitResult.getError())
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())
91 const endTime = Date.now()
92 const timeToSquashInSeconds = Math.floor((endTime - startTime) / 1000)
94 metrics.docs_squashes_latency_histogram.observe({
96 updates: metricsBucketNumberForUpdateCount(decryptedCommit.updates.length),
98 Value: timeToSquashInSeconds,
101 metrics.docs_squashes_total.increment({})
103 return Result.ok(true)
106 async squashTheCommit(
107 decryptedCommit: DecryptedCommit,
108 squashLock: SquashLock,
110 ): Promise<Result<SquashCommit>> {
111 const updatePairs: UpdatePair[] = decryptedCommit.updates.map((update, index) => ({
112 encrypted: squashLock.commit.updates.documentUpdates[index],
116 const squashResult = await this.squashAlgoritm.execute(updatePairs, {
117 limit: GetCommitDULimit(),
118 factor: SQUASH_FACTOR,
120 if (squashResult.isFailed()) {
121 return Result.fail(squashResult.getError())
124 const squashValue = squashResult.getValue()
126 if (!squashValue.updatesAsSquashed) {
127 return Result.fail('Squash failed; nothing to squash.')
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())
136 const commit = CreateCommit({
137 updates: encryptedResult.getValue(),
138 version: CommitVersion.V1,
139 lockId: squashLock.lockId,
142 const squashCommit = CreateSquashCommit({
143 lockId: squashLock.lockId,
144 commitId: squashLock.commitId,
148 return Result.ok(squashCommit)
151 async performVerification(commit: DecryptedCommit): Promise<Result<true>> {
152 const verificationResult = await this.verifyCommit.execute({
156 if (verificationResult.isFailed()) {
157 return Result.fail(verificationResult.getError())
160 const verificationValue = verificationResult.getValue()
162 if (!verificationValue.allVerified) {
163 return Result.fail('Verification failed')
166 return Result.ok(true)
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) {
175 version: DocumentUpdateVersion.V1,
176 authorAddress: keys.userOwnAddress,
177 timestamp: Date.now(),
180 const encryptedUpdate = await this.encryptMessage.execute(squashResult.updatesAsSquashed, metadata, keys)
181 if (encryptedUpdate.isFailed()) {
182 return Result.fail(encryptedUpdate.getError())
185 const update = CreateDocumentUpdate({
186 content: encryptedUpdate.getValue(),
187 authorAddress: metadata.authorAddress,
188 timestamp: metadata.timestamp,
189 version: metadata.version,
190 uuid: GenerateUUID(),
193 resultingUpdates.push(update)
196 return Result.ok(resultingUpdates)