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 '@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 = {
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,
44 private logger: LoggerInterface,
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)
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,
68 if (decryptionResult.isFailed()) {
69 metrics.docs_aborted_squashes_total.increment({ reason: 'decryption_error' })
70 return Result.fail(decryptionResult.getError())
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')
84 const squashCommitResult = await this.squashTheCommit(decryptedCommit, squashLock, keys)
85 if (squashCommitResult.isFailed()) {
86 return Result.fail(squashCommitResult.getError())
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)
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({
105 updates: metricsBucketNumberForUpdateCount(decryptedCommit.updates.length),
107 Value: timeToSquashInSeconds,
110 metrics.docs_squashes_total.increment({})
112 return Result.ok(true)
115 async squashTheCommit(
116 decryptedCommit: DecryptedCommit,
117 squashLock: SquashLock,
119 ): Promise<Result<SquashCommit>> {
120 const updatePairs: UpdatePair[] = decryptedCommit.updates.map((update, index) => ({
121 encrypted: squashLock.commit.updates.documentUpdates[index],
125 this.logger.info('[Squash] Executing squash algorithm...')
127 const squashResult = await this.squashAlgoritm.execute(updatePairs, {
128 limit: GetCommitDULimit(),
129 factor: SQUASH_FACTOR,
131 if (squashResult.isFailed()) {
132 return Result.fail(squashResult.getError())
135 const squashValue = squashResult.getValue()
137 if (!squashValue.updatesAsSquashed) {
138 return Result.fail('Squash failed; nothing to squash.')
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())
149 const commit = CreateCommit({
150 updates: encryptedResult.getValue(),
151 version: CommitVersion.V1,
152 lockId: squashLock.lockId,
155 const squashCommit = CreateSquashCommit({
156 lockId: squashLock.lockId,
157 commitId: squashLock.commitId,
161 return Result.ok(squashCommit)
164 async performVerification(commit: DecryptedCommit): Promise<Result<true>> {
165 this.logger.info('[Squash] Verifying commit...')
167 const verificationResult = await this.verifyCommit.execute({
171 if (verificationResult.isFailed()) {
172 return Result.fail(verificationResult.getError())
175 const verificationValue = verificationResult.getValue()
177 if (!verificationValue.allVerified) {
178 return Result.fail('Verification failed')
181 return Result.ok(true)
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) {
190 version: DocumentUpdateVersion.V1,
191 authorAddress: keys.userOwnAddress,
192 timestamp: Date.now(),
195 const encryptedUpdate = await this.encryptMessage.execute(squashResult.updatesAsSquashed, metadata, keys)
196 if (encryptedUpdate.isFailed()) {
197 return Result.fail(encryptedUpdate.getError())
200 const update = CreateDocumentUpdate({
201 content: encryptedUpdate.getValue(),
202 authorAddress: metadata.authorAddress,
203 timestamp: metadata.timestamp,
204 version: metadata.version,
205 uuid: GenerateUUID(),
208 resultingUpdates.push(update)
211 return Result.ok(resultingUpdates)