UBERF-9747

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-05-06 16:54:48 +07:00
parent d3a40bb48e
commit 8e9e875936
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
36 changed files with 1227 additions and 1115 deletions

3
.vscode/launch.json vendored
View File

@ -103,7 +103,8 @@
"UPLOAD_URL": "/files", "UPLOAD_URL": "/files",
"AI_BOT_URL": "http://localhost:4010", "AI_BOT_URL": "http://localhost:4010",
"STATS_URL": "http://huly.local:4900", "STATS_URL": "http://huly.local:4900",
"QUEUE_CONFIG": "localhost:19092" "QUEUE_CONFIG": "localhost:19092",
"RATE_LIMIT_MAX": "25000"
}, },
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20", "runtimeVersion": "20",

View File

@ -49,9 +49,6 @@ export class TTx extends TDoc implements Tx {
objectSpace!: Ref<Space> objectSpace!: Ref<Space>
} }
@Model(core.class.TxModelUpgrade, core.class.Tx, DOMAIN_TX)
export class TTxModelUpgrade extends TTx {}
@Model(core.class.TxCUD, core.class.Tx) @Model(core.class.TxCUD, core.class.Tx)
export class TTxCUD<T extends Doc> extends TTx implements TxCUD<T> { export class TTxCUD<T extends Doc> extends TTx implements TxCUD<T> {
@Prop(TypeRef(core.class.Doc), core.string.Object) @Prop(TypeRef(core.class.Doc), core.string.Object)

View File

@ -14,8 +14,8 @@
// limitations under the License. // limitations under the License.
// //
import { IntlString, Plugin } from '@hcengineering/platform' import { IntlString, Plugin } from '@hcengineering/platform'
import { ClientConnectEvent, DocChunk } from '..' import { ClientConnectEvent, DocChunk, type WorkspaceUuid } from '..'
import type { Class, Data, Doc, Domain, PluginConfiguration, Ref, Timestamp } from '../classes' import type { Account, Class, Data, Doc, Domain, PluginConfiguration, Ref, Timestamp } from '../classes'
import { ClassifierKind, DOMAIN_MODEL, Space } from '../classes' import { ClassifierKind, DOMAIN_MODEL, Space } from '../classes'
import { ClientConnection, createClient } from '../client' import { ClientConnection, createClient } from '../client'
import core from '../component' import core from '../component'
@ -104,21 +104,25 @@ describe('client', () => {
} }
return new (class implements ClientConnection { return new (class implements ClientConnection {
handler?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void> handler?: (event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>
set onConnect ( set onConnect (
handler: ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>) | undefined handler: ((event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>) | undefined
) { ) {
this.handler = handler this.handler = handler
void this.handler?.(ClientConnectEvent.Connected, '', {}) void this.handler?.(ClientConnectEvent.Connected, {}, {})
} }
get onConnect (): get onConnect ():
| ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>) | ((event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>)
| undefined { | undefined {
return this.handler return this.handler
} }
getAccount (): Promise<Account > {
throw new Error('Method not implemented.')
}
isConnected = (): boolean => true isConnected = (): boolean => true
findAll = findAll findAll = findAll
pushHandler = (): void => {} pushHandler = (): void => {}

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
// //
import { ClientConnectEvent, DocChunk, generateId } from '..' import { ClientConnectEvent, DocChunk, generateId, type WorkspaceUuid } from '..'
import type { Class, Doc, Domain, Ref, Timestamp } from '../classes' import type { Account, Class, Doc, Domain, Ref, Timestamp } from '../classes'
import { ClientConnection } from '../client' import { ClientConnection } from '../client'
import core from '../component' import core from '../component'
import { Hierarchy } from '../hierarchy' import { Hierarchy } from '../hierarchy'
@ -47,19 +47,23 @@ export async function connect (handler: (tx: Tx) => void): Promise<ClientConnect
isConnected = (): boolean => true isConnected = (): boolean => true
findAll = findAll findAll = findAll
handler?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void> handler?: (event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>
set onConnect ( set onConnect (
handler: ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>) | undefined handler: ((event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>) | undefined
) { ) {
this.handler = handler this.handler = handler
void this.handler?.(ClientConnectEvent.Connected, '', {}) void this.handler?.(ClientConnectEvent.Connected, {}, {})
} }
get onConnect (): ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>) | undefined { get onConnect (): ((event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>) | undefined {
return this.handler return this.handler
} }
getAccount (): Promise<Account> {
throw new Error('Method not implemented.')
}
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> { async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
return { docs: [] } return { docs: [] }
} }

View File

@ -71,7 +71,10 @@ export interface Obj {
export interface Account { export interface Account {
uuid: AccountUuid uuid: AccountUuid
role: AccountRole roles: Record<WorkspaceUuid, AccountRole>
role: AccountRole // Role in current workspace
targetWorkspace: WorkspaceUuid // In case target workspace is used, it will be filled with personal workspace UUID in case of all workspaces mode.
primarySocialId: PersonId primarySocialId: PersonId
socialIds: PersonId[] socialIds: PersonId[]
fullSocialIds: SocialId[] fullSocialIds: SocialId[]
@ -519,7 +522,8 @@ export enum AccountRole {
Guest = 'GUEST', Guest = 'GUEST',
User = 'USER', User = 'USER',
Maintainer = 'MAINTAINER', Maintainer = 'MAINTAINER',
Owner = 'OWNER' Owner = 'OWNER',
Undetermined = 'UNDETERMINED' // In case of multi workspaces mode
} }
/** /**
@ -528,6 +532,7 @@ export enum AccountRole {
export const roleOrder: Record<AccountRole, number> = { export const roleOrder: Record<AccountRole, number> = {
[AccountRole.DocGuest]: 10, [AccountRole.DocGuest]: 10,
[AccountRole.Guest]: 20, [AccountRole.Guest]: 20,
[AccountRole.Undetermined]: 25,
[AccountRole.User]: 30, [AccountRole.User]: 30,
[AccountRole.Maintainer]: 40, [AccountRole.Maintainer]: 40,
[AccountRole.Owner]: 50 [AccountRole.Owner]: 50

View File

@ -15,7 +15,7 @@
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import { BackupClient, DocChunk } from './backup' import { BackupClient, DocChunk } from './backup'
import { Class, DOMAIN_MODEL, Doc, Domain, Ref, Timestamp } from './classes' import { Class, DOMAIN_MODEL, Doc, Domain, Ref, Timestamp, type Account } 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'
@ -23,7 +23,8 @@ 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 } from './storage' import { SearchOptions, SearchQuery, SearchResult } from './storage'
import { Tx, TxCUD, WorkspaceEvent, type TxWorkspaceEvent } from './tx' import { Tx, TxCUD, WorkspaceEvent, type TxWorkspaceEvent } from './tx'
import { platformNow, platformNowDiff, toFindResult } from './utils' import { platformNow, platformNowDiff, toFindResult, type WorkspaceUuid } from './utils'
import { deepEqual } from 'fast-equals'
/** /**
* @public * @public
@ -80,13 +81,15 @@ export interface ClientConnection extends Storage, FulltextStorage, BackupClient
isConnected: () => boolean isConnected: () => boolean
close: () => Promise<void> close: () => Promise<void>
onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void> onConnect?: (event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>
// If hash is passed, will return LoadModelResponse // If hash is passed, will return LoadModelResponse
loadModel: (last: Timestamp, hash?: string) => Promise<Tx[] | LoadModelResponse> loadModel: (last: Timestamp, hash?: string) => Promise<Tx[] | LoadModelResponse>
getLastHash?: (ctx: MeasureContext) => Promise<string | undefined> getLastHash?: (ctx: MeasureContext) => Promise<Record<WorkspaceUuid, string | undefined>>
pushHandler: (handler: Handler) => void pushHandler: (handler: Handler) => void
getAccount: () => Promise<Account>
} }
class ClientImpl implements Client, BackupClient { class ClientImpl implements Client, BackupClient {
@ -237,7 +240,7 @@ export async function createClient (
let hierarchy = new Hierarchy() let hierarchy = new Hierarchy()
let model = new ModelDb(hierarchy) let model = new ModelDb(hierarchy)
let lastTx: string | undefined let lastTx: Record<WorkspaceUuid, string | undefined> | undefined
function txHandler (...tx: Tx[]): void { function txHandler (...tx: Tx[]): void {
if (tx == null || tx.length === 0) { if (tx == null || tx.length === 0) {
@ -282,7 +285,7 @@ export async function createClient (
txBuffer = undefined txBuffer = undefined
const oldOnConnect: const oldOnConnect:
| ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>) | ((event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>)
| undefined = conn.onConnect | undefined = conn.onConnect
conn.onConnect = async (event, _lastTx, data) => { conn.onConnect = async (event, _lastTx, data) => {
console.log('Client: onConnect', event) console.log('Client: onConnect', event)
@ -324,7 +327,7 @@ export async function createClient (
return return
} }
if (lastTx === _lastTx) { if (deepEqual(lastTx, _lastTx)) {
// Same lastTx, no need to refresh // Same lastTx, no need to refresh
await oldOnConnect?.(ClientConnectEvent.Reconnected, _lastTx, data) await oldOnConnect?.(ClientConnectEvent.Reconnected, _lastTx, data)
return return
@ -362,10 +365,14 @@ async function loadModel (
hash: '' hash: ''
} }
if (conn.getLastHash !== undefined && (await conn.getLastHash(ctx)) === current.hash) { if (conn.getLastHash !== undefined) {
const lastHash = await conn.getLastHash(ctx)
const account = await conn.getAccount()
if (lastHash[account.targetWorkspace] === current.hash) {
// We have same model hash. // We have same model hash.
return { mode: 'same', current: current.transactions, addition: [] } return { mode: 'same', current: current.transactions, addition: [] }
} }
}
const lastTxTime = getLastTxTime(current.transactions) const lastTxTime = getLastTxTime(current.transactions)
const result = await ctx.with('connection-load-model', { hash: current.hash !== '' }, (ctx) => const result = await ctx.with('connection-load-model', { hash: current.hash !== '' }, (ctx) =>
conn.loadModel(lastTxTime, current.hash) conn.loadModel(lastTxTime, current.hash)

View File

@ -15,9 +15,9 @@
import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform' import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import type { BenchmarkDoc } from './benchmark' import type { BenchmarkDoc } from './benchmark'
import { AccountRole } from './classes'
import type { import type {
Account, Account,
AccountUuid,
AnyAttribute, AnyAttribute,
ArrOf, ArrOf,
Association, Association,
@ -59,9 +59,9 @@ import type {
TypeAny, TypeAny,
TypedSpace, TypedSpace,
UserStatus, UserStatus,
Version, Version
AccountUuid
} from './classes' } from './classes'
import { AccountRole } from './classes'
import { Status, StatusCategory } from './status' import { Status, StatusCategory } from './status'
import type { import type {
Tx, Tx,
@ -69,11 +69,11 @@ import type {
TxCUD, TxCUD,
TxCreateDoc, TxCreateDoc,
TxMixin, TxMixin,
TxModelUpgrade,
TxRemoveDoc, TxRemoveDoc,
TxUpdateDoc, TxUpdateDoc,
TxWorkspaceEvent TxWorkspaceEvent
} from './tx' } from './tx'
import type { WorkspaceUuid } from './utils'
/** /**
* @public * @public
@ -88,7 +88,9 @@ export const systemAccountEmail = 'anticrm@hc.engineering'
export const systemAccountUuid = '1749089e-22e6-48de-af4e-165e18fbd2f9' as AccountUuid export const systemAccountUuid = '1749089e-22e6-48de-af4e-165e18fbd2f9' as AccountUuid
export const systemAccount: Account = { export const systemAccount: Account = {
uuid: systemAccountUuid, uuid: systemAccountUuid,
roles: { },
role: AccountRole.Owner, role: AccountRole.Owner,
targetWorkspace: systemAccountUuid as any as WorkspaceUuid,
primarySocialId: '' as PersonId, primarySocialId: '' as PersonId,
socialIds: [], socialIds: [],
fullSocialIds: [] fullSocialIds: []
@ -107,7 +109,6 @@ export default plugin(coreId, {
Interface: '' as Ref<Class<Interface<Doc>>>, Interface: '' as Ref<Class<Interface<Doc>>>,
Attribute: '' as Ref<Class<AnyAttribute>>, Attribute: '' as Ref<Class<AnyAttribute>>,
Tx: '' as Ref<Class<Tx>>, Tx: '' as Ref<Class<Tx>>,
TxModelUpgrade: '' as Ref<Class<TxModelUpgrade>>,
TxWorkspaceEvent: '' as Ref<Class<TxWorkspaceEvent>>, TxWorkspaceEvent: '' as Ref<Class<TxWorkspaceEvent>>,
TxApplyIf: '' as Ref<Class<TxApplyIf>>, TxApplyIf: '' as Ref<Class<TxApplyIf>>,
TxCUD: '' as Ref<Class<TxCUD<Doc>>>, TxCUD: '' as Ref<Class<TxCUD<Doc>>>,

View File

@ -34,7 +34,7 @@ import { setObjectValue } from './objvalue'
import { _getOperator } from './operator' import { _getOperator } from './operator'
import { _toDoc } from './proxy' import { _toDoc } from './proxy'
import type { DocumentQuery, TxResult } from './storage' import type { DocumentQuery, TxResult } from './storage'
import { generateId } from './utils' import { generateId, type WorkspaceUuid } from './utils'
/** /**
* @public * @public
@ -52,7 +52,9 @@ export enum WorkspaceEvent {
SecurityChange, SecurityChange,
MaintenanceNotification, MaintenanceNotification,
BulkUpdate, BulkUpdate,
LastTx LastTx,
WorkpaceActive,
ModelUpgrade = 7
} }
/** /**
@ -62,6 +64,7 @@ export enum WorkspaceEvent {
export interface TxWorkspaceEvent<T = any> extends Tx { export interface TxWorkspaceEvent<T = any> extends Tx {
event: WorkspaceEvent event: WorkspaceEvent
params: T params: T
workspace?: WorkspaceUuid
} }
/** /**
@ -78,11 +81,6 @@ export interface BulkUpdateEvent {
_class: Ref<Class<Doc>>[] _class: Ref<Class<Doc>>[]
} }
/**
* @public
*/
export interface TxModelUpgrade extends Tx {}
/** /**
* @public * @public
*/ */

View File

@ -762,7 +762,10 @@ export function isOwnerOrMaintainer (): boolean {
} }
export function hasAccountRole (acc: Account, targerRole: AccountRole): boolean { export function hasAccountRole (acc: Account, targerRole: AccountRole): boolean {
return roleOrder[acc.role] >= roleOrder[targerRole] if (acc.targetWorkspace == null) {
throw new Error('Account has no target workspace')
}
return roleOrder[acc.roles[acc.targetWorkspace]] >= roleOrder[targerRole]
} }
export function getBranding (brandings: BrandingMap, key: string | undefined): Branding | null { export function getBranding (brandings: BrandingMap, key: string | undefined): Branding | null {

View File

@ -39,7 +39,9 @@ import core, {
Timestamp, Timestamp,
Tx, Tx,
TxDb, TxDb,
TxResult TxResult,
type Account,
type WorkspaceUuid
} from '@hcengineering/core' } from '@hcengineering/core'
import { genMinModel } from './minmodel' import { genMinModel } from './minmodel'
@ -50,6 +52,7 @@ FulltextStorage & {
isConnected: () => boolean isConnected: () => boolean
loadModel: (last: Timestamp, hash?: string) => Promise<Tx[] | LoadModelResponse> loadModel: (last: Timestamp, hash?: string) => Promise<Tx[] | LoadModelResponse>
pushHandler: (handler: Handler) => void pushHandler: (handler: Handler) => void
getAccount: () => Promise<Account>
} }
> { > {
const txes = genMinModel() const txes = genMinModel()
@ -150,18 +153,22 @@ FulltextStorage & {
async sendForceClose (): Promise<void> {} async sendForceClose (): Promise<void> {}
handler?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void> handler?: (event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>
set onConnect ( set onConnect (
handler: ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>) | undefined handler: ((event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>) | undefined
) { ) {
this.handler = handler this.handler = handler
void this.handler?.(ClientConnectEvent.Connected, '', {}) void this.handler?.(ClientConnectEvent.Connected, {}, {})
} }
get onConnect (): ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>) | undefined { get onConnect (): ((event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>) | undefined {
return this.handler return this.handler
} }
getAccount (): Promise<Account> {
throw new Error('Method not implemented.')
}
} }
return new TestConnection(hierarchy, model, transactions) return new TestConnection(hierarchy, model, transactions)

View File

@ -22,13 +22,24 @@ import client, {
pingConst, pingConst,
pongConst pongConst
} from '@hcengineering/client' } from '@hcengineering/client'
import { EventResult } from '@hcengineering/communication-sdk-types'
import {
FindLabelsParams,
FindMessagesGroupsParams,
FindMessagesParams,
FindNotificationContextParams,
FindNotificationsParams,
Label,
Message,
MessagesGroup,
NotificationContext
} from '@hcengineering/communication-types'
import core, { import core, {
Account, Account,
Class, Class,
ClientConnectEvent, ClientConnectEvent,
ClientConnection, ClientConnection,
clone, clone,
Handler,
Doc, Doc,
DocChunk, DocChunk,
DocumentQuery, DocumentQuery,
@ -36,6 +47,7 @@ import core, {
FindOptions, FindOptions,
FindResult, FindResult,
generateId, generateId,
Handler,
LoadModelResponse, LoadModelResponse,
type MeasureContext, type MeasureContext,
MeasureMetricsContext, MeasureMetricsContext,
@ -50,6 +62,8 @@ import core, {
TxApplyIf, TxApplyIf,
TxHandler, TxHandler,
TxResult, TxResult,
type TxWorkspaceEvent,
WorkspaceEvent,
type WorkspaceUuid type WorkspaceUuid
} from '@hcengineering/core' } from '@hcengineering/core'
import platform, { import platform, {
@ -60,20 +74,8 @@ import platform, {
Status, Status,
UNAUTHORIZED UNAUTHORIZED
} from '@hcengineering/platform' } from '@hcengineering/platform'
import { HelloRequest, HelloResponse, type RateLimitInfo, ReqId, type Response, RPCHandler } from '@hcengineering/rpc'
import { uncompress } from 'snappyjs' import { uncompress } from 'snappyjs'
import { HelloRequest, HelloResponse, ReqId, type Response, RPCHandler, type RateLimitInfo } from '@hcengineering/rpc'
import { EventResult } from '@hcengineering/communication-sdk-types'
import {
FindLabelsParams,
FindMessagesGroupsParams,
FindMessagesParams,
FindNotificationContextParams,
FindNotificationsParams,
Label,
Message,
MessagesGroup,
NotificationContext
} from '@hcengineering/communication-types'
const SECOND = 1000 const SECOND = 1000
const pingTimeout = 10 * SECOND const pingTimeout = 10 * SECOND
@ -134,11 +136,11 @@ class Connection implements ClientConnection {
private account: Account | undefined private account: Account | undefined
onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void> onConnect?: (event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>
rpcHandler: RPCHandler rpcHandler: RPCHandler
lastHash?: string lastHash?: Record<WorkspaceUuid, string | undefined>
handlers: Handler[] = [] handlers: Handler[] = []
@ -179,9 +181,9 @@ class Connection implements ClientConnection {
this.handlers.push(handler) this.handlers.push(handler)
} }
async getLastHash (ctx: MeasureContext): Promise<string | undefined> { async getLastHash (ctx: MeasureContext): Promise<Record<WorkspaceUuid, string | undefined>> {
await this.waitOpenConnection(ctx) await this.waitOpenConnection(ctx)
return this.lastHash return this.lastHash ?? {}
} }
private schedulePing (socketId: number): void { private schedulePing (socketId: number): void {
@ -506,9 +508,13 @@ class Connection implements ClientConnection {
const txArr = Array.isArray(resp.result) ? (resp.result as Tx[]) : [resp.result as Tx] const txArr = Array.isArray(resp.result) ? (resp.result as Tx[]) : [resp.result as Tx]
for (const tx of txArr) { for (const tx of txArr) {
if (tx?._class === core.class.TxModelUpgrade) { if (tx?._class === core.class.TxWorkspaceEvent) {
const event = tx as TxWorkspaceEvent
// TODO: Check
if (event.event === WorkspaceEvent.ModelUpgrade) {
console.log('Processing upgrade', this.workspace, this.user) console.log('Processing upgrade', this.workspace, this.user)
this.opt?.onUpgrade?.() this.opt?.onUpgrade?.(event)
}
return return
} }
} }

View File

@ -16,10 +16,17 @@
import clientPlugin from '@hcengineering/client' import clientPlugin from '@hcengineering/client'
import type { ClientFactoryOptions } from '@hcengineering/client/src' import type { ClientFactoryOptions } from '@hcengineering/client/src'
import core, { import core, {
type Class,
Client, Client,
type ClientConnection,
type Doc,
LoadModelResponse, LoadModelResponse,
type ModelFilter,
type PersonUuid, type PersonUuid,
type PluginConfiguration,
type Ref,
Tx, Tx,
type TxCUD,
TxHandler, TxHandler,
TxPersistenceStore, TxPersistenceStore,
TxWorkspaceEvent, TxWorkspaceEvent,
@ -28,15 +35,8 @@ import core, {
concatLink, concatLink,
createClient, createClient,
fillConfiguration, fillConfiguration,
pluginFilterTx, platformNow,
type Class, pluginFilterTx
type ClientConnection,
type Doc,
type ModelFilter,
type PluginConfiguration,
type Ref,
type TxCUD,
platformNow
} 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'
@ -103,12 +103,12 @@ export default async () => {
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) {
opt?.onUpgrade?.()
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.ModelUpgrade) {
// TODO: Add a workspace here
opt?.onUpgrade?.(event)
}
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, {

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import type { Client, ClientConnectEvent, MeasureContext, TxPersistenceStore } from '@hcengineering/core' import type { Client, ClientConnectEvent, MeasureContext, TxPersistenceStore, TxWorkspaceEvent, WorkspaceUuid } from '@hcengineering/core'
import { type Plugin, type Resource, type Metadata, plugin } from '@hcengineering/platform' import { type Plugin, type Resource, type Metadata, plugin } from '@hcengineering/platform'
/** /**
@ -60,11 +60,11 @@ export interface ClientFactoryOptions {
useProtocolCompression?: boolean useProtocolCompression?: boolean
connectionTimeout?: number connectionTimeout?: number
onHello?: (serverVersion?: string) => boolean onHello?: (serverVersion?: string) => boolean
onUpgrade?: () => void onUpgrade?: (event?: TxWorkspaceEvent) => void
onUnauthorized?: () => void onUnauthorized?: () => void
onArchived?: () => void onArchived?: () => void
onMigration?: () => void onMigration?: () => void
onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void> onConnect?: (event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>
ctx?: MeasureContext ctx?: MeasureContext
onDialTimeout?: () => void | Promise<void> onDialTimeout?: () => void | Promise<void>

View File

@ -186,6 +186,8 @@ export async function connect (title: string): Promise<Client | undefined> {
const me: Account = { const me: Account = {
uuid: account, uuid: account,
role: workspaceLoginInfo.role, role: workspaceLoginInfo.role,
roles: {},
targetWorkspace: workspaceLoginInfo.workspace,
primarySocialId: '' as PersonId, primarySocialId: '' as PersonId,
socialIds: [], socialIds: [],
fullSocialIds: [] fullSocialIds: []

View File

@ -343,11 +343,16 @@ export async function connect (title: string): Promise<Client | undefined> {
// TODO: should we take the function from some resource like fetchWorkspace/selectWorkspace // TODO: should we take the function from some resource like fetchWorkspace/selectWorkspace
// to remove account client dependency? // to remove account client dependency?
const accountsUrl = getMetadata(login.metadata.AccountsUrl) const accountsUrl = getMetadata(login.metadata.AccountsUrl)
const socialIds: SocialId[] = await getAccountClient(accountsUrl, token).getSocialIds()
const me: Account = { const accountVal = await newClient.getConnection?.()?.getAccount()
const socialIds: SocialId[] = accountVal?.fullSocialIds ?? await getAccountClient(accountsUrl, token).getSocialIds()
const me: Account = (await newClient.getConnection?.()?.getAccount()) ?? {
uuid: account, uuid: account,
role: workspaceLoginInfo.role, role: workspaceLoginInfo.role,
roles: {},
targetWorkspace: workspaceLoginInfo.workspace,
primarySocialId: pickPrimarySocialId(socialIds)._id, primarySocialId: pickPrimarySocialId(socialIds)._id,
socialIds: socialIds.map((si) => si._id), socialIds: socialIds.map((si) => si._id),
fullSocialIds: socialIds fullSocialIds: socialIds

View File

@ -1,3 +1,11 @@
import { getClient as getAccountClientRaw, type AccountClient } from '@hcengineering/account-client'
import contact, {
AvatarType,
combineName,
type Person,
type SocialIdentity,
type SocialIdentityRef
} from '@hcengineering/contact'
import core, { import core, {
buildSocialIdString, buildSocialIdString,
generateId, generateId,
@ -5,26 +13,19 @@ import core, {
TxFactory, TxFactory,
TxProcessor, TxProcessor,
type AttachedData, type AttachedData,
type Data,
type Class, type Class,
type Data,
type Doc, type Doc,
type MeasureContext, type MeasureContext,
type Ref, type Ref,
type SearchOptions, type SearchOptions,
type SearchQuery, type SearchQuery,
type TxCUD type TxCUD,
type WorkspaceUuid
} from '@hcengineering/core' } from '@hcengineering/core'
import type { ClientSessionCtx, ConnectionSocket, Session, SessionManager } from '@hcengineering/server-core'
import { decodeToken } from '@hcengineering/server-token'
import { rpcJSONReplacer, type RateLimitInfo } from '@hcengineering/rpc' import { rpcJSONReplacer, type RateLimitInfo } from '@hcengineering/rpc'
import contact, { import type { ConnectionSocket } from '@hcengineering/server-core'
AvatarType, import { decodeToken } from '@hcengineering/server-token'
combineName,
type SocialIdentity,
type Person,
type SocialIdentityRef
} from '@hcengineering/contact'
import { type AccountClient, getClient as getAccountClientRaw } from '@hcengineering/account-client'
import { createHash } from 'crypto' import { createHash } from 'crypto'
import { type Express, type Response as ExpressResponse, type Request } from 'express' import { type Express, type Response as ExpressResponse, type Request } from 'express'
@ -35,6 +36,7 @@ import { gzip } from 'zlib'
import { retrieveJson } from './utils' import { retrieveJson } from './utils'
import { unknownError } from '@hcengineering/platform' import { unknownError } from '@hcengineering/platform'
import type { ClientSessionCtx, Session, SessionManager } from '@hcengineering/server'
interface RPCClientInfo { interface RPCClientInfo {
client: ConnectionSocket client: ConnectionSocket
session: Session session: Session
@ -137,7 +139,8 @@ export function registerRPC (app: Express, sessions: SessionManager, ctx: Measur
ctx: ClientSessionCtx, ctx: ClientSessionCtx,
session: Session, session: Session,
rateLimit: RateLimitInfo | undefined, rateLimit: RateLimitInfo | undefined,
token: string token: string,
workspaceId: WorkspaceUuid
) => Promise<void> ) => Promise<void>
): Promise<void> { ): Promise<void> {
try { try {
@ -151,34 +154,35 @@ export function registerRPC (app: Express, sessions: SessionManager, ctx: Measur
sendError(res, 401, { message: 'Missing Authorization header' }) sendError(res, 401, { message: 'Missing Authorization header' })
return return
} }
const workspaceId = decodeURIComponent(req.params.workspaceId) const workspaceId = decodeURIComponent(req.params.workspaceId) as WorkspaceUuid
token = token.split(' ')[1] token = token.split(' ')[1]
const decodedToken = decodeToken(token) const decodedToken = decodeToken(token)
if (workspaceId !== decodedToken.workspace) { // if (workspaceId !== decodedToken.workspace) {
sendError(res, 401, { message: 'Invalid workspace', workspace: decodedToken.workspace }) // sendError(res, 401, { message: 'Invalid workspace', workspace: decodedToken.workspace })
return // return
} // }
let transactorRpc = rpcSessions.get(token) let transactorRpc = rpcSessions.get(token)
if (transactorRpc === undefined) { if (transactorRpc === undefined) {
const cs: ConnectionSocket = createClosingSocket(token, rpcSessions) const cs: ConnectionSocket = createClosingSocket(token, rpcSessions)
const s = await sessions.addSession(ctx, cs, decodedToken, token, token) const session = await sessions.addSession(ctx, cs, decodedToken, token, token)
if (!('session' in s)) { if (!('session' in session)) {
sendError(res, 401, { sendError(res, 401, {
message: 'Failed to create session', message: 'Failed to create session',
mode: 'specialError' in s ? s.specialError ?? '' : 'upgrading' mode: 'specialError' in session ? session.specialError ?? '' : 'upgrading'
}) })
return return
} }
transactorRpc = { session: s.session, client: cs, workspaceId: s.workspaceId } transactorRpc = { session, client: cs, workspaceId }
rpcSessions.set(token, transactorRpc) rpcSessions.set(token, transactorRpc)
} }
const rpc = transactorRpc const rpc = transactorRpc
const rateLimit = await sessions.handleRPC(ctx, rpc.session, rpc.client, async (ctx, rateLimit) => { const rateLimit = await sessions.handleRPC(ctx, workspaceId,
await operation(ctx, rpc.session, rateLimit, token) rpc.session, rpc.client, async (ctx, rateLimit) => {
await operation(ctx, rpc.session, rateLimit, token, workspaceId)
}) })
if (rateLimit !== undefined) { if (rateLimit !== undefined) {
const { remaining, limit, reset, retryAfter } = rateLimit const { remaining, limit, reset, retryAfter } = rateLimit
@ -206,15 +210,15 @@ export function registerRPC (app: Express, sessions: SessionManager, ctx: Measur
} }
app.get('/api/v1/ping/:workspaceId', (req, res) => { app.get('/api/v1/ping/:workspaceId', (req, res) => {
void withSession(req, res, async (ctx, session, rateLimit) => { void withSession(req, res, async (ctx, session, rateLimit, token, workspaceId) => {
await session.ping(ctx) await session.ping(ctx)
await sendJson( await sendJson(
req, req,
res, res,
{ {
pong: true, pong: true,
lastTx: ctx.pipeline.context.lastTx, ...await sessions.getLastTxHash(workspaceId)
lastHash: ctx.pipeline.context.lastHash
}, },
rateLimitToHeaders(rateLimit) rateLimitToHeaders(rateLimit)
) )
@ -273,7 +277,9 @@ export function registerRPC (app: Express, sessions: SessionManager, ctx: Measur
core.class.Space, core.class.Space,
core.class.Tx core.class.Tx
] ]
const h = ctx.pipeline.context.hierarchy const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline) => {
const h = pipeline.context.hierarchy
const filtered = txes.filter( const filtered = txes.filter(
(it) => (it) =>
TxProcessor.isExtendsCUD(it._class) && TxProcessor.isExtendsCUD(it._class) &&
@ -283,6 +289,7 @@ export function registerRPC (app: Express, sessions: SessionManager, ctx: Measur
await sendJson(req, res, filtered, rateLimitToHeaders(rateLimit)) await sendJson(req, res, filtered, rateLimitToHeaders(rateLimit))
}) })
}) })
})
app.get('/api/v1/search-fulltext/:workspaceId', (req, res) => { app.get('/api/v1/search-fulltext/:workspaceId', (req, res) => {
void withSession(req, res, async (ctx, session, rateLimit) => { void withSession(req, res, async (ctx, session, rateLimit) => {

View File

@ -17,11 +17,10 @@
import { type BrandingMap, type MeasureContext, type Tx } from '@hcengineering/core' import { type BrandingMap, type MeasureContext, type Tx } from '@hcengineering/core'
import { buildStorageFromConfig } from '@hcengineering/server-storage' import { buildStorageFromConfig } from '@hcengineering/server-storage'
import { startSessionManager } from '@hcengineering/server' import { startSessionManager, type SessionManager } from '@hcengineering/server'
import { import {
type CommunicationApiFactory, type CommunicationApiFactory,
type PlatformQueue, type PlatformQueue,
type SessionManager,
type StorageConfiguration type StorageConfiguration
} from '@hcengineering/server-core' } from '@hcengineering/server-core'

View File

@ -28,7 +28,7 @@ import {
type WorkspaceIds, type WorkspaceIds,
type WorkspaceUuid type WorkspaceUuid
} from '@hcengineering/core' } from '@hcengineering/core'
import platform, { Severity, Status, UNAUTHORIZED, unknownStatus } from '@hcengineering/platform' import { UNAUTHORIZED } from '@hcengineering/platform'
import { RPCHandler, type Response } from '@hcengineering/rpc' import { RPCHandler, type Response } from '@hcengineering/rpc'
import { import {
doSessionOp, doSessionOp,
@ -38,6 +38,7 @@ import {
processRequest, processRequest,
wipeStatistics, wipeStatistics,
type BlobResponse, type BlobResponse,
type SessionManager,
type WebsocketData type WebsocketData
} from '@hcengineering/server' } from '@hcengineering/server'
import { import {
@ -45,7 +46,6 @@ import {
pingConst, pingConst,
pongConst, pongConst,
type ConnectionSocket, type ConnectionSocket,
type SessionManager,
type StorageAdapter type StorageAdapter
} from '@hcengineering/server-core' } from '@hcengineering/server-core'
import { decodeToken, type Token } from '@hcengineering/server-token' import { decodeToken, type Token } from '@hcengineering/server-token'
@ -261,7 +261,7 @@ export function startHttpServer (
} }
case 'force-close': { case 'force-close': {
const wsId = req.query.wsId as WorkspaceUuid const wsId = req.query.wsId as WorkspaceUuid
void sessions.forceClose(wsId ?? payload.workspace) void sessions.forceCloseWorkspace(ctx, wsId ?? payload.workspace)
res.writeHead(200) res.writeHead(200)
res.end() res.end()
return return
@ -454,69 +454,20 @@ export function startHttpServer (
} }
const cs: ConnectionSocket = createWebsocketClientSocket(ws, data) const cs: ConnectionSocket = createWebsocketClientSocket(ws, data)
const session = sessions.addSession(ctx, cs, token, rawToken, sessionId)
void session.catch(() => {
// Ignore err
ws.close()
})
const webSocketData: WebsocketData = { const webSocketData: WebsocketData = {
connectionSocket: cs, connectionSocket: cs,
payload: token, payload: token,
token: rawToken, token: rawToken,
session: sessions.addSession(ctx, cs, token, rawToken, sessionId), session,
url: '' url: ''
} }
if (webSocketData.session instanceof Promise) {
void webSocketData.session.then((s) => {
if ('error' in s) {
if (s.specialError === 'archived') {
void cs.send(
ctx,
{
id: -1,
error: new Status(Severity.ERROR, platform.status.WorkspaceArchived, {
workspaceUuid: token.workspace
}),
terminate: s.terminate
},
false,
false
)
} else if (s.specialError === 'migration') {
void cs.send(
ctx,
{
id: -1,
error: new Status(Severity.ERROR, platform.status.WorkspaceMigration, {
workspaceUuid: token.workspace
}),
terminate: s.terminate
},
false,
false
)
} else {
void cs.send(
ctx,
{ id: -1, error: unknownStatus(s.error.message ?? 'Unknown error'), terminate: s.terminate },
false,
false
)
}
// No connection to account service, retry from client.
setTimeout(() => {
cs.close()
}, 1000)
}
if ('upgrade' in s) {
void cs
.send(ctx, { id: -1, result: { state: 'upgrading', stats: (s as any).upgradeInfo } }, false, false)
.then(() => {
cs.close()
})
}
})
void webSocketData.session.catch((err) => {
ctx.error('unexpected error in websocket', { err })
})
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
ws.on('message', (msg: RawData) => { ws.on('message', (msg: RawData) => {
try { try {
@ -530,8 +481,7 @@ export function startHttpServer (
doSessionOp( doSessionOp(
webSocketData, webSocketData,
(s, buff) => { (s, buff) => {
s.context.measure('receive-data', buff?.length ?? 0) processRequest(ctx, s, cs, buff, sessions)
processRequest(s.session, cs, s.context, s.workspaceId, buff, sessions)
}, },
buff buff
) )
@ -543,32 +493,30 @@ export function startHttpServer (
} }
} }
}) })
// eslint-disable-next-line @typescript-eslint/no-misused-promises
ws.on('close', (code: number, reason: Buffer) => { const handleClose = (err: Error | null): void => {
doSessionOp( doSessionOp(
webSocketData, webSocketData,
(s) => { (s) => {
if (!(s.session.workspaceClosed ?? false)) { if (err !== null) {
// remove session after 1seconds, give a time to reconnect. ctx.error('error', { err, user: s.getUser() })
void sessions.close(ctx, cs, token.workspace)
} }
// remove session after 1seconds, give a time to reconnect.
void sessions.close(ctx, s).catch(err => {
ctx.error('failed to close session', { err })
})
}, },
Buffer.from('') Buffer.from('')
) )
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
ws.on('close', (code: number, reason: Buffer) => {
handleClose(null)
}) })
ws.on('error', (err) => { ws.on('error', (err) => {
doSessionOp( handleClose(err)
webSocketData,
(s) => {
ctx.error('error', { err, user: s.session.getUser() })
if (!(s.session.workspaceClosed ?? false)) {
// remove session after 1seconds, give a time to reconnect.
void sessions.close(ctx, cs, token.workspace)
}
},
Buffer.from('')
)
}) })
} }
wss.on('connection', handleConnection as any) wss.on('connection', handleConnection as any)

View File

@ -75,7 +75,8 @@ async function createMessages (
for (const data of result) { for (const data of result) {
void api.event( void api.event(
{ {
account: systemAccount // TODO: Fix me, Undetermined role is missing in communication API
account: systemAccount as any
}, },
{ {
type: MessageRequestEventType.CreateMessage, type: MessageRequestEventType.CreateMessage,

View File

@ -410,7 +410,10 @@ async function updateCollaborators (control: TriggerControl, ctx: TxCreateDoc<Ca
if (collaborators.length === 0) continue if (collaborators.length === 0) continue
void communicationApi.event( void communicationApi.event(
{ account: systemAccount }, {
// TODO: Fix me, Undetermined role is missing in communication API
account: systemAccount as any
},
{ {
type: NotificationRequestEventType.AddCollaborators, type: NotificationRequestEventType.AddCollaborators,
card: tx.objectId, card: tx.objectId,

View File

@ -14,19 +14,9 @@
// //
import { import {
type ServerApi as CommunicationApi, type ServerApi as CommunicationApi
type RequestEvent as CommunicationEvent,
type EventResult
} from '@hcengineering/communication-sdk-types' } from '@hcengineering/communication-sdk-types'
import { import {
type FindMessagesGroupsParams,
type FindMessagesParams,
type Message,
type MessagesGroup
} from '@hcengineering/communication-types'
import {
type Account,
type AccountUuid,
type Branding, type Branding,
type Class, type Class,
type Doc, type Doc,
@ -46,7 +36,6 @@ import {
type SearchQuery, type SearchQuery,
type SearchResult, type SearchResult,
type SessionData, type SessionData,
type SocialId,
type Space, type Space,
type Timestamp, type Timestamp,
type Tx, type Tx,
@ -57,14 +46,12 @@ import {
} from '@hcengineering/core' } from '@hcengineering/core'
import type { Asset, Resource } from '@hcengineering/platform' import type { Asset, Resource } from '@hcengineering/platform'
import type { LiveQuery } from '@hcengineering/query' import type { LiveQuery } from '@hcengineering/query'
import type { RateLimitInfo, ReqId, Request, Response } from '@hcengineering/rpc' import type { Request, Response } from '@hcengineering/rpc'
import type { Token } from '@hcengineering/server-token'
import { type Readable } from 'stream' import { type Readable } from 'stream'
import type { DbAdapter, DomainHelper } from './adapter' import type { DbAdapter, DomainHelper } from './adapter'
import type { StatisticsElement, WorkspaceStatistics } from './stats' import { type PlatformQueue, type PlatformQueueProducer, type QueueTopic } from './queue'
import { type StorageAdapter } from './storage' import { type StorageAdapter } from './storage'
import { type PlatformQueueProducer, type QueueTopic, type PlatformQueue } from './queue'
export interface ServerFindOptions<T extends Doc> extends FindOptions<T> { export interface ServerFindOptions<T extends Doc> extends FindOptions<T> {
domain?: Domain // Allow to find for Doc's in specified domain only. domain?: Domain // Allow to find for Doc's in specified domain only.
@ -540,99 +527,8 @@ export interface SessionRequest {
id: string id: string
params: any params: any
start: number start: number
}
export interface ClientSessionCtx { workspaceId?: WorkspaceUuid
ctx: MeasureContext
pipeline: Pipeline
communicationApi: CommunicationApi
socialStringsToUsers: Map<PersonId, AccountUuid>
requestId: ReqId | undefined
sendResponse: (id: ReqId | undefined, msg: any) => Promise<void>
sendPong: () => void
sendError: (id: ReqId | undefined, msg: any, error: any) => Promise<void>
}
/**
* @public
*/
export interface Session {
workspace: WorkspaceIds
createTime: number
// Session restore information
sessionId: string
sessionInstanceId?: string
workspaceClosed?: boolean
requests: Map<string, SessionRequest>
binaryMode: boolean
useCompression: boolean
total: StatisticsElement
current: StatisticsElement
mins5: StatisticsElement
lastRequest: number
lastPing: number
isUpgradeClient: () => boolean
getMode: () => string
broadcast: (ctx: MeasureContext, socket: ConnectionSocket, tx: Tx[]) => void
// Client methods
ping: (ctx: ClientSessionCtx) => Promise<void>
getUser: () => AccountUuid
getUserSocialIds: () => PersonId[]
getSocialIds: () => SocialId[]
loadModel: (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string) => Promise<void>
loadModelRaw: (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string) => Promise<LoadModelResponse | Tx[]>
getRawAccount: () => Account
findAll: <T extends Doc>(
ctx: ClientSessionCtx,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
) => Promise<void>
findAllRaw: <T extends Doc>(
ctx: ClientSessionCtx,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
) => Promise<FindResult<T>>
searchFulltext: (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions) => Promise<void>
searchFulltextRaw: (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions) => Promise<SearchResult>
tx: (ctx: ClientSessionCtx, tx: Tx) => Promise<void>
txRaw: (
ctx: ClientSessionCtx,
tx: Tx
) => Promise<{
result: TxResult
broadcastPromise: Promise<void>
asyncsPromise: Promise<void> | undefined
}>
loadChunk: (ctx: ClientSessionCtx, domain: Domain, idx?: number) => Promise<void>
getDomainHash: (ctx: ClientSessionCtx, domain: Domain) => Promise<void>
closeChunk: (ctx: ClientSessionCtx, idx: number) => Promise<void>
loadDocs: (ctx: ClientSessionCtx, domain: Domain, docs: Ref<Doc>[]) => Promise<void>
upload: (ctx: ClientSessionCtx, domain: Domain, docs: Doc[]) => Promise<void>
clean: (ctx: ClientSessionCtx, domain: Domain, docs: Ref<Doc>[]) => Promise<void>
includeSessionContext: (ctx: ClientSessionCtx) => void
eventRaw: (ctx: ClientSessionCtx, event: CommunicationEvent) => Promise<EventResult>
findMessagesRaw: (ctx: ClientSessionCtx, params: FindMessagesParams) => Promise<Message[]>
findMessagesGroupsRaw: (ctx: ClientSessionCtx, params: FindMessagesGroupsParams) => Promise<MessagesGroup[]>
} }
/** /**
@ -666,76 +562,5 @@ export function disableLogging (): void {
LOGGING_ENABLED = false LOGGING_ENABLED = false
} }
export interface AddSessionActive {
session: Session
context: MeasureContext
workspaceId: WorkspaceUuid
}
export type GetWorkspaceResponse =
| { upgrade: true, progress?: number }
| { error: any, terminate?: boolean, specialError?: 'archived' | 'migration' }
export type AddSessionResponse = AddSessionActive | GetWorkspaceResponse
/**
* @public
*/
export interface SessionManager {
// workspaces: Map<WorkspaceUuid, Workspace>
sessions: Map<string, { session: Session, socket: ConnectionSocket }>
addSession: (
ctx: MeasureContext,
ws: ConnectionSocket,
token: Token,
rawToken: string,
sessionId: string | undefined
) => Promise<AddSessionResponse>
broadcastAll: (workspace: WorkspaceUuid, tx: Tx[], targets?: string[]) => void
close: (ctx: MeasureContext, ws: ConnectionSocket, workspaceId: WorkspaceUuid) => Promise<void>
forceClose: (wsId: WorkspaceUuid, ignoreSocket?: ConnectionSocket) => Promise<void>
closeWorkspaces: (ctx: MeasureContext) => Promise<void>
scheduleMaintenance: (timeMinutes: number) => void
profiling?: {
start: () => void
stop: () => Promise<string | undefined>
}
handleRequest: <S extends Session>(
requestCtx: MeasureContext,
service: S,
ws: ConnectionSocket,
request: Request<any>,
workspace: WorkspaceUuid
) => Promise<void>
handleRPC: <S extends Session>(
requestCtx: MeasureContext,
service: S,
ws: ConnectionSocket,
operation: (ctx: ClientSessionCtx, rateLimit?: RateLimitInfo) => Promise<void>
) => Promise<RateLimitInfo | undefined>
createOpContext: (
ctx: MeasureContext,
sendCtx: MeasureContext,
pipeline: Pipeline,
communicationApi: CommunicationApi,
requestId: Request<any>['id'],
service: Session,
ws: ConnectionSocket,
rateLimit?: RateLimitInfo
) => ClientSessionCtx
getStatistics: () => WorkspaceStatistics[]
}
export const pingConst = 'ping' export const pingConst = 'ping'
export const pongConst = 'pong!' export const pongConst = 'pong!'

View File

@ -27,7 +27,8 @@ import core, {
type Tx, type Tx,
type TxResult, type TxResult,
type TxWorkspaceEvent, type TxWorkspaceEvent,
type WorkspaceIds type WorkspaceIds,
type WorkspaceUuid
} from '@hcengineering/core' } from '@hcengineering/core'
import { PlatformError, unknownError } from '@hcengineering/platform' import { PlatformError, unknownError } from '@hcengineering/platform'
import { createHash, type Hash } from 'crypto' import { createHash, type Hash } from 'crypto'
@ -284,19 +285,23 @@ export function wrapAdapterToClient (ctx: MeasureContext, storageAdapter: DbAdap
class TestClientConnection implements ClientConnection { class TestClientConnection implements ClientConnection {
isConnected = (): boolean => true isConnected = (): boolean => true
handler?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void> handler?: (event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>
set onConnect ( set onConnect (
handler: ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>) | undefined handler: ((event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>) | undefined
) { ) {
this.handler = handler this.handler = handler
void this.handler?.(ClientConnectEvent.Connected, '', {}) void this.handler?.(ClientConnectEvent.Connected, {}, {})
} }
get onConnect (): ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>) | undefined { get onConnect (): ((event: ClientConnectEvent, lastTx: Record<WorkspaceUuid, string | undefined> | undefined, data: any) => Promise<void>) | undefined {
return this.handler return this.handler
} }
getAccount (): Promise<Account> {
throw new Error('Method not implemented.')
}
pushHandler (): void {} pushHandler (): void {}
async findAll<T extends Doc>( async findAll<T extends Doc>(

View File

@ -24,7 +24,6 @@ export * from './domainFind'
export * from './domainTx' export * from './domainTx'
export * from './fulltext' export * from './fulltext'
export * from './liveQuery' export * from './liveQuery'
export * from './lookup'
export * from './lowLevel' export * from './lowLevel'
export * from './model' export * from './model'
export * from './modified' export * from './modified'

View File

@ -1,118 +0,0 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
Class,
Doc,
DocumentQuery,
FindOptions,
FindResult,
MeasureContext,
Ref,
clone,
toFindResult
} from '@hcengineering/core'
import { BaseMiddleware, Middleware, type PipelineContext } from '@hcengineering/server-core'
/**
* @public
*/
export class LookupMiddleware extends BaseMiddleware implements Middleware {
private constructor (context: PipelineContext, next?: Middleware) {
super(context, next)
}
static async create (
ctx: MeasureContext,
context: PipelineContext,
next: Middleware | undefined
): Promise<LookupMiddleware> {
return new LookupMiddleware(context, next)
}
override async findAll<T extends Doc>(
ctx: MeasureContext,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<FindResult<T>> {
const result = await this.provideFindAll(ctx, _class, query, options)
// Fill lookup map to make more compact representation
if (options?.lookup !== undefined) {
const newResult: T[] = []
let counter = 0
const idClassMap: Record<string, { id: number, doc: Doc, count: number }> = {}
function mapDoc (doc: Doc): number {
const key = doc._class + '@' + doc._id
let docRef = idClassMap[key]
if (docRef === undefined) {
docRef = { id: ++counter, doc, count: -1 }
idClassMap[key] = docRef
}
docRef.count++
return docRef.id
}
for (const d of result) {
const newDoc: any = { ...d }
if (d.$lookup !== undefined) {
newDoc.$lookup = clone(d.$lookup)
newResult.push(newDoc)
for (const [k, v] of Object.entries(d.$lookup)) {
if (!Array.isArray(v)) {
newDoc.$lookup[k] = v != null ? mapDoc(v) : v
} else {
newDoc.$lookup[k] = v.map((it) => (it != null ? mapDoc(it) : it))
}
}
} else {
newResult.push(newDoc)
}
}
const lookupMap = Object.fromEntries(Array.from(Object.values(idClassMap)).map((it) => [it.id, it.doc]))
return this.cleanQuery<T>(toFindResult(newResult, result.total, lookupMap), query, lookupMap)
}
// We need to get rid of simple query parameters matched in documents
return this.cleanQuery<T>(result, query)
}
private cleanQuery<T extends Doc>(
result: FindResult<T>,
query: DocumentQuery<T>,
lookupMap?: Record<string, Doc>
): FindResult<T> {
const newResult: T[] = []
for (const doc of result) {
let _doc = doc
let cloned = false
for (const [k, v] of Object.entries(query)) {
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
if ((_doc as any)[k] === v) {
if (!cloned) {
_doc = { ...doc } as any
cloned = true
}
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (_doc as any)[k]
}
}
}
newResult.push(_doc)
}
return toFindResult(newResult, result.total, lookupMap)
}
}

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import type { Account } from '@hcengineering/core' import type { Account, WorkspaceUuid } from '@hcengineering/core'
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
import { Packr } from 'msgpackr' import { Packr } from 'msgpackr'
@ -47,8 +47,8 @@ export interface HelloResponse extends Response<any> {
binary: boolean binary: boolean
reconnect?: boolean reconnect?: boolean
serverVersion: string serverVersion: string
lastTx?: string lastTx?: Record<WorkspaceUuid, string | undefined>
lastHash?: string // Last model hash lastHash?: Record<WorkspaceUuid, string | undefined> // Last model hash
account: Account account: Account
useCompression?: boolean useCompression?: boolean
} }

View File

@ -25,7 +25,6 @@ import {
FullTextMiddleware, FullTextMiddleware,
IdentityMiddleware, IdentityMiddleware,
LiveQueryMiddleware, LiveQueryMiddleware,
LookupMiddleware,
LowLevelMiddleware, LowLevelMiddleware,
MarkDerivedEntryMiddleware, MarkDerivedEntryMiddleware,
ModelMiddleware, ModelMiddleware,
@ -116,7 +115,6 @@ export function createServerPipeline (
const conf = getConfig(metrics, dbUrl, wsMetrics, opt, extensions) const conf = getConfig(metrics, dbUrl, wsMetrics, opt, extensions)
const middlewares: MiddlewareCreator[] = [ const middlewares: MiddlewareCreator[] = [
LookupMiddleware.create,
IdentityMiddleware.create, IdentityMiddleware.create,
ModifiedMiddleware.create, ModifiedMiddleware.create,
PluginConfigurationMiddleware.create, PluginConfigurationMiddleware.create,

View File

@ -31,6 +31,7 @@ import {
import { import {
AccountUuid, AccountUuid,
generateId, generateId,
toFindResult,
TxProcessor, TxProcessor,
type Account, type Account,
type Class, type Class,
@ -60,14 +61,18 @@ import {
BackupClientOps, BackupClientOps,
createBroadcastEvent, createBroadcastEvent,
SessionDataImpl, SessionDataImpl,
type ClientSessionCtx,
type ConnectionSocket, type ConnectionSocket,
type Pipeline, type Pipeline,
type Session,
type SessionRequest, type SessionRequest,
type StatisticsElement type StatisticsElement
} from '@hcengineering/server-core' } from '@hcengineering/server-core'
import { type Token } from '@hcengineering/server-token' import { type Token } from '@hcengineering/server-token'
import { mapLookup } from './lookup'
import {
type ClientSessionCtx,
type Session
} from './types'
const useReserveContext = (process.env.USE_RESERVE_CTX ?? 'true') === 'true' const useReserveContext = (process.env.USE_RESERVE_CTX ?? 'true') === 'true'
@ -92,10 +97,13 @@ export class ClientSession implements Session {
ops: BackupClientOps | undefined ops: BackupClientOps | undefined
opsPipeline: Pipeline | undefined opsPipeline: Pipeline | undefined
isAdmin: boolean isAdmin: boolean
workspaceClosed = false
constructor ( constructor (
readonly ctx: MeasureContext,
protected readonly token: Token, protected readonly token: Token,
readonly workspace: WorkspaceIds, readonly socket: ConnectionSocket,
readonly workspaces: { wsIds: WorkspaceIds, enabled: boolean }[],
readonly account: Account, readonly account: Account,
readonly info: LoginInfoWithWorkspaces, readonly info: LoginInfoWithWorkspaces,
readonly allowUpload: boolean readonly allowUpload: boolean
@ -133,42 +141,50 @@ export class ClientSession implements Session {
} }
async loadModel (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string): Promise<void> { async loadModel (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string): Promise<void> {
// TODO: Model is from first workspace for now.
const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline, communicationApi) => {
try { try {
this.includeSessionContext(ctx) this.includeSessionContext(ctx, pipeline)
const result = await ctx.ctx.with('load-model', {}, () => ctx.pipeline.loadModel(ctx.ctx, lastModelTx, hash)) const result = await ctx.ctx.with('load-model', {}, () => pipeline.loadModel(ctx.ctx, lastModelTx, hash))
await ctx.sendResponse(ctx.requestId, result) await ctx.sendResponse(ctx.requestId, result)
} catch (err) { } catch (err) {
await ctx.sendError(ctx.requestId, 'Failed to loadModel', unknownError(err)) await ctx.sendError(ctx.requestId, 'Failed to loadModel', unknownError(err))
ctx.ctx.error('failed to loadModel', { err }) ctx.ctx.error('failed to loadModel', { err })
} }
})
} }
async loadModelRaw (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string): Promise<LoadModelResponse | Tx[]> { async loadModelRaw (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string): Promise<LoadModelResponse | Tx[]> {
this.includeSessionContext(ctx) // TODO: Model is from first workspace for now.
return await ctx.ctx.with('load-model', {}, (_ctx) => ctx.pipeline.loadModel(_ctx, lastModelTx, hash)) const workspace = ctx.workspaces[0]
return await workspace.with(async (pipeline, communicationApi) => {
this.includeSessionContext(ctx, pipeline)
return await ctx.ctx.with('load-model', {}, (_ctx) => pipeline.loadModel(_ctx, lastModelTx, hash))
})
} }
includeSessionContext (ctx: ClientSessionCtx): void { includeSessionContext (ctx: ClientSessionCtx, pipeline: Pipeline): void {
const dataId = this.workspace.dataId ?? (this.workspace.uuid as unknown as WorkspaceDataId) const dataId = pipeline.context.workspace.dataId ?? (pipeline.context.workspace.uuid as unknown as WorkspaceDataId)
const contextData = new SessionDataImpl( const contextData = new SessionDataImpl(
this.account, this.account,
this.sessionId, this.sessionId,
this.isAdmin, this.isAdmin,
undefined, undefined,
{ {
...this.workspace, ...pipeline.context.workspace,
dataId dataId
}, },
false, false,
undefined, undefined,
undefined, undefined,
ctx.pipeline.context.modelDb, pipeline.context.modelDb,
ctx.socialStringsToUsers ctx.socialStringsToUsers
) )
ctx.ctx.contextData = contextData ctx.ctx.contextData = contextData
} }
findAllRaw<T extends Doc>( async findAllRaw<T extends Doc>(
ctx: ClientSessionCtx, ctx: ClientSessionCtx,
_class: Ref<Class<T>>, _class: Ref<Class<T>>,
query: DocumentQuery<T>, query: DocumentQuery<T>,
@ -177,8 +193,23 @@ export class ClientSession implements Session {
this.lastRequest = Date.now() this.lastRequest = Date.now()
this.total.find++ this.total.find++
this.current.find++ this.current.find++
this.includeSessionContext(ctx)
return ctx.pipeline.findAll(ctx.ctx, _class, query, options) const result: FindResult<T> = toFindResult([], -1)
for (const workspace of ctx.workspaces) {
await workspace.with(async (pipeline, communicationApi) => {
this.includeSessionContext(ctx, pipeline)
const part = await pipeline.findAll(ctx.ctx, _class, query, options)
result.push(...part)
if (part.total !== -1) {
if (result.total === -1) {
result.total = 0
}
result.total += part.total
}
})
}
return result
} }
async findAll<T extends Doc>( async findAll<T extends Doc>(
@ -188,7 +219,8 @@ export class ClientSession implements Session {
options?: FindOptions<T> options?: FindOptions<T>
): Promise<void> { ): Promise<void> {
try { try {
await ctx.sendResponse(ctx.requestId, await this.findAllRaw(ctx, _class, query, options)) const result = await this.findAllRaw(ctx, _class, query, options)
await ctx.sendResponse(ctx.requestId, mapLookup<T>(query, result, options))
} catch (err) { } catch (err) {
await ctx.sendError(ctx.requestId, 'Failed to findAll', unknownError(err)) await ctx.sendError(ctx.requestId, 'Failed to findAll', unknownError(err))
ctx.ctx.error('failed to findAll', { err }) ctx.ctx.error('failed to findAll', { err })
@ -198,8 +230,11 @@ export class ClientSession implements Session {
async searchFulltext (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions): Promise<void> { async searchFulltext (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions): Promise<void> {
try { try {
this.lastRequest = Date.now() this.lastRequest = Date.now()
this.includeSessionContext(ctx) const workspace = ctx.workspaces[0]
await ctx.sendResponse(ctx.requestId, await ctx.pipeline.searchFulltext(ctx.ctx, query, options)) await workspace.with(async (pipeline, communicationApi) => {
this.includeSessionContext(ctx, pipeline)
await ctx.sendResponse(ctx.requestId, await pipeline.searchFulltext(ctx.ctx, query, options))
})
} catch (err) { } catch (err) {
await ctx.sendError(ctx.requestId, 'Failed to searchFulltext', unknownError(err)) await ctx.sendError(ctx.requestId, 'Failed to searchFulltext', unknownError(err))
ctx.ctx.error('failed to searchFulltext', { err }) ctx.ctx.error('failed to searchFulltext', { err })
@ -208,8 +243,11 @@ export class ClientSession implements Session {
async searchFulltextRaw (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions): Promise<SearchResult> { async searchFulltextRaw (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
this.lastRequest = Date.now() this.lastRequest = Date.now()
this.includeSessionContext(ctx) const workspace = ctx.workspaces[0]
return await ctx.pipeline.searchFulltext(ctx.ctx, query, options) return await workspace.with(async (pipeline, communicationApi) => {
this.includeSessionContext(ctx, pipeline)
return await pipeline.searchFulltext(ctx.ctx, query, options)
})
} }
async txRaw ( async txRaw (
@ -223,14 +261,16 @@ export class ClientSession implements Session {
this.lastRequest = Date.now() this.lastRequest = Date.now()
this.total.tx++ this.total.tx++
this.current.tx++ this.current.tx++
this.includeSessionContext(ctx) const workspace = ctx.workspaces[0]
return await workspace.with(async (pipeline, communicationApi) => {
this.includeSessionContext(ctx, pipeline)
let cid = 'client_' + generateId() let cid = 'client_' + generateId()
ctx.ctx.id = cid ctx.ctx.id = cid
let onEnd = useReserveContext ? ctx.pipeline.context.adapterManager?.reserveContext?.(cid) : undefined let onEnd = useReserveContext ? pipeline.context.adapterManager?.reserveContext?.(cid) : undefined
let result: TxResult let result: TxResult
try { try {
result = await ctx.pipeline.tx(ctx.ctx, [tx]) result = await pipeline.tx(ctx.ctx, [tx])
} finally { } finally {
onEnd?.() onEnd?.()
} }
@ -238,7 +278,7 @@ export class ClientSession implements Session {
await ctx.sendResponse(ctx.requestId, result) await ctx.sendResponse(ctx.requestId, result)
// We need to broadcast all collected transactions // We need to broadcast all collected transactions
const broadcastPromise = ctx.pipeline.handleBroadcast(ctx.ctx) const broadcastPromise = pipeline.handleBroadcast(ctx.ctx)
// ok we could perform async requests if any // ok we could perform async requests if any
const asyncs = (ctx.ctx.contextData as SessionData).asyncRequests ?? [] const asyncs = (ctx.ctx.contextData as SessionData).asyncRequests ?? []
@ -246,7 +286,7 @@ export class ClientSession implements Session {
if (asyncs.length > 0) { if (asyncs.length > 0) {
cid = 'client_async_' + generateId() cid = 'client_async_' + generateId()
ctx.ctx.id = cid ctx.ctx.id = cid
onEnd = useReserveContext ? ctx.pipeline.context.adapterManager?.reserveContext?.(cid) : undefined onEnd = useReserveContext ? pipeline.context.adapterManager?.reserveContext?.(cid) : undefined
const handleAyncs = async (): Promise<void> => { const handleAyncs = async (): Promise<void> => {
try { try {
for (const r of (ctx.ctx.contextData as SessionData).asyncRequests ?? []) { for (const r of (ctx.ctx.contextData as SessionData).asyncRequests ?? []) {
@ -260,6 +300,7 @@ export class ClientSession implements Session {
} }
return { result, broadcastPromise, asyncsPromise } return { result, broadcastPromise, asyncsPromise }
})
} }
async tx (ctx: ClientSessionCtx, tx: Tx): Promise<void> { async tx (ctx: ClientSessionCtx, tx: Tx): Promise<void> {
@ -315,8 +356,11 @@ export class ClientSession implements Session {
async loadChunk (ctx: ClientSessionCtx, domain: Domain, idx?: number): Promise<void> { async loadChunk (ctx: ClientSessionCtx, domain: Domain, idx?: number): Promise<void> {
this.lastRequest = Date.now() this.lastRequest = Date.now()
try { try {
const result = await this.getOps(ctx.pipeline).loadChunk(ctx.ctx, domain, idx) const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline, communicationApi) => {
const result = await this.getOps(pipeline).loadChunk(ctx.ctx, domain, idx)
await ctx.sendResponse(ctx.requestId, result) await ctx.sendResponse(ctx.requestId, result)
})
} catch (err: any) { } catch (err: any) {
await ctx.sendError(ctx.requestId, 'Failed to upload', unknownError(err)) await ctx.sendError(ctx.requestId, 'Failed to upload', unknownError(err))
ctx.ctx.error('failed to loadChunk', { domain, err }) ctx.ctx.error('failed to loadChunk', { domain, err })
@ -326,8 +370,11 @@ export class ClientSession implements Session {
async getDomainHash (ctx: ClientSessionCtx, domain: Domain): Promise<void> { async getDomainHash (ctx: ClientSessionCtx, domain: Domain): Promise<void> {
this.lastRequest = Date.now() this.lastRequest = Date.now()
try { try {
const result = await this.getOps(ctx.pipeline).getDomainHash(ctx.ctx, domain) const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline, communicationApi) => {
const result = await this.getOps(pipeline).getDomainHash(ctx.ctx, domain)
await ctx.sendResponse(ctx.requestId, result) await ctx.sendResponse(ctx.requestId, result)
})
} catch (err: any) { } catch (err: any) {
await ctx.sendError(ctx.requestId, 'Failed to upload', unknownError(err)) await ctx.sendError(ctx.requestId, 'Failed to upload', unknownError(err))
ctx.ctx.error('failed to getDomainHash', { domain, err }) ctx.ctx.error('failed to getDomainHash', { domain, err })
@ -337,8 +384,11 @@ export class ClientSession implements Session {
async closeChunk (ctx: ClientSessionCtx, idx: number): Promise<void> { async closeChunk (ctx: ClientSessionCtx, idx: number): Promise<void> {
try { try {
this.lastRequest = Date.now() this.lastRequest = Date.now()
await this.getOps(ctx.pipeline).closeChunk(ctx.ctx, idx) const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline, communicationApi) => {
await this.getOps(pipeline).closeChunk(ctx.ctx, idx)
await ctx.sendResponse(ctx.requestId, {}) await ctx.sendResponse(ctx.requestId, {})
})
} catch (err: any) { } catch (err: any) {
await ctx.sendError(ctx.requestId, 'Failed to closeChunk', unknownError(err)) await ctx.sendError(ctx.requestId, 'Failed to closeChunk', unknownError(err))
ctx.ctx.error('failed to closeChunk', { err }) ctx.ctx.error('failed to closeChunk', { err })
@ -348,8 +398,11 @@ export class ClientSession implements Session {
async loadDocs (ctx: ClientSessionCtx, domain: Domain, docs: Ref<Doc>[]): Promise<void> { async loadDocs (ctx: ClientSessionCtx, domain: Domain, docs: Ref<Doc>[]): Promise<void> {
this.lastRequest = Date.now() this.lastRequest = Date.now()
try { try {
const result = await this.getOps(ctx.pipeline).loadDocs(ctx.ctx, domain, docs) const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline, communicationApi) => {
const result = await this.getOps(pipeline).loadDocs(ctx.ctx, domain, docs)
await ctx.sendResponse(ctx.requestId, result) await ctx.sendResponse(ctx.requestId, result)
})
} catch (err: any) { } catch (err: any) {
await ctx.sendError(ctx.requestId, 'Failed to loadDocs', unknownError(err)) await ctx.sendError(ctx.requestId, 'Failed to loadDocs', unknownError(err))
ctx.ctx.error('failed to loadDocs', { domain, err }) ctx.ctx.error('failed to loadDocs', { domain, err })
@ -362,7 +415,10 @@ export class ClientSession implements Session {
} }
this.lastRequest = Date.now() this.lastRequest = Date.now()
try { try {
await this.getOps(ctx.pipeline).upload(ctx.ctx, domain, docs) const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline, communicationApi) => {
await this.getOps(pipeline).upload(ctx.ctx, domain, docs)
})
} catch (err: any) { } catch (err: any) {
await ctx.sendError(ctx.requestId, 'Failed to upload', unknownError(err)) await ctx.sendError(ctx.requestId, 'Failed to upload', unknownError(err))
ctx.ctx.error('failed to loadDocs', { domain, err }) ctx.ctx.error('failed to loadDocs', { domain, err })
@ -377,7 +433,10 @@ export class ClientSession implements Session {
} }
this.lastRequest = Date.now() this.lastRequest = Date.now()
try { try {
await this.getOps(ctx.pipeline).clean(ctx.ctx, domain, docs) const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline, communicationApi) => {
await this.getOps(pipeline).clean(ctx.ctx, domain, docs)
})
} catch (err: any) { } catch (err: any) {
await ctx.sendError(ctx.requestId, 'Failed to clean', unknownError(err)) await ctx.sendError(ctx.requestId, 'Failed to clean', unknownError(err))
ctx.ctx.error('failed to clean', { domain, err }) ctx.ctx.error('failed to clean', { domain, err })
@ -388,7 +447,10 @@ export class ClientSession implements Session {
async eventRaw (ctx: ClientSessionCtx, event: CommunicationEvent): Promise<EventResult> { async eventRaw (ctx: ClientSessionCtx, event: CommunicationEvent): Promise<EventResult> {
this.lastRequest = Date.now() this.lastRequest = Date.now()
return await ctx.communicationApi.event(this.getCommunicationCtx(), event) const workspace = ctx.workspaces[0]
return await workspace.with(async (pipeline, communicationApi) => {
return await communicationApi.event(this.getCommunicationCtx(workspace.wsId), event)
})
} }
async event (ctx: ClientSessionCtx, event: CommunicationEvent): Promise<void> { async event (ctx: ClientSessionCtx, event: CommunicationEvent): Promise<void> {
@ -398,7 +460,10 @@ export class ClientSession implements Session {
async findMessagesRaw (ctx: ClientSessionCtx, params: FindMessagesParams, queryId?: number): Promise<Message[]> { async findMessagesRaw (ctx: ClientSessionCtx, params: FindMessagesParams, queryId?: number): Promise<Message[]> {
this.lastRequest = Date.now() this.lastRequest = Date.now()
return await ctx.communicationApi.findMessages(this.getCommunicationCtx(), params, queryId) const workspace = ctx.workspaces[0]
return await workspace.with(async (pipeline, communicationApi) => {
return await communicationApi.findMessages(this.getCommunicationCtx(workspace.wsId), params, queryId)
})
} }
async findMessages (ctx: ClientSessionCtx, params: FindMessagesParams, queryId?: number): Promise<void> { async findMessages (ctx: ClientSessionCtx, params: FindMessagesParams, queryId?: number): Promise<void> {
@ -408,7 +473,10 @@ export class ClientSession implements Session {
async findMessagesGroupsRaw (ctx: ClientSessionCtx, params: FindMessagesGroupsParams): Promise<MessagesGroup[]> { async findMessagesGroupsRaw (ctx: ClientSessionCtx, params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
this.lastRequest = Date.now() this.lastRequest = Date.now()
return await ctx.communicationApi.findMessagesGroups(this.getCommunicationCtx(), params) const workspace = ctx.workspaces[0]
return await workspace.with(async (pipeline, communicationApi) => {
return await communicationApi.findMessagesGroups(this.getCommunicationCtx(workspace.wsId), params)
})
} }
async findMessagesGroups (ctx: ClientSessionCtx, params: FindMessagesGroupsParams): Promise<void> { async findMessagesGroups (ctx: ClientSessionCtx, params: FindMessagesGroupsParams): Promise<void> {
@ -417,8 +485,11 @@ export class ClientSession implements Session {
} }
async findNotifications (ctx: ClientSessionCtx, params: FindNotificationsParams): Promise<void> { async findNotifications (ctx: ClientSessionCtx, params: FindNotificationsParams): Promise<void> {
const result = await ctx.communicationApi.findNotifications(this.getCommunicationCtx(), params) const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline, communicationApi) => {
const result = await communicationApi.findNotifications(this.getCommunicationCtx(workspace.wsId), params)
await ctx.sendResponse(ctx.requestId, result) await ctx.sendResponse(ctx.requestId, result)
})
} }
async findNotificationContexts ( async findNotificationContexts (
@ -426,25 +497,42 @@ export class ClientSession implements Session {
params: FindNotificationContextParams, params: FindNotificationContextParams,
queryId?: number queryId?: number
): Promise<void> { ): Promise<void> {
const result = await ctx.communicationApi.findNotificationContexts(this.getCommunicationCtx(), params, queryId) const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline, communicationApi) => {
const result = await communicationApi.findNotificationContexts(
this.getCommunicationCtx(workspace.wsId),
params,
queryId
)
await ctx.sendResponse(ctx.requestId, result) await ctx.sendResponse(ctx.requestId, result)
})
} }
async findLabels (ctx: ClientSessionCtx, params: FindLabelsParams): Promise<void> { async findLabels (ctx: ClientSessionCtx, params: FindLabelsParams): Promise<void> {
const result = await ctx.communicationApi.findLabels(this.getCommunicationCtx(), params) const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline, communicationApi) => {
const result = await communicationApi.findLabels(this.getCommunicationCtx(workspace.wsId), params)
await ctx.sendResponse(ctx.requestId, result) await ctx.sendResponse(ctx.requestId, result)
})
} }
async unsubscribeQuery (ctx: ClientSessionCtx, id: number): Promise<void> { async unsubscribeQuery (ctx: ClientSessionCtx, id: number): Promise<void> {
this.lastRequest = Date.now() this.lastRequest = Date.now()
await ctx.communicationApi.unsubscribeQuery(this.getCommunicationCtx(), id) const workspace = ctx.workspaces[0]
await workspace.with(async (pipeline, communicationApi) => {
await communicationApi.unsubscribeQuery(this.getCommunicationCtx(workspace.wsId), id)
await ctx.sendResponse(ctx.requestId, {}) await ctx.sendResponse(ctx.requestId, {})
})
} }
private getCommunicationCtx (): CommunicationSession { private getCommunicationCtx (workspaceId: WorkspaceIds): CommunicationSession {
return { return {
sessionId: this.sessionId, sessionId: this.sessionId,
account: this.account account: {
...this.account,
// TODO: Fix me, Undetermined role is missing in communication API
role: (this.account.roles[workspaceId.uuid] ?? this.account.role) as any
}
} }
} }
} }

View File

@ -20,3 +20,4 @@ export * from './sessionManager'
export * from './starter' export * from './starter'
export * from './stats' export * from './stats'
export * from './utils' export * from './utils'
export * from './types'

View File

@ -0,0 +1,98 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
Doc,
DocumentQuery,
FindOptions,
FindResult,
clone,
toFindResult
} from '@hcengineering/core'
/**
* @public
*/
export function mapLookup<T extends Doc> (
query: DocumentQuery<T>,
result: FindResult<T>,
options?: FindOptions<T>
): FindResult<T> {
// Fill lookup map to make more compact representation
if (options?.lookup !== undefined) {
const newResult: T[] = []
let counter = 0
const idClassMap: Record<string, { id: number, doc: Doc, count: number }> = {}
function mapDoc (doc: Doc): number {
const key = doc._class + '@' + doc._id
let docRef = idClassMap[key]
if (docRef === undefined) {
docRef = { id: ++counter, doc, count: -1 }
idClassMap[key] = docRef
}
docRef.count++
return docRef.id
}
for (const d of result) {
const newDoc: any = { ...d }
if (d.$lookup !== undefined) {
newDoc.$lookup = clone(d.$lookup)
newResult.push(newDoc)
for (const [k, v] of Object.entries(d.$lookup)) {
if (!Array.isArray(v)) {
newDoc.$lookup[k] = v != null ? mapDoc(v) : v
} else {
newDoc.$lookup[k] = v.map((it) => (it != null ? mapDoc(it) : it))
}
}
} else {
newResult.push(newDoc)
}
}
const lookupMap = Object.fromEntries(Array.from(Object.values(idClassMap)).map((it) => [it.id, it.doc]))
return cleanQuery<T>(toFindResult(newResult, result.total, lookupMap), query, lookupMap)
}
// We need to get rid of simple query parameters matched in documents
return cleanQuery<T>(result, query)
}
function cleanQuery<T extends Doc> (
result: FindResult<T>,
query: DocumentQuery<T>,
lookupMap?: Record<string, Doc>
): FindResult<T> {
const newResult: T[] = []
for (const doc of result) {
let _doc = doc
let cloned = false
for (const [k, v] of Object.entries(query)) {
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
if ((_doc as any)[k] === v) {
if (!cloned) {
_doc = { ...doc } as any
cloned = true
}
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (_doc as any)[k]
}
}
}
newResult.push(_doc)
}
return toFindResult(newResult, result.total, lookupMap)
}

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,8 @@ import {
metricsAggregate, metricsAggregate,
type MetricsData type MetricsData
} from '@hcengineering/core' } from '@hcengineering/core'
import { type SessionManager } from '@hcengineering/server-core'
import os from 'node:os' import os from 'node:os'
import { type SessionManager } from './types'
/** /**
* @public * @public

233
server/server/src/types.ts Normal file
View File

@ -0,0 +1,233 @@
//
// Copyright © 2022, 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type RequestEvent as CommunicationEvent,
type EventResult
} from '@hcengineering/communication-sdk-types'
import {
type FindMessagesGroupsParams,
type FindMessagesParams,
type Message,
type MessagesGroup
} from '@hcengineering/communication-types'
import {
type Account,
type AccountUuid,
type Class,
type Doc,
type DocumentQuery,
type Domain,
type FindOptions,
type FindResult,
type LoadModelResponse,
type MeasureContext,
type PersonId,
type Ref,
type SearchOptions,
type SearchQuery,
type SearchResult,
type SocialId,
type Timestamp,
type Tx,
type TxResult,
type WorkspaceIds,
type WorkspaceUuid
} from '@hcengineering/core'
import type { RateLimitInfo, ReqId, Request, Response } from '@hcengineering/rpc'
import type { Token } from '@hcengineering/server-token'
import type { StatisticsElement, WorkspaceStatistics } from '@hcengineering/server-core'
import type { Workspace } from './workspace'
/**
* @public
*/
export interface SessionRequest {
id: string
params: any
start: number
workspaceId?: WorkspaceUuid
}
export interface ClientSessionCtx {
ctx: MeasureContext
workspaces: Workspace[]
socialStringsToUsers: Map<PersonId, AccountUuid>
requestId: ReqId | undefined
sendResponse: (id: ReqId | undefined, msg: any) => Promise<void>
sendPong: () => void
sendError: (id: ReqId | undefined, msg: any, error: any) => Promise<void>
}
/**
* @public
*/
export interface Session {
workspaces: { wsIds: WorkspaceIds, enabled: boolean }[]
socket: ConnectionSocket
createTime: number
// Session restore information
sessionId: string
requests: Map<string, SessionRequest>
binaryMode: boolean
useCompression: boolean
total: StatisticsElement
current: StatisticsElement
mins5: StatisticsElement
lastRequest: number
lastPing: number
isUpgradeClient: () => boolean
getMode: () => string
broadcast: (ctx: MeasureContext, socket: ConnectionSocket, tx: Tx[]) => void
// Client methods
ping: (ctx: ClientSessionCtx) => Promise<void>
getUser: () => AccountUuid
getUserSocialIds: () => PersonId[]
getSocialIds: () => SocialId[]
loadModel: (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string) => Promise<void>
loadModelRaw: (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string) => Promise<LoadModelResponse | Tx[]>
getRawAccount: () => Account
findAll: <T extends Doc>(
ctx: ClientSessionCtx,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
) => Promise<void>
findAllRaw: <T extends Doc>(
ctx: ClientSessionCtx,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
) => Promise<FindResult<T>>
searchFulltext: (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions) => Promise<void>
searchFulltextRaw: (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions) => Promise<SearchResult>
tx: (ctx: ClientSessionCtx, tx: Tx) => Promise<void>
txRaw: (
ctx: ClientSessionCtx,
tx: Tx
) => Promise<{
result: TxResult
broadcastPromise: Promise<void>
asyncsPromise: Promise<void> | undefined
}>
loadChunk: (ctx: ClientSessionCtx, domain: Domain, idx?: number) => Promise<void>
getDomainHash: (ctx: ClientSessionCtx, domain: Domain) => Promise<void>
closeChunk: (ctx: ClientSessionCtx, idx: number) => Promise<void>
loadDocs: (ctx: ClientSessionCtx, domain: Domain, docs: Ref<Doc>[]) => Promise<void>
upload: (ctx: ClientSessionCtx, domain: Domain, docs: Doc[]) => Promise<void>
clean: (ctx: ClientSessionCtx, domain: Domain, docs: Ref<Doc>[]) => Promise<void>
eventRaw: (ctx: ClientSessionCtx, event: CommunicationEvent) => Promise<EventResult>
findMessagesRaw: (ctx: ClientSessionCtx, params: FindMessagesParams) => Promise<Message[]>
findMessagesGroupsRaw: (ctx: ClientSessionCtx, params: FindMessagesGroupsParams) => Promise<MessagesGroup[]>
}
/**
* @public
*/
export interface ConnectionSocket {
id: string
isClosed: boolean
close: () => void
send: (ctx: MeasureContext, msg: Response<any>, binary: boolean, compression: boolean) => Promise<void>
sendPong: () => void
data: () => Record<string, any>
readRequest: (buffer: Buffer, binary: boolean) => Request<any>
isBackpressure: () => boolean // In bytes
backpressure: (ctx: MeasureContext) => Promise<void>
checkState: () => boolean
}
export interface SessionInfoRecord { session: Session, socket: ConnectionSocket, tickHash: number }
/**
* @public
*/
export interface SessionManager {
// workspaces: Map<WorkspaceUuid, Workspace>
sessions: Map<string, SessionInfoRecord>
addSession: (
ctx: MeasureContext,
ws: ConnectionSocket,
token: Token,
rawToken: string,
sessionId: string | undefined
) => Promise<Session>
broadcastAll: (workspace: WorkspaceUuid, tx: Tx[], targets?: string[]) => void
close: (ctx: MeasureContext, sessionRef: Session) => Promise<void>
closeWorkspaces: (ctx: MeasureContext) => Promise<void>
scheduleMaintenance: (timeMinutes: number) => void
profiling?: {
start: () => void
stop: () => Promise<string | undefined>
}
handleRequest: (
requestCtx: MeasureContext,
service: Session,
ws: ConnectionSocket,
request: Request<any>
) => Promise<void>
handleRPC: (
requestCtx: MeasureContext,
workspaceId: WorkspaceUuid,
service: Session,
ws: ConnectionSocket,
operation: (ctx: ClientSessionCtx, rateLimit?: RateLimitInfo | undefined) => Promise<void>
) => Promise<RateLimitInfo | undefined>
createOpContext: (
ctx: MeasureContext,
sendCtx: MeasureContext,
workspaces: Workspace[],
requestId: Request<any>['id'],
service: Session,
ws: ConnectionSocket,
rateLimit: RateLimitInfo | undefined
) => ClientSessionCtx
getStatistics: () => WorkspaceStatistics[]
forceCloseWorkspace: (ctx: MeasureContext, workspace: WorkspaceUuid) => Promise<void>
getLastTxHash: (workspaceId: WorkspaceUuid) => Promise<{ lastTx: string | undefined, lastHash: string | undefined }>
}

View File

@ -13,64 +13,47 @@
// limitations under the License. // limitations under the License.
// //
import { WorkspaceUuid, type MeasureContext } from '@hcengineering/core' import { type MeasureContext, type WorkspaceUuid } from '@hcengineering/core'
import type { import type { ConnectionSocket } from '@hcengineering/server-core'
AddSessionActive,
AddSessionResponse,
ConnectionSocket,
Session,
SessionManager
} from '@hcengineering/server-core'
import { type Response } from '@hcengineering/rpc' import { type Response } from '@hcengineering/rpc'
import type { Token } from '@hcengineering/server-token' import type { Token } from '@hcengineering/server-token'
import type { Session, SessionManager } from './types'
import type { Workspace } from './workspace'
export interface WebsocketData { export interface WebsocketData {
connectionSocket?: ConnectionSocket connectionSocket?: ConnectionSocket
payload: Token payload: Token
token: string token: string
session: Promise<AddSessionResponse> | AddSessionResponse | undefined session: Promise<Session> | Session | undefined
url: string url: string
} }
export function doSessionOp ( export function doSessionOp (data: WebsocketData, op: (session: Session, msg: Buffer) => void, msg: Buffer): void {
data: WebsocketData,
op: (session: AddSessionActive, msg: Buffer) => void,
msg: Buffer
): void {
if (data.session instanceof Promise) { if (data.session instanceof Promise) {
// We need to copy since we will out of protected buffer area // We need to copy since we will out of protected buffer area
const msgCopy = Buffer.copyBytesFrom(new Uint8Array(msg)) const msgCopy = Buffer.copyBytesFrom(new Uint8Array(msg))
void data.session void data.session
.then((_session) => { .then((_session) => {
data.session = _session data.session = _session
if ('session' in _session) { op(data.session, msgCopy)
op(_session, msgCopy)
}
}) })
.catch((err) => { .catch((err) => {
console.error({ message: 'Failed to process session operation', err }) console.error({ message: 'Failed to process session operation', err })
}) })
} else { } else {
if (data.session !== undefined && 'session' in data.session) { if (data.session !== undefined) {
op(data.session, msg) op(data.session, msg)
} }
} }
} }
export function processRequest ( export function processRequest (ctx: MeasureContext, session: Session, cs: ConnectionSocket, buff: any, sessions: SessionManager): void {
session: Session,
cs: ConnectionSocket,
context: MeasureContext,
workspaceId: WorkspaceUuid,
buff: any,
sessions: SessionManager
): void {
try { try {
const request = cs.readRequest(buff, session.binaryMode) const request = cs.readRequest(buff, session.binaryMode)
void sessions.handleRequest(context, session, cs, request, workspaceId).catch((err) => { void sessions.handleRequest(ctx, session, cs, request).catch((err) => {
context.error('failed to handle request', { err, request }) ctx.error('failed to handle request', { err })
}) })
} catch (err: any) { } catch (err: any) {
if (((err.message as string) ?? '').includes('Data read, but end of buffer not reached')) { if (((err.message as string) ?? '').includes('Data read, but end of buffer not reached')) {
@ -89,3 +72,19 @@ export function sendResponse (
): Promise<void> { ): Promise<void> {
return socket.send(ctx, resp, session.binaryMode, session.useCompression) return socket.send(ctx, resp, session.binaryMode, session.useCompression)
} }
export function getLastHashInfo (workspaces: Workspace[]): {
lastTx: Record<WorkspaceUuid, string | undefined>
lastHash: Record<WorkspaceUuid, string | undefined>
} {
const lastTx: Record<WorkspaceUuid, string | undefined> = {}
for (const workspace of workspaces) {
lastTx[workspace.wsId.uuid] = workspace.getLastTx()
}
const lastHash: Record<WorkspaceUuid, string | undefined> = {}
for (const workspace of workspaces) {
lastHash[workspace.wsId.uuid] = workspace.getLastHash()
}
return { lastTx, lastHash }
}

View File

@ -15,13 +15,9 @@
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import { type ServerApi as CommunicationApi } from '@hcengineering/communication-sdk-types' import { type ServerApi as CommunicationApi } from '@hcengineering/communication-sdk-types'
import { type Branding, type MeasureContext, type WorkspaceIds } from '@hcengineering/core' import { isMigrationMode, isRestoringMode, isWorkspaceCreating, systemAccountUuid, type Branding, type Data, type MeasureContext, type Version, type WorkspaceIds, type WorkspaceInfoWithStatus } from '@hcengineering/core'
import type { ConnectionSocket, Pipeline, Session } from '@hcengineering/server-core' import type { ConnectionSocket, Pipeline } from '@hcengineering/server-core'
import type { Session } from './types'
interface TickHandler {
ticks: number
operation: () => void
}
export interface PipelinePair { export interface PipelinePair {
pipeline: Pipeline pipeline: Pipeline
@ -34,18 +30,18 @@ export type WorkspacePipelineFactory = () => Promise<PipelinePair>
*/ */
export class Workspace { export class Workspace {
pipeline?: PipelinePair | Promise<PipelinePair> pipeline?: PipelinePair | Promise<PipelinePair>
upgrade: boolean = false
closing?: Promise<void>
workspaceInitCompleted: boolean = false
softShutdown: number softShutdown: number
sessions = new Map<string, { session: Session, socket: ConnectionSocket, tickHash: number }>() sessions = new Map<string, { session: Session, socket: ConnectionSocket, tickHash: number }>()
tickHandlers = new Map<string, TickHandler>()
operations: number = 0 operations: number = 0
maintenance: boolean = false
lastTx: string | undefined // TODO: Do not cache for proxy case
lastHash: string | undefined // TODO: Do not cache for proxy case
constructor ( constructor (
readonly context: MeasureContext, readonly context: MeasureContext,
readonly token: string, // Account workspace update token. readonly token: string, // Account workspace update token.
@ -61,6 +57,24 @@ export class Workspace {
this.softShutdown = softShutdown this.softShutdown = softShutdown
} }
open (): void {
const pair = this.getPipelinePair()
if (pair instanceof Promise) {
void pair.then((it) => {
this.lastHash = it.pipeline.context.lastHash
this.lastTx = it.pipeline.context.lastTx
})
}
}
getLastTx (): string | undefined {
return this.lastTx
}
getLastHash (): string | undefined {
return this.lastHash
}
private getPipelinePair (): PipelinePair | Promise<PipelinePair> { private getPipelinePair (): PipelinePair | Promise<PipelinePair> {
if (this.pipeline === undefined) { if (this.pipeline === undefined) {
this.pipeline = this.factory() this.pipeline = this.factory()
@ -76,7 +90,10 @@ export class Workspace {
this.pipeline = pair this.pipeline = pair
} }
try { try {
return await op(pair.pipeline, pair.communicationApi) const result = await op(pair.pipeline, pair.communicationApi)
this.lastHash = pair.pipeline.context.lastHash
this.lastTx = pair.pipeline.context.lastTx
return result
} finally { } finally {
this.operations-- this.operations--
} }
@ -115,8 +132,16 @@ export class Workspace {
to.cancelHandle() to.cancelHandle()
}) })
} }
}
checkHasUser (): boolean {
for (const val of this.sessions.values()) {
if (val.session.getUser() !== systemAccountUuid) {
return true
}
}
return false
}
}
function timeoutPromise (time: number): { promise: Promise<void>, cancelHandle: () => void } { function timeoutPromise (time: number): { promise: Promise<void>, cancelHandle: () => void } {
let timer: any let timer: any
return { return {

View File

@ -31,6 +31,7 @@ import contact, {
import core, { import core, {
type Account, type Account,
AccountRole, AccountRole,
AccountUuid,
Blob, Blob,
Class, Class,
Client, Client,
@ -38,6 +39,7 @@ import core, {
MeasureContext, MeasureContext,
PersonId, PersonId,
PersonUuid, PersonUuid,
pickPrimarySocialId,
RateLimiter, RateLimiter,
Ref, Ref,
SocialId, SocialId,
@ -45,30 +47,28 @@ import core, {
Tx, Tx,
TxCUD, TxCUD,
TxOperations, TxOperations,
type WorkspaceUuid,
type WorkspaceIds, type WorkspaceIds,
AccountUuid, type WorkspaceUuid
pickPrimarySocialId
} from '@hcengineering/core' } from '@hcengineering/core'
import { Room } from '@hcengineering/love' import { Room } from '@hcengineering/love'
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot' import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
import fs from 'fs' import fs from 'fs'
import { Tiktoken } from 'js-tiktoken'
import { WithId } from 'mongodb' import { WithId } from 'mongodb'
import OpenAI from 'openai' import OpenAI from 'openai'
import { Tiktoken } from 'js-tiktoken'
import { countTokens } from '@hcengineering/openai'
import { getAccountClient } from '@hcengineering/server-client'
import { StorageAdapter } from '@hcengineering/server-core' import { StorageAdapter } from '@hcengineering/server-core'
import { jsonToMarkup, markupToText } from '@hcengineering/text'
import { markdownToMarkup } from '@hcengineering/text-markdown'
import config from '../config' import config from '../config'
import { DbStorage } from '../storage'
import { HistoryRecord } from '../types' import { HistoryRecord } from '../types'
import { getGlobalPerson } from '../utils/account'
import { createChatCompletionWithTools, requestSummary } from '../utils/openai' import { createChatCompletionWithTools, requestSummary } from '../utils/openai'
import { connectPlatform } from '../utils/platform' import { connectPlatform } from '../utils/platform'
import { LoveController } from './love' import { LoveController } from './love'
import { DbStorage } from '../storage'
import { jsonToMarkup, markupToText } from '@hcengineering/text'
import { markdownToMarkup } from '@hcengineering/text-markdown'
import { countTokens } from '@hcengineering/openai'
import { getAccountClient } from '@hcengineering/server-client'
import { getGlobalPerson } from '../utils/account'
export class WorkspaceClient { export class WorkspaceClient {
client: Client | undefined client: Client | undefined
@ -109,6 +109,8 @@ export class WorkspaceClient {
private async ensureEmployee (client: Client): Promise<void> { private async ensureEmployee (client: Client): Promise<void> {
const me: Account = { const me: Account = {
uuid: this.personUuid, uuid: this.personUuid,
targetWorkspace: this.wsIds.uuid,
roles: { },
role: AccountRole.User, role: AccountRole.User,
primarySocialId: this.primarySocialId._id, primarySocialId: this.primarySocialId._id,
socialIds: this.socialIds.map((it) => it._id), socialIds: this.socialIds.map((it) => it._id),

View File

@ -31,7 +31,8 @@ import core, {
type SocialId, type SocialId,
type Space, type Space,
type TxCreateDoc, type TxCreateDoc,
type TxOperations type TxOperations,
type WorkspaceUuid
} from '@hcengineering/core' } from '@hcengineering/core'
import { type AccountClient, getClient as getAccountClient } from '@hcengineering/account-client' import { type AccountClient, getClient as getAccountClient } from '@hcengineering/account-client'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
@ -78,7 +79,9 @@ describe('rest-api-server', () => {
testCtx, testCtx,
{ {
uuid: apiWorkspace1.info.account, uuid: apiWorkspace1.info.account,
roles: {},
role: apiWorkspace1.info.role, role: apiWorkspace1.info.role,
targetWorkspace: apiWorkspace1.workspaceId,
primarySocialId: pickPrimarySocialId(socialIds)._id, primarySocialId: pickPrimarySocialId(socialIds)._id,
socialIds: socialIds.map((si) => si._id), socialIds: socialIds.map((si) => si._id),
fullSocialIds: socialIds fullSocialIds: socialIds
@ -92,7 +95,9 @@ describe('rest-api-server', () => {
testCtx, testCtx,
{ {
uuid: apiWorkspace2.info.account, uuid: apiWorkspace2.info.account,
roles: { },
role: apiWorkspace2.info.role, role: apiWorkspace2.info.role,
targetWorkspace: apiWorkspace2.workspaceId,
primarySocialId: pickPrimarySocialId(socialIds)._id, primarySocialId: pickPrimarySocialId(socialIds)._id,
socialIds: socialIds.map((si) => si._id), socialIds: socialIds.map((si) => si._id),
fullSocialIds: socialIds fullSocialIds: socialIds
@ -118,7 +123,7 @@ describe('rest-api-server', () => {
const account = await conn.getAccount() const account = await conn.getAccount()
expect(account.primarySocialId).toEqual(expect.any(String)) expect(account.primarySocialId).toEqual(expect.any(String))
expect(account.role).toBe('USER') // expect(account.role).toBe('USER')
// expect(account.space).toBe(core.space.Model) // expect(account.space).toBe(core.space.Model)
// expect(account.modifiedBy).toBe(core.account.System) // expect(account.modifiedBy).toBe(core.account.System)
// expect(account.createdBy).toBe(core.account.System) // expect(account.createdBy).toBe(core.account.System)