|
@@ -23,6 +23,8 @@ import OError from '@overleaf/o-error'
|
|
|
import ClsiMetrics from './Metrics.js'
|
|
import ClsiMetrics from './Metrics.js'
|
|
|
import { promiseMapSettledWithLimit } from '@overleaf/promise-utils'
|
|
import { promiseMapSettledWithLimit } from '@overleaf/promise-utils'
|
|
|
import Metrics from '@overleaf/metrics'
|
|
import Metrics from '@overleaf/metrics'
|
|
|
|
|
+import TikzManager from './TikzManager.js'
|
|
|
|
|
+import DraftModeManager from './DraftModeManager.js'
|
|
|
|
|
|
|
|
const gzip = promisify(zlib.gzip)
|
|
const gzip = promisify(zlib.gzip)
|
|
|
const gunzip = promisify(zlib.gunzip)
|
|
const gunzip = promisify(zlib.gunzip)
|
|
@@ -76,7 +78,7 @@ function isENOENT(err) {
|
|
|
* @param {string} userId
|
|
* @param {string} userId
|
|
|
* @param {number} remoteBaseVersion
|
|
* @param {number} remoteBaseVersion
|
|
|
* @param {boolean} populateClsiCache
|
|
* @param {boolean} populateClsiCache
|
|
|
- * @return {Promise<{rawSnapshot: import('overleaf-editor-core/lib/types.js').RawSnapshot, globalBlobs: string[], fullSync: boolean,localBaseVersion: number}>}
|
|
|
|
|
|
|
+ * @return {Promise<{rawSnapshot: import('overleaf-editor-core/lib/types.js').RawSnapshot, globalBlobs: string[], fullSync: boolean,localBaseVersion: number, dirty: string[]}>}
|
|
|
*/
|
|
*/
|
|
|
async function loadSnapshot(
|
|
async function loadSnapshot(
|
|
|
projectId,
|
|
projectId,
|
|
@@ -134,7 +136,7 @@ async function loadSnapshot(
|
|
|
* @param {string} projectId
|
|
* @param {string} projectId
|
|
|
* @param {string} userId
|
|
* @param {string} userId
|
|
|
* @param {number} remoteBaseVersion
|
|
* @param {number} remoteBaseVersion
|
|
|
- * @return {Promise<{rawSnapshot: import('overleaf-editor-core/lib/types.js').RawSnapshot, globalBlobs: string[], fullSync: boolean,localBaseVersion: number}>}
|
|
|
|
|
|
|
+ * @return {Promise<{rawSnapshot: import('overleaf-editor-core/lib/types.js').RawSnapshot, globalBlobs: string[], fullSync: boolean,localBaseVersion: number, dirty: string[]}>}
|
|
|
*/
|
|
*/
|
|
|
async function loadSnapshotFromClsiCache(projectId, userId, remoteBaseVersion) {
|
|
async function loadSnapshotFromClsiCache(projectId, userId, remoteBaseVersion) {
|
|
|
const { dir, resyncPath } = snapshotPath(projectId, userId)
|
|
const { dir, resyncPath } = snapshotPath(projectId, userId)
|
|
@@ -160,20 +162,23 @@ async function loadSnapshotFromClsiCache(projectId, userId, remoteBaseVersion) {
|
|
|
* @param {string} path
|
|
* @param {string} path
|
|
|
* @param {number} remoteBaseVersion
|
|
* @param {number} remoteBaseVersion
|
|
|
* @param {boolean} fullSync
|
|
* @param {boolean} fullSync
|
|
|
- * @return {Promise<{rawSnapshot: import('overleaf-editor-core/lib/types.js').RawSnapshot, globalBlobs: string[], localBaseVersion: number, fullSync: boolean}>}
|
|
|
|
|
|
|
+ * @return {Promise<{rawSnapshot: import('overleaf-editor-core/lib/types.js').RawSnapshot, globalBlobs: string[], localBaseVersion: number, fullSync: boolean, dirty: string[]}>}
|
|
|
*/
|
|
*/
|
|
|
async function loadSnapshotFromFile(path, remoteBaseVersion, fullSync) {
|
|
async function loadSnapshotFromFile(path, remoteBaseVersion, fullSync) {
|
|
|
let blob = await fs.promises.readFile(path)
|
|
let blob = await fs.promises.readFile(path)
|
|
|
blob = await gunzip(blob)
|
|
blob = await gunzip(blob)
|
|
|
- const { rawSnapshot, globalBlobs, localBaseVersion } = JSON.parse(
|
|
|
|
|
- blob.toString('utf-8')
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ const {
|
|
|
|
|
+ rawSnapshot,
|
|
|
|
|
+ globalBlobs,
|
|
|
|
|
+ localBaseVersion,
|
|
|
|
|
+ dirty = [], // added later, provide a default value.
|
|
|
|
|
+ } = JSON.parse(blob.toString('utf-8'))
|
|
|
if (localBaseVersion < remoteBaseVersion) {
|
|
if (localBaseVersion < remoteBaseVersion) {
|
|
|
throw new Errors.MissingUpdatesError('missing updates', {
|
|
throw new Errors.MissingUpdatesError('missing updates', {
|
|
|
baseHistoryVersion: localBaseVersion,
|
|
baseHistoryVersion: localBaseVersion,
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
- return { rawSnapshot, globalBlobs, localBaseVersion, fullSync }
|
|
|
|
|
|
|
+ return { rawSnapshot, globalBlobs, localBaseVersion, fullSync, dirty }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -182,6 +187,7 @@ async function loadSnapshotFromFile(path, remoteBaseVersion, fullSync) {
|
|
|
* @param {Snapshot} snapshot
|
|
* @param {Snapshot} snapshot
|
|
|
* @param {number} localBaseVersion
|
|
* @param {number} localBaseVersion
|
|
|
* @param {string[]} globalBlobs
|
|
* @param {string[]} globalBlobs
|
|
|
|
|
+ * @param {string[]} dirty
|
|
|
* @return {Promise<void>}
|
|
* @return {Promise<void>}
|
|
|
*/
|
|
*/
|
|
|
async function saveSnapshot(
|
|
async function saveSnapshot(
|
|
@@ -189,7 +195,8 @@ async function saveSnapshot(
|
|
|
userId,
|
|
userId,
|
|
|
snapshot,
|
|
snapshot,
|
|
|
localBaseVersion,
|
|
localBaseVersion,
|
|
|
- globalBlobs
|
|
|
|
|
|
|
+ globalBlobs,
|
|
|
|
|
+ dirty
|
|
|
) {
|
|
) {
|
|
|
const { dir, path } = snapshotPath(projectId, userId)
|
|
const { dir, path } = snapshotPath(projectId, userId)
|
|
|
await fs.promises.mkdir(dir, { recursive: true })
|
|
await fs.promises.mkdir(dir, { recursive: true })
|
|
@@ -201,6 +208,7 @@ async function saveSnapshot(
|
|
|
globalBlobs,
|
|
globalBlobs,
|
|
|
localBaseVersion,
|
|
localBaseVersion,
|
|
|
rawSnapshot: snapshot.toRaw(),
|
|
rawSnapshot: snapshot.toRaw(),
|
|
|
|
|
+ dirty,
|
|
|
}),
|
|
}),
|
|
|
// use cheapest gzip compression level
|
|
// use cheapest gzip compression level
|
|
|
{ level: 1 }
|
|
{ level: 1 }
|
|
@@ -360,10 +368,9 @@ export async function syncResourcesToDisk(
|
|
|
timings
|
|
timings
|
|
|
) {
|
|
) {
|
|
|
const remoteBaseVersion = request.baseHistoryVersion
|
|
const remoteBaseVersion = request.baseHistoryVersion
|
|
|
- let rawSnapshot, globalBlobs, localBaseVersion, source
|
|
|
|
|
- let fullSync = true
|
|
|
|
|
|
|
+ let rawSnapshot, globalBlobs, localBaseVersion, source, dirty, fullSync
|
|
|
try {
|
|
try {
|
|
|
- ;({ rawSnapshot, globalBlobs, fullSync, localBaseVersion } =
|
|
|
|
|
|
|
+ ;({ rawSnapshot, globalBlobs, fullSync, localBaseVersion, dirty } =
|
|
|
await loadSnapshot(
|
|
await loadSnapshot(
|
|
|
projectId,
|
|
projectId,
|
|
|
userId,
|
|
userId,
|
|
@@ -391,6 +398,8 @@ export async function syncResourcesToDisk(
|
|
|
localBaseVersion = remoteBaseVersion
|
|
localBaseVersion = remoteBaseVersion
|
|
|
rawSnapshot = request.rawSnapshot
|
|
rawSnapshot = request.rawSnapshot
|
|
|
globalBlobs = []
|
|
globalBlobs = []
|
|
|
|
|
+ dirty = []
|
|
|
|
|
+ fullSync = true
|
|
|
}
|
|
}
|
|
|
globalBlobs = Array.from(new Set(globalBlobs.concat(request.globalBlobs)))
|
|
globalBlobs = Array.from(new Set(globalBlobs.concat(request.globalBlobs)))
|
|
|
|
|
|
|
@@ -417,7 +426,10 @@ export async function syncResourcesToDisk(
|
|
|
changedPaths.push(...snapshot.getFilePathnames())
|
|
changedPaths.push(...snapshot.getFilePathnames())
|
|
|
logger.debug({ projectId, userId }, 'compile from cache: full sync')
|
|
logger.debug({ projectId, userId }, 'compile from cache: full sync')
|
|
|
} else {
|
|
} else {
|
|
|
- const dedupe = new Set()
|
|
|
|
|
|
|
+ const dedupe = new Set(dirty)
|
|
|
|
|
+ if (request.draft) {
|
|
|
|
|
+ dedupe.add(request.rootResourcePath)
|
|
|
|
|
+ }
|
|
|
for (const change of changes) {
|
|
for (const change of changes) {
|
|
|
for (const operation of change.getOperations()) {
|
|
for (const operation of change.getOperations()) {
|
|
|
if (operation instanceof AddFileOperation) {
|
|
if (operation instanceof AddFileOperation) {
|
|
@@ -462,6 +474,8 @@ export async function syncResourcesToDisk(
|
|
|
await ensureHasParentFolder(compileDir, path, entriesDepthFirst)
|
|
await ensureHasParentFolder(compileDir, path, entriesDepthFirst)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const wasDirty = dirty.length > 0
|
|
|
|
|
+ dirty = []
|
|
|
let createCacheFolder
|
|
let createCacheFolder
|
|
|
// Use Promise.allSettled to ensure that all writes have stopped when we exit.
|
|
// Use Promise.allSettled to ensure that all writes have stopped when we exit.
|
|
|
const allDone = await promiseMapSettledWithLimit(
|
|
const allDone = await promiseMapSettledWithLimit(
|
|
@@ -471,8 +485,19 @@ export async function syncResourcesToDisk(
|
|
|
const file = snapshot.getFile(path)
|
|
const file = snapshot.getFile(path)
|
|
|
if (!file) return // deleted, handled by removeExtraneousEntries
|
|
if (!file) return // deleted, handled by removeExtraneousEntries
|
|
|
|
|
|
|
|
- const content = file.getContent({ filterTrackedDeletes: true })
|
|
|
|
|
|
|
+ let content = file.getContent({ filterTrackedDeletes: true })
|
|
|
if (typeof content === 'string') {
|
|
if (typeof content === 'string') {
|
|
|
|
|
+ if (path === request.rootResourcePath) {
|
|
|
|
|
+ if (request.draft) {
|
|
|
|
|
+ content = DraftModeManager.PREFIX + content
|
|
|
|
|
+ dirty.push(path)
|
|
|
|
|
+ }
|
|
|
|
|
+ await TikzManager.writeOutputFileIfNeeded(
|
|
|
|
|
+ compileDir,
|
|
|
|
|
+ snapshot,
|
|
|
|
|
+ content
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
await fs.promises.writeFile(
|
|
await fs.promises.writeFile(
|
|
|
Path.join(compileDir, path),
|
|
Path.join(compileDir, path),
|
|
|
content,
|
|
content,
|
|
@@ -514,13 +539,14 @@ export async function syncResourcesToDisk(
|
|
|
throw OError.tag(result.reason, 'write failed', { path })
|
|
throw OError.tag(result.reason, 'write failed', { path })
|
|
|
}
|
|
}
|
|
|
const baseHistoryVersion = localBaseVersion + changes.length
|
|
const baseHistoryVersion = localBaseVersion + changes.length
|
|
|
- if (fullSync || changes.length) {
|
|
|
|
|
|
|
+ if (fullSync || changes.length || wasDirty || dirty.length) {
|
|
|
await saveSnapshot(
|
|
await saveSnapshot(
|
|
|
projectId,
|
|
projectId,
|
|
|
userId,
|
|
userId,
|
|
|
snapshot,
|
|
snapshot,
|
|
|
baseHistoryVersion,
|
|
baseHistoryVersion,
|
|
|
- globalBlobs
|
|
|
|
|
|
|
+ globalBlobs,
|
|
|
|
|
+ dirty
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
if (fullSync) {
|
|
if (fullSync) {
|