[monorepo] read empty content from its hash, in a shared blob store base (#36831)

The hash of empty content is a property of the content rather than of anything
stored: every project's empty file has it, whether or not a blob was ever written
under it, and a file may reference it before anything has. Eight blob stores were
each paying a lookup -- a database query, an HTTP request, a read of a cache that
offline mode may be the only thing standing in for -- to be told what the hash
already says, and clsi had already reinvented the answer on its own.

overleaf-editor-core gains BlobStoreBase, which answers getString for that hash
and delegates the rest to the fetchString its subclasses provide. getObject comes
with it: four stores spelled out the same JSON.parse of getString, and a fifth
wrapped it in a log line.

Adopted by history-v1's BlobStore, BackupBlobStore, HashCheckBlobStore and
recover_zip's RecoveryBlobStore, by project-history's BlobStore, by clsi's
BlobStore, and by web's HistoryBlobStore, SimpleBlobStore and FrontendBlobStore.
history-v1 keeps its own answers in getStream, getBlob and getBlobs, which no
other store implements. Writing and copying a blob are untouched, so a project
that wrote empty content still has a blob for it and a read answers the same
either way.

Two paths could not handle a project holding a tracked change or a comment at
all. Loading such a file needs the ranges blob its rangesHash points at, and
hashing one again needs to write it back, so a store missing getObject or
putObject fails outright:

- recover_zip died on `blobStore.getObject is not a function`, making every such
  project unrecoverable. The test covering it asserts a tracked delete is absent
  from the zip, which the script already asked for and never reached.
- GET /projects/:project_id/latest/hashed_content returned 500 from the same
  cause, then would have hit a missing putObject in snapshot.store.
  HashCheckBlobStore becomes a class over the base and gains putObject.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
GitOrigin-RevId: c84f62465beff6cf77ffac26aa772984e1b554cb
This commit is contained in:
Jakob Ackermann
2026-09-17 12:06:30 +00:00
committed by Copybot
co-authored by Claude Opus 5
parent 7608d738a6
commit 6caa5fc3a3
18 changed files with 646 additions and 71 deletions
@@ -1,4 +1,5 @@
import { text } from 'node:stream/consumers'
import { BlobStoreBase } from 'overleaf-editor-core'
import HistoryManager from './HistoryManager.mjs'
/**
@@ -8,11 +9,12 @@ import HistoryManager from './HistoryManager.mjs'
* eagerly here -- needed for the ones with pending edit operations, whose content
* is not in a single blob.
*/
class HistoryBlobStore {
class HistoryBlobStore extends BlobStoreBase {
/**
* @param {string} historyId
*/
constructor(historyId) {
super()
this.historyId = historyId
}
@@ -20,21 +22,13 @@ class HistoryBlobStore {
* @param {string} hash
* @return {Promise<string>}
*/
async getString(hash) {
async fetchString(hash) {
const { stream } = await HistoryManager.promises.requestBlob(
this.historyId,
hash
)
return await text(stream)
}
/**
* @param {string} hash
* @return {Promise<object>}
*/
async getObject(hash) {
return JSON.parse(await this.getString(hash))
}
}
export default HistoryBlobStore