|
@@ -8,6 +8,11 @@ const {
|
|
|
History,
|
|
History,
|
|
|
File,
|
|
File,
|
|
|
AddFileOperation,
|
|
AddFileOperation,
|
|
|
|
|
+ EditFileOperation,
|
|
|
|
|
+ AddCommentOperation,
|
|
|
|
|
+ TextOperation,
|
|
|
|
|
+ Range,
|
|
|
|
|
+ TrackingProps,
|
|
|
Change,
|
|
Change,
|
|
|
} = require('overleaf-editor-core')
|
|
} = require('overleaf-editor-core')
|
|
|
const cleanup = require('./support/cleanup')
|
|
const cleanup = require('./support/cleanup')
|
|
@@ -59,6 +64,9 @@ describe('chunk buffer', function () {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('should load from chunk store and update cache on first access (cache miss)', async function () {
|
|
it('should load from chunk store and update cache on first access (cache miss)', async function () {
|
|
|
|
|
+ // Load the underlying chunk from the chunk store for verification
|
|
|
|
|
+ const storedChunk = await chunkStore.loadLatest(projectId)
|
|
|
|
|
+
|
|
|
// First access should load from chunk store and populate cache
|
|
// First access should load from chunk store and populate cache
|
|
|
const firstResult = await chunkBuffer.loadLatest(projectId)
|
|
const firstResult = await chunkBuffer.loadLatest(projectId)
|
|
|
|
|
|
|
@@ -67,6 +75,9 @@ describe('chunk buffer', function () {
|
|
|
expect(firstResult.getStartVersion()).to.equal(1)
|
|
expect(firstResult.getStartVersion()).to.equal(1)
|
|
|
expect(firstResult.getEndVersion()).to.equal(2)
|
|
expect(firstResult.getEndVersion()).to.equal(2)
|
|
|
|
|
|
|
|
|
|
+ // Verify the chunk is the same as the one in the store
|
|
|
|
|
+ expect(firstResult).to.deep.equal(storedChunk)
|
|
|
|
|
+
|
|
|
// Verify that we got a cache miss metric
|
|
// Verify that we got a cache miss metric
|
|
|
expect(
|
|
expect(
|
|
|
metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
|
|
metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
|
|
@@ -85,6 +96,9 @@ describe('chunk buffer', function () {
|
|
|
expect(secondResult.getStartVersion()).to.equal(1)
|
|
expect(secondResult.getStartVersion()).to.equal(1)
|
|
|
expect(secondResult.getEndVersion()).to.equal(2)
|
|
expect(secondResult.getEndVersion()).to.equal(2)
|
|
|
|
|
|
|
|
|
|
+ // Verify the chunk is the same as the one in the store
|
|
|
|
|
+ expect(secondResult).to.deep.equal(storedChunk)
|
|
|
|
|
+
|
|
|
// Verify that we got a cache hit metric
|
|
// Verify that we got a cache hit metric
|
|
|
expect(
|
|
expect(
|
|
|
metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
|
|
metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
|
|
@@ -99,6 +113,7 @@ describe('chunk buffer', function () {
|
|
|
expect(secondResult.getEndVersion()).to.equal(
|
|
expect(secondResult.getEndVersion()).to.equal(
|
|
|
firstResult.getEndVersion()
|
|
firstResult.getEndVersion()
|
|
|
)
|
|
)
|
|
|
|
|
+ expect(secondResult).to.deep.equal(firstResult)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('should refresh the cache when chunk changes in the store', async function () {
|
|
it('should refresh the cache when chunk changes in the store', async function () {
|
|
@@ -129,12 +144,17 @@ describe('chunk buffer', function () {
|
|
|
// Store the new chunk directly in the chunk store
|
|
// Store the new chunk directly in the chunk store
|
|
|
await chunkStore.create(projectId, newChunk)
|
|
await chunkStore.create(projectId, newChunk)
|
|
|
|
|
|
|
|
|
|
+ // Load the underlying chunk from the chunk store for verification
|
|
|
|
|
+ const storedChunk = await chunkStore.loadLatest(projectId)
|
|
|
|
|
+
|
|
|
// Access again - should detect the change and refresh cache
|
|
// Access again - should detect the change and refresh cache
|
|
|
const secondResult = await chunkBuffer.loadLatest(projectId)
|
|
const secondResult = await chunkBuffer.loadLatest(projectId)
|
|
|
|
|
|
|
|
// Verify we got the updated chunk
|
|
// Verify we got the updated chunk
|
|
|
expect(secondResult.getStartVersion()).to.equal(2)
|
|
expect(secondResult.getStartVersion()).to.equal(2)
|
|
|
expect(secondResult.getEndVersion()).to.equal(3)
|
|
expect(secondResult.getEndVersion()).to.equal(3)
|
|
|
|
|
+ // Verify that the chunk content is the same
|
|
|
|
|
+ expect(secondResult).to.deep.equal(storedChunk)
|
|
|
|
|
|
|
|
// Verify that we got a cache miss metric (since the cached chunk was invalidated)
|
|
// Verify that we got a cache miss metric (since the cached chunk was invalidated)
|
|
|
expect(
|
|
expect(
|
|
@@ -145,6 +165,9 @@ describe('chunk buffer', function () {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('should continue using cache when chunk in store has not changed', async function () {
|
|
it('should continue using cache when chunk in store has not changed', async function () {
|
|
|
|
|
+ // Load the underlying chunk from the chunk store for verification
|
|
|
|
|
+ const storedChunk = await chunkStore.loadLatest(projectId)
|
|
|
|
|
+
|
|
|
// First access to load into cache
|
|
// First access to load into cache
|
|
|
await chunkBuffer.loadLatest(projectId)
|
|
await chunkBuffer.loadLatest(projectId)
|
|
|
|
|
|
|
@@ -157,6 +180,7 @@ describe('chunk buffer', function () {
|
|
|
// Verify we got the same chunk
|
|
// Verify we got the same chunk
|
|
|
expect(result.getStartVersion()).to.equal(1)
|
|
expect(result.getStartVersion()).to.equal(1)
|
|
|
expect(result.getEndVersion()).to.equal(2)
|
|
expect(result.getEndVersion()).to.equal(2)
|
|
|
|
|
+ expect(result).to.deep.equal(storedChunk)
|
|
|
|
|
|
|
|
// Verify that we got a cache hit metric
|
|
// Verify that we got a cache hit metric
|
|
|
expect(
|
|
expect(
|
|
@@ -167,11 +191,122 @@ describe('chunk buffer', function () {
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ it('should handle a chunk with metadata, comments and tracked changes', async function () {
|
|
|
|
|
+ // Create a snapshot and initial file
|
|
|
|
|
+ const snapshot = new Snapshot()
|
|
|
|
|
+ const initialFileOp = new AddFileOperation(
|
|
|
|
|
+ 'test.tex',
|
|
|
|
|
+ File.fromString('Initial line.\\nSecond line.', {
|
|
|
|
|
+ meta1: 'abc',
|
|
|
|
|
+ meta2: 'def',
|
|
|
|
|
+ })
|
|
|
|
|
+ )
|
|
|
|
|
+ const initialChange = new Change([initialFileOp], new Date(), [])
|
|
|
|
|
+
|
|
|
|
|
+ // Add a comment
|
|
|
|
|
+ const commentOp = new AddCommentOperation(
|
|
|
|
|
+ 'comment1',
|
|
|
|
|
+ [new Range(0, 7)] // Range for "Initial"
|
|
|
|
|
+ )
|
|
|
|
|
+ const commentChange = new Change(
|
|
|
|
|
+ [new EditFileOperation('test.tex', commentOp)],
|
|
|
|
|
+ new Date(),
|
|
|
|
|
+ []
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // Tracked insert
|
|
|
|
|
+ const trackedInsertOp = new TextOperation()
|
|
|
|
|
+ .retain(14)
|
|
|
|
|
+ .insert('Hello', {
|
|
|
|
|
+ commentIds: ['comment1'],
|
|
|
|
|
+ tracking: TrackingProps.fromRaw({
|
|
|
|
|
+ ts: '2024-01-01T00:00:00.000Z',
|
|
|
|
|
+ type: 'insert',
|
|
|
|
|
+ userId: 'user1',
|
|
|
|
|
+ }),
|
|
|
|
|
+ })
|
|
|
|
|
+ .retain(12)
|
|
|
|
|
+ const insertChange = new Change(
|
|
|
|
|
+ [new EditFileOperation('test.tex', trackedInsertOp)],
|
|
|
|
|
+ new Date(),
|
|
|
|
|
+ []
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // Tracked delete
|
|
|
|
|
+ const trackedDeleteOp = new TextOperation().retain(14, {
|
|
|
|
|
+ tracking: TrackingProps.fromRaw({
|
|
|
|
|
+ ts: '2024-01-01T00:00:00.000Z',
|
|
|
|
|
+ type: 'delete',
|
|
|
|
|
+ userId: 'user1',
|
|
|
|
|
+ }),
|
|
|
|
|
+ })
|
|
|
|
|
+ const deleteChange = new Change(
|
|
|
|
|
+ [new EditFileOperation('test.tex', trackedDeleteOp)],
|
|
|
|
|
+ new Date(),
|
|
|
|
|
+ []
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // Combine changes into history and create chunk
|
|
|
|
|
+ const history = new History(snapshot, [
|
|
|
|
|
+ initialChange,
|
|
|
|
|
+ commentChange,
|
|
|
|
|
+ insertChange,
|
|
|
|
|
+ deleteChange,
|
|
|
|
|
+ ])
|
|
|
|
|
+ const chunk = new Chunk(history, 1) // Start version 0
|
|
|
|
|
+ // Store the chunk
|
|
|
|
|
+ await chunkStore.create(projectId, chunk)
|
|
|
|
|
+ // Clear the cache
|
|
|
|
|
+ await redisBackend.clearCache(projectId)
|
|
|
|
|
+ metrics.inc.resetHistory()
|
|
|
|
|
+
|
|
|
|
|
+ // Load the underlying chunk from the chunk store for verification
|
|
|
|
|
+ const storedChunk = await chunkStore.loadLatest(projectId)
|
|
|
|
|
+
|
|
|
|
|
+ // Load the chunk via buffer (cache miss)
|
|
|
|
|
+ const firstResult = await chunkBuffer.loadLatest(projectId)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify chunk details
|
|
|
|
|
+ expect(firstResult.getStartVersion()).to.equal(1)
|
|
|
|
|
+ expect(firstResult.getEndVersion()).to.equal(5) // 4 changes
|
|
|
|
|
+ expect(firstResult.history.changes.length).to.equal(4)
|
|
|
|
|
+ expect(firstResult).to.deep.equal(storedChunk)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify cache miss metric
|
|
|
|
|
+ expect(
|
|
|
|
|
+ metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
|
|
|
|
|
+ status: 'cache-miss',
|
|
|
|
|
+ })
|
|
|
|
|
+ ).to.be.true
|
|
|
|
|
+
|
|
|
|
|
+ // Reset metrics
|
|
|
|
|
+ metrics.inc.resetHistory()
|
|
|
|
|
+
|
|
|
|
|
+ // Second access should hit the cache
|
|
|
|
|
+ const secondResult = await chunkBuffer.loadLatest(projectId)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify we got the same chunk
|
|
|
|
|
+ expect(secondResult.getStartVersion()).to.equal(1)
|
|
|
|
|
+ expect(secondResult.getEndVersion()).to.equal(5)
|
|
|
|
|
+ expect(secondResult.history.changes.length).to.equal(4)
|
|
|
|
|
+ expect(secondResult).to.deep.equal(storedChunk)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify cache hit metric
|
|
|
|
|
+ expect(
|
|
|
|
|
+ metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
|
|
|
|
|
+ status: 'cache-hit',
|
|
|
|
|
+ })
|
|
|
|
|
+ ).to.be.true
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
describe('with an empty project', function () {
|
|
describe('with an empty project', function () {
|
|
|
it('should handle a case with empty chunks (no changes)', async function () {
|
|
it('should handle a case with empty chunks (no changes)', async function () {
|
|
|
// Clear the cache
|
|
// Clear the cache
|
|
|
await redisBackend.clearCache(projectId)
|
|
await redisBackend.clearCache(projectId)
|
|
|
|
|
|
|
|
|
|
+ // Load the underlying chunk from the chunk store for verification
|
|
|
|
|
+ const storedChunk = await chunkStore.loadLatest(projectId)
|
|
|
|
|
+
|
|
|
// Load the initial empty chunk via buffer
|
|
// Load the initial empty chunk via buffer
|
|
|
const result = await chunkBuffer.loadLatest(projectId)
|
|
const result = await chunkBuffer.loadLatest(projectId)
|
|
|
|
|
|
|
@@ -180,6 +315,9 @@ describe('chunk buffer', function () {
|
|
|
expect(result.getEndVersion()).to.equal(0) // Start equals end for empty chunks
|
|
expect(result.getEndVersion()).to.equal(0) // Start equals end for empty chunks
|
|
|
expect(result.history.changes.length).to.equal(0)
|
|
expect(result.history.changes.length).to.equal(0)
|
|
|
|
|
|
|
|
|
|
+ // Verify that the chunk is the same as the one in the store
|
|
|
|
|
+ expect(result).to.deep.equal(storedChunk)
|
|
|
|
|
+
|
|
|
// Verify cache miss metric
|
|
// Verify cache miss metric
|
|
|
expect(
|
|
expect(
|
|
|
metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
|
|
metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
|
|
@@ -198,6 +336,9 @@ describe('chunk buffer', function () {
|
|
|
expect(secondResult.getEndVersion()).to.equal(0)
|
|
expect(secondResult.getEndVersion()).to.equal(0)
|
|
|
expect(secondResult.history.changes.length).to.equal(0)
|
|
expect(secondResult.history.changes.length).to.equal(0)
|
|
|
|
|
|
|
|
|
|
+ // Verify that the chunk is the same as the one in the store
|
|
|
|
|
+ expect(secondResult).to.deep.equal(storedChunk)
|
|
|
|
|
+
|
|
|
// Verify cache hit metric
|
|
// Verify cache hit metric
|
|
|
expect(
|
|
expect(
|
|
|
metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
|
|
metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
|