Refresh queries (#85)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina
2025-07-10 16:24:24 +04:00
committed by GitHub
parent 41c511fffd
commit a3743876d1
9 changed files with 324 additions and 150 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
import { CollaboratorsQuery, LabelsQuery, MessagesQuery, NotificationContextsQuery, NotificationsQuery } from './query'
export type { MessageQueryParams } from '@hcengineering/communication-query'
export { initLiveQueries } from './init'
export { initLiveQueries, refreshLiveQueries } from './init'
export function createMessagesQuery (dontDestroy?: boolean): MessagesQuery {
return new MessagesQuery(dontDestroy)
+6
View File
@@ -46,3 +46,9 @@ export function initLiveQueries (
lq = new LiveQueries(client, workspace, filesUrl)
}
export async function refreshLiveQueries (): Promise<void> {
if (lq != null) {
await lq.refresh()
}
}
+15 -10
View File
@@ -15,15 +15,15 @@
import type { AccountID, Collaborator, FindCollaboratorsParams, WorkspaceID } from '@hcengineering/communication-types'
import {
AddCollaboratorsEvent,
CardEventType,
type Event,
type EventResult,
type FindClient,
type QueryCallback,
type Event,
NotificationEventType,
CardEventType,
AddCollaboratorsEvent,
RemoveCollaboratorsEvent,
RemoveCardEvent
type QueryCallback,
RemoveCardEvent,
RemoveCollaboratorsEvent
} from '@hcengineering/communication-sdk-types'
import { QueryResult } from '../result'
@@ -47,6 +47,9 @@ export class CollaboratorsQuery implements Query<Collaborator, FindCollaborators
void this.notify()
} else {
this.result = this.initResult()
void this.result.then(() => {
void this.notify()
})
}
}
@@ -123,10 +126,7 @@ export class CollaboratorsQuery implements Query<Collaborator, FindCollaborators
private async initResult (): Promise<QueryResult<Collaborator>> {
try {
const res = await this.find(this.params)
const result = new QueryResult(res, (c) => c.account)
void this.notify()
return result
return new QueryResult(res, (c) => c.account)
} catch (error) {
console.error('Failed to initialize query:', error)
return new QueryResult([] as Collaborator[], (c) => c.account)
@@ -175,4 +175,9 @@ export class CollaboratorsQuery implements Query<Collaborator, FindCollaborators
}
return true
}
public async refresh (): Promise<void> {
this.result = new QueryResult([] as Collaborator[], (c) => c.account)
await this.initResult()
}
}
+16 -11
View File
@@ -15,16 +15,16 @@
import type { FindLabelsParams, Label, WorkspaceID } from '@hcengineering/communication-types'
import {
type EventResult,
type FindClient,
type QueryCallback,
type Event,
LabelEventType,
CardEventType,
CreateLabelEvent,
type Event,
type EventResult,
type FindClient,
LabelEventType,
type QueryCallback,
RemoveCardEvent,
RemoveLabelEvent,
UpdateCardTypeEvent,
RemoveCardEvent
UpdateCardTypeEvent
} from '@hcengineering/communication-sdk-types'
import { QueryResult } from '../result'
@@ -52,6 +52,9 @@ export class LabelsQuery implements Query<Label, FindLabelsParams> {
void this.notify()
} else {
this.result = this.initResult()
void this.result.then(() => {
void this.notify()
})
}
}
@@ -186,10 +189,7 @@ export class LabelsQuery implements Query<Label, FindLabelsParams> {
private async initResult (): Promise<QueryResult<Label>> {
try {
const res = await this.find(this.params)
const result = new QueryResult(res, getId)
void this.notify()
return result
return new QueryResult(res, getId)
} catch (error) {
console.error('Failed to initialize query:', error)
return new QueryResult([] as Label[], getId)
@@ -251,4 +251,9 @@ export class LabelsQuery implements Query<Label, FindLabelsParams> {
}
return true
}
async refresh (): Promise<void> {
this.result = new QueryResult([] as Label[], getId)
await this.initResult()
}
}
+15
View File
@@ -221,4 +221,19 @@ export class LiveQueries {
this.queries.clear()
this.unsubscribed.clear()
}
async refresh (): Promise<void> {
for (const [id, query] of this.queries.entries()) {
if (this.unsubscribed.has(id)) {
this.unsubscribe(id)
continue
}
try {
await query.refresh()
} catch (e) {
console.error('Failed to refresh live query', e, query.id, query.params)
}
}
}
}
+74 -21
View File
@@ -57,7 +57,7 @@ const GROUPS_LIMIT = 4
export class MessagesQuery implements PagedQuery<Message, MessageQueryParams> {
private result: Promise<QueryResult<Message>> | QueryResult<Message>
private readonly groupsBuffer: MessagesGroup[] = []
private groupsBuffer: MessagesGroup[] = []
private firstGroup?: MessagesGroup
private lastGroup?: MessagesGroup
@@ -69,6 +69,9 @@ export class MessagesQuery implements PagedQuery<Message, MessageQueryParams> {
private readonly limit: number
private initialized = false
nexLoadedPagesCount = 0
prevLoadedPagesCount = 0
private readonly next = {
hasMessages: true,
hasGroups: true,
@@ -250,33 +253,37 @@ export class MessagesQuery implements PagedQuery<Message, MessageQueryParams> {
await this.client.unsubscribeQuery(this.id)
}
async requestLoadNextPage (): Promise<void> {
if (this.isCardRemoved) return
async requestLoadNextPage (notify = true): Promise<{ isDone: boolean }> {
if (this.isCardRemoved) return { isDone: true }
if (this.result instanceof Promise) this.result = await this.result
if (!this.result.isTail()) {
this.result = this.loadPage(Direction.Forward, this.result)
void this.result
.then(() => this.notify())
.catch((error) => {
console.error('Failed to load messages', error)
void this.notify()
})
if (this.result.isTail()) return { isDone: true }
const pagePromise = this.loadPage(Direction.Forward, this.result)
this.nexLoadedPagesCount++
this.result = pagePromise
const r = await pagePromise
if (notify) {
await this.notify()
}
return { isDone: r.isTail() }
}
async requestLoadPrevPage (): Promise<void> {
if (this.isCardRemoved) return
async requestLoadPrevPage (notify = true): Promise<{ isDone: boolean }> {
if (this.isCardRemoved) return { isDone: true }
if (this.result instanceof Promise) this.result = await this.result
if (!this.result.isHead()) {
this.result = this.loadPage(Direction.Backward, this.result)
void this.result
.then(() => this.notify())
.catch((error) => {
console.error('Failed to load messages', error)
void this.notify()
})
if (this.result.isHead()) return { isDone: true }
const pagePromise = this.loadPage(Direction.Backward, this.result)
this.prevLoadedPagesCount++
this.result = pagePromise
const r = await pagePromise
if (notify) {
await this.notify()
}
return { isDone: r.isHead() }
}
removeCallback (): void {
@@ -845,4 +852,50 @@ export class MessagesQuery implements PagedQuery<Message, MessageQueryParams> {
return result
}
async refresh (): Promise<void> {
const nextPagesCount = this.nexLoadedPagesCount
const prevPagesCount = this.prevLoadedPagesCount
this.nexLoadedPagesCount = 0
this.prevLoadedPagesCount = 0
this.groupsBuffer = []
this.firstGroup = undefined
this.lastGroup = undefined
this.firstLoadedGroup = undefined
this.lastLoadedGroup = undefined
this.lastGroupsDirection = undefined
this.next.hasMessages = true
this.next.hasGroups = true
this.next.buffer = []
this.prev.hasMessages = true
this.prev.hasGroups = true
this.prev.buffer = []
this.createdPatches.clear()
this.tmpMessages.clear()
this.result = new QueryResult([] as Message[], (x) => x.id)
this.result.setTail(this.params.from == null)
this.result.setHead(this.params.from == null)
this.initialized = false
for (let i = 0; i < nextPagesCount; i++) {
const { isDone } = await this.requestLoadNextPage(false)
this.initialized = true
if (!isDone) break
}
for (let i = 0; i < prevPagesCount; i++) {
const { isDone } = await this.requestLoadPrevPage(false)
this.initialized = true
if (!isDone) break
}
await this.notify()
}
}
@@ -59,8 +59,11 @@ import { findMessage, loadMessageFromGroup, matchNotification } from '../utils'
const allowedPatchTypes = [PatchType.update, PatchType.remove, PatchType.blob]
export class NotificationContextsQuery implements PagedQuery<NotificationContext, FindNotificationContextParams> {
private result: QueryResult<NotificationContext> | Promise<QueryResult<NotificationContext>>
private forward: Promise<NotificationContext[]> | NotificationContext[] = []
private backward: Promise<NotificationContext[]> | NotificationContext[] = []
private forward: Promise<{ isDone: boolean }> | { isDone: boolean } = { isDone: false }
private backward: Promise<{ isDone: boolean }> | { isDone: boolean } = { isDone: false }
nexLoadedPagesCount = 0
prevLoadedPagesCount = 0
constructor (
private readonly client: FindClient,
@@ -76,39 +79,46 @@ export class NotificationContextsQuery implements PagedQuery<NotificationContext
limit: params.limit,
order: params.order ?? defaultQueryParams.order
}
const limit = params.limit != null ? params.limit + 1 : undefined
if (initialResult !== undefined) {
this.result = initialResult
void this.notify()
} else {
this.result = this.initResult()
void this.result.then(() => {
void this.notify()
})
}
}
private async rawInitResult (): Promise<QueryResult<NotificationContext>> {
const limit = this.params.limit != null ? this.params.limit + 1 : undefined
const findParams: FindNotificationContextParams = {
...this.params,
order: this.params.order ?? defaultQueryParams.order,
limit
}
if (initialResult !== undefined) {
this.result = initialResult
void this.notify()
} else {
const findPromise = this.find(findParams)
this.result = findPromise.then((res) => {
const allLoaded = limit == null || res.length < limit
const isTail = allLoaded || (params.lastNotify == null && params.order === SortingOrder.Descending)
const isHead = allLoaded || (params.lastNotify == null && params.order === SortingOrder.Ascending)
const res = await this.find(findParams)
const isComplete = limit == null || res.length < limit
if (!isComplete) res.pop()
if (limit != null && res.length >= limit) {
res.pop()
}
const qResult = new QueryResult(res, (x) => x.id)
qResult.setTail(isTail)
qResult.setHead(isHead)
const isTail = isComplete || (this.params.lastNotify == null && this.params.order === SortingOrder.Descending)
const isHead = isComplete || (this.params.lastNotify == null && this.params.order === SortingOrder.Ascending)
return qResult
})
this.result
.then(async () => {
await this.notify()
})
.catch((err: any) => {
console.error('Failed to update Live query: ', err)
})
const result = new QueryResult(res, (it) => it.id)
result.setTail(isTail)
result.setHead(isHead)
return result
}
private async initResult (): Promise<QueryResult<NotificationContext>> {
try {
return await this.rawInitResult()
} catch (error) {
console.error('Failed to initialize query:', error)
return new QueryResult([] as NotificationContext[], (it) => it.id)
}
}
@@ -156,18 +166,14 @@ export class NotificationContextsQuery implements PagedQuery<NotificationContext
await this.client.unsubscribeQuery(this.id)
}
async requestLoadNextPage (): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
if (this.forward instanceof Promise) {
this.forward = await this.forward
}
async requestLoadNextPage (notify = true): Promise<{ isDone: boolean }> {
if (this.result instanceof Promise) this.result = await this.result
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.result.isTail()) return
if (this.result.isTail()) return { isDone: true }
const last = this.result.getLast()
if (last === undefined) return
if (last === undefined) return { isDone: false }
const limit = this.params.limit ?? defaultQueryParams.limit
const findParams: FindNotificationContextParams = {
@@ -180,8 +186,7 @@ export class NotificationContextsQuery implements PagedQuery<NotificationContext
}
const forward = this.find(findParams)
this.forward = forward.then(async (res) => {
const forwardPromise = forward.then(async (res) => {
if (this.result instanceof Promise) {
this.result = await this.result
}
@@ -191,23 +196,28 @@ export class NotificationContextsQuery implements PagedQuery<NotificationContext
}
this.result.append(res)
this.result.setTail(isTail)
await this.notify()
return res
this.nexLoadedPagesCount++
if (notify) {
await this.notify()
}
return { isDone: isTail }
})
this.forward = forwardPromise
return await forwardPromise
}
async requestLoadPrevPage (): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
if (this.backward instanceof Promise) {
this.backward = await this.backward
}
async requestLoadPrevPage (notify = true): Promise<{ isDone: boolean }> {
if (this.result instanceof Promise) this.result = await this.result
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result.isHead()) return
if (this.result.isHead()) return { isDone: true }
const first = this.params.order === SortingOrder.Ascending ? this.result.getFirst() : this.result.getLast()
if (first === undefined) return
if (first === undefined) return { isDone: false }
const limit = this.params.limit ?? defaultQueryParams.limit
const findParams: FindNotificationContextParams = {
@@ -220,10 +230,9 @@ export class NotificationContextsQuery implements PagedQuery<NotificationContext
}
const backward = this.find(findParams)
this.backward = backward.then(async (res) => {
if (this.result instanceof Promise) {
this.result = await this.result
}
const backwardPromise = backward.then(async (res) => {
if (this.result instanceof Promise) this.result = await this.result
const isHead = res.length <= limit
if (!isHead) {
res.pop()
@@ -236,9 +245,16 @@ export class NotificationContextsQuery implements PagedQuery<NotificationContext
this.result.append(res)
}
this.result.setHead(isHead)
await this.notify()
return res
this.prevLoadedPagesCount++
if (notify) {
await this.notify()
}
return { isDone: isHead }
})
this.backward = backwardPromise
return await backwardPromise
}
removeCallback (): void {
@@ -711,4 +727,27 @@ export class NotificationContextsQuery implements PagedQuery<NotificationContext
return true
}
async refresh (): Promise<void> {
const nextPagesCount = this.nexLoadedPagesCount
const prevPagesCount = this.prevLoadedPagesCount
this.result = new QueryResult([] as NotificationContext[], (it) => it.id)
this.nexLoadedPagesCount = 0
this.prevLoadedPagesCount = 0
this.result = await this.rawInitResult()
for (let i = 0; i < nextPagesCount; i++) {
const { isDone } = await this.requestLoadNextPage(false)
if (!isDone) break
}
for (let i = 0; i < prevPagesCount; i++) {
const { isDone } = await this.requestLoadPrevPage(false)
if (!isDone) break
}
await this.notify()
}
}
+96 -50
View File
@@ -25,19 +25,19 @@ import {
type WorkspaceID
} from '@hcengineering/communication-types'
import {
type FindClient,
type PagedQueryCallback,
type Event,
NotificationEventType,
MessageEventType,
CardEventType,
CreateNotificationEvent,
UpdateNotificationContextEvent,
UpdateNotificationEvent,
RemoveNotificationsEvent,
RemoveNotificationContextEvent,
type Event,
type FindClient,
MessageEventType,
NotificationEventType,
type PagedQueryCallback,
PatchEvent,
RemoveCardEvent,
PatchEvent
RemoveNotificationContextEvent,
RemoveNotificationsEvent,
UpdateNotificationContextEvent,
UpdateNotificationEvent
} from '@hcengineering/communication-sdk-types'
import { applyPatches, MessageProcessor, NotificationProcessor } from '@hcengineering/communication-shared'
@@ -51,6 +51,9 @@ const allowedPatchTypes = [PatchType.update, PatchType.remove, PatchType.blob]
export class NotificationQuery implements PagedQuery<Notification, NotificationQueryParams> {
private result: QueryResult<Notification> | Promise<QueryResult<Notification>>
nexLoadedPagesCount = 0
prevLoadedPagesCount = 0
constructor (
private readonly client: FindClient,
private readonly workspace: WorkspaceID,
@@ -60,33 +63,39 @@ export class NotificationQuery implements PagedQuery<Notification, NotificationQ
private callback?: PagedQueryCallback<Notification>,
initialResult?: QueryResult<Notification>
) {
const limit = this.params.limit ?? defaultQueryParams.limit
const findParams: FindNotificationsParams = {
...this.params,
order: this.params.order ?? defaultQueryParams.order,
limit: this.params.strict === true ? limit : limit + 1
}
if (initialResult !== undefined) {
this.result = initialResult
void this.notify()
} else {
this.result = this.initResult(findParams, limit)
this.result = this.initResult()
void this.result.then(() => {
void this.notify()
})
}
}
private async initResult (findParams: FindNotificationsParams, limit: number): Promise<QueryResult<Notification>> {
private async rawInitResult (): Promise<QueryResult<Notification>> {
const limit = this.params.limit != null ? this.params.limit + 1 : undefined
const findParams: FindNotificationsParams = {
...this.params,
order: this.params.order ?? defaultQueryParams.order,
limit: this.params.strict === true ? this.params.limit : limit
}
const res = await this.find(findParams)
const isComplete = this.params.limit == null || this.params.strict === true || res.length < this.params.limit
if (!isComplete) res.pop()
const result = new QueryResult(res, (it) => it.id)
result.setTail(isComplete)
result.setHead(isComplete)
return result
}
private async initResult (): Promise<QueryResult<Notification>> {
try {
const res = await this.find(findParams)
const isComplete = res.length <= limit
if (!isComplete) res.pop()
const result = new QueryResult(res, (it) => it.id)
result.setTail(isComplete)
result.setHead(isComplete)
void this.notify()
return result
return await this.rawInitResult()
} catch (error) {
console.error('Failed to initialize query:', error)
return new QueryResult([] as Notification[], (it) => it.id)
@@ -131,24 +140,33 @@ export class NotificationQuery implements PagedQuery<Notification, NotificationQ
await this.client.unsubscribeQuery(this.id)
}
async requestLoadNextPage (): Promise<void> {
if (this.params.strict === true) return
async requestLoadNextPage (notify = true): Promise<{ isDone: boolean }> {
if (this.params.strict === true) return { isDone: true }
if (this.result instanceof Promise) this.result = await this.result
if (this.result.isTail()) return { isDone: true }
await this.loadPage(SortingOrder.Ascending, this.result.getLast()?.created)
const result = await this.loadPage(SortingOrder.Ascending, this.result.getLast()?.created, notify)
this.nexLoadedPagesCount++
return result
}
async requestLoadPrevPage (): Promise<void> {
if (this.params.strict === true) return
async requestLoadPrevPage (notify = true): Promise<{ isDone: boolean }> {
if (this.params.strict === true) return { isDone: true }
if (this.result instanceof Promise) this.result = await this.result
await this.loadPage(SortingOrder.Descending, this.result.getFirst()?.created)
if (this.result.isHead()) return { isDone: true }
const result = await this.loadPage(SortingOrder.Descending, this.result.getFirst()?.created, notify)
this.prevLoadedPagesCount++
return result
}
private async loadPage (order: SortingOrder, created?: Date): Promise<void> {
if (created == null) return
private async loadPage (order: SortingOrder, created?: Date, notify = true): Promise<{ isDone: boolean }> {
if (created == null) return { isDone: false }
if (this.params.limit == null) return { isDone: true }
if (this.result instanceof Promise) this.result = await this.result
const limit = this.getLimit()
const limit = this.params.limit
const findParams: FindNotificationsParams = {
...this.params,
created: order === SortingOrder.Ascending ? { greater: created } : { less: created },
@@ -158,21 +176,27 @@ export class NotificationQuery implements PagedQuery<Notification, NotificationQ
try {
const res = await this.find(findParams)
const isComplete = res.length <= limit
if (!isComplete) res.pop()
const isDone = res.length <= limit
if (!isDone) res.pop()
if (order === SortingOrder.Ascending) {
this.result.append(res)
this.result.setTail(isComplete)
this.result.setTail(isDone)
} else {
this.result.prepend(res)
this.result.setHead(isComplete)
this.result.setHead(isDone)
}
await this.notify()
if (notify) {
await this.notify()
}
return { isDone }
} catch (error) {
console.error(`Failed to load ${order === SortingOrder.Ascending ? 'next' : 'previous'} page:`, error)
}
return { isDone: false }
}
removeCallback (): void {
@@ -278,8 +302,9 @@ export class NotificationQuery implements PagedQuery<Notification, NotificationQ
}
const newLength = result.length
const { limit } = this.params
if (currentLength !== newLength && currentLength >= this.getLimit() && newLength < this.getLimit()) {
if (limit != null && currentLength !== newLength && currentLength >= limit && newLength < limit) {
await this.reinit(currentLength)
} else {
void this.notify()
@@ -312,8 +337,9 @@ export class NotificationQuery implements PagedQuery<Notification, NotificationQ
}
const newLength = this.result.length
const { limit } = this.params
if (currentLength >= this.getLimit() && newLength < this.getLimit()) {
if (limit != null && currentLength >= limit && newLength < limit) {
void this.reinit(currentLength)
} else if (isDeleted) {
void this.notify()
@@ -339,7 +365,8 @@ export class NotificationQuery implements PagedQuery<Notification, NotificationQ
this.result.delete(notification.id)
}
if (length >= this.getLimit() && this.result.length < this.getLimit()) {
const { limit } = this.params
if (limit != null && length >= limit && this.result.length < limit) {
void this.reinit(this.result.length)
} else {
void this.notify()
@@ -380,10 +407,6 @@ export class NotificationQuery implements PagedQuery<Notification, NotificationQ
this.callback(window)
}
private getLimit (): number {
return this.params.limit ?? defaultQueryParams.limit
}
private async reinit (limit: number): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
this.result = this.find({ ...this.params, limit: limit + 1 }).then((res) => {
@@ -429,4 +452,27 @@ export class NotificationQuery implements PagedQuery<Notification, NotificationQ
return isUpdated
}
async refresh (): Promise<void> {
const nextPagesCount = this.nexLoadedPagesCount
const prevPagesCount = this.prevLoadedPagesCount
this.result = new QueryResult([] as Notification[], (it) => it.id)
this.nexLoadedPagesCount = 0
this.prevLoadedPagesCount = 0
this.result = await this.rawInitResult()
for (let i = 0; i < nextPagesCount; i++) {
const { isDone } = await this.requestLoadNextPage(false)
if (!isDone) break
}
for (let i = 0; i < prevPagesCount; i++) {
const { isDone } = await this.requestLoadPrevPage(false)
if (!isDone) break
}
await this.notify()
}
}
+7 -2
View File
@@ -51,13 +51,18 @@ interface BaseQuery<R = any, P = FindParams> {
removeCallback: () => void
setCallback: (callback: (result: any) => void) => void
copyResult: () => QueryResult<R> | undefined
refresh: () => Promise<void>
}
export interface PagedQuery<R = any, P = FindParams> extends BaseQuery<R, P> {
readonly id: QueryId
readonly params: P
nexLoadedPagesCount: number
prevLoadedPagesCount: number
requestLoadNextPage: () => Promise<void>
requestLoadPrevPage: () => Promise<void>
requestLoadNextPage: (notify?: boolean) => Promise<{ isDone: boolean }>
requestLoadPrevPage: (notify?: boolean) => Promise<{ isDone: boolean }>
setCallback: (callback: (window: Window<R>) => void) => void
}