diff --git a/packages/client-query/src/index.ts b/packages/client-query/src/index.ts index 6c69e81797..57bf5c1e99 100644 --- a/packages/client-query/src/index.ts +++ b/packages/client-query/src/index.ts @@ -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) diff --git a/packages/client-query/src/init.ts b/packages/client-query/src/init.ts index 86dbdd99be..33e951e111 100644 --- a/packages/client-query/src/init.ts +++ b/packages/client-query/src/init.ts @@ -46,3 +46,9 @@ export function initLiveQueries ( lq = new LiveQueries(client, workspace, filesUrl) } + +export async function refreshLiveQueries (): Promise { + if (lq != null) { + await lq.refresh() + } +} diff --git a/packages/query/src/collaborators/query.ts b/packages/query/src/collaborators/query.ts index 7942521fac..4cf3207989 100644 --- a/packages/query/src/collaborators/query.ts +++ b/packages/query/src/collaborators/query.ts @@ -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 { + void this.notify() + }) } } @@ -123,10 +126,7 @@ export class CollaboratorsQuery implements Query> { 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 { + this.result = new QueryResult([] as Collaborator[], (c) => c.account) + await this.initResult() + } } diff --git a/packages/query/src/label/query.ts b/packages/query/src/label/query.ts index 02c6aaec05..eb1ac407d1 100644 --- a/packages/query/src/label/query.ts +++ b/packages/query/src/label/query.ts @@ -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 { void this.notify() } else { this.result = this.initResult() + void this.result.then(() => { + void this.notify() + }) } } @@ -186,10 +189,7 @@ export class LabelsQuery implements Query { private async initResult (): Promise> { 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 { } return true } + + async refresh (): Promise { + this.result = new QueryResult([] as Label[], getId) + await this.initResult() + } } diff --git a/packages/query/src/lq.ts b/packages/query/src/lq.ts index 450b2c7052..37c9de4505 100644 --- a/packages/query/src/lq.ts +++ b/packages/query/src/lq.ts @@ -221,4 +221,19 @@ export class LiveQueries { this.queries.clear() this.unsubscribed.clear() } + + async refresh (): Promise { + 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) + } + } + } } diff --git a/packages/query/src/messages/query.ts b/packages/query/src/messages/query.ts index 891820cd69..e3da5c32a5 100644 --- a/packages/query/src/messages/query.ts +++ b/packages/query/src/messages/query.ts @@ -57,7 +57,7 @@ const GROUPS_LIMIT = 4 export class MessagesQuery implements PagedQuery { private result: Promise> | QueryResult - private readonly groupsBuffer: MessagesGroup[] = [] + private groupsBuffer: MessagesGroup[] = [] private firstGroup?: MessagesGroup private lastGroup?: MessagesGroup @@ -69,6 +69,9 @@ export class MessagesQuery implements PagedQuery { 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 { await this.client.unsubscribeQuery(this.id) } - async requestLoadNextPage (): Promise { - 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 { - 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 { return result } + + async refresh (): Promise { + 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() + } } diff --git a/packages/query/src/notification-contexts/query.ts b/packages/query/src/notification-contexts/query.ts index 000c44b60c..3869b31a41 100644 --- a/packages/query/src/notification-contexts/query.ts +++ b/packages/query/src/notification-contexts/query.ts @@ -59,8 +59,11 @@ import { findMessage, loadMessageFromGroup, matchNotification } from '../utils' const allowedPatchTypes = [PatchType.update, PatchType.remove, PatchType.blob] export class NotificationContextsQuery implements PagedQuery { private result: QueryResult | Promise> - private forward: Promise | NotificationContext[] = [] - private backward: Promise | 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 { + void this.notify() + }) + } + } + + private async rawInitResult (): Promise> { + 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> { + 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 { - 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 { + 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 { - 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 { - 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 { + 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() + } } diff --git a/packages/query/src/notifications/query.ts b/packages/query/src/notifications/query.ts index fb6df5cb53..c343cf4134 100644 --- a/packages/query/src/notifications/query.ts +++ b/packages/query/src/notifications/query.ts @@ -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 { private result: QueryResult | Promise> + nexLoadedPagesCount = 0 + prevLoadedPagesCount = 0 + constructor ( private readonly client: FindClient, private readonly workspace: WorkspaceID, @@ -60,33 +63,39 @@ export class NotificationQuery implements PagedQuery, initialResult?: QueryResult ) { - 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> { + private async rawInitResult (): Promise> { + 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> { 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 { - 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 { - 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 { - 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= 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= 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= 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 { 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 { + 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() + } } diff --git a/packages/query/src/types.ts b/packages/query/src/types.ts index 9ea0766282..96ee09936a 100644 --- a/packages/query/src/types.ts +++ b/packages/query/src/types.ts @@ -51,13 +51,18 @@ interface BaseQuery { removeCallback: () => void setCallback: (callback: (result: any) => void) => void copyResult: () => QueryResult | undefined + + refresh: () => Promise } + export interface PagedQuery extends BaseQuery { readonly id: QueryId readonly params: P + nexLoadedPagesCount: number + prevLoadedPagesCount: number - requestLoadNextPage: () => Promise - requestLoadPrevPage: () => Promise + requestLoadNextPage: (notify?: boolean) => Promise<{ isDone: boolean }> + requestLoadPrevPage: (notify?: boolean) => Promise<{ isDone: boolean }> setCallback: (callback: (window: Window) => void) => void }