diff --git a/dev/prod/webpack.config.js b/dev/prod/webpack.config.js index 6948e7e9aa..d5dbf24323 100644 --- a/dev/prod/webpack.config.js +++ b/dev/prod/webpack.config.js @@ -15,6 +15,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin') const Dotenv = require('dotenv-webpack') +const DefinePlugin = require('webpack').DefinePlugin const path = require('path') const autoprefixer = require('autoprefixer') const CompressionPlugin = require('compression-webpack-plugin') @@ -81,7 +82,10 @@ module.exports = { test: /\.ts?$/, loader:'esbuild-loader', options: { - target: 'es2021' + target: 'es2021', + keepNames: true, + minify: !prod, + sourcemap: !prod }, exclude: /node_modules/, }, @@ -208,10 +212,8 @@ module.exports = { // filename: '[name].[id][contenthash].css' // }), new Dotenv({path: prod ? '.env-prod' : '.env'}), - new EsbuildPlugin({ - define: { - 'process.env.CLIENT_TYPE': JSON.stringify(process.env.CLIENT_TYPE ?? '') - } + new DefinePlugin({ + 'process.env.CLIENT_TYPE': JSON.stringify(process.env.CLIENT_TYPE) }), new ForkTsCheckerWebpackPlugin() ], diff --git a/packages/core/src/__tests__/memdb.test.ts b/packages/core/src/__tests__/memdb.test.ts index 1ca542a524..dcb73fa91d 100644 --- a/packages/core/src/__tests__/memdb.test.ts +++ b/packages/core/src/__tests__/memdb.test.ts @@ -34,7 +34,7 @@ import { genMinModel, test, TestMixin } from './minmodel' const txes = genMinModel() class ClientModel extends ModelDb implements Client { - notify?: ((tx: Tx) => void) | undefined + notify?: ((...tx: Tx[]) => void) | undefined getHierarchy (): Hierarchy { return this.hierarchy diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 04571aff1c..8c0e600a0f 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -29,13 +29,13 @@ const transactionThreshold = 500 /** * @public */ -export type TxHandler = (tx: Tx) => void +export type TxHandler = (...tx: Tx[]) => void /** * @public */ export interface Client extends Storage, FulltextStorage { - notify?: (tx: Tx) => void + notify?: (...tx: Tx[]) => void getHierarchy: () => Hierarchy getModel: () => ModelDb findOne: ( @@ -98,7 +98,7 @@ export interface ClientConnection extends Storage, FulltextStorage, BackupClient } class ClientImpl implements AccountClient, BackupClient, MeasureClient { - notify?: (tx: Tx) => void + notify?: (...tx: Tx[]) => void hierarchy!: Hierarchy model!: ModelDb constructor (private readonly conn: ClientConnection) {} @@ -164,12 +164,14 @@ class ClientImpl implements AccountClient, BackupClient, MeasureClient { return await this.conn.measure(operationName) } - async updateFromRemote (tx: Tx): Promise { - if (tx.objectSpace === core.space.Model) { - this.hierarchy.tx(tx) - await this.model.tx(tx) + async updateFromRemote (...tx: Tx[]): Promise { + for (const t of tx) { + if (t.objectSpace === core.space.Model) { + this.hierarchy.tx(t) + await this.model.tx(t) + } } - this.notify?.(tx) + this.notify?.(...tx) } async close (): Promise { @@ -228,17 +230,17 @@ export async function createClient ( let lastTx: number - function txHandler (tx: Tx): void { - if (tx == null) { + function txHandler (...tx: Tx[]): void { + if (tx == null || tx.length === 0) { return } if (client === null) { - txBuffer?.push(tx) + txBuffer?.push(...tx) } else { // eslint-disable-next-line @typescript-eslint/no-floating-promises - client.updateFromRemote(tx) + client.updateFromRemote(...tx) } - lastTx = tx.modifiedOn + lastTx = tx.reduce((cur, it) => (it.modifiedOn > cur ? it.modifiedOn : cur), 0) } const configs = new Map, PluginConfiguration>() @@ -251,9 +253,7 @@ export async function createClient ( client = new ClientImpl(conn) client.setModel(hierarchy, model) - for (const tx of txBuffer) { - txHandler(tx) - } + txHandler(...txBuffer) txBuffer = undefined const oldOnConnect: ((event: ClientConnectEvent) => Promise) | undefined = conn.onConnect @@ -302,9 +302,7 @@ export async function createClient ( if (atxes.length < transactionThreshold && !needFullRefresh) { console.log('applying input transactions', atxes.length) - for (const tx of atxes) { - txHandler(tx) - } + txHandler(...atxes) await oldOnConnect?.(ClientConnectEvent.Reconnected) } else { // We need to trigger full refresh on queries, etc. diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index ec9cb5e98f..ed81d3d416 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -307,7 +307,7 @@ export class DocManager { export class RateLimiter { idCounter: number = 0 - processingQueue = new Map>() + processingQueue = new Map>() last: number = 0 rate: number @@ -318,7 +318,7 @@ export class RateLimiter { } async exec = any>(op: (args?: B) => Promise, args?: B): Promise { - const processingId = `${this.idCounter++}` + const processingId = this.idCounter++ while (this.processingQueue.size > this.rate) { await Promise.race(this.processingQueue.values()) diff --git a/packages/presentation/src/pipeline.ts b/packages/presentation/src/pipeline.ts index 5d818de45f..fd15864042 100644 --- a/packages/presentation/src/pipeline.ts +++ b/packages/presentation/src/pipeline.ts @@ -28,7 +28,7 @@ export interface PresentationMiddleware { tx: (tx: Tx) => Promise - notifyTx: (tx: Tx) => Promise + notifyTx: (...tx: Tx[]) => Promise findAll: ( _class: Ref>, @@ -84,8 +84,8 @@ export class PresentationPipelineImpl implements PresentationPipeline { return this.client.getModel() } - async notifyTx (tx: Tx): Promise { - await this.head?.notifyTx(tx) + async notifyTx (...tx: Tx[]): Promise { + await this.head?.notifyTx(...tx) } async measure (operationName: string): Promise { @@ -168,8 +168,8 @@ export abstract class BasePresentationMiddleware { readonly next?: PresentationMiddleware ) {} - async provideNotifyTx (tx: Tx): Promise { - await this.next?.notifyTx(tx) + async provideNotifyTx (...tx: Tx[]): Promise { + await this.next?.notifyTx(...tx) } async provideClose (): Promise { @@ -270,8 +270,8 @@ export class OptimizeQueryMiddleware extends BasePresentationMiddleware implemen return new OptimizeQueryMiddleware(client, next) } - async notifyTx (tx: Tx): Promise { - await this.provideNotifyTx(tx) + async notifyTx (...tx: Tx[]): Promise { + await this.provideNotifyTx(...tx) } async close (): Promise { diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index 8279558734..291661b8df 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -56,7 +56,7 @@ let liveQuery: LQ let client: TxOperations & MeasureClient let pipeline: PresentationPipeline -const txListeners: Array<(tx: Tx) => void> = [] +const txListeners: Array<(...tx: Tx[]) => void> = [] /** * @public @@ -86,17 +86,17 @@ class UIClient extends TxOperations implements Client, MeasureClient { afterMeasure: Tx[] = [] measureOp?: MeasureDoneOperation - async doNotify (tx: Tx): Promise { + async doNotify (...tx: Tx[]): Promise { if (this.measureOp !== undefined) { - this.afterMeasure.push(tx) + this.afterMeasure.push(...tx) } else { try { - await pipeline.notifyTx(tx) + await pipeline.notifyTx(...tx) - await liveQuery.tx(tx) + await liveQuery.tx(...tx) txListeners.forEach((it) => { - it(tx) + it(...tx) }) } catch (err: any) { console.log(err) @@ -175,8 +175,8 @@ export async function setClient (_client: MeasureClient): Promise { const uiClient = new UIClient(pipeline, liveQuery) client = uiClient - _client.notify = (tx: Tx) => { - void uiClient.doNotify(tx) + _client.notify = (...tx: Tx[]) => { + void uiClient.doNotify(...tx) } if (needRefresh || globalQueries.length > 0) { await refreshClient() diff --git a/packages/query/src/__tests__/query.test.ts b/packages/query/src/__tests__/query.test.ts index 289681340b..365649b87a 100644 --- a/packages/query/src/__tests__/query.test.ts +++ b/packages/query/src/__tests__/query.test.ts @@ -36,8 +36,8 @@ interface Channel extends Space { 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) => { + storage.notify = (...tx: Tx[]) => { + liveQuery.tx(...tx).catch((err) => { console.log(err) }) } diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index 283d5d6f0b..7410d8b9d2 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -45,6 +45,7 @@ import core, { TxUpdateDoc, TxWorkspaceEvent, WithLookup, + WithTx, WorkspaceEvent, checkMixinKey, findProperty, @@ -73,13 +74,12 @@ interface Query { /** * @public */ -export class LiveQuery extends TxProcessor implements Client { +export class LiveQuery implements WithTx, Client { private client: Client private readonly queries: Map>, Query[]> = new Map>, Query[]>() private readonly queue: Query[] = [] constructor (client: Client) { - super() this.client = client } @@ -421,8 +421,33 @@ export class LiveQuery extends TxProcessor implements Client { return false } - private async getCurrentDoc (q: Query, _id: Ref, space: Ref): Promise { - const current = await this.client.findOne(q._class, { _id, space }, q.options) + private async getDocFromCache ( + docCache: Map, + _id: Ref, + _class: Ref>, + space: Ref, + q: Query + ): Promise { + const lookup = q.options?.lookup + const docIdKey = _id + JSON.stringify(lookup ?? {}) + q._class + const current = + docCache.get(docIdKey) ?? + (await this.client.findOne(q._class, { _id, space }, lookup !== undefined ? { lookup } : undefined)) + if (current !== undefined) { + docCache.set(docIdKey, current) + } else { + docCache.delete(docIdKey) + } + return current + } + + private async getCurrentDoc ( + q: Query, + _id: Ref, + space: Ref, + docCache: Map + ): Promise { + const current = await this.getDocFromCache(docCache, _id, q._class, space, q) if (q.result instanceof Promise) { q.result = await q.result } @@ -434,7 +459,7 @@ export class LiveQuery extends TxProcessor implements Client { if (q.options?.limit === q.result.length) { await this.refresh(q) return true - } else { + } else if (pos !== -1) { q.result.splice(pos, 1) if (q.options?.total === true) { q.total-- @@ -476,7 +501,7 @@ export class LiveQuery extends TxProcessor implements Client { return false } - protected override async txMixin (tx: TxMixin): Promise { + protected async txMixin (tx: TxMixin, docCache: Map): Promise { const hierarchy = this.client.getHierarchy() for (const queries of this.queries) { @@ -485,7 +510,7 @@ export class LiveQuery extends TxProcessor implements Client { for (const q of queries[1]) { if (isTx) { // handle add since Txes are immutable - await this.handleDocAdd(q, tx) + await this.handleDocAdd(q, tx, true, docCache) continue } if (q.result instanceof Promise) { @@ -508,7 +533,7 @@ export class LiveQuery extends TxProcessor implements Client { continue } } else { - const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id, updatedDoc.space) + const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id, updatedDoc.space, docCache) if (currentRefresh) { continue } @@ -521,7 +546,7 @@ export class LiveQuery extends TxProcessor implements Client { // Mixin potentially added to object we doesn't have in out results const doc = await this.client.findOne(q._class, { ...q.query, _id: tx.objectId }, q.options) if (doc !== undefined) { - await this.handleDocAdd(q, doc, false) + await this.handleDocAdd(q, doc, false, docCache) } } await this.handleDocUpdateLookup(q, tx) @@ -530,14 +555,16 @@ export class LiveQuery extends TxProcessor implements Client { return {} } - protected async txCollectionCUD (tx: TxCollectionCUD): Promise { - const docCache = new Map, Doc>() + protected async txCollectionCUD ( + tx: TxCollectionCUD, + docCache: Map + ): Promise { for (const queries of this.queries) { const isTx = this.client.getHierarchy().isDerived(queries[0], core.class.Tx) for (const q of queries[1]) { if (isTx) { // handle add since Txes are immutable - await this.handleDocAdd(q, tx) + await this.handleDocAdd(q, tx, true, docCache) continue } @@ -552,7 +579,7 @@ export class LiveQuery extends TxProcessor implements Client { collection: tx.collection } } - await this.handleDocAdd(q, TxProcessor.createDoc2Doc(d)) + await this.handleDocAdd(q, TxProcessor.createDoc2Doc(d), true, docCache) } else if (tx.tx._class === core.class.TxUpdateDoc) { await this.handleDocUpdate(q, tx.tx as unknown as TxUpdateDoc, docCache) } else if (tx.tx._class === core.class.TxRemoveDoc) { @@ -563,14 +590,13 @@ export class LiveQuery extends TxProcessor implements Client { return {} } - protected async txUpdateDoc (tx: TxUpdateDoc): Promise { - const docCache = new Map() + async txUpdateDoc (tx: TxUpdateDoc, docCache: Map): Promise { for (const queries of this.queries) { const isTx = this.client.getHierarchy().isDerived(queries[0], core.class.Tx) for (const q of queries[1]) { if (isTx) { // handle add since Txes are immutable - await this.handleDocAdd(q, tx) + await this.handleDocAdd(q, tx, true, docCache) continue } await this.handleDocUpdate(q, tx, docCache) @@ -579,7 +605,7 @@ export class LiveQuery extends TxProcessor implements Client { return {} } - private async handleDocUpdate (q: Query, tx: TxUpdateDoc, docCache?: Map): Promise { + private async handleDocUpdate (q: Query, tx: TxUpdateDoc, docCache: Map): Promise { if (q.result instanceof Promise) { q.result = await q.result } @@ -596,7 +622,7 @@ export class LiveQuery extends TxProcessor implements Client { const updateRefresh = await this.checkUpdatedDocMatch(q, updatedDoc) if (updateRefresh) return } else { - const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id, updatedDoc.space) + const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id, updatedDoc.space, docCache) if (currentRefresh) return } } @@ -724,7 +750,7 @@ export class LiveQuery extends TxProcessor implements Client { } // Check if query is partially matched. - private async matchQuery (q: Query, tx: TxUpdateDoc, docCache?: Map): Promise { + private async matchQuery (q: Query, tx: TxUpdateDoc, docCache: Map): Promise { const clazz = this.getHierarchy().isMixin(q._class) ? this.getHierarchy().getBaseClass(q._class) : q._class if (!this.client.getHierarchy().isDerived(tx.objectClass, clazz)) { return false @@ -765,12 +791,8 @@ export class LiveQuery extends TxProcessor implements Client { } if (matched) { - const docIdKey = doc._id + JSON.stringify(q.options?.lookup) + Hierarchy.mixinClass(doc) + const realDoc = await this.getDocFromCache(docCache, doc._id, Hierarchy.mixinOrClass(doc), doc.space, q) - const lookup = q.options?.lookup - const realDoc = - docCache?.get(docIdKey) ?? - (await this.client.findOne(q._class, { _id: doc._id }, lookup !== undefined ? { lookup } : undefined)) if (realDoc == null) return false if (this.getHierarchy().isMixin(q._class)) { @@ -778,10 +800,6 @@ export class LiveQuery extends TxProcessor implements Client { return false } } - - if (docCache != null) { - docCache?.set(docIdKey, realDoc) - } const res = matchQuery([realDoc], q.query, q._class, this.client.getHierarchy()) if (res.length === 1) { if (q.result instanceof Promise) { @@ -863,18 +881,18 @@ export class LiveQuery extends TxProcessor implements Client { ;(doc as WithLookup).$lookup = result } - protected async txCreateDoc (tx: TxCreateDoc): Promise { + protected async txCreateDoc (tx: TxCreateDoc, docCache: Map): Promise { const docTx = TxProcessor.createDoc2Doc(tx) for (const queries of this.queries) { const doc = this.client.getHierarchy().isDerived(queries[0], core.class.Tx) ? tx : docTx for (const q of queries[1]) { - await this.handleDocAdd(q, doc) + await this.handleDocAdd(q, doc, true, docCache) } } return {} } - private async handleDocAdd (q: Query, doc: Doc, handleLookup = true): Promise { + private async handleDocAdd (q: Query, doc: Doc, handleLookup = true, docCache: Map): Promise { if (this.match(q, doc, q.options?.lookup !== undefined)) { let needPush = true if (q.result instanceof Promise) { @@ -978,13 +996,13 @@ export class LiveQuery extends TxProcessor implements Client { return needCallback } - protected async txRemoveDoc (tx: TxRemoveDoc): Promise { + protected async txRemoveDoc (tx: TxRemoveDoc, docCache: Map): Promise { for (const queries of this.queries) { const isTx = this.client.getHierarchy().isDerived(queries[0], core.class.Tx) for (const q of queries[1]) { if (isTx) { // handle add since Txes are immutable - await this.handleDocAdd(q, tx) + await this.handleDocAdd(q, tx, true, docCache) continue } await this.handleDocRemove(q, tx) @@ -1096,14 +1114,33 @@ export class LiveQuery extends TxProcessor implements Client { return result } + async _tx (tx: Tx, docCache: Map): Promise { + switch (tx._class) { + case core.class.TxCreateDoc: + return await this.txCreateDoc(tx as TxCreateDoc, docCache) + case core.class.TxCollectionCUD: + return await this.txCollectionCUD(tx as TxCollectionCUD, docCache) + case core.class.TxUpdateDoc: + return await this.txUpdateDoc(tx as TxUpdateDoc, docCache) + case core.class.TxRemoveDoc: + return await this.txRemoveDoc(tx as TxRemoveDoc, docCache) + case core.class.TxMixin: + return await this.txMixin(tx as TxMixin, docCache) + case core.class.TxApplyIf: + return await Promise.resolve([]) + } + return {} + } + async tx (...txes: Tx[]): Promise { const result: TxResult[] = [] + const docCache = new Map() for (const tx of txes) { if (tx._class === core.class.TxWorkspaceEvent) { await this.checkUpdateEvents(tx) await this.changePrivateHandler(tx) } - result.push(...(await super.tx(tx))) + result.push(await this._tx(tx, docCache)) } return result } diff --git a/plugins/client-resources/src/connection.ts b/plugins/client-resources/src/connection.ts index bb7b158ded..9e6485ab82 100644 --- a/plugins/client-resources/src/connection.ts +++ b/plugins/client-resources/src/connection.ts @@ -306,8 +306,8 @@ class Connection implements ClientConnection { this.onUpgrade?.() return } - this.handler(tx) } + this.handler(...txArr) clearTimeout(this.incomingTimer) void broadcastEvent(client.event.NetworkRequests, this.requests.size + 1) @@ -452,8 +452,9 @@ class Connection implements ClientConnection { if (tx._class === core.class.TxApplyIf) { // We need to check extra broadcast's and perform them before const r = result as TxApplyResult - for (const d of r?.derived ?? []) { - this.handler(d) + const dr = r?.derived ?? [] + if (dr.length > 0) { + this.handler(...dr) } } } diff --git a/plugins/client-resources/src/index.ts b/plugins/client-resources/src/index.ts index 668db40b4e..4304f5e2dd 100644 --- a/plugins/client-resources/src/index.ts +++ b/plugins/client-resources/src/index.ts @@ -18,6 +18,7 @@ import core, { AccountClient, ClientConnectEvent, LoadModelResponse, + Tx, TxHandler, TxPersistenceStore, TxWorkspaceEvent, @@ -53,18 +54,22 @@ export default async () => { (handler: TxHandler) => { const url = new URL(`/${token}`, endpoint) - const upgradeHandler: TxHandler = (tx) => { - if (tx?._class === core.class.TxWorkspaceEvent) { - const event = tx as TxWorkspaceEvent - if (event.event === WorkspaceEvent.Upgrade) { - onUpgrade?.() - } else if (event.event === WorkspaceEvent.MaintenanceNotification) { - void setPlatformStatus( - new Status(Severity.WARNING, platform.status.MaintenanceWarning, { time: event.params.timeMinutes }) - ) + const upgradeHandler: TxHandler = (...txes: Tx[]) => { + for (const tx of txes) { + if (tx?._class === core.class.TxWorkspaceEvent) { + const event = tx as TxWorkspaceEvent + if (event.event === WorkspaceEvent.Upgrade) { + onUpgrade?.() + } else if (event.event === WorkspaceEvent.MaintenanceNotification) { + void setPlatformStatus( + new Status(Severity.WARNING, platform.status.MaintenanceWarning, { + time: event.params.timeMinutes + }) + ) + } } } - handler(tx) + handler(...txes) } return connect(url.href, upgradeHandler, onUpgrade, onUnauthorized, onConnect) diff --git a/plugins/devmodel-resources/src/index.ts b/plugins/devmodel-resources/src/index.ts index 4567252914..528f758e03 100644 --- a/plugins/devmodel-resources/src/index.ts +++ b/plugins/devmodel-resources/src/index.ts @@ -31,7 +31,8 @@ import core, { type SearchQuery, type SearchOptions, type SearchResult, - type MeasureDoneOperation + type MeasureDoneOperation, + DOMAIN_MODEL } from '@hcengineering/core' import { devModelId } from '@hcengineering/devmodel' import { Builder } from '@hcengineering/model' @@ -59,8 +60,8 @@ class ModelClient implements AccountClient { constructor (readonly client: AccountClient) { this.notifyEnabled = (localStorage.getItem('#platform.notification.logging') ?? 'true') === 'true' - client.notify = (tx) => { - this.notify?.(tx) + client.notify = (...tx) => { + this.notify?.(...tx) if (this.notifyEnabled) { console.debug('devmodel# notify=>', tx, this.client.getModel(), getMetadata(devmodel.metadata.DevModel)) } @@ -71,7 +72,7 @@ class ModelClient implements AccountClient { return await this.client.measure(operationName) } - notify?: (tx: Tx) => void + notify?: (...tx: Tx[]) => void getHierarchy (): Hierarchy { return this.client.getHierarchy() @@ -91,10 +92,13 @@ class ModelClient implements AccountClient { options?: FindOptions ): Promise | undefined> { const startTime = Date.now() + const isModel = this.getHierarchy().findDomain(_class) === DOMAIN_MODEL const result = await this.client.findOne(_class, query, options) - if (this.notifyEnabled) { + if (this.notifyEnabled && !isModel) { console.debug( 'devmodel# findOne=>', + isModel, + this.getHierarchy().findDomain(_class), _class, query, options, @@ -115,8 +119,9 @@ class ModelClient implements AccountClient { options?: FindOptions ): Promise> { const startTime = Date.now() + const isModel = this.getHierarchy().findDomain(_class) === DOMAIN_MODEL const result = await this.client.findAll(_class, query, options) - if (this.notifyEnabled) { + if (this.notifyEnabled && !isModel) { console.debug( 'devmodel# findAll=>', _class, diff --git a/plugins/tracker-resources/src/component.ts b/plugins/tracker-resources/src/component.ts index 2749b0c7f1..cf911765b3 100644 --- a/plugins/tracker-resources/src/component.ts +++ b/plugins/tracker-resources/src/component.ts @@ -92,8 +92,8 @@ export class ComponentAggregationManager implements AggregationManager { this.query?.() } - async notifyTx (tx: Tx): Promise { - await this.lq.tx(tx) + async notifyTx (...tx: Tx[]): Promise { + await this.lq.tx(...tx) } getAttrClass (): Ref> { diff --git a/plugins/view-resources/src/actionImpl.ts b/plugins/view-resources/src/actionImpl.ts index ef8608602d..e6ac2c7b1d 100644 --- a/plugins/view-resources/src/actionImpl.ts +++ b/plugins/view-resources/src/actionImpl.ts @@ -28,7 +28,8 @@ import { type SelectionStore, focusStore, previewDocument, - selectionStore + selectionStore, + selectionLimit } from './selection' import { deleteObjects, getObjectLinkFragment } from './utils' import contact from '@hcengineering/contact' @@ -185,7 +186,7 @@ function SelectItemAll (doc: Doc | undefined, evt: Event): void { const provider = $selectionStore.provider ?? $focusStore.provider if (provider !== undefined) { const docs = provider.docs() ?? [] - provider.selection.set(docs) + provider.selection.set(docs.slice(0, selectionLimit)) previewDocument.set(undefined) evt.preventDefault() } diff --git a/plugins/view-resources/src/components/ValueSelector.svelte b/plugins/view-resources/src/components/ValueSelector.svelte index 334bfdf03c..1c9045ed4b 100644 --- a/plugins/view-resources/src/components/ValueSelector.svelte +++ b/plugins/view-resources/src/components/ValueSelector.svelte @@ -36,7 +36,7 @@ const dispatch = createEventDispatcher() const client = getClient() const hierarchy = client.getHierarchy() - const changeStatus = async (newStatus: any) => { + const changeValue = async (newStatus: any): Promise => { if (newStatus === '#null') { newStatus = null return @@ -46,10 +46,14 @@ return } progress = true - const docs = Array.isArray(value) ? value : [value] + const docs = [...(Array.isArray(value) ? value : [value])] const changed = (d: Doc) => (d as any)[attribute] !== newStatus const ops = client.apply('value-selector:' + generateId()) + + // We need to sort docs by modified date, to have same modified order impacted. + docs.sort((a: Doc, b: Doc) => b._id.localeCompare(a._id)) + for (const it of docs.filter(changed)) { const cl = Hierarchy.mixinOrClass(it) const attr = @@ -128,7 +132,7 @@ { - changeStatus(evt.detail) + changeValue(evt.detail) }} placeholder={placeholder ?? view.string.Filter} searchable @@ -147,7 +151,7 @@ allowDeselect={true} selected={current} on:close={(evt) => { - changeStatus(evt.detail === null ? null : evt.detail?._id) + changeValue(evt.detail === null ? null : evt.detail?._id) }} placeholder={placeholder ?? view.string.Filter} {width} diff --git a/plugins/view-resources/src/components/list/ListCategory.svelte b/plugins/view-resources/src/components/list/ListCategory.svelte index a703994c79..e722b20e6e 100644 --- a/plugins/view-resources/src/components/list/ListCategory.svelte +++ b/plugins/view-resources/src/components/list/ListCategory.svelte @@ -115,7 +115,7 @@ } $: if (lastLevel) { - limiter.add(async () => { + void limiter.add(async () => { loading = docsQuery.query( _class, { ...resultQuery, ...docKeys }, @@ -127,7 +127,7 @@ handleRowFocused(focusDoc) } }, - { ...resultOptions, limit: Math.min(limit ?? 200, 200) } + { ...resultOptions, limit: limit ?? 200 } ) }) } else { @@ -143,7 +143,7 @@ const dispatch = createEventDispatcher() function limitGroup (items: Doc[], limit: number | undefined): Doc[] { - const res = limit !== undefined ? items.slice(0, Math.min(limit, 200)) : items + const res = limit !== undefined ? items.slice(0, limit) : items return res } @@ -164,6 +164,10 @@ dispatch('row-focus', object) } + $: limited = limitGroup(items, limit) + + $: selectedObjectIdsSet = new Set>(selectedObjectIds.map((it) => it._id)) + const handleMenuOpened = async (event: MouseEvent, object: Doc) => { event.preventDefault() handleRowFocused(object) @@ -177,10 +181,6 @@ showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(event)) } - $: limited = limitGroup(items, limit) - - $: selectedObjectIdsSet = new Set>(selectedObjectIds.map((it) => it._id)) - $: _newObjectProps = (doc: Doc | undefined): Record | undefined => { const groupValue = typeof category === 'object' ? category.values.find((it) => it.space === doc?.space)?._id : category @@ -213,7 +213,7 @@ return false } - function dragOverCat (ev: MouseEvent) { + function dragOverCat (ev: MouseEvent): void { ev.preventDefault() ev.stopPropagation() } @@ -225,7 +225,7 @@ return Math.abs(ev.clientY - target.getBoundingClientRect()[direction]) < 5 } - function dragEnterCat (ev: MouseEvent) { + function dragEnterCat (ev: MouseEvent): void { ev.preventDefault() if (dragItemIndex === undefined && dragItem.doc !== undefined) { const index = items.findIndex((p) => p._id === dragItem.doc?._id) @@ -250,7 +250,7 @@ } } - function dragLeaveCat (ev: MouseEvent) { + function dragLeaveCat (ev: MouseEvent): void { ev.stopPropagation() if (dragItemIndex !== undefined) { items.splice(dragItemIndex, 1) @@ -259,7 +259,7 @@ } } - function dragItemLeave (ev: MouseEvent, i: number) { + function dragItemLeave (ev: MouseEvent, i: number): void { if (dragItemIndex !== undefined) { const isLastItem = i === limited.length - 1 const isFirstItemWithoutHeader = i === 0 && disableHeader @@ -274,7 +274,7 @@ } } - function dragover (ev: MouseEvent, i: number) { + function dragover (ev: MouseEvent, i: number): void { if (dragItemIndex === undefined || !lastLevel) return ev.preventDefault() ev.stopPropagation() @@ -287,7 +287,7 @@ } } - function dropItemHandle (ev: MouseEvent) { + function dropItemHandle (ev: MouseEvent): void { ev.stopPropagation() ev.preventDefault() const update: DocumentUpdate = {} @@ -301,10 +301,10 @@ } } catch {} } - drop(update) + void drop(update) } - async function drop (update: DocumentUpdate = {}) { + async function drop (update: DocumentUpdate = {}): Promise { if (dragItem.doc !== undefined) { const props = _newObjectProps(dragItem.doc) if (props !== undefined) { @@ -341,14 +341,14 @@ } } - function dragStartHandler (e: CustomEvent) { + function dragStartHandler (e: CustomEvent): void { const { target, index } = e.detail dragItemIndex = index ;(target as EventTarget).addEventListener('dragend', (e) => dragEndListener(e, index)) } - function dragStart (ev: DragEvent, docObject: Doc, i: number) { - if (ev.dataTransfer) { + function dragStart (ev: DragEvent, docObject: Doc, i: number): void { + if (ev.dataTransfer != null) { ev.dataTransfer.effectAllowed = 'move' ev.dataTransfer.dropEffect = 'move' } @@ -404,7 +404,7 @@ for (const ac of client.getHierarchy().getAncestors(docClass)) { res = itemModels.get(ac) - if (res) { + if (res !== undefined) { return res } } @@ -493,7 +493,7 @@ dragItem dragstart={dragStartHandler} /> - {:else if itemModels && itemModels.size > 0 && (!collapsed || wasLoaded || dragItemIndex !== undefined)} + {:else if itemModels != null && itemModels.size > 0 && (!collapsed || wasLoaded || dragItemIndex !== undefined)} {#if limited} {#key configurationsVersion} {#each limited as docObject, i (docObject._id)} diff --git a/plugins/view-resources/src/components/list/ListHeader.svelte b/plugins/view-resources/src/components/list/ListHeader.svelte index 1b654f1e1d..e5065f796a 100644 --- a/plugins/view-resources/src/components/list/ListHeader.svelte +++ b/plugins/view-resources/src/components/list/ListHeader.svelte @@ -37,7 +37,7 @@ import { AttributeModel, ViewOptions } from '@hcengineering/view' import { createEventDispatcher } from 'svelte' import view from '../../plugin' - import { SelectionFocusProvider } from '../../selection' + import { SelectionFocusProvider, selectionLimit } from '../../selection' import { noCategory } from '../../viewOptions' export let groupByKey: string @@ -201,7 +201,7 @@ const smap = new Map(selected.map((it) => [it._id, it])) newSelection = newSelection.filter((it) => !smap.has(it._id)) } else { - for (const s of itemsProj) { + for (const s of itemsProj.slice(0, selectionLimit)) { if (!selectionIds.has(s._id)) { newSelection.push(s) } diff --git a/plugins/view-resources/src/middleware.ts b/plugins/view-resources/src/middleware.ts index 1343619a5b..8923578610 100644 --- a/plugins/view-resources/src/middleware.ts +++ b/plugins/view-resources/src/middleware.ts @@ -51,13 +51,13 @@ export class AggregationMiddleware extends BasePresentationMiddleware implements return new AggregationMiddleware(client, next) } - async notifyTx (tx: Tx): Promise { + async notifyTx (...tx: Tx[]): Promise { const promises: Array> = [] for (const [, value] of this.mgrs) { - promises.push(value.notifyTx(tx)) + promises.push(value.notifyTx(...tx)) } await Promise.all(promises) - await this.provideNotifyTx(tx) + await this.provideNotifyTx(...tx) } async close (): Promise { @@ -252,8 +252,8 @@ export class AnalyticsMiddleware extends BasePresentationMiddleware implements P super(client, next) } - async notifyTx (tx: Tx): Promise { - await this.provideNotifyTx(tx) + async notifyTx (...tx: Tx[]): Promise { + await this.provideNotifyTx(...tx) } async close (): Promise { @@ -269,25 +269,25 @@ export class AnalyticsMiddleware extends BasePresentationMiddleware implements P return await this.provideTx(tx) } - private async handleTx (tx: Tx): Promise { - const etx = TxProcessor.extractTx(tx) - if (etx._class === core.class.TxApplyIf) { - const applyIf = etx as TxApplyIf - applyIf.txes.forEach((it) => { - void this.handleTx(it) - }) - } - if (this.client.getHierarchy().isDerived(etx._class, core.class.TxCUD)) { - const cud = etx as TxCUD - const _class = this.client.getHierarchy().getClass(cud.objectClass) - if (_class.label !== undefined) { - const label = await translate(_class.label, {}, 'en') - if (cud._class === core.class.TxCreateDoc) { - Analytics.handleEvent(`Create ${label}`) - } else if (cud._class === core.class.TxUpdateDoc || cud._class === core.class.TxMixin) { - Analytics.handleEvent(`Update ${label}`) - } else if (cud._class === core.class.TxRemoveDoc) { - Analytics.handleEvent(`Delete ${label}`) + private async handleTx (...txes: Tx[]): Promise { + for (const tx of txes) { + const etx = TxProcessor.extractTx(tx) + if (etx._class === core.class.TxApplyIf) { + const applyIf = etx as TxApplyIf + void this.handleTx(...applyIf.txes) + } + if (this.client.getHierarchy().isDerived(etx._class, core.class.TxCUD)) { + const cud = etx as TxCUD + const _class = this.client.getHierarchy().getClass(cud.objectClass) + if (_class.label !== undefined) { + const label = await translate(_class.label, {}, 'en') + if (cud._class === core.class.TxCreateDoc) { + Analytics.handleEvent(`Create ${label}`) + } else if (cud._class === core.class.TxUpdateDoc || cud._class === core.class.TxMixin) { + Analytics.handleEvent(`Update ${label}`) + } else if (cud._class === core.class.TxRemoveDoc) { + Analytics.handleEvent(`Delete ${label}`) + } } } } diff --git a/plugins/view-resources/src/selection.ts b/plugins/view-resources/src/selection.ts index 62f80f9c6b..ac7e06eefc 100644 --- a/plugins/view-resources/src/selection.ts +++ b/plugins/view-resources/src/selection.ts @@ -9,6 +9,9 @@ import { type Unsubscriber, type Writable, writable } from 'svelte/store' */ export type SelectDirection = 'vertical' | 'horizontal' +// Default selection limit +export const selectionLimit = 200 + export interface SelectionFocusProvider { // -1 - previous // 0 - selec of as current diff --git a/plugins/view/src/types.ts b/plugins/view/src/types.ts index f8551943ab..e4d57f6093 100644 --- a/plugins/view/src/types.ts +++ b/plugins/view/src/types.ts @@ -376,7 +376,7 @@ export interface Groupping extends Class { */ export interface AggregationManager { close: () => void - notifyTx: (tx: Tx) => Promise + notifyTx: (...tx: Tx[]) => Promise updateLookup: (resultDoc: WithLookup, attr: Attribute) => Promise categorize: (target: Array>, attr: AnyAttribute) => Promise>> getAttrClass: () => Ref> diff --git a/server/collaborator/src/platform.ts b/server/collaborator/src/platform.ts index e569c57b54..b072581864 100644 --- a/server/collaborator/src/platform.ts +++ b/server/collaborator/src/platform.ts @@ -94,14 +94,14 @@ export class Controller { * @public */ export class WorkspaceClient { - private readonly txHandlers: ((tx: Tx) => Promise)[] = [] + private readonly txHandlers: ((...tx: Tx[]) => Promise)[] = [] private constructor ( readonly workspace: WorkspaceId, readonly client: Client ) { - this.client.notify = (tx) => { - void this.txHandler(tx) + this.client.notify = (...tx: Tx[]) => { + void this.txHandler(...tx) } } @@ -115,9 +115,9 @@ export class WorkspaceClient { await this.client.close() } - private async txHandler (tx: Tx): Promise { + private async txHandler (...tx: Tx[]): Promise { for (const h of this.txHandlers) { - await h(tx) + await h(...tx) } } } diff --git a/server/ws/src/client.ts b/server/ws/src/client.ts index 5dd75e6373..0fb23fd35b 100644 --- a/server/ws/src/client.ts +++ b/server/ws/src/client.ts @@ -15,12 +15,13 @@ import core, { AccountRole, + type BulkUpdateEvent, TxFactory, TxProcessor, + type TxWorkspaceEvent, WorkspaceEvent, generateId, type Account, - type BulkUpdateEvent, type Class, type Doc, type DocumentQuery, @@ -37,8 +38,7 @@ import core, { type TxApplyIf, type TxApplyResult, type TxCUD, - type TxResult, - type TxWorkspaceEvent + type TxResult } from '@hcengineering/core' import { type Pipeline, type SessionContext } from '@hcengineering/server-core' import { type Token } from '@hcengineering/server-token' @@ -188,8 +188,9 @@ export class ClientSession implements Session { } } } else { - for (const dtx of derived) { - this.broadcast(null, this.token.workspace, { result: dtx }, target) + while (derived.length > 0) { + const part = derived.splice(0, 250) + this.broadcast(null, this.token.workspace, { result: part }, target) } } }