UBERF-8518: Optimize client model (#7000)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-10-21 13:12:45 +07:00 committed by GitHub
parent 001b2dd0d6
commit 0b1af0da90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 241 additions and 165 deletions

View File

@ -305,7 +305,7 @@ export async function configurePlatform (): Promise<void> {
addLocation(printId, () => import(/* webpackChunkName: "print" */ '@hcengineering/print-resources')) addLocation(printId, () => import(/* webpackChunkName: "print" */ '@hcengineering/print-resources'))
addLocation(textEditorId, () => import(/* webpackChunkName: "text-editor" */ '@hcengineering/text-editor-resources')) addLocation(textEditorId, () => import(/* webpackChunkName: "text-editor" */ '@hcengineering/text-editor-resources'))
setMetadata(client.metadata.FilterModel, true) setMetadata(client.metadata.FilterModel, 'ui')
setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin]) setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin])
// Use binary response transfer for faster performance and small transfer sizes. // Use binary response transfer for faster performance and small transfer sizes.

View File

@ -397,7 +397,7 @@ export async function configurePlatform() {
addLocation(textEditorId, () => import(/* webpackChunkName: "text-editor" */ '@hcengineering/text-editor-resources')) addLocation(textEditorId, () => import(/* webpackChunkName: "text-editor" */ '@hcengineering/text-editor-resources'))
addLocation(uploaderId, () => import(/* webpackChunkName: "uploader" */ '@hcengineering/uploader-resources')) addLocation(uploaderId, () => import(/* webpackChunkName: "uploader" */ '@hcengineering/uploader-resources'))
setMetadata(client.metadata.FilterModel, true) setMetadata(client.metadata.FilterModel, 'ui')
setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin]) setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin])
// Use binary response transfer for faster performance and small transfer sizes. // Use binary response transfer for faster performance and small transfer sizes.

View File

@ -13,19 +13,30 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import { Plugin, IntlString } from '@hcengineering/platform' import { IntlString, Plugin } from '@hcengineering/platform'
import type { Account, Class, Data, Doc, Domain, PluginConfiguration, Ref, Timestamp } from '../classes' import type { Account, Class, Data, Doc, Domain, PluginConfiguration, Ref, Timestamp } from '../classes'
import { Space, ClassifierKind, DOMAIN_MODEL } from '../classes' import { ClassifierKind, DOMAIN_MODEL, Space } from '../classes'
import { createClient, ClientConnection } from '../client' import { ClientConnection, createClient } from '../client'
import { clone } from '../clone'
import core from '../component' import core from '../component'
import { Hierarchy } from '../hierarchy' import { Hierarchy } from '../hierarchy'
import { ModelDb, TxDb } from '../memdb' import { ModelDb, TxDb } from '../memdb'
import { TxOperations } from '../operations' import { TxOperations } from '../operations'
import type { DocumentQuery, FindResult, TxResult, SearchQuery, SearchOptions, SearchResult } from '../storage' import type { DocumentQuery, FindResult, SearchOptions, SearchQuery, SearchResult, TxResult } from '../storage'
import { Tx, TxFactory, TxProcessor } from '../tx' import { Tx, TxFactory, TxProcessor } from '../tx'
import { fillConfiguration, pluginFilterTx } from '../utils'
import { connect } from './connection' import { connect } from './connection'
import { genMinModel } from './minmodel' import { genMinModel } from './minmodel'
import { clone } from '../clone'
function filterPlugin (plugin: Plugin): (txes: Tx[]) => Promise<Tx[]> {
return async (txes) => {
const configs = new Map<Ref<PluginConfiguration>, PluginConfiguration>()
fillConfiguration(txes, configs)
const excludedPlugins = Array.from(configs.values()).filter((it) => !it.enabled || it.pluginId !== plugin)
return pluginFilterTx(excludedPlugins, configs, txes)
}
}
describe('client', () => { describe('client', () => {
it('should create client and spaces', async () => { it('should create client and spaces', async () => {
@ -136,7 +147,10 @@ describe('client', () => {
} }
const txCreateDoc1 = txFactory.createTxCreateDoc(core.class.PluginConfiguration, core.space.Model, pluginData1) const txCreateDoc1 = txFactory.createTxCreateDoc(core.class.PluginConfiguration, core.space.Model, pluginData1)
txes.push(txCreateDoc1) txes.push(txCreateDoc1)
const client1 = new TxOperations(await createClient(connectPlugin, ['testPlugin1' as Plugin]), core.account.System) const client1 = new TxOperations(
await createClient(connectPlugin, filterPlugin('testPlugin1' as Plugin)),
core.account.System
)
const result1 = await client1.findAll(core.class.PluginConfiguration, {}) const result1 = await client1.findAll(core.class.PluginConfiguration, {})
expect(result1).toHaveLength(1) expect(result1).toHaveLength(1)
@ -153,7 +167,10 @@ describe('client', () => {
} }
const txCreateDoc2 = txFactory.createTxCreateDoc(core.class.PluginConfiguration, core.space.Model, pluginData2) const txCreateDoc2 = txFactory.createTxCreateDoc(core.class.PluginConfiguration, core.space.Model, pluginData2)
txes.push(txCreateDoc2) txes.push(txCreateDoc2)
const client2 = new TxOperations(await createClient(connectPlugin, ['testPlugin1' as Plugin]), core.account.System) const client2 = new TxOperations(
await createClient(connectPlugin, filterPlugin('testPlugin1' as Plugin)),
core.account.System
)
const result2 = await client2.findAll(core.class.PluginConfiguration, {}) const result2 = await client2.findAll(core.class.PluginConfiguration, {})
expect(result2).toHaveLength(2) expect(result2).toHaveLength(2)
@ -176,7 +193,10 @@ describe('client', () => {
pluginData3 pluginData3
) )
txes.push(txUpdateDoc) txes.push(txUpdateDoc)
const client3 = new TxOperations(await createClient(connectPlugin, ['testPlugin2' as Plugin]), core.account.System) const client3 = new TxOperations(
await createClient(connectPlugin, filterPlugin('testPlugin2' as Plugin)),
core.account.System
)
const result3 = await client3.findAll(core.class.PluginConfiguration, {}) const result3 = await client3.findAll(core.class.PluginConfiguration, {})
expect(result3).toHaveLength(1) expect(result3).toHaveLength(1)

View File

@ -13,17 +13,16 @@
// limitations under the License. // limitations under the License.
// //
import { Plugin } from '@hcengineering/platform'
import { BackupClient, DocChunk } from './backup' import { BackupClient, DocChunk } from './backup'
import { Account, AttachedDoc, Class, DOMAIN_MODEL, Doc, Domain, PluginConfiguration, Ref, Timestamp } from './classes' import { Account, AttachedDoc, Class, DOMAIN_MODEL, Doc, Domain, Ref, Timestamp } from './classes'
import core from './component' import core from './component'
import { Hierarchy } from './hierarchy' import { Hierarchy } from './hierarchy'
import { MeasureContext, MeasureMetricsContext } from './measurements' import { MeasureContext, MeasureMetricsContext } from './measurements'
import { ModelDb } from './memdb' import { ModelDb } from './memdb'
import type { DocumentQuery, FindOptions, FindResult, FulltextStorage, Storage, TxResult, WithLookup } from './storage' import type { DocumentQuery, FindOptions, FindResult, FulltextStorage, Storage, TxResult, WithLookup } from './storage'
import { SearchOptions, SearchQuery, SearchResult, SortingOrder } from './storage' import { SearchOptions, SearchQuery, SearchResult, SortingOrder } from './storage'
import { Tx, TxCUD, TxCollectionCUD, TxCreateDoc, TxProcessor, TxUpdateDoc } from './tx' import { Tx, TxCUD, TxCollectionCUD } from './tx'
import { toFindResult, toIdMap } from './utils' import { toFindResult } from './utils'
const transactionThreshold = 500 const transactionThreshold = 500
@ -215,13 +214,15 @@ export interface TxPersistenceStore {
store: (model: LoadModelResponse) => Promise<void> store: (model: LoadModelResponse) => Promise<void>
} }
export type ModelFilter = (tx: Tx[]) => Promise<Tx[]>
/** /**
* @public * @public
*/ */
export async function createClient ( export async function createClient (
connect: (txHandler: TxHandler) => Promise<ClientConnection>, connect: (txHandler: TxHandler) => Promise<ClientConnection>,
// If set will build model with only allowed plugins. // If set will build model with only allowed plugins.
allowedPlugins?: Plugin[], modelFilter?: ModelFilter,
txPersistence?: TxPersistenceStore, txPersistence?: TxPersistenceStore,
_ctx?: MeasureContext _ctx?: MeasureContext
): Promise<AccountClient> { ): Promise<AccountClient> {
@ -248,14 +249,12 @@ export async function createClient (
} }
lastTx = tx.reduce((cur, it) => (it.modifiedOn > cur ? it.modifiedOn : cur), 0) lastTx = tx.reduce((cur, it) => (it.modifiedOn > cur ? it.modifiedOn : cur), 0)
} }
const configs = new Map<Ref<PluginConfiguration>, PluginConfiguration>()
const conn = await ctx.with('connect', {}, async () => await connect(txHandler)) const conn = await ctx.with('connect', {}, async () => await connect(txHandler))
await ctx.with( await ctx.with(
'load-model', 'load-model',
{ reload: false }, { reload: false },
async (ctx) => await loadModel(ctx, conn, allowedPlugins, configs, hierarchy, model, false, txPersistence) async (ctx) => await loadModel(ctx, conn, modelFilter, hierarchy, model, false, txPersistence)
) )
txBuffer = txBuffer.filter((tx) => tx.space !== core.space.Model) txBuffer = txBuffer.filter((tx) => tx.space !== core.space.Model)
@ -277,7 +276,7 @@ export async function createClient (
const loadModelResponse = await ctx.with( const loadModelResponse = await ctx.with(
'connect', 'connect',
{ reload: true }, { reload: true },
async (ctx) => await loadModel(ctx, conn, allowedPlugins, configs, hierarchy, model, true, txPersistence) async (ctx) => await loadModel(ctx, conn, modelFilter, hierarchy, model, true, txPersistence)
) )
if (event === ClientConnectEvent.Reconnected && loadModelResponse.full) { if (event === ClientConnectEvent.Reconnected && loadModelResponse.full) {
@ -286,7 +285,7 @@ export async function createClient (
model = new ModelDb(hierarchy) model = new ModelDb(hierarchy)
await ctx.with('build-model', {}, async (ctx) => { await ctx.with('build-model', {}, async (ctx) => {
await buildModel(ctx, loadModelResponse, allowedPlugins, configs, hierarchy, model) await buildModel(ctx, loadModelResponse, modelFilter, hierarchy, model)
}) })
await oldOnConnect?.(ClientConnectEvent.Upgraded) await oldOnConnect?.(ClientConnectEvent.Upgraded)
@ -393,8 +392,7 @@ function isPersonAccount (tx: Tx): boolean {
async function loadModel ( async function loadModel (
ctx: MeasureContext, ctx: MeasureContext,
conn: ClientConnection, conn: ClientConnection,
allowedPlugins: Plugin[] | undefined, modelFilter: ModelFilter | undefined,
configs: Map<Ref<PluginConfiguration>, PluginConfiguration>,
hierarchy: Hierarchy, hierarchy: Hierarchy,
model: ModelDb, model: ModelDb,
reload = false, reload = false,
@ -418,19 +416,18 @@ async function loadModel (
) )
} }
await ctx.with('build-model', {}, (ctx) => buildModel(ctx, modelResponse, allowedPlugins, configs, hierarchy, model)) await ctx.with('build-model', {}, (ctx) => buildModel(ctx, modelResponse, modelFilter, hierarchy, model))
return modelResponse return modelResponse
} }
async function buildModel ( async function buildModel (
ctx: MeasureContext, ctx: MeasureContext,
modelResponse: LoadModelResponse, modelResponse: LoadModelResponse,
allowedPlugins: Plugin[] | undefined, modelFilter: ModelFilter | undefined,
configs: Map<Ref<PluginConfiguration>, PluginConfiguration>,
hierarchy: Hierarchy, hierarchy: Hierarchy,
model: ModelDb model: ModelDb
): Promise<void> { ): Promise<void> {
let systemTx: Tx[] = [] const systemTx: Tx[] = []
const userTx: Tx[] = [] const userTx: Tx[] = []
const atxes = modelResponse.transactions const atxes = modelResponse.transactions
@ -444,23 +441,11 @@ async function buildModel (
) )
}) })
if (allowedPlugins != null) { let txes = systemTx.concat(userTx)
await ctx.with('fill config system', {}, async () => { if (modelFilter !== undefined) {
fillConfiguration(systemTx, configs) txes = await modelFilter(txes)
})
await ctx.with('fill config user', {}, async () => {
fillConfiguration(userTx, configs)
})
const excludedPlugins = Array.from(configs.values()).filter(
(it) => !it.enabled || !allowedPlugins.includes(it.pluginId)
)
await ctx.with('filter txes', {}, async () => {
systemTx = pluginFilterTx(excludedPlugins, configs, systemTx)
})
} }
const txes = systemTx.concat(userTx)
await ctx.with('build hierarchy', {}, async () => { await ctx.with('build hierarchy', {}, async () => {
for (const tx of txes) { for (const tx of txes) {
try { try {
@ -488,60 +473,3 @@ function getLastTxTime (txes: Tx[]): number {
} }
return lastTxTime return lastTxTime
} }
function fillConfiguration (systemTx: Tx[], configs: Map<Ref<PluginConfiguration>, PluginConfiguration>): void {
for (const t of systemTx) {
if (t._class === core.class.TxCreateDoc) {
const ct = t as TxCreateDoc<Doc>
if (ct.objectClass === core.class.PluginConfiguration) {
configs.set(ct.objectId as Ref<PluginConfiguration>, TxProcessor.createDoc2Doc(ct) as PluginConfiguration)
}
} else if (t._class === core.class.TxUpdateDoc) {
const ut = t as TxUpdateDoc<Doc>
if (ut.objectClass === core.class.PluginConfiguration) {
const c = configs.get(ut.objectId as Ref<PluginConfiguration>)
if (c !== undefined) {
TxProcessor.updateDoc2Doc(c, ut)
}
}
}
}
}
function pluginFilterTx (
excludedPlugins: PluginConfiguration[],
configs: Map<Ref<PluginConfiguration>, PluginConfiguration>,
systemTx: Tx[]
): Tx[] {
const stx = toIdMap(systemTx)
const totalExcluded = new Set<Ref<Tx>>()
let msg = ''
for (const a of excludedPlugins) {
for (const c of configs.values()) {
if (a.pluginId === c.pluginId) {
for (const id of c.transactions) {
if (c.classFilter !== undefined) {
const filter = new Set(c.classFilter)
const tx = stx.get(id as Ref<Tx>)
if (
tx?._class === core.class.TxCreateDoc ||
tx?._class === core.class.TxUpdateDoc ||
tx?._class === core.class.TxRemoveDoc
) {
const cud = tx as TxCUD<Doc>
if (filter.has(cud.objectClass)) {
totalExcluded.add(id as Ref<Tx>)
}
}
} else {
totalExcluded.add(id as Ref<Tx>)
}
}
msg += ` ${c.pluginId}:${c.transactions.length}`
}
}
}
console.log('exclude plugin', msg)
systemTx = systemTx.filter((t) => !totalExcluded.has(t._id))
return systemTx
}

View File

@ -30,7 +30,7 @@ export class Hierarchy {
private readonly attributes = new Map<Ref<Classifier>, Map<string, AnyAttribute>>() private readonly attributes = new Map<Ref<Classifier>, Map<string, AnyAttribute>>()
private readonly attributesById = new Map<Ref<AnyAttribute>, AnyAttribute>() private readonly attributesById = new Map<Ref<AnyAttribute>, AnyAttribute>()
private readonly descendants = new Map<Ref<Classifier>, Ref<Classifier>[]>() private readonly descendants = new Map<Ref<Classifier>, Ref<Classifier>[]>()
private readonly ancestors = new Map<Ref<Classifier>, { ordered: Ref<Classifier>[], set: Set<Ref<Classifier>> }>() private readonly ancestors = new Map<Ref<Classifier>, Set<Ref<Classifier>>>()
private readonly proxies = new Map<Ref<Mixin<Doc>>, ProxyHandler<Doc>>() private readonly proxies = new Map<Ref<Mixin<Doc>>, ProxyHandler<Doc>>()
private readonly classifierProperties = new Map<Ref<Classifier>, Record<string, any>>() private readonly classifierProperties = new Map<Ref<Classifier>, Record<string, any>>()
@ -166,7 +166,7 @@ export class Hierarchy {
if (result === undefined) { if (result === undefined) {
throw new Error('ancestors not found: ' + _class) throw new Error('ancestors not found: ' + _class)
} }
return result.ordered return Array.from(result)
} }
getClass<T extends Obj = Obj>(_class: Ref<Class<T>>): Class<T> { getClass<T extends Obj = Obj>(_class: Ref<Class<T>>): Class<T> {
@ -301,7 +301,7 @@ export class Hierarchy {
* It will iterate over parents. * It will iterate over parents.
*/ */
isDerived<T extends Obj>(_class: Ref<Class<T>>, from: Ref<Class<T>>): boolean { isDerived<T extends Obj>(_class: Ref<Class<T>>, from: Ref<Class<T>>): boolean {
return this.ancestors.get(_class)?.set?.has(from) ?? false return this.ancestors.get(_class)?.has(from) ?? false
} }
/** /**
@ -388,19 +388,17 @@ export class Hierarchy {
const list = this.ancestors.get(_class) const list = this.ancestors.get(_class)
if (list === undefined) { if (list === undefined) {
if (add) { if (add) {
this.ancestors.set(_class, { ordered: [classifier], set: new Set([classifier]) }) this.ancestors.set(_class, new Set([classifier]))
} }
} else { } else {
if (add) { if (add) {
if (!list.set.has(classifier)) { if (!list.has(classifier)) {
list.ordered.push(classifier) list.add(classifier)
list.set.add(classifier)
} }
} else { } else {
const pos = list.ordered.indexOf(classifier) const pos = list.has(classifier)
if (pos !== -1) { if (pos) {
list.ordered.splice(pos, 1) list.delete(classifier)
list.set.delete(classifier)
} }
} }
} }

View File

@ -40,7 +40,8 @@ import {
roleOrder, roleOrder,
Space, Space,
TypedSpace, TypedSpace,
WorkspaceMode WorkspaceMode,
type PluginConfiguration
} from './classes' } from './classes'
import core from './component' import core from './component'
import { Hierarchy } from './hierarchy' import { Hierarchy } from './hierarchy'
@ -48,7 +49,7 @@ import { TxOperations } from './operations'
import { isPredicate } from './predicate' import { isPredicate } from './predicate'
import { Branding, BrandingMap } from './server' import { Branding, BrandingMap } from './server'
import { DocumentQuery, FindResult } from './storage' import { DocumentQuery, FindResult } from './storage'
import { DOMAIN_TX } from './tx' import { DOMAIN_TX, TxProcessor, type Tx, type TxCreateDoc, type TxCUD, type TxUpdateDoc } from './tx'
function toHex (value: number, chars: number): string { function toHex (value: number, chars: number): string {
const result = value.toString(16) const result = value.toString(16)
@ -835,3 +836,60 @@ export function getBranding (brandings: BrandingMap, key: string | undefined): B
return Object.values(brandings).find((branding) => branding.key === key) ?? null return Object.values(brandings).find((branding) => branding.key === key) ?? null
} }
export function fillConfiguration (systemTx: Tx[], configs: Map<Ref<PluginConfiguration>, PluginConfiguration>): void {
for (const t of systemTx) {
if (t._class === core.class.TxCreateDoc) {
const ct = t as TxCreateDoc<Doc>
if (ct.objectClass === core.class.PluginConfiguration) {
configs.set(ct.objectId as Ref<PluginConfiguration>, TxProcessor.createDoc2Doc(ct) as PluginConfiguration)
}
} else if (t._class === core.class.TxUpdateDoc) {
const ut = t as TxUpdateDoc<Doc>
if (ut.objectClass === core.class.PluginConfiguration) {
const c = configs.get(ut.objectId as Ref<PluginConfiguration>)
if (c !== undefined) {
TxProcessor.updateDoc2Doc(c, ut)
}
}
}
}
}
export function pluginFilterTx (
excludedPlugins: PluginConfiguration[],
configs: Map<Ref<PluginConfiguration>, PluginConfiguration>,
systemTx: Tx[]
): Tx[] {
const stx = toIdMap(systemTx)
const totalExcluded = new Set<Ref<Tx>>()
let msg = ''
for (const a of excludedPlugins) {
for (const c of configs.values()) {
if (a.pluginId === c.pluginId) {
for (const id of c.transactions) {
if (c.classFilter !== undefined) {
const filter = new Set(c.classFilter)
const tx = stx.get(id as Ref<Tx>)
if (
tx?._class === core.class.TxCreateDoc ||
tx?._class === core.class.TxUpdateDoc ||
tx?._class === core.class.TxRemoveDoc
) {
const cud = tx as TxCUD<Doc>
if (filter.has(cud.objectClass)) {
totalExcluded.add(id as Ref<Tx>)
}
}
} else {
totalExcluded.add(id as Ref<Tx>)
}
}
msg += ` ${c.pluginId}:${c.transactions.length}`
}
}
}
console.log('exclude plugin', msg)
systemTx = systemTx.filter((t) => !totalExcluded.has(t._id))
return systemTx
}

View File

@ -24,7 +24,16 @@ import core, {
TxWorkspaceEvent, TxWorkspaceEvent,
WorkspaceEvent, WorkspaceEvent,
concatLink, concatLink,
createClient createClient,
fillConfiguration,
pluginFilterTx,
type Class,
type ClientConnection,
type Doc,
type ModelFilter,
type PluginConfiguration,
type Ref,
type TxCUD
} from '@hcengineering/core' } from '@hcengineering/core'
import platform, { Severity, Status, getMetadata, getPlugins, setPlatformStatus } from '@hcengineering/platform' import platform, { Severity, Status, getMetadata, getPlugins, setPlatformStatus } from '@hcengineering/platform'
import { connect } from './connection' import { connect } from './connection'
@ -70,68 +79,123 @@ export default async () => {
return { return {
function: { function: {
GetClient: async (token: string, endpoint: string, opt?: ClientFactoryOptions): Promise<AccountClient> => { GetClient: async (token: string, endpoint: string, opt?: ClientFactoryOptions): Promise<AccountClient> => {
const filterModel = getMetadata(clientPlugin.metadata.FilterModel) ?? false const filterModel = getMetadata(clientPlugin.metadata.FilterModel) ?? 'none'
const client = createClient( const handler = async (handler: TxHandler): Promise<ClientConnection> => {
async (handler: TxHandler) => { const url = concatLink(endpoint, `/${token}`)
const url = concatLink(endpoint, `/${token}`)
const upgradeHandler: TxHandler = (...txes: Tx[]) => { const upgradeHandler: TxHandler = (...txes: Tx[]) => {
for (const tx of txes) { for (const tx of txes) {
if (tx?._class === core.class.TxModelUpgrade) { if (tx?._class === core.class.TxModelUpgrade) {
opt?.onUpgrade?.() opt?.onUpgrade?.()
return return
} }
if (tx?._class === core.class.TxWorkspaceEvent) { if (tx?._class === core.class.TxWorkspaceEvent) {
const event = tx as TxWorkspaceEvent const event = tx as TxWorkspaceEvent
if (event.event === WorkspaceEvent.MaintenanceNotification) { if (event.event === WorkspaceEvent.MaintenanceNotification) {
void setPlatformStatus( void setPlatformStatus(
new Status(Severity.WARNING, platform.status.MaintenanceWarning, { new Status(Severity.WARNING, platform.status.MaintenanceWarning, {
time: event.params.timeMinutes time: event.params.timeMinutes
}) })
) )
}
} }
} }
handler(...txes)
} }
const tokenPayload: { workspace: string, email: string } = decodeTokenPayload(token) handler(...txes)
}
const tokenPayload: { workspace: string, email: string } = decodeTokenPayload(token)
const newOpt = { ...opt } const newOpt = { ...opt }
const connectTimeout = getMetadata(clientPlugin.metadata.ConnectionTimeout) const connectTimeout = getMetadata(clientPlugin.metadata.ConnectionTimeout)
let connectPromise: Promise<void> | undefined let connectPromise: Promise<void> | undefined
if ((connectTimeout ?? 0) > 0) { if ((connectTimeout ?? 0) > 0) {
connectPromise = new Promise<void>((resolve, reject) => { connectPromise = new Promise<void>((resolve, reject) => {
const connectTO = setTimeout(() => { const connectTO = setTimeout(() => {
if (!clientConnection.isConnected()) { if (!clientConnection.isConnected()) {
newOpt.onConnect = undefined newOpt.onConnect = undefined
void clientConnection?.close() void clientConnection?.close()
void opt?.onDialTimeout?.() void opt?.onDialTimeout?.()
reject(new Error(`Connection timeout, and no connection established to ${endpoint}`)) reject(new Error(`Connection timeout, and no connection established to ${endpoint}`))
}
}, connectTimeout)
newOpt.onConnect = (event) => {
// Any event is fine, it means server is alive.
clearTimeout(connectTO)
resolve()
} }
}) }, connectTimeout)
} newOpt.onConnect = (event) => {
const clientConnection = connect(url, upgradeHandler, tokenPayload.workspace, tokenPayload.email, newOpt) // Any event is fine, it means server is alive.
if (connectPromise !== undefined) { clearTimeout(connectTO)
await connectPromise resolve()
} }
return await Promise.resolve(clientConnection) })
}, }
filterModel ? [...getPlugins(), ...(getMetadata(clientPlugin.metadata.ExtraPlugins) ?? [])] : undefined, const clientConnection = connect(url, upgradeHandler, tokenPayload.workspace, tokenPayload.email, newOpt)
createModelPersistence(getWSFromToken(token)), if (connectPromise !== undefined) {
opt?.ctx await connectPromise
) }
return await Promise.resolve(clientConnection)
}
const modelFilter: ModelFilter = async (txes) => {
if (filterModel === 'client') {
return returnClientTxes(txes)
}
if (filterModel === 'ui') {
return returnUITxes(txes)
}
return txes
}
const client = createClient(handler, modelFilter, createModelPersistence(getWSFromToken(token)), opt?.ctx)
return await client return await client
} }
} }
} }
} }
function returnUITxes (txes: Tx[]): Tx[] {
const configs = new Map<Ref<PluginConfiguration>, PluginConfiguration>()
fillConfiguration(txes, configs)
const allowedPlugins = [...getPlugins(), ...(getMetadata(clientPlugin.metadata.ExtraPlugins) ?? [])]
const excludedPlugins = Array.from(configs.values()).filter(
(it) => !it.enabled || !allowedPlugins.includes(it.pluginId)
)
return pluginFilterTx(excludedPlugins, configs, txes)
}
function returnClientTxes (txes: Tx[]): Tx[] {
const configs = new Map<Ref<PluginConfiguration>, PluginConfiguration>()
fillConfiguration(txes, configs)
const excludedPlugins = Array.from(configs.values()).filter((it) => !it.enabled || it.pluginId.startsWith('server-'))
const toExclude = new Set([
'workbench:class:Application' as Ref<Class<Doc>>,
'presentation:class:ComponentPointExtension' as Ref<Class<Doc>>,
'presentation:class:ObjectSearchCategory' as Ref<Class<Doc>>,
'notification:class:NotificationGroup' as Ref<Class<Doc>>,
'notification:class:NotificationType' as Ref<Class<Doc>>,
'view:class:Action' as Ref<Class<Doc>>,
'view:class:Viewlet' as Ref<Class<Doc>>,
'text-editor:class:TextEditorAction' as Ref<Class<Doc>>,
'templates:class:TemplateField' as Ref<Class<Doc>>,
'activity:class:DocUpdateMessageViewlet' as Ref<Class<Doc>>,
'core:class:PluginConfiguration' as Ref<Class<Doc>>,
'core:class:DomainIndexConfiguration' as Ref<Class<Doc>>
])
const result = pluginFilterTx(excludedPlugins, configs, txes).filter((tx) => {
// Exclude all matched UI plugins
if (
tx?._class === core.class.TxCreateDoc ||
tx?._class === core.class.TxUpdateDoc ||
tx?._class === core.class.TxRemoveDoc
) {
const cud = tx as TxCUD<Doc>
if (toExclude.has(cud.objectClass)) {
return false
}
}
return true
})
return result
}
function createModelPersistence (workspace: string): TxPersistenceStore | undefined { function createModelPersistence (workspace: string): TxPersistenceStore | undefined {
const overrideStore = getMetadata(clientPlugin.metadata.OverridePersistenceStore) const overrideStore = getMetadata(clientPlugin.metadata.OverridePersistenceStore)
if (overrideStore !== undefined) { if (overrideStore !== undefined) {

View File

@ -69,10 +69,15 @@ export interface ClientFactoryOptions {
*/ */
export type ClientFactory = (token: string, endpoint: string, opt?: ClientFactoryOptions) => Promise<AccountClient> export type ClientFactory = (token: string, endpoint: string, opt?: ClientFactoryOptions) => Promise<AccountClient>
// client - will filter out all server model elements
// It will also filter out all UI Elements, like Actions, View declarations etc.
// ui - will filter out all server element's and all UI disabled elements.
export type FilterMode = 'none' | 'client' | 'ui'
export default plugin(clientId, { export default plugin(clientId, {
metadata: { metadata: {
ClientSocketFactory: '' as Metadata<ClientSocketFactory>, ClientSocketFactory: '' as Metadata<ClientSocketFactory>,
FilterModel: '' as Metadata<boolean>, FilterModel: '' as Metadata<FilterMode>,
ExtraPlugins: '' as Metadata<Plugin[]>, ExtraPlugins: '' as Metadata<Plugin[]>,
UseBinaryProtocol: '' as Metadata<boolean>, UseBinaryProtocol: '' as Metadata<boolean>,
UseProtocolCompression: '' as Metadata<boolean>, UseProtocolCompression: '' as Metadata<boolean>,

View File

@ -104,6 +104,5 @@ export async function configurePlatform() {
setMetadata(uiPlugin.metadata.PlatformTitle, 'Tracker') setMetadata(uiPlugin.metadata.PlatformTitle, 'Tracker')
setMetadata(workbench.metadata.PlatformTitle, 'Tracker') setMetadata(workbench.metadata.PlatformTitle, 'Tracker')
setMetadata(client.metadata.FilterModel, true) setMetadata(client.metadata.FilterModel, 'ui')
setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin])
} }

View File

@ -13,10 +13,13 @@
// limitations under the License. // limitations under the License.
// //
import client from '@hcengineering/client'
import { type Client } from '@hcengineering/core' import { type Client } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform'
import { createClient, getTransactorEndpoint } from '@hcengineering/server-client' import { createClient, getTransactorEndpoint } from '@hcengineering/server-client'
export async function getClient (token: string): Promise<Client> { export async function getClient (token: string): Promise<Client> {
const endpoint = await getTransactorEndpoint(token) const endpoint = await getTransactorEndpoint(token)
setMetadata(client.metadata.FilterModel, 'client')
return await createClient(endpoint, token) return await createClient(endpoint, token)
} }

View File

@ -37,6 +37,7 @@ export async function createPlatformClient (
{ mode: 'github' } { mode: 'github' }
) )
setMetadata(client.metadata.ConnectionTimeout, timeout) setMetadata(client.metadata.ConnectionTimeout, timeout)
setMetadata(client.metadata.FilterModel, 'client')
const endpoint = await getTransactorEndpoint(token) const endpoint = await getTransactorEndpoint(token)
const connection = await ( const connection = await (
await clientResources() await clientResources()