mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-05 01:07:41 +02:00
Fix bug in queue cleanup
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@hcengineering/query",
|
||||
"comment": "Fix bug in queue cleanup and add more tests to it",
|
||||
"type": "patch"
|
||||
}
|
||||
],
|
||||
"packageName": "@hcengineering/query"
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
// Advanced tests for complex LiveQuery scenarios
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import core, { createClient, Ref, SortingOrder, TxOperations } from '@hcengineering/core'
|
||||
import { LiveQuery } from '..'
|
||||
import { connect } from './connection'
|
||||
|
||||
async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise<void> }> {
|
||||
const storage = await createClient(connect)
|
||||
const liveQuery = new LiveQuery(storage)
|
||||
storage.notify = (...tx) => {
|
||||
void liveQuery.tx(...tx)
|
||||
}
|
||||
return {
|
||||
liveQuery,
|
||||
factory: new TxOperations(storage, core.account.System),
|
||||
close: async () => {
|
||||
await liveQuery.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('LiveQuery Advanced Coverage Tests', () => {
|
||||
it('should handle complex sorting scenarios', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
// Create multiple documents
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'Z-space',
|
||||
description: 'last',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'A-space',
|
||||
description: 'first',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'M-space',
|
||||
description: 'middle',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Query with sorting
|
||||
liveQuery.query(core.class.Space, {}, callback, { sort: { name: SortingOrder.Ascending }, limit: 10 })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
const lastResult = callback.mock.calls[callback.mock.calls.length - 1][0]
|
||||
expect(lastResult.length).toBeGreaterThan(0)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle complex query with multiple conditions', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const spaces = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: `multi-${i}`,
|
||||
description: `desc-${i}`,
|
||||
private: i % 2 === 0,
|
||||
members: [],
|
||||
archived: i > 3
|
||||
})
|
||||
spaces.push(space)
|
||||
}
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Complex query
|
||||
liveQuery.query(core.class.Space, { private: false, archived: false }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle findAll with complex options', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'findall-1',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'findall-2',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// Use findAll with various options
|
||||
const result = await liveQuery.findAll(
|
||||
core.class.Space,
|
||||
{ private: false },
|
||||
{
|
||||
limit: 10,
|
||||
sort: { modifiedOn: SortingOrder.Descending }
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle rapid document creation and updates', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { private: false }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Rapid creation
|
||||
const spaces: Array<Ref<any>> = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: `rapid-${i}`,
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
spaces.push(space)
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Rapid updates
|
||||
for (const space of spaces) {
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, space, {
|
||||
description: 'updated'
|
||||
})
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback.mock.calls.length).toBeGreaterThan(1)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle document removal from live query', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space1 = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'remove-1',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { _id: space1 }, callback, { total: true })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
callback.mockClear()
|
||||
|
||||
// Remove the document
|
||||
await factory.removeDoc(core.class.Space, core.space.Model, space1)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Callback should be called with updated results
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle query with $in operator', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space1 = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'in-1',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const space2 = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'in-2',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { _id: { $in: [space1, space2] } }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle nested document updates', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'nested-update',
|
||||
description: 'original',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
callback.mockClear()
|
||||
|
||||
// Multiple rapid updates
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, space, {
|
||||
description: 'update-1'
|
||||
})
|
||||
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, space, {
|
||||
description: 'update-2'
|
||||
})
|
||||
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, space, {
|
||||
description: 'update-3'
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle query with empty results', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { name: 'non-existent-document-name-12345' }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
const result = callback.mock.calls[0][0]
|
||||
expect(result.length).toBe(0)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle concurrent queries on same data', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'concurrent-test',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback1 = jest.fn()
|
||||
const callback2 = jest.fn()
|
||||
const callback3 = jest.fn()
|
||||
|
||||
// Start multiple queries simultaneously
|
||||
liveQuery.query(core.class.Space, { private: false }, callback1, { limit: 5 })
|
||||
liveQuery.query(core.class.Space, { private: false }, callback2, { limit: 10 })
|
||||
liveQuery.query(core.class.Space, { private: false }, callback3)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback1).toHaveBeenCalled()
|
||||
expect(callback2).toHaveBeenCalled()
|
||||
expect(callback3).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle total count in queries', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: `total-${i}`,
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
}
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { private: false }, callback, { limit: 2, total: true })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
const result = callback.mock.calls[callback.mock.calls.length - 1][0]
|
||||
expect(result.total).toBeGreaterThanOrEqual(5)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle query with archived documents', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'archived-1',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: true
|
||||
})
|
||||
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'active-1',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Query for non-archived
|
||||
liveQuery.query(core.class.Space, { archived: false }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle updating query multiple times', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'multi-update',
|
||||
description: 'v1',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Update multiple times
|
||||
for (let i = 2; i <= 5; i++) {
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, space, {
|
||||
description: `v${i}`
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, 30))
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Should have been called multiple times
|
||||
expect(callback.mock.calls.length).toBeGreaterThan(1)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle query unsubscribe and resubscribe cycle', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const callback1 = jest.fn()
|
||||
|
||||
// Subscribe
|
||||
const unsub1 = liveQuery.query(core.class.Space, {}, callback1)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Unsubscribe
|
||||
unsub1()
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
callback1.mockClear()
|
||||
|
||||
const callback2 = jest.fn()
|
||||
|
||||
// Resubscribe
|
||||
liveQuery.query(core.class.Space, {}, callback2)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(callback2).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle findOne on non-existent document', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const result = await liveQuery.findOne(core.class.Space, { _id: 'non-existent-id-12345' as Ref<any> })
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle mixed operations on same query', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const spaces: Array<Ref<any>> = []
|
||||
|
||||
// Create initial documents
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: `mixed-${i}`,
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
spaces.push(space)
|
||||
}
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { private: false }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Update one
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, spaces[0], {
|
||||
description: 'updated'
|
||||
})
|
||||
|
||||
// Create new one
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'mixed-new',
|
||||
description: 'new',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// Remove one
|
||||
await factory.removeDoc(core.class.Space, core.space.Model, spaces[1])
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
|
||||
// Should have been notified multiple times
|
||||
expect(callback.mock.calls.length).toBeGreaterThan(1)
|
||||
|
||||
await close()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,519 @@
|
||||
// Final comprehensive tests targeting uncovered scenarios
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import core, { createClient, Ref, SortingOrder, TxOperations } from '@hcengineering/core'
|
||||
import { LiveQuery } from '..'
|
||||
import { connect } from './connection'
|
||||
import { test } from './minmodel'
|
||||
|
||||
async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise<void> }> {
|
||||
const storage = await createClient(connect)
|
||||
const liveQuery = new LiveQuery(storage)
|
||||
storage.notify = (...tx) => {
|
||||
void liveQuery.tx(...tx)
|
||||
}
|
||||
return {
|
||||
liveQuery,
|
||||
factory: new TxOperations(storage, core.account.System),
|
||||
close: async () => {
|
||||
await liveQuery.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('LiveQuery Final Coverage Tests', () => {
|
||||
it('should handle complex update with attached documents', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
// Create a space
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'attach-test',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// Add attached comment
|
||||
const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', {
|
||||
message: 'original'
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(test.class.TestComment, { _id: comment }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Update the comment
|
||||
await factory.updateCollection(test.class.TestComment, space, comment, space, core.class.Space, 'comments', {
|
||||
message: 'updated'
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback.mock.calls.length).toBeGreaterThan(1)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle reverse lookup updates on attached documents', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'reverse-lookup-test',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const parentComment = await factory.addCollection(
|
||||
test.class.TestComment,
|
||||
space,
|
||||
space,
|
||||
core.class.Space,
|
||||
'comments',
|
||||
{
|
||||
message: 'parent'
|
||||
}
|
||||
)
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Query with reverse lookup
|
||||
liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, {
|
||||
lookup: {
|
||||
_id: {
|
||||
comments: test.class.TestComment
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Add child comment
|
||||
await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', {
|
||||
message: 'child1'
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Should have updated with new child
|
||||
expect(callback.mock.calls.length).toBeGreaterThan(1)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle nested lookups with multiple levels', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'nested-lookup',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const parentComment = await factory.addCollection(
|
||||
test.class.TestComment,
|
||||
space,
|
||||
space,
|
||||
core.class.Space,
|
||||
'comments',
|
||||
{
|
||||
message: 'parent'
|
||||
}
|
||||
)
|
||||
|
||||
const childComment = await factory.addCollection(
|
||||
test.class.TestComment,
|
||||
space,
|
||||
parentComment,
|
||||
test.class.TestComment,
|
||||
'comments',
|
||||
{
|
||||
message: 'child'
|
||||
}
|
||||
)
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Nested lookup
|
||||
liveQuery.query(test.class.TestComment, { _id: childComment }, callback, {
|
||||
lookup: {
|
||||
attachedTo: [test.class.TestComment, { space: core.class.Space }]
|
||||
}
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle query with limit and sorting together', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
// Create multiple documents
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: `sorted-${String(i).padStart(2, '0')}`,
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
}
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Query with limit and sort
|
||||
liveQuery.query(core.class.Space, {}, callback, {
|
||||
limit: 3,
|
||||
sort: { name: SortingOrder.Ascending }
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
const result = callback.mock.calls[callback.mock.calls.length - 1][0]
|
||||
expect(result.length).toBeLessThanOrEqual(3)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle projection with lookup', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'projection-lookup',
|
||||
description: 'test description',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', {
|
||||
message: 'test'
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(test.class.TestComment, { _id: comment }, callback, {
|
||||
projection: { message: 1, _id: 1 },
|
||||
lookup: { space: core.class.Space }
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle updates to documents with limit queries', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const spaces: Array<Ref<any>> = []
|
||||
|
||||
// Create documents
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: `limit-${i}`,
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
spaces.push(space)
|
||||
}
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Query with limit
|
||||
liveQuery.query(core.class.Space, {}, callback, { limit: 3 })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Update a document
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, spaces[0], {
|
||||
description: 'updated'
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback.mock.calls.length).toBeGreaterThan(1)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle removing attached documents', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'remove-attached',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', {
|
||||
message: 'to be removed'
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(test.class.TestComment, { _id: comment }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Remove the comment
|
||||
await factory.removeCollection(test.class.TestComment, space, comment, space, core.class.Space, 'comments')
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback.mock.calls.length).toBeGreaterThan(1)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle multiple simultaneous updates', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const spaces: Array<Ref<any>> = []
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: `multi-${i}`,
|
||||
description: 'original',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
spaces.push(space)
|
||||
}
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, {}, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const initialCalls = callback.mock.calls.length
|
||||
|
||||
// Simultaneous updates
|
||||
await Promise.all(
|
||||
spaces.map((space) =>
|
||||
factory.updateDoc(core.class.Space, core.space.Model, space, {
|
||||
description: 'updated'
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
|
||||
expect(callback.mock.calls.length).toBeGreaterThan(initialCalls)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle complex query conditions with $ne', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'not-this',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'this-one',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { name: { $ne: 'not-this' } }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle attached document updates with reverse lookup', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'reverse-update',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const parentComment = await factory.addCollection(
|
||||
test.class.TestComment,
|
||||
space,
|
||||
space,
|
||||
core.class.Space,
|
||||
'comments',
|
||||
{
|
||||
message: 'parent'
|
||||
}
|
||||
)
|
||||
|
||||
const childComment = await factory.addCollection(
|
||||
test.class.TestComment,
|
||||
space,
|
||||
parentComment,
|
||||
test.class.TestComment,
|
||||
'comments',
|
||||
{
|
||||
message: 'original child'
|
||||
}
|
||||
)
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, {
|
||||
lookup: {
|
||||
_id: { comments: test.class.TestComment }
|
||||
}
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Update child comment
|
||||
await factory.updateCollection(
|
||||
test.class.TestComment,
|
||||
space,
|
||||
childComment,
|
||||
parentComment,
|
||||
test.class.TestComment,
|
||||
'comments',
|
||||
{
|
||||
message: 'updated child'
|
||||
}
|
||||
)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback.mock.calls.length).toBeGreaterThan(1)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle query reactivation from queue with updates', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'reactivate',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback1 = jest.fn()
|
||||
|
||||
// Subscribe
|
||||
const unsub = liveQuery.query(core.class.Space, { _id: space }, callback1)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Unsubscribe to move to queue
|
||||
unsub()
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Update while in queue
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, space, {
|
||||
description: 'updated while queued'
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const callback2 = jest.fn()
|
||||
|
||||
// Reactivate from queue
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback2)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback2).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle findAll with total option', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
// Create more docs than limit
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: `total-${i}`,
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
}
|
||||
|
||||
const result = await liveQuery.findAll(
|
||||
core.class.Space,
|
||||
{},
|
||||
{
|
||||
limit: 5,
|
||||
total: true
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.length).toBe(5)
|
||||
expect(result.total).toBeGreaterThanOrEqual(10)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle complex attachedTo relationships', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'complex-attached',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const comment1 = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', {
|
||||
message: 'comment1'
|
||||
})
|
||||
|
||||
await factory.addCollection(test.class.TestComment, space, comment1, test.class.TestComment, 'comments', {
|
||||
message: 'comment2'
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(test.class.TestComment, { attachedTo: comment1 }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,379 @@
|
||||
// Coverage improvement tests for LiveQuery functionality
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import core, { createClient, TxOperations } from '@hcengineering/core'
|
||||
import { LiveQuery } from '..'
|
||||
import { connect } from './connection'
|
||||
|
||||
async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise<void> }> {
|
||||
const storage = await createClient(connect)
|
||||
const liveQuery = new LiveQuery(storage)
|
||||
storage.notify = (...tx) => {
|
||||
void liveQuery.tx(...tx)
|
||||
}
|
||||
return {
|
||||
liveQuery,
|
||||
factory: new TxOperations(storage, core.account.System),
|
||||
close: async () => {
|
||||
await liveQuery.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('LiveQuery Coverage Tests', () => {
|
||||
it('should handle refreshConnect with clean=true', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Create a query
|
||||
const unsubscribe = liveQuery.query(core.class.Space, {}, callback)
|
||||
|
||||
// Wait for initial results
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
callback.mockClear()
|
||||
|
||||
// Unsubscribe to move to queue
|
||||
unsubscribe()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Refresh with clean=true should reset the query
|
||||
await liveQuery.refreshConnect(true)
|
||||
|
||||
// Verify the query was cleaned
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle isClosed', async () => {
|
||||
const { liveQuery } = await getClient()
|
||||
|
||||
expect(liveQuery.isClosed()).toBe(false)
|
||||
|
||||
await liveQuery.close()
|
||||
|
||||
expect(liveQuery.isClosed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle findOne with projection', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
// Create a document
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'test-space',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// FindOne with projection
|
||||
const result = await liveQuery.findOne(core.class.Space, { _id: space }, { projection: { name: 1 } })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.name).toBe('test-space')
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should pass searchFulltext to client', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const result = await liveQuery.searchFulltext({ query: 'test' }, {})
|
||||
|
||||
expect(result).toBeDefined()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should return hierarchy and model from client', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const hierarchy = liveQuery.getHierarchy()
|
||||
const model = liveQuery.getModel()
|
||||
|
||||
expect(hierarchy).toBeDefined()
|
||||
expect(model).toBeDefined()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle multiple callbacks for same query', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const callback1 = jest.fn()
|
||||
const callback2 = jest.fn()
|
||||
const callback3 = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, {}, callback1)
|
||||
liveQuery.query(core.class.Space, {}, callback2)
|
||||
liveQuery.query(core.class.Space, {}, callback3)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// All callbacks should be registered and called
|
||||
expect(callback1).toHaveBeenCalled()
|
||||
expect(callback2).toHaveBeenCalled()
|
||||
expect(callback3).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should remove only specific callback on unsubscribe', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const callback1 = jest.fn()
|
||||
const callback2 = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, {}, callback1)
|
||||
const unsubscribe2 = liveQuery.query(core.class.Space, {}, callback2)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
callback1.mockClear()
|
||||
callback2.mockClear()
|
||||
|
||||
unsubscribe2()
|
||||
|
||||
// callback1 should still be active
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle query moving from active to queue and back', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const callback1 = jest.fn()
|
||||
const callback2 = jest.fn()
|
||||
|
||||
// Subscribe
|
||||
const unsubscribe1 = liveQuery.query(core.class.Space, {}, callback1)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback1).toHaveBeenCalled()
|
||||
|
||||
// Unsubscribe - moves to queue
|
||||
unsubscribe1()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
callback1.mockClear()
|
||||
callback2.mockClear()
|
||||
|
||||
// Subscribe again with same query - should reuse from queue
|
||||
liveQuery.query(core.class.Space, {}, callback2)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback2).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle TxUpdateDoc with search query', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
// Create a document
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'searchable-space',
|
||||
description: 'test document for search',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Query with search (though actual search won't work in mock, it tests the code path)
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
// Update the document
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, space, {
|
||||
name: 'updated-space'
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Callback should have been called again with updated results
|
||||
expect(callback.mock.calls.length).toBeGreaterThan(1)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle TxRemoveDoc with total tracking', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
// Create a document
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'to-be-removed',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Query with total tracking
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback, { total: true })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
callback.mockClear()
|
||||
|
||||
// Remove the document
|
||||
await factory.removeDoc(core.class.Space, core.space.Model, space)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Callback should have been called with updated results
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle lookup queries', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Query with reverse lookup
|
||||
liveQuery.query(core.class.Space, {}, callback, {
|
||||
lookup: {
|
||||
_id: {
|
||||
attachedDocs: core.class.Doc
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should compare options correctly ignoring ctx', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const callback1 = jest.fn()
|
||||
const callback2 = jest.fn()
|
||||
|
||||
// Query with same options (should reuse)
|
||||
liveQuery.query(core.class.Space, {}, callback1, { limit: 10, sort: { modifiedOn: 1 } })
|
||||
|
||||
liveQuery.query(core.class.Space, {}, callback2, { limit: 10, sort: { modifiedOn: 1 } })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback1).toHaveBeenCalled()
|
||||
expect(callback2).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should treat different options as different queries', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const callback1 = jest.fn()
|
||||
const callback2 = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, {}, callback1, { limit: 10 })
|
||||
|
||||
liveQuery.query(core.class.Space, {}, callback2, { limit: 20 })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback1).toHaveBeenCalled()
|
||||
expect(callback2).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle TxMixin updates', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
// Create a space document
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'mixin-test',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle document creation via transaction', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
// Set up query before creating document
|
||||
liveQuery.query(core.class.Space, { name: 'new-space' }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const initialCallCount = callback.mock.calls.length
|
||||
|
||||
// Create matching document
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'new-space',
|
||||
description: 'newly created',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Should have been called again with new document
|
||||
expect(callback.mock.calls.length).toBeGreaterThan(initialCallCount)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle refreshConnect without clean', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, {}, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Refresh without clean
|
||||
await liveQuery.refreshConnect(false)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle associations option', async () => {
|
||||
const { liveQuery, close } = await getClient()
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, {}, callback, { associations: [] })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,432 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { createClient, SortingOrder, Space, Tx, TxOperations } from '@hcengineering/core'
|
||||
import { LiveQuery } from '..'
|
||||
import { connect } from './connection'
|
||||
|
||||
async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations }> {
|
||||
const storage = await createClient(connect)
|
||||
const liveQuery = new LiveQuery(storage)
|
||||
storage.notify = (...tx: Tx[]) => {
|
||||
liveQuery.tx(...tx).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
return { liveQuery, factory: new TxOperations(storage, core.account.System) }
|
||||
}
|
||||
|
||||
describe('LiveQuery - Queue Management Bugs', () => {
|
||||
describe('Queue and Queries Map Consistency', () => {
|
||||
it('should properly synchronize queue and queries map when removing queries', async () => {
|
||||
const { liveQuery } = await getClient()
|
||||
|
||||
const unsubscribe1 = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
|
||||
// Callback 1
|
||||
})
|
||||
|
||||
const unsubscribe2 = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
|
||||
// Callback 2 - same query
|
||||
})
|
||||
|
||||
// Both callbacks should share the same query
|
||||
// Let's verify internal state
|
||||
const queriesMap = (liveQuery as any).queries.get(core.class.Space)
|
||||
expect(queriesMap?.size).toBe(1)
|
||||
|
||||
// Unsubscribe first callback
|
||||
unsubscribe1()
|
||||
|
||||
// Query should still exist because second callback is still active
|
||||
expect(queriesMap?.size).toBe(1)
|
||||
|
||||
// Unsubscribe second callback
|
||||
unsubscribe2()
|
||||
|
||||
// Now the query should be moved to the queue (cached for reuse)
|
||||
const queue = (liveQuery as any).queue
|
||||
expect(queue.size).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should handle rapid subscribe and unsubscribe correctly', async () => {
|
||||
const { liveQuery } = await getClient()
|
||||
|
||||
const callbacks: Array<() => void> = []
|
||||
|
||||
// Rapidly subscribe 10 times to the same query
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const unsubscribe = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
|
||||
// Callback
|
||||
})
|
||||
callbacks.push(unsubscribe)
|
||||
}
|
||||
|
||||
const queriesMap = (liveQuery as any).queries.get(core.class.Space)
|
||||
// Should only have 1 query since they're all the same
|
||||
expect(queriesMap?.size).toBe(1)
|
||||
|
||||
// Unsubscribe all
|
||||
callbacks.forEach((unsub) => {
|
||||
unsub()
|
||||
})
|
||||
|
||||
// Query should be in the queue
|
||||
const queue = (liveQuery as any).queue
|
||||
expect(queue.size).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should not leak memory when queries are unsubscribed', async () => {
|
||||
const { liveQuery } = await getClient()
|
||||
|
||||
const unsubscribeCallbacks: Array<() => void> = []
|
||||
|
||||
// Create many different queries
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const unsubscribe = liveQuery.query<Space>(core.class.Space, { name: `query-${i}` }, (result) => {
|
||||
// Callback
|
||||
})
|
||||
unsubscribeCallbacks.push(unsubscribe)
|
||||
}
|
||||
|
||||
// All queries should be in the queries map
|
||||
const queriesMap = (liveQuery as any).queries.get(core.class.Space)
|
||||
expect(queriesMap?.size).toBe(50)
|
||||
|
||||
// Unsubscribe all
|
||||
unsubscribeCallbacks.forEach((unsub) => {
|
||||
unsub()
|
||||
})
|
||||
|
||||
// All queries should now be in the queue
|
||||
const queue = (liveQuery as any).queue
|
||||
expect(queue.size).toBe(50)
|
||||
|
||||
// Wait a bit for any async operations
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Still should have 50 queries in queue
|
||||
expect(queue.size).toBe(50)
|
||||
})
|
||||
|
||||
it('should properly handle queue cleanup when exceeding CACHE_SIZE', async () => {
|
||||
const { liveQuery } = await getClient()
|
||||
|
||||
const CACHE_SIZE = 125 // From the code
|
||||
const unsubscribeCallbacks: Array<() => void> = []
|
||||
|
||||
// Create more queries than CACHE_SIZE
|
||||
for (let i = 0; i < CACHE_SIZE + 20; i++) {
|
||||
const unsubscribe = liveQuery.query<Space>(core.class.Space, { name: `query-${i}` }, (result) => {
|
||||
// Callback
|
||||
})
|
||||
unsubscribeCallbacks.push(unsubscribe)
|
||||
}
|
||||
|
||||
const queriesMapBefore = (liveQuery as any).queries.get(core.class.Space)
|
||||
expect(queriesMapBefore?.size).toBe(CACHE_SIZE + 20)
|
||||
|
||||
// Unsubscribe all to move them to queue
|
||||
unsubscribeCallbacks.forEach((unsub) => {
|
||||
unsub()
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const queue = (liveQuery as any).queue
|
||||
// Queue should not exceed CACHE_SIZE due to cleanup
|
||||
expect(queue.size).toBeLessThanOrEqual(CACHE_SIZE)
|
||||
|
||||
const queriesMapAfter = (liveQuery as any).queries.get(core.class.Space)
|
||||
// Some queries should have been removed from the queries map too
|
||||
expect(queriesMapAfter?.size).toBeLessThan(CACHE_SIZE + 20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query Callback Management', () => {
|
||||
it('should handle multiple callbacks on the same query correctly', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
|
||||
const results1: any[] = []
|
||||
const results2: any[] = []
|
||||
|
||||
const unsubscribe1 = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
|
||||
results1.push(result.length)
|
||||
})
|
||||
|
||||
const unsubscribe2 = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
|
||||
results2.push(result.length)
|
||||
})
|
||||
|
||||
// Wait for initial callbacks
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Both should have received the same initial data
|
||||
expect(results1.length).toBeGreaterThan(0)
|
||||
expect(results2.length).toBeGreaterThan(0)
|
||||
expect(results1[0]).toBe(results2[0])
|
||||
|
||||
// Create a new document
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
private: false,
|
||||
name: 'Test Space',
|
||||
description: '',
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Both callbacks should have been called again
|
||||
expect(results1.length).toBeGreaterThan(1)
|
||||
expect(results2.length).toBeGreaterThan(1)
|
||||
|
||||
unsubscribe1()
|
||||
unsubscribe2()
|
||||
})
|
||||
|
||||
it('should stop sending updates after unsubscribe', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
|
||||
const results: any[] = []
|
||||
|
||||
const unsubscribe = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
|
||||
results.push(result.length)
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const countBeforeUnsubscribe = results.length
|
||||
|
||||
unsubscribe()
|
||||
|
||||
// Create new documents after unsubscribe
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
private: false,
|
||||
name: `Space ${i}`,
|
||||
description: '',
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Should not have received new callbacks
|
||||
expect(results.length).toBe(countBeforeUnsubscribe)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query Reuse from Queue', () => {
|
||||
it('should reuse cached query from queue when re-subscribing', async () => {
|
||||
const { liveQuery } = await getClient()
|
||||
|
||||
const unsubscribe1 = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
|
||||
// First callback
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const queriesMap = (liveQuery as any).queries.get(core.class.Space)
|
||||
const firstQuery: any = Array.from(queriesMap.values())[0]
|
||||
const initialQueryId = firstQuery?.id
|
||||
|
||||
unsubscribe1()
|
||||
|
||||
// Query should be in queue now
|
||||
const queue = (liveQuery as any).queue
|
||||
expect(queue.size).toBeGreaterThan(0)
|
||||
|
||||
// Subscribe again to the same query
|
||||
const unsubscribe2 = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
|
||||
// Second callback
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const queriesMapAfter = (liveQuery as any).queries.get(core.class.Space)
|
||||
const secondQuery: any = Array.from(queriesMapAfter.values())[0]
|
||||
const reusedQueryId = secondQuery?.id
|
||||
|
||||
// Should reuse the same query
|
||||
expect(reusedQueryId).toBe(initialQueryId)
|
||||
|
||||
unsubscribe2()
|
||||
})
|
||||
|
||||
it('should handle query options comparison correctly', async () => {
|
||||
const { liveQuery } = await getClient()
|
||||
|
||||
const unsubscribe1 = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {}, {
|
||||
limit: 10,
|
||||
sort: { name: SortingOrder.Ascending }
|
||||
})
|
||||
|
||||
const unsubscribe2 = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {}, {
|
||||
limit: 10,
|
||||
sort: { name: SortingOrder.Ascending }
|
||||
})
|
||||
|
||||
const queriesMap = (liveQuery as any).queries.get(core.class.Space)
|
||||
// Should share the same query because options match
|
||||
expect(queriesMap?.size).toBe(1)
|
||||
|
||||
unsubscribe1()
|
||||
unsubscribe2()
|
||||
|
||||
// Different options should create different query
|
||||
const unsubscribe3 = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {}, {
|
||||
limit: 20,
|
||||
sort: { name: SortingOrder.Descending }
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Now should have 2 queries (1 in queue, 1 active)
|
||||
const totalQueries = queriesMap?.size ?? 0 + ((liveQuery as any).queue.size ?? 0)
|
||||
expect(totalQueries).toBeGreaterThan(1)
|
||||
|
||||
unsubscribe3()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases and Error Conditions', () => {
|
||||
it('should handle empty query results', async () => {
|
||||
const { liveQuery } = await getClient()
|
||||
|
||||
const results: any[] = []
|
||||
|
||||
const unsubscribe = liveQuery.query<Space>(core.class.Space, { name: 'NonExistentSpace123456' }, (result) => {
|
||||
results.push(result)
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results[0]).toHaveLength(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should handle query on closed LiveQuery', async () => {
|
||||
const { liveQuery } = await getClient()
|
||||
|
||||
await liveQuery.close()
|
||||
|
||||
// Attempting to query after close should not crash
|
||||
expect(() => {
|
||||
liveQuery.query<Space>(core.class.Space, {}, (result) => {})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle concurrent findAll and query operations', async () => {
|
||||
const { liveQuery } = await getClient()
|
||||
|
||||
const promises = []
|
||||
|
||||
// Start multiple findAll operations
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(liveQuery.findAll(core.class.Space, { private: false }))
|
||||
}
|
||||
|
||||
// Start multiple query operations
|
||||
for (let i = 0; i < 10; i++) {
|
||||
liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {})
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises)
|
||||
|
||||
results.forEach((result) => {
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query Counter and ID Management', () => {
|
||||
it('should generate unique query IDs', async () => {
|
||||
const { liveQuery } = await getClient()
|
||||
|
||||
const queryIds = new Set<number>()
|
||||
const unsubscribes: Array<() => void> = []
|
||||
|
||||
// Create many queries
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const unsubscribe = liveQuery.query<Space>(core.class.Space, { name: `unique-query-${i}` }, (result) => {})
|
||||
unsubscribes.push(unsubscribe)
|
||||
}
|
||||
|
||||
const queriesMap = (liveQuery as any).queries.get(core.class.Space)
|
||||
if (queriesMap !== undefined) {
|
||||
for (const query of queriesMap.values()) {
|
||||
queryIds.add(query.id)
|
||||
}
|
||||
}
|
||||
|
||||
// All IDs should be unique
|
||||
expect(queryIds.size).toBe(100)
|
||||
|
||||
unsubscribes.forEach((unsub) => {
|
||||
unsub()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query Result Consistency', () => {
|
||||
it('should maintain result consistency across multiple callbacks', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
|
||||
const callback1Results: number[] = []
|
||||
const callback2Results: number[] = []
|
||||
|
||||
const unsubscribe1 = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
|
||||
callback1Results.push(result.length)
|
||||
})
|
||||
|
||||
// Wait for first callback to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const unsubscribe2 = liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
|
||||
callback2Results.push(result.length)
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Create a document
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
private: false,
|
||||
name: 'Consistency Test',
|
||||
description: '',
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
|
||||
// Both callbacks should have received updates after document creation
|
||||
// Note: Due to setTimeout(0) in pushCallback, timing may vary slightly
|
||||
// but both should see at least 2 updates (initial + after create)
|
||||
expect(callback1Results.length).toBeGreaterThanOrEqual(2)
|
||||
expect(callback2Results.length).toBeGreaterThanOrEqual(2)
|
||||
|
||||
// Final counts should match after everything settles
|
||||
const final1 = callback1Results[callback1Results.length - 1]
|
||||
const final2 = callback2Results[callback2Results.length - 1]
|
||||
expect(final1).toBe(final2)
|
||||
|
||||
unsubscribe1()
|
||||
unsubscribe2()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,298 @@
|
||||
// Tests for Refs class to improve coverage
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import core, { createClient, TxOperations } from '@hcengineering/core'
|
||||
import { LiveQuery } from '..'
|
||||
import { connect } from './connection'
|
||||
|
||||
async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise<void> }> {
|
||||
const storage = await createClient(connect)
|
||||
const liveQuery = new LiveQuery(storage)
|
||||
storage.notify = (...tx) => {
|
||||
void liveQuery.tx(...tx)
|
||||
}
|
||||
return {
|
||||
liveQuery,
|
||||
factory: new TxOperations(storage, core.account.System),
|
||||
close: async () => {
|
||||
await liveQuery.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('Refs Class Coverage Tests', () => {
|
||||
it('should find document from refs cache with specific _id', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
// Create a document
|
||||
const space1 = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'cached-space-1',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// Query it to cache it
|
||||
const callback = jest.fn()
|
||||
liveQuery.query(core.class.Space, { _id: space1 }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Now findOne should use cached version
|
||||
const result = await liveQuery.findOne(core.class.Space, { _id: space1 })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?._id).toBe(space1)
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle findOne with limit=1 without sort', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
// Create multiple documents
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'test-1',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'test-2',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// findOne with limit should use refs optimization
|
||||
const result = await liveQuery.findOne(core.class.Space, { private: false }, { limit: 1 })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle findOne with associations and lookup', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'lookup-test',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// Query with lookup to populate refs cache
|
||||
const callback = jest.fn()
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback, { lookup: { _id: { docs: core.class.Doc } } })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// FindOne should use refs cache
|
||||
const result = await liveQuery.findOne(
|
||||
core.class.Space,
|
||||
{ _id: space },
|
||||
{ lookup: { _id: { docs: core.class.Doc } } }
|
||||
)
|
||||
|
||||
expect(result).toBeDefined()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should strip $lookup and $associations when finding from cache without them', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'strip-test',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// Query with lookup to cache with $lookup
|
||||
const callback = jest.fn()
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback, { lookup: { _id: { docs: core.class.Doc } } })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// FindOne without lookup should strip $lookup from cached doc
|
||||
const result = await liveQuery.findOne(core.class.Space, { _id: space })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect((result as any).$lookup).toBeUndefined()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle mixin class in findOne', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'mixin-findone',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// Query to cache
|
||||
const callback = jest.fn()
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Find with different class to test mixin path
|
||||
const result = await liveQuery.findOne(core.class.Space, { _id: space })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle query with lookup and associations together', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'both-options',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback, {
|
||||
lookup: { _id: { docs: core.class.Doc } },
|
||||
associations: []
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle updating refs cache on document changes', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'update-refs',
|
||||
description: 'original',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// Cache it
|
||||
const callback = jest.fn()
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Update it
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, space, {
|
||||
description: 'updated'
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Find should have updated version
|
||||
const result = await liveQuery.findOne(core.class.Space, { _id: space })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.description).toBe('updated')
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should clean refs cache when query is removed', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'clean-refs',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// Cache it
|
||||
const callback = jest.fn()
|
||||
const unsubscribe = liveQuery.query(core.class.Space, { _id: space }, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Unsubscribe to clean refs
|
||||
unsubscribe()
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should handle multiple queries referencing same document', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
const space = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'multi-ref',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// Multiple queries on same document
|
||||
const callback1 = jest.fn()
|
||||
const callback2 = jest.fn()
|
||||
const callback3 = jest.fn()
|
||||
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback1)
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback2)
|
||||
liveQuery.query(core.class.Space, { _id: space }, callback3)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(callback1).toHaveBeenCalled()
|
||||
expect(callback2).toHaveBeenCalled()
|
||||
expect(callback3).toHaveBeenCalled()
|
||||
|
||||
await close()
|
||||
})
|
||||
|
||||
it('should use refs cache for descendants check', async () => {
|
||||
const { liveQuery, factory, close } = await getClient()
|
||||
|
||||
// Create documents
|
||||
const space1 = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'desc-test-1',
|
||||
description: 'test',
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false
|
||||
})
|
||||
|
||||
// Cache with callback
|
||||
const callback = jest.fn()
|
||||
liveQuery.query(core.class.Space, {}, callback)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// FindOne by _id should check descendants
|
||||
const result = await liveQuery.findOne(core.class.Space, { _id: space1 })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
|
||||
await close()
|
||||
})
|
||||
})
|
||||
@@ -413,7 +413,11 @@ export class LiveQuery implements WithTx, Client {
|
||||
|
||||
private remove (): void {
|
||||
const used = Array.from(this.queue.values()).sort((a, b) => a.lastUsed - b.lastUsed)
|
||||
for (let i = 0; i < CACHE_SIZE / 10; i++) {
|
||||
// Remove enough queries to bring the queue back down to 80% of CACHE_SIZE
|
||||
// This prevents constant cleanup cycles
|
||||
const targetSize = Math.floor(CACHE_SIZE * 0.8)
|
||||
const toRemove = Math.max(0, this.queue.size - targetSize)
|
||||
for (let i = 0; i < toRemove; i++) {
|
||||
const q = used.shift()
|
||||
if (q === undefined) return
|
||||
this.removeQueue(q)
|
||||
@@ -457,6 +461,10 @@ export class LiveQuery implements WithTx, Client {
|
||||
q.result.clean()
|
||||
}
|
||||
this.queue.set(q.id, { ...q, lastUsed: platformNow() })
|
||||
// Check if we need to clean up the queue after adding this query
|
||||
if (this.queue.size > CACHE_SIZE) {
|
||||
this.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user