mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-08 10:47:42 +02:00
UBERF-9521: Refactor session manager (#8560)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Vendored
+10
-8
@@ -97,7 +97,7 @@
|
||||
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
|
||||
// "SERVER_PROVIDER":"uweb"
|
||||
"SERVER_PROVIDER": "ws",
|
||||
"MODEL_VERSION": "0.7.48",
|
||||
"MODEL_VERSION": "0.7.75",
|
||||
// "VERSION": "0.6.289",
|
||||
"ELASTIC_INDEX_NAME": "local_storage_index",
|
||||
"UPLOAD_URL": "/files",
|
||||
@@ -120,9 +120,9 @@
|
||||
"args": ["src/__start.ts"],
|
||||
"env": {
|
||||
"FULLTEXT_URL": "http://localhost:4710",
|
||||
"DB_URL": "mongodb://localhost:27018",
|
||||
// "DB_URL": "mongodb://localhost:27018",
|
||||
// "DB_URL": "postgresql://postgres:example@localhost:5432",
|
||||
// "DB_URL": "postgresql://root@huly.local:26258/defaultdb?sslmode=disable",
|
||||
"DB_URL": "postgresql://root@huly.local:26258/defaultdb?sslmode=disable",
|
||||
// "GREEN_URL": "http://huly.local:6767?token=secret",
|
||||
"SERVER_PORT": "3334",
|
||||
"METRICS_CONSOLE": "false",
|
||||
@@ -135,8 +135,9 @@
|
||||
"FRONT_URL": "http://localhost:8083",
|
||||
"ACCOUNTS_URL": "http://localhost:3003",
|
||||
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
|
||||
"MODEL_VERSION": "0.7.1",
|
||||
"STATS_URL": "http://huly.local:4901"
|
||||
"MODEL_VERSION": "0.7.75",
|
||||
"STATS_URL": "http://huly.local:4901",
|
||||
"QUEUE_CONFIG": "localhost:19093"
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"runtimeVersion": "20",
|
||||
@@ -213,12 +214,13 @@
|
||||
"request": "launch",
|
||||
"args": ["src/__start.ts"],
|
||||
"env": {
|
||||
"MONGO_URL": "mongodb://localhost:27018",
|
||||
"DB_URL": "mongodb://localhost:27018",
|
||||
// "MONGO_URL": "mongodb://localhost:27018",
|
||||
// "DB_URL": "mongodb://localhost:27018",
|
||||
// "DB_URL": "postgresql://postgres:example@localhost:5432",
|
||||
"DB_URL": "postgresql://root@huly.local:26258/defaultdb?sslmode=disable",
|
||||
"SERVER_SECRET": "secret",
|
||||
"REGION_INFO": "|Mongo;pg|Postgres;cockroach|CockroachDB",
|
||||
"TRANSACTOR_URL": "ws://huly.local:3334;;,ws://huly.local:3335;;europe",
|
||||
"TRANSACTOR_URL": "ws://transactor:3334;ws://localhost:3334",
|
||||
"ACCOUNTS_URL": "http://localhost:3003",
|
||||
"ACCOUNT_PORT": "3003",
|
||||
"FRONT_URL": "http://localhost:8083",
|
||||
|
||||
Generated
+6
-5
@@ -774,7 +774,7 @@ importers:
|
||||
version: file:projects/pod-print.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||
'@rush-temp/pod-server':
|
||||
specifier: file:./projects/pod-server.tgz
|
||||
version: file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(utf-8-validate@6.0.4)
|
||||
version: file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))
|
||||
'@rush-temp/pod-ses':
|
||||
specifier: file:./projects/pod-ses.tgz
|
||||
version: file:projects/pod-ses.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))
|
||||
@@ -4806,7 +4806,7 @@ packages:
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/pod-server@file:projects/pod-server.tgz':
|
||||
resolution: {integrity: sha512-nlXF3FUjnhvEM9Jz2qnS0pYDDJC4Akt9EZEmku7H+TFP6znCb/pBVelUlY5W+CSRBUFpRkmNv2e/KS/knh4b7w==, tarball: file:projects/pod-server.tgz}
|
||||
resolution: {integrity: sha512-9Ff7F+voylF+V6WMOoCiftEMWTcPA4odM1CleTnf6cfA8Kg8vUz4Di2kzNcH/yxmiywvp47OGSsfuly+djtnkw==, tarball: file:projects/pod-server.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/pod-ses@file:projects/pod-ses.tgz':
|
||||
@@ -5274,7 +5274,7 @@ packages:
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/server@file:projects/server.tgz':
|
||||
resolution: {integrity: sha512-zYTPrh4adUZ36sz8IfoCcJ2yGCSbXgLaeYBv4++J+g/njPytlCfEN9L1x5aYT5zu91cZ/Gt4DkHGFgHsPwNjAw==, tarball: file:projects/server.tgz}
|
||||
resolution: {integrity: sha512-ZeFqWmcBAIlHDDCSms07YvcEuy5G2LJ/Y4wAw743ykDDgqxQ7ptxJpQ0DATsSdOnAF916DE8TYktKbV9Hu1cug==, tarball: file:projects/server.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/setting-assets@file:projects/setting-assets.tgz':
|
||||
@@ -22126,7 +22126,7 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@rush-temp/pod-server@file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(utf-8-validate@6.0.4)':
|
||||
'@rush-temp/pod-server@file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))':
|
||||
dependencies:
|
||||
'@hcengineering/communication-server': 0.1.170(typescript@5.3.3)
|
||||
'@types/body-parser': 1.19.5
|
||||
@@ -22160,6 +22160,7 @@ snapshots:
|
||||
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
|
||||
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
utf-8-validate: 6.0.4
|
||||
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -22170,7 +22171,6 @@ snapshots:
|
||||
- babel-plugin-macros
|
||||
- node-notifier
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@rush-temp/pod-ses@file:projects/pod-ses.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))':
|
||||
dependencies:
|
||||
@@ -25474,6 +25474,7 @@ snapshots:
|
||||
prettier: 3.2.5
|
||||
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
utf-8-validate: 6.0.4
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
|
||||
@@ -164,7 +164,7 @@ export async function backupRestore (
|
||||
recheck: false,
|
||||
storageAdapter: workspaceStorage,
|
||||
getConnection: async () => {
|
||||
return wrapPipeline(ctx, await pipelineFactory(ctx, wsUrl, true, () => {}, null, null), wsUrl)
|
||||
return wrapPipeline(ctx, await pipelineFactory(ctx, wsUrl, () => {}, null, null), wsUrl)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@@ -13,38 +13,39 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
import {
|
||||
type AccountRole,
|
||||
type AccountInfo,
|
||||
type AccountRole,
|
||||
type AccountUuid,
|
||||
BackupStatus,
|
||||
concatLink,
|
||||
Data,
|
||||
type Person,
|
||||
type PersonUuid,
|
||||
type PersonId,
|
||||
type PersonInfo,
|
||||
type PersonUuid,
|
||||
type SocialIdType,
|
||||
Version,
|
||||
type WorkspaceInfoWithStatus,
|
||||
type WorkspaceMemberInfo,
|
||||
WorkspaceMode,
|
||||
concatLink,
|
||||
type WorkspaceUserOperation,
|
||||
type WorkspaceUuid,
|
||||
type PersonId,
|
||||
type SocialIdType,
|
||||
type AccountUuid
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
import type {
|
||||
LoginInfo,
|
||||
MailboxOptions,
|
||||
OtpInfo,
|
||||
WorkspaceLoginInfo,
|
||||
RegionInfo,
|
||||
WorkspaceOperation,
|
||||
MailboxInfo,
|
||||
Integration,
|
||||
IntegrationKey,
|
||||
IntegrationSecret,
|
||||
IntegrationSecretKey,
|
||||
SocialId
|
||||
LoginInfo,
|
||||
LoginInfoWithWorkspaces,
|
||||
MailboxInfo,
|
||||
MailboxOptions,
|
||||
OtpInfo,
|
||||
RegionInfo,
|
||||
SocialId,
|
||||
WorkspaceLoginInfo,
|
||||
WorkspaceOperation
|
||||
} from './types'
|
||||
import { getClientTimezone } from './utils'
|
||||
|
||||
@@ -63,6 +64,7 @@ export interface AccountClient {
|
||||
validateOtp: (email: string, code: string) => Promise<LoginInfo>
|
||||
loginOtp: (email: string) => Promise<OtpInfo>
|
||||
getLoginInfoByToken: () => Promise<LoginInfo | WorkspaceLoginInfo>
|
||||
getLoginWithWorkspaceInfo: () => Promise<LoginInfoWithWorkspaces>
|
||||
restorePassword: (password: string) => Promise<LoginInfo>
|
||||
confirm: () => Promise<LoginInfo>
|
||||
requestPasswordReset: (email: string) => Promise<void>
|
||||
@@ -308,6 +310,15 @@ class AccountClientImpl implements AccountClient {
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async getLoginWithWorkspaceInfo (): Promise<LoginInfoWithWorkspaces> {
|
||||
const request = {
|
||||
method: 'getLoginWithWorkspaceInfo' as const,
|
||||
params: {}
|
||||
}
|
||||
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async restorePassword (password: string): Promise<LoginInfo> {
|
||||
const request = {
|
||||
method: 'restorePassword' as const,
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
type AccountRole,
|
||||
type Timestamp,
|
||||
type SocialId as SocialIdBase,
|
||||
PersonUuid
|
||||
PersonUuid,
|
||||
type WorkspaceMode
|
||||
} from '@hcengineering/core'
|
||||
|
||||
export interface LoginInfo {
|
||||
@@ -16,6 +17,33 @@ export interface LoginInfo {
|
||||
token?: string
|
||||
}
|
||||
|
||||
export interface EndpointInfo {
|
||||
internalUrl: string
|
||||
externalUrl: string
|
||||
region: string
|
||||
}
|
||||
export interface WorkspaceVersion {
|
||||
versionMajor: number
|
||||
versionMinor: number
|
||||
versionPatch: number
|
||||
}
|
||||
|
||||
export interface LoginInfoWorkspace {
|
||||
url: string
|
||||
dataId?: WorkspaceDataId
|
||||
mode: WorkspaceMode
|
||||
version: WorkspaceVersion
|
||||
endpoint: EndpointInfo
|
||||
role: AccountRole | null
|
||||
progress?: number
|
||||
}
|
||||
|
||||
export interface LoginInfoWithWorkspaces extends LoginInfo {
|
||||
// Information necessary to handle user <--> transactor connectivity.
|
||||
workspaces: Record<WorkspaceUuid, LoginInfoWorkspace>
|
||||
socialIds: SocialId[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -50,7 +50,6 @@ export interface SessionData {
|
||||
admin?: boolean
|
||||
isTriggerCtx?: boolean
|
||||
workspace: WorkspaceIds
|
||||
branding: Branding | null
|
||||
socialStringsToUsers: Map<PersonId, AccountUuid>
|
||||
|
||||
asyncRequests?: (() => Promise<void>)[]
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"@hcengineering/server-storage": "^0.6.0",
|
||||
"@hcengineering/server-telegram": "^0.6.0",
|
||||
"@hcengineering/server-token": "^0.6.11",
|
||||
"utf-8-validate": "^6.0.4",
|
||||
"bufferutil": "^4.0.8",
|
||||
"msgpackr": "^1.11.2",
|
||||
"msgpackr-extract": "^3.0.3",
|
||||
|
||||
@@ -16,11 +16,7 @@ import serverCalendar from '@hcengineering/server-calendar'
|
||||
import serverCore, {
|
||||
initStatisticsContext,
|
||||
loadBrandingMap,
|
||||
type ConnectionSocket,
|
||||
type Session,
|
||||
type StorageConfiguration,
|
||||
type UserStatistics,
|
||||
type Workspace,
|
||||
type WorkspaceStatistics
|
||||
} from '@hcengineering/server-core'
|
||||
import serverNotification from '@hcengineering/server-notification'
|
||||
@@ -33,7 +29,7 @@ import { profileStart, profileStop } from './inspector'
|
||||
configureAnalytics(process.env.SENTRY_DSN, {})
|
||||
Analytics.setTag('application', 'transactor')
|
||||
|
||||
let getUsers: () => WorkspaceStatistics[] = () => {
|
||||
let getStats: () => WorkspaceStatistics[] = () => {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -50,8 +46,8 @@ void queue.createTopics(10).catch((err) => {
|
||||
|
||||
// Force create server metrics context with proper logging
|
||||
const metricsContext = initStatisticsContext('transactor', {
|
||||
getUsers: (): WorkspaceStatistics[] => {
|
||||
return getUsers()
|
||||
getStats: (): WorkspaceStatistics[] => {
|
||||
return getStats()
|
||||
},
|
||||
factory: () =>
|
||||
new MeasureMetricsContext(
|
||||
@@ -103,29 +99,8 @@ const { shutdown, sessionManager } = start(metricsContext, config.dbUrl, {
|
||||
queue
|
||||
})
|
||||
|
||||
const entryToUserStats = (session: Session, socket: ConnectionSocket): UserStatistics => {
|
||||
return {
|
||||
current: session.current,
|
||||
mins5: session.mins5,
|
||||
userId: session.getUser(),
|
||||
sessionId: socket.id,
|
||||
total: session.total,
|
||||
data: socket.data
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceToWorkspaceStats = (ws: Workspace): WorkspaceStatistics => {
|
||||
return {
|
||||
clientsTotal: new Set(Array.from(ws.sessions.values()).map((it) => it.session.getUser())).size,
|
||||
sessionsTotal: ws.sessions.size,
|
||||
workspaceName: ws.workspaceName,
|
||||
wsId: ws.workspaceUuid,
|
||||
sessions: Array.from(ws.sessions.values()).map((it) => entryToUserStats(it.session, it.socket))
|
||||
}
|
||||
}
|
||||
|
||||
getUsers = () => {
|
||||
return Array.from(sessionManager.workspaces.values()).map((it) => workspaceToWorkspaceStats(it))
|
||||
getStats = (): WorkspaceStatistics[] => {
|
||||
return sessionManager.getStatistics()
|
||||
}
|
||||
|
||||
const close = (): void => {
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
type TxResult,
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import { ClientSession, startSessionManager, type SessionManagerOptions } from '@hcengineering/server'
|
||||
import { startSessionManager, type SessionManagerOptions } from '@hcengineering/server'
|
||||
import { createDummyQueue, createDummyStorageAdapter } from '@hcengineering/server-core'
|
||||
import { startHttpServer } from '../server_http'
|
||||
import { genMinModel } from './minmodel'
|
||||
@@ -100,7 +100,6 @@ describe('server', () => {
|
||||
communicationApiFactory: async () => {
|
||||
return {} as any
|
||||
},
|
||||
sessionFactory: (token, workspace, account) => new ClientSession(token, workspace, account, true),
|
||||
brandingMap: {},
|
||||
accountsUrl: '',
|
||||
queue: createDummyQueue()
|
||||
@@ -216,7 +215,6 @@ describe('server', () => {
|
||||
communicationApiFactory: async () => {
|
||||
return {} as any
|
||||
},
|
||||
sessionFactory: (token, workspace, account) => new ClientSession(token, workspace, account, true),
|
||||
brandingMap: {},
|
||||
accountsUrl: '',
|
||||
queue: createDummyQueue()
|
||||
|
||||
@@ -14,19 +14,16 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Account, 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 { ClientSession, startSessionManager } from '@hcengineering/server'
|
||||
import { startSessionManager } from '@hcengineering/server'
|
||||
import {
|
||||
type CommunicationApiFactory,
|
||||
type PlatformQueue,
|
||||
type Session,
|
||||
type SessionManager,
|
||||
type StorageConfiguration,
|
||||
type Workspace
|
||||
type StorageConfiguration
|
||||
} from '@hcengineering/server-core'
|
||||
import { type Token } from '@hcengineering/server-token'
|
||||
|
||||
import { Api as CommunicationApi } from '@hcengineering/communication-server'
|
||||
import {
|
||||
@@ -120,9 +117,6 @@ export function start (
|
||||
{ ...opt, externalStorage, adapterSecurity: isAdapterSecurity(dbUrl), queue: opt.queue },
|
||||
{}
|
||||
)
|
||||
const sessionFactory = (token: Token, workspace: Workspace, account: Account): Session => {
|
||||
return new ClientSession(token, workspace, account, token.extra?.mode === 'backup')
|
||||
}
|
||||
const communicationApiFactory: CommunicationApiFactory = async (ctx, workspace, broadcastSessions) => {
|
||||
if (dbUrl.startsWith('mongodb')) {
|
||||
return {
|
||||
@@ -150,7 +144,6 @@ export function start (
|
||||
|
||||
const sessionManager = startSessionManager(metrics, {
|
||||
pipelineFactory,
|
||||
sessionFactory,
|
||||
communicationApiFactory,
|
||||
brandingMap: opt.brandingMap,
|
||||
enableCompression: opt.enableCompression,
|
||||
|
||||
@@ -89,7 +89,6 @@ const rpcHandler = new RPCHandler()
|
||||
const backpressureSize = 100 * 1024
|
||||
/**
|
||||
* @public
|
||||
* @param sessionFactory -
|
||||
* @param port -
|
||||
* @param host -
|
||||
*/
|
||||
@@ -403,27 +402,24 @@ export function startHttpServer (
|
||||
try {
|
||||
const token = (req.query.token as string) ?? (req.headers.authorization ?? '').split(' ')[1]
|
||||
decodeToken(token)
|
||||
const ws = sessions.workspaces.get(req.query.workspace as WorkspaceUuid)
|
||||
if (ws !== undefined) {
|
||||
// push the data to body
|
||||
void retrieveJson(req)
|
||||
.then((data) => {
|
||||
if (Array.isArray(data)) {
|
||||
sessions.broadcastAll(ws, data as Tx[])
|
||||
} else {
|
||||
sessions.broadcastAll(ws, [data as unknown as Tx])
|
||||
}
|
||||
res.end()
|
||||
})
|
||||
.catch((err) => {
|
||||
ctx.error('JSON parse error', { err })
|
||||
res.writeHead(400, {})
|
||||
res.end()
|
||||
})
|
||||
} else {
|
||||
res.writeHead(404, {})
|
||||
res.end()
|
||||
}
|
||||
|
||||
const ws = req.query.workspace as WorkspaceUuid
|
||||
|
||||
// push the data to body
|
||||
void retrieveJson(req)
|
||||
.then((data) => {
|
||||
if (Array.isArray(data)) {
|
||||
sessions.broadcastAll(ws, data as Tx[])
|
||||
} else {
|
||||
sessions.broadcastAll(ws, [data as unknown as Tx])
|
||||
}
|
||||
res.end()
|
||||
})
|
||||
.catch((err) => {
|
||||
ctx.error('JSON parse error', { err })
|
||||
res.writeHead(400, {})
|
||||
res.end()
|
||||
})
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
ctx.error('error', { err })
|
||||
|
||||
@@ -347,10 +347,6 @@ describe('account utils', () => {
|
||||
})
|
||||
|
||||
describe('getEndpoint', () => {
|
||||
const mockCtx = {
|
||||
error: jest.fn()
|
||||
} as unknown as MeasureContext
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
@@ -392,8 +388,7 @@ describe('account utils', () => {
|
||||
'should handle workspace="%s" region="%s" kind=%s (%s)',
|
||||
(workspace, region, kind, transactors, expected, description) => {
|
||||
;(getMetadata as jest.Mock).mockReturnValue(transactors)
|
||||
expect(getEndpoint(mockCtx, workspace, region, kind)).toBe(expected)
|
||||
expect(mockCtx.error).not.toHaveBeenCalled()
|
||||
expect(getEndpoint(workspace as WorkspaceUuid, region, kind)).toBe(expected)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -401,21 +396,17 @@ describe('account utils', () => {
|
||||
const transactors = 'http://internal:3000;http://external:3000;'
|
||||
;(getMetadata as jest.Mock).mockReturnValue(transactors)
|
||||
|
||||
expect(getEndpoint(mockCtx, 'workspace1', 'nonexistent', EndpointKind.Internal)).toBe('http://internal:3000')
|
||||
|
||||
expect(mockCtx.error).toHaveBeenCalledWith('No transactors for the target region, will use default region', {
|
||||
group: 'nonexistent'
|
||||
})
|
||||
expect(getEndpoint('workspace1' as WorkspaceUuid, 'nonexistent', EndpointKind.Internal)).toBe(
|
||||
'http://internal:3000'
|
||||
)
|
||||
})
|
||||
|
||||
test('should throw error when no transactors available', () => {
|
||||
;(getMetadata as jest.Mock).mockReturnValue('http://internal:3000;http://external:3000;us')
|
||||
|
||||
expect(() => getEndpoint(mockCtx, 'workspace1', 'nonexistent', EndpointKind.Internal)).toThrow(
|
||||
expect(() => getEndpoint('workspace1' as WorkspaceUuid, 'nonexistent', EndpointKind.Internal)).toThrow(
|
||||
'Please provide transactor endpoint url'
|
||||
)
|
||||
|
||||
expect(mockCtx.error).toHaveBeenCalledWith('No transactors for the default region')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -12,7 +12,17 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { UUID } from 'mongodb'
|
||||
import {
|
||||
type AccountRole,
|
||||
type Data,
|
||||
type Person,
|
||||
type Version,
|
||||
type WorkspaceMemberInfo,
|
||||
type WorkspaceUuid,
|
||||
AccountUuid,
|
||||
buildSocialIdString,
|
||||
SocialKey
|
||||
} from '@hcengineering/core'
|
||||
import type {
|
||||
Collection,
|
||||
CreateIndexesOptions,
|
||||
@@ -22,38 +32,28 @@ import type {
|
||||
OptionalUnlessRequiredId,
|
||||
Sort as RawSort
|
||||
} from 'mongodb'
|
||||
import {
|
||||
type Person,
|
||||
type WorkspaceMemberInfo,
|
||||
buildSocialIdString,
|
||||
SocialKey,
|
||||
type AccountRole,
|
||||
type Data,
|
||||
type Version,
|
||||
type WorkspaceUuid,
|
||||
AccountUuid
|
||||
} from '@hcengineering/core'
|
||||
import { UUID } from 'mongodb'
|
||||
|
||||
import type {
|
||||
DbCollection,
|
||||
Query,
|
||||
Operations,
|
||||
WorkspaceOperation,
|
||||
AccountDB,
|
||||
Account,
|
||||
SocialId,
|
||||
WorkspaceInvite,
|
||||
OTP,
|
||||
WorkspaceStatus,
|
||||
AccountDB,
|
||||
AccountEvent,
|
||||
WorkspaceData,
|
||||
WorkspaceInfoWithStatus,
|
||||
WorkspaceStatusData,
|
||||
Sort,
|
||||
DbCollection,
|
||||
Integration,
|
||||
IntegrationSecret,
|
||||
Mailbox,
|
||||
MailboxSecret,
|
||||
Integration,
|
||||
IntegrationSecret
|
||||
Operations,
|
||||
OTP,
|
||||
Query,
|
||||
SocialId,
|
||||
Sort,
|
||||
WorkspaceData,
|
||||
WorkspaceInfoWithStatus,
|
||||
WorkspaceInvite,
|
||||
WorkspaceOperation,
|
||||
WorkspaceStatus,
|
||||
WorkspaceStatusData
|
||||
} from '../types'
|
||||
import { isShallowEqual } from '../utils'
|
||||
|
||||
@@ -716,6 +716,17 @@ export class MongoAccountDB implements AccountDB {
|
||||
return assignment?.role ?? null
|
||||
}
|
||||
|
||||
async getWorkspaceRoles (accountId: AccountUuid): Promise<Map<WorkspaceUuid, AccountRole | null>> {
|
||||
const assignment = await this.workspaceMembers.find({
|
||||
accountUuid: accountId
|
||||
})
|
||||
|
||||
return assignment.reduce<Map<WorkspaceUuid, AccountRole | null>>((acc, it) => {
|
||||
acc.set(it.workspaceUuid, it.role)
|
||||
return acc
|
||||
}, new Map())
|
||||
}
|
||||
|
||||
async getWorkspaceMembers (workspaceId: WorkspaceUuid): Promise<WorkspaceMemberInfo[]> {
|
||||
return (await this.workspaceMembers.find({ workspaceUuid: workspaceId })).map((wmi) => ({
|
||||
person: wmi.accountUuid,
|
||||
|
||||
@@ -490,8 +490,6 @@ export class PostgresAccountDB implements AccountDB {
|
||||
if (res.count === 1) {
|
||||
console.log(`Applying migration: ${name}`)
|
||||
await client.unsafe(ddl)
|
||||
} else {
|
||||
console.log(`Migration ${name} already applied`)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -535,12 +533,19 @@ export class PostgresAccountDB implements AccountDB {
|
||||
}
|
||||
|
||||
async getWorkspaceRole (accountUuid: AccountUuid, workspaceUuid: WorkspaceUuid): Promise<AccountRole | null> {
|
||||
const res: any = await this
|
||||
const res = await this
|
||||
.client`SELECT role FROM ${this.client(this.getWsMembersTableName())} WHERE workspace_uuid = ${workspaceUuid} AND account_uuid = ${accountUuid}`
|
||||
|
||||
return res[0]?.role ?? null
|
||||
}
|
||||
|
||||
async getWorkspaceRoles (accountUuid: AccountUuid): Promise<Map<WorkspaceUuid, AccountRole | null>> {
|
||||
const res = await this
|
||||
.client`SELECT workspace_uuid, role FROM ${this.client(this.getWsMembersTableName())} WHERE account_uuid = ${accountUuid}`
|
||||
|
||||
return new Map(res.map((it) => [it.workspace_uuid as WorkspaceUuid, it.role]))
|
||||
}
|
||||
|
||||
async getWorkspaceMembers (workspaceUuid: WorkspaceUuid): Promise<WorkspaceMemberInfo[]> {
|
||||
const res: any = await this
|
||||
.client`SELECT account_uuid, role FROM ${this.client(this.getWsMembersTableName())} WHERE workspace_uuid = ${workspaceUuid}`
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Analytics } from '@hcengineering/analytics'
|
||||
import {
|
||||
AccountInfo,
|
||||
AccountRole,
|
||||
type AccountUuid,
|
||||
type Branding,
|
||||
buildSocialIdString,
|
||||
concatLink,
|
||||
@@ -28,18 +29,19 @@ import {
|
||||
SocialIdType,
|
||||
systemAccountUuid,
|
||||
type WorkspaceMemberInfo,
|
||||
type WorkspaceUuid,
|
||||
type AccountUuid
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform'
|
||||
import { decodeTokenVerbose, generateToken } from '@hcengineering/server-token'
|
||||
|
||||
import { isAdminEmail } from './admin'
|
||||
import { accountPlugin } from './plugin'
|
||||
import { type AccountServiceMethods, getServiceMethods } from './serviceOperations'
|
||||
import type {
|
||||
AccountDB,
|
||||
AccountMethodHandler,
|
||||
LoginInfo,
|
||||
LoginInfoWithWorkspaces,
|
||||
Mailbox,
|
||||
MailboxOptions,
|
||||
Meta,
|
||||
@@ -55,6 +57,7 @@ import {
|
||||
checkInvite,
|
||||
cleanEmail,
|
||||
confirmEmail,
|
||||
confirmHulyIds,
|
||||
createAccount,
|
||||
createWorkspaceRecord,
|
||||
doJoinByInvite,
|
||||
@@ -63,6 +66,7 @@ import {
|
||||
getAccount,
|
||||
getEmailSocialId,
|
||||
getEndpoint,
|
||||
getEndpointInfo,
|
||||
getFrontUrl,
|
||||
getInviteEmail,
|
||||
getMailUrl,
|
||||
@@ -70,9 +74,12 @@ import {
|
||||
getRegions,
|
||||
getRolePower,
|
||||
getWorkspaceById,
|
||||
getWorkspaceByUrl,
|
||||
getWorkspaceEndpoint,
|
||||
getWorkspaceInfoWithStatusById,
|
||||
getWorkspaceInvite,
|
||||
getWorkspaceRole,
|
||||
getWorkspaceRoles,
|
||||
GUEST_ACCOUNT,
|
||||
isEmail,
|
||||
isOtpValid,
|
||||
@@ -89,11 +96,8 @@ import {
|
||||
verifyAllowedRole,
|
||||
verifyAllowedServices,
|
||||
verifyPassword,
|
||||
wrap,
|
||||
getWorkspaceByUrl,
|
||||
confirmHulyIds
|
||||
wrap
|
||||
} from './utils'
|
||||
import { type AccountServiceMethods, getServiceMethods } from './serviceOperations'
|
||||
|
||||
// Note: it is IMPORTANT to always destructure params passed here to avoid sending extra params
|
||||
// to the database layer when searching/inserting as they may contain SQL injection
|
||||
@@ -383,7 +387,7 @@ export async function createWorkspace (
|
||||
socialId: socialId._id,
|
||||
name: getPersonName(person),
|
||||
token: generateToken(account, workspaceUuid),
|
||||
endpoint: getEndpoint(ctx, workspaceUuid, region, EndpointKind.External),
|
||||
endpoint: getEndpoint(workspaceUuid, region, EndpointKind.External),
|
||||
workspace: workspaceUuid,
|
||||
workspaceUrl,
|
||||
role: AccountRole.Owner
|
||||
@@ -1326,7 +1330,7 @@ export async function getLoginInfoByToken (
|
||||
return {
|
||||
...loginInfo,
|
||||
workspace: workspaceUuid,
|
||||
endpoint: getEndpoint(ctx, workspace.uuid, workspace.region, EndpointKind.External),
|
||||
endpoint: getEndpoint(workspace.uuid, workspace.region, EndpointKind.External),
|
||||
role: AccountRole.DocGuest
|
||||
}
|
||||
}
|
||||
@@ -1342,7 +1346,7 @@ export async function getLoginInfoByToken (
|
||||
...loginInfo,
|
||||
workspace: workspace.uuid,
|
||||
workspaceDataId: workspace.dataId,
|
||||
endpoint: getEndpoint(ctx, workspace.uuid, workspace.region, EndpointKind.External),
|
||||
endpoint: getEndpoint(workspace.uuid, workspace.region, EndpointKind.External),
|
||||
role
|
||||
}
|
||||
} else {
|
||||
@@ -1350,6 +1354,101 @@ export async function getLoginInfoByToken (
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the token and returns the decoded account information.
|
||||
*/
|
||||
export async function getLoginWithWorkspaceInfo (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
branding: Branding | null,
|
||||
token: string
|
||||
): Promise<LoginInfoWithWorkspaces> {
|
||||
let accountUuid: AccountUuid
|
||||
let extra: any
|
||||
try {
|
||||
;({ account: accountUuid, extra } = decodeTokenVerbose(ctx, token))
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
ctx.error('Invalid token', { token })
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Unauthorized, {}))
|
||||
}
|
||||
|
||||
if (accountUuid == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: accountUuid }))
|
||||
}
|
||||
|
||||
const isDocGuest = accountUuid === GUEST_ACCOUNT && extra?.guest === 'true'
|
||||
const isSystem = accountUuid === systemAccountUuid
|
||||
let socialIds: SocialId[] = []
|
||||
|
||||
if (!isDocGuest && !isSystem) {
|
||||
// Any confirmed social ID will do
|
||||
socialIds = await db.socialId.find({ personUuid: accountUuid, verifiedOn: { $gt: 0 } })
|
||||
if (socialIds.length === 0) {
|
||||
return {
|
||||
account: accountUuid,
|
||||
workspaces: {},
|
||||
socialIds: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let person: Person | null
|
||||
if (isDocGuest) {
|
||||
person = {
|
||||
uuid: accountUuid,
|
||||
firstName: 'Guest',
|
||||
lastName: 'User'
|
||||
}
|
||||
} else if (isSystem) {
|
||||
person = {
|
||||
uuid: accountUuid,
|
||||
firstName: 'System',
|
||||
lastName: 'User'
|
||||
}
|
||||
} else {
|
||||
person = await db.person.findOne({ uuid: accountUuid })
|
||||
}
|
||||
|
||||
if (person == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.InternalServerError, {}))
|
||||
}
|
||||
|
||||
const userWorkspaces = (await db.getAccountWorkspaces(accountUuid)).filter((it) => isActiveMode(it.status.mode))
|
||||
const roles: Map<WorkspaceUuid, AccountRole | null> = await getWorkspaceRoles(db, accountUuid)
|
||||
|
||||
const info = getEndpointInfo()
|
||||
const loginInfo: LoginInfoWithWorkspaces = {
|
||||
account: accountUuid,
|
||||
name: getPersonName(person),
|
||||
socialId: socialIds[0]?._id,
|
||||
token,
|
||||
workspaces: Object.fromEntries(
|
||||
isSystem || isDocGuest
|
||||
? []
|
||||
: userWorkspaces.map((it, idx) => [
|
||||
it.uuid,
|
||||
{
|
||||
url: it.url,
|
||||
dataId: it.dataId,
|
||||
mode: it.status.mode,
|
||||
endpoint: getWorkspaceEndpoint(info, it.uuid, it.region),
|
||||
role: roles.get(it.uuid) ?? null,
|
||||
version: {
|
||||
versionMajor: it.status.versionMajor,
|
||||
versionMinor: it.status.versionMinor,
|
||||
versionPatch: it.status.versionPatch
|
||||
},
|
||||
progress: it.status.processingProgress
|
||||
}
|
||||
])
|
||||
),
|
||||
socialIds
|
||||
}
|
||||
|
||||
return loginInfo
|
||||
}
|
||||
|
||||
export async function getSocialIds (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
@@ -1650,7 +1749,6 @@ async function exchangeGuestToken (
|
||||
if (tokenObj.account == null) {
|
||||
// Check if it's old guest token
|
||||
const oldGuestEmail = '#guest@hc.engineering'
|
||||
const guestAccount = 'b6996120-416f-49cd-841e-e4a5d2e49c9b' as PersonUuid
|
||||
const { linkId, guest, email, workspace: workspaceUrl } = tokenObj as any
|
||||
|
||||
if (linkId == null || guest == null || email !== oldGuestEmail || workspaceUrl == null) {
|
||||
@@ -1663,7 +1761,7 @@ async function exchangeGuestToken (
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUrl }))
|
||||
}
|
||||
|
||||
return generateToken(guestAccount, workspace.uuid, { linkId, guest: 'true' })
|
||||
return generateToken(GUEST_ACCOUNT as PersonUuid, workspace.uuid, { linkId, guest: 'true' })
|
||||
}
|
||||
|
||||
return token
|
||||
@@ -1699,6 +1797,7 @@ export type AccountMethods =
|
||||
| 'getWorkspaceInfo'
|
||||
| 'getWorkspacesInfo'
|
||||
| 'getLoginInfoByToken'
|
||||
| 'getLoginWithWorkspaceInfo'
|
||||
| 'getSocialIds'
|
||||
| 'getPerson'
|
||||
| 'getWorkspaceMembers'
|
||||
@@ -1759,6 +1858,7 @@ export function getMethods (hasSignUp: boolean = true): Partial<Record<AccountMe
|
||||
getWorkspaceInfo: wrap(getWorkspaceInfo),
|
||||
getWorkspacesInfo: wrap(getWorkspacesInfo),
|
||||
getLoginInfoByToken: wrap(getLoginInfoByToken),
|
||||
getLoginWithWorkspaceInfo: wrap(getLoginWithWorkspaceInfo),
|
||||
getSocialIds: wrap(getSocialIds),
|
||||
getPerson: wrap(getPerson),
|
||||
findPersonBySocialKey: wrap(findPersonBySocialKey),
|
||||
|
||||
+34
-12
@@ -14,22 +14,23 @@
|
||||
//
|
||||
import {
|
||||
AccountRole,
|
||||
type Person,
|
||||
BackupStatus,
|
||||
Branding,
|
||||
Data,
|
||||
MeasureContext,
|
||||
Timestamp,
|
||||
Version,
|
||||
WorkspaceMode,
|
||||
WorkspaceMemberInfo,
|
||||
BackupStatus,
|
||||
type SocialId as SocialIdBase,
|
||||
type PersonUuid,
|
||||
type WorkspaceUuid,
|
||||
type WorkspaceDataId,
|
||||
WorkspaceMode,
|
||||
type AccountUuid,
|
||||
type Person,
|
||||
type PersonId,
|
||||
type AccountUuid
|
||||
type PersonUuid,
|
||||
type SocialId as SocialIdBase,
|
||||
type WorkspaceDataId,
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import type { EndpointInfo } from './utils'
|
||||
|
||||
/* ========= D A T A B A S E E N T I T I E S ========= */
|
||||
export enum Location {
|
||||
@@ -77,13 +78,16 @@ export interface Member {
|
||||
role: AccountRole
|
||||
}
|
||||
|
||||
export interface WorkspaceStatus {
|
||||
workspaceUuid: WorkspaceUuid
|
||||
mode: WorkspaceMode
|
||||
processingProgress?: number
|
||||
export interface WorkspaceVersion {
|
||||
versionMajor: number
|
||||
versionMinor: number
|
||||
versionPatch: number
|
||||
}
|
||||
|
||||
export interface WorkspaceStatus extends WorkspaceVersion {
|
||||
workspaceUuid: WorkspaceUuid
|
||||
mode: WorkspaceMode
|
||||
processingProgress?: number
|
||||
lastProcessingTime?: Timestamp
|
||||
lastVisit?: Timestamp
|
||||
isDisabled: boolean
|
||||
@@ -196,6 +200,7 @@ export interface AccountDB {
|
||||
updateWorkspaceRole: (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise<void>
|
||||
unassignWorkspace: (accountId: AccountUuid, workspaceId: WorkspaceUuid) => Promise<void>
|
||||
getWorkspaceRole: (accountId: AccountUuid, workspaceId: WorkspaceUuid) => Promise<AccountRole | null>
|
||||
getWorkspaceRoles: (accountId: AccountUuid) => Promise<Map<WorkspaceUuid, AccountRole | null>>
|
||||
getWorkspaceMembers: (workspaceId: WorkspaceUuid) => Promise<WorkspaceMemberInfo[]>
|
||||
getAccountWorkspaces: (accountId: AccountUuid) => Promise<WorkspaceInfoWithStatus[]>
|
||||
getPendingWorkspace: (
|
||||
@@ -276,6 +281,23 @@ export interface LoginInfo {
|
||||
token?: string
|
||||
}
|
||||
|
||||
export interface LoginInfoWorkspace {
|
||||
url: string
|
||||
dataId?: WorkspaceDataId
|
||||
mode: WorkspaceMode
|
||||
version: WorkspaceVersion
|
||||
endpoint: EndpointInfo
|
||||
role: AccountRole | null
|
||||
|
||||
progress?: number
|
||||
}
|
||||
|
||||
export interface LoginInfoWithWorkspaces extends LoginInfo {
|
||||
// Information necessary to handle user <--> transactor connectivity.
|
||||
workspaces: Record<WorkspaceUuid, LoginInfoWorkspace>
|
||||
socialIds: SocialId[]
|
||||
}
|
||||
|
||||
export interface WorkspaceLoginInfo extends LoginInfo {
|
||||
workspace: WorkspaceUuid
|
||||
workspaceUrl: string
|
||||
|
||||
+62
-42
@@ -13,54 +13,54 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
import {
|
||||
AccountRole,
|
||||
AccountUuid,
|
||||
Branding,
|
||||
concatLink,
|
||||
generateId,
|
||||
groupByArray,
|
||||
isActiveMode,
|
||||
MeasureContext,
|
||||
AccountRole,
|
||||
roleOrder,
|
||||
SocialIdType,
|
||||
WorkspaceUuid,
|
||||
WorkspaceMode,
|
||||
SocialKey,
|
||||
systemAccountUuid,
|
||||
type WorkspaceInfoWithStatus as WorkspaceInfoWithStatusCore,
|
||||
isActiveMode,
|
||||
type PersonUuid,
|
||||
type PersonId,
|
||||
WorkspaceMode,
|
||||
WorkspaceUuid,
|
||||
type Person,
|
||||
AccountUuid
|
||||
type PersonId,
|
||||
type PersonUuid,
|
||||
type WorkspaceInfoWithStatus as WorkspaceInfoWithStatusCore
|
||||
} from '@hcengineering/core'
|
||||
import { getMongoClient } from '@hcengineering/mongo' // TODO: get rid of this import later
|
||||
import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform'
|
||||
import { getDBClient } from '@hcengineering/postgres'
|
||||
import otpGenerator from 'otp-generator'
|
||||
import { pbkdf2Sync, randomBytes } from 'crypto'
|
||||
import otpGenerator from 'otp-generator'
|
||||
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { sharedPipelineContextVars } from '@hcengineering/server-pipeline'
|
||||
import { decodeTokenVerbose, generateToken, TokenError } from '@hcengineering/server-token'
|
||||
import { MongoAccountDB } from './collections/mongo'
|
||||
import { PostgresAccountDB } from './collections/postgres'
|
||||
import { accountPlugin } from './plugin'
|
||||
import { sharedPipelineContextVars } from '@hcengineering/server-pipeline'
|
||||
import {
|
||||
AccountEventType,
|
||||
AccountMethodHandler,
|
||||
Integration,
|
||||
LoginInfo,
|
||||
Meta,
|
||||
OtpInfo,
|
||||
WorkspaceInvite,
|
||||
WorkspaceInfoWithStatus,
|
||||
WorkspaceInvite,
|
||||
WorkspaceLoginInfo,
|
||||
WorkspaceStatus,
|
||||
type Account,
|
||||
type AccountDB,
|
||||
type RegionInfo,
|
||||
type SocialId,
|
||||
type Workspace,
|
||||
LoginInfo,
|
||||
WorkspaceLoginInfo,
|
||||
WorkspaceStatus,
|
||||
AccountEventType,
|
||||
Meta,
|
||||
Integration
|
||||
type Workspace
|
||||
} from './types'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { TokenError, decodeTokenVerbose, generateToken } from '@hcengineering/server-token'
|
||||
|
||||
export const GUEST_ACCOUNT = 'b6996120-416f-49cd-841e-e4a5d2e49c9b'
|
||||
|
||||
@@ -172,7 +172,7 @@ export enum EndpointKind {
|
||||
External
|
||||
}
|
||||
|
||||
const toTransactor = (line: string): { internalUrl: string, region: string, externalUrl: string } => {
|
||||
const toTransactor = (line: string): EndpointInfo => {
|
||||
const [internalUrl, externalUrl, region] = line
|
||||
.split(';')
|
||||
.map((it) => it.trim())
|
||||
@@ -236,33 +236,46 @@ export const _getRegions = (): RegionInfo[] => {
|
||||
return _regionInfo
|
||||
}
|
||||
|
||||
export const getEndpoint = (
|
||||
ctx: MeasureContext,
|
||||
workspace: string,
|
||||
region: string | undefined,
|
||||
kind: EndpointKind
|
||||
): string => {
|
||||
const byRegions = groupByArray(getEndpoints().map(toTransactor), (it) => it.region)
|
||||
let transactors = (byRegions.get(region ?? '') ?? [])
|
||||
.map((it) => (kind === EndpointKind.Internal ? it.internalUrl : it.externalUrl))
|
||||
.flat()
|
||||
export interface EndpointInfo {
|
||||
internalUrl: string
|
||||
externalUrl: string
|
||||
region: string
|
||||
}
|
||||
|
||||
export function getEndpointInfo (): Map<string, EndpointInfo[]> {
|
||||
return groupByArray(getEndpoints().map(toTransactor), (it) => it.region)
|
||||
}
|
||||
|
||||
export const selectKind = (kind: EndpointKind, it: EndpointInfo): string => {
|
||||
return kind === EndpointKind.Internal ? it.internalUrl : it.externalUrl
|
||||
}
|
||||
|
||||
export const getEndpoint = (workspace: WorkspaceUuid, region: string | undefined, kind: EndpointKind): string => {
|
||||
const hash = hashWorkspace(workspace)
|
||||
const _endpointInfo = getEndpointInfo()
|
||||
|
||||
let transactors = _endpointInfo.get(region ?? '') ?? []
|
||||
|
||||
// This is really bad
|
||||
if (transactors.length === 0) {
|
||||
ctx.error('No transactors for the target region, will use default region', { group: region })
|
||||
|
||||
transactors = (byRegions.get('') ?? [])
|
||||
.map((it) => (kind === EndpointKind.Internal ? it.internalUrl : it.externalUrl))
|
||||
.flat()
|
||||
console.warn('No transactors for the target region, will use default region', { group: region })
|
||||
transactors = _endpointInfo.get('') ?? []
|
||||
}
|
||||
|
||||
if (transactors.length === 0) {
|
||||
ctx.error('No transactors for the default region')
|
||||
throw new Error('Please provide transactor endpoint url')
|
||||
}
|
||||
|
||||
return selectKind(kind, transactors[Math.abs(hash % transactors.length)])
|
||||
}
|
||||
|
||||
export const getWorkspaceEndpoint = (
|
||||
info: Map<string, EndpointInfo[]>,
|
||||
workspace: WorkspaceUuid,
|
||||
region: string | undefined
|
||||
): EndpointInfo => {
|
||||
const hash = hashWorkspace(workspace)
|
||||
return transactors[Math.abs(hash % transactors.length)]
|
||||
const byRegion = info.get(region ?? '') ?? []
|
||||
return byRegion[Math.abs(hash % byRegion.length)]
|
||||
}
|
||||
|
||||
export function getAllTransactors (kind: EndpointKind): string[] {
|
||||
@@ -541,7 +554,7 @@ export async function selectWorkspace (
|
||||
// Guest mode select workspace
|
||||
return {
|
||||
account: accountUuid,
|
||||
endpoint: getEndpoint(ctx, workspace.uuid, workspace.region, getKind(workspace.region)),
|
||||
endpoint: getEndpoint(workspace.uuid, workspace.region, getKind(workspace.region)),
|
||||
token,
|
||||
workspace: workspace.uuid,
|
||||
workspaceUrl: workspace.url,
|
||||
@@ -572,7 +585,7 @@ export async function selectWorkspace (
|
||||
return {
|
||||
account: accountUuid,
|
||||
token: generateToken(accountUuid, workspace.uuid, extra),
|
||||
endpoint: getEndpoint(ctx, workspace.uuid, workspace.region, getKind(workspace.region)),
|
||||
endpoint: getEndpoint(workspace.uuid, workspace.region, getKind(workspace.region)),
|
||||
workspace: workspace.uuid,
|
||||
workspaceUrl: workspace.url,
|
||||
role: AccountRole.Owner
|
||||
@@ -604,7 +617,7 @@ export async function selectWorkspace (
|
||||
return {
|
||||
account: accountUuid,
|
||||
token: generateToken(accountUuid, workspace.uuid, extra),
|
||||
endpoint: getEndpoint(ctx, workspace.uuid, workspace.region, getKind(workspace.region)),
|
||||
endpoint: getEndpoint(workspace.uuid, workspace.region, getKind(workspace.region)),
|
||||
workspace: workspace.uuid,
|
||||
workspaceUrl: workspace.url,
|
||||
workspaceDataId: workspace.dataId,
|
||||
@@ -1413,6 +1426,13 @@ export async function getWorkspaceRole (
|
||||
return await db.getWorkspaceRole(account, workspace)
|
||||
}
|
||||
|
||||
export async function getWorkspaceRoles (
|
||||
db: AccountDB,
|
||||
account: AccountUuid
|
||||
): Promise<Map<WorkspaceUuid, AccountRole | null>> {
|
||||
return await db.getWorkspaceRoles(account)
|
||||
}
|
||||
|
||||
export function generatePassword (len: number = 24): string {
|
||||
return randomBytes(len).toString('base64').slice(0, len)
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ class BackupWorker {
|
||||
},
|
||||
getConnection: async () => {
|
||||
if (pipeline === undefined) {
|
||||
pipeline = await this.pipelineFactory(ctx, wsIds, true, () => {}, null, null)
|
||||
pipeline = await this.pipelineFactory(ctx, wsIds, () => {}, null, null)
|
||||
}
|
||||
return wrapPipeline(ctx, pipeline, wsIds)
|
||||
},
|
||||
@@ -484,7 +484,7 @@ export async function doRestoreWorkspace (
|
||||
cleanIndexState,
|
||||
getConnection: async () => {
|
||||
if (pipeline === undefined) {
|
||||
pipeline = await pipelineFactory(ctx, wsIds, true, () => {}, null, null)
|
||||
pipeline = await pipelineFactory(ctx, wsIds, () => {}, null, null)
|
||||
}
|
||||
return wrapPipeline(ctx, pipeline, wsIds)
|
||||
},
|
||||
|
||||
@@ -81,7 +81,7 @@ export function initStatisticsContext (
|
||||
logFile?: string
|
||||
logConsole?: boolean
|
||||
factory?: () => MeasureMetricsContext
|
||||
getUsers?: () => WorkspaceStatistics[]
|
||||
getStats?: () => WorkspaceStatistics[]
|
||||
statsUrl?: string
|
||||
serviceName?: () => string
|
||||
}
|
||||
@@ -144,7 +144,7 @@ export function initStatisticsContext (
|
||||
cpu: getCPUInfo(),
|
||||
memory: getMemoryInfo(),
|
||||
stats: metricsContext.metrics,
|
||||
workspaces: ops?.getUsers?.()
|
||||
workspaces: ops?.getStats?.()
|
||||
}
|
||||
|
||||
const statData = JSON.stringify(data)
|
||||
|
||||
+12
-52
@@ -46,12 +46,12 @@ import {
|
||||
type SearchQuery,
|
||||
type SearchResult,
|
||||
type SessionData,
|
||||
type SocialId,
|
||||
type Space,
|
||||
type Timestamp,
|
||||
type Tx,
|
||||
type TxFactory,
|
||||
type TxResult,
|
||||
type WorkspaceDataId,
|
||||
type WorkspaceIds,
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
@@ -62,7 +62,7 @@ import type { Token } from '@hcengineering/server-token'
|
||||
import { type Readable } from 'stream'
|
||||
|
||||
import type { DbAdapter, DomainHelper } from './adapter'
|
||||
import type { StatisticsElement } from './stats'
|
||||
import type { StatisticsElement, WorkspaceStatistics } from './stats'
|
||||
import { type StorageAdapter } from './storage'
|
||||
import { type PlatformQueue } from './queue'
|
||||
|
||||
@@ -239,7 +239,6 @@ export interface Pipeline {
|
||||
export type PipelineFactory = (
|
||||
ctx: MeasureContext,
|
||||
ws: WorkspaceIds,
|
||||
upgrade: boolean,
|
||||
broadcast: BroadcastFunc,
|
||||
branding: Branding | null,
|
||||
communicationApi: CommunicationApi | null
|
||||
@@ -559,7 +558,7 @@ export interface ClientSessionCtx {
|
||||
* @public
|
||||
*/
|
||||
export interface Session {
|
||||
workspace: Workspace
|
||||
workspace: WorkspaceIds
|
||||
createTime: number
|
||||
|
||||
// Session restore information
|
||||
@@ -587,8 +586,11 @@ export interface Session {
|
||||
// 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
|
||||
@@ -663,61 +665,25 @@ export function disableLogging (): void {
|
||||
LOGGING_ENABLED = false
|
||||
}
|
||||
|
||||
interface TickHandler {
|
||||
ticks: number
|
||||
operation: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Workspace {
|
||||
context: MeasureContext
|
||||
id: string
|
||||
token: string // Account workspace update token.
|
||||
pipeline: Promise<Pipeline> | Pipeline
|
||||
communicationApi: Promise<CommunicationApi> | CommunicationApi
|
||||
|
||||
tickHash: number
|
||||
|
||||
tickHandlers: Map<string, TickHandler>
|
||||
|
||||
sessions: Map<string, { session: Session, socket: ConnectionSocket, tickHash: number }>
|
||||
upgrade: boolean
|
||||
|
||||
closing?: Promise<void>
|
||||
softShutdown: number
|
||||
workspaceInitCompleted: boolean
|
||||
|
||||
workspaceName: string
|
||||
workspaceUuid: WorkspaceUuid
|
||||
workspaceUrl: string
|
||||
workspaceDataId?: WorkspaceDataId
|
||||
branding: Branding | null
|
||||
}
|
||||
|
||||
export interface AddSessionActive {
|
||||
session: Session
|
||||
context: MeasureContext
|
||||
workspaceId: WorkspaceUuid
|
||||
}
|
||||
|
||||
export type AddSessionResponse =
|
||||
| AddSessionActive
|
||||
export type GetWorkspaceResponse =
|
||||
| { upgrade: true, progress?: number }
|
||||
| { error: any, terminate?: boolean, specialError?: 'archived' | 'migration' }
|
||||
|
||||
export type SessionFactory = (token: Token, workspace: Workspace, account: Account) => Session
|
||||
export type AddSessionResponse = AddSessionActive | GetWorkspaceResponse
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SessionManager {
|
||||
workspaces: Map<WorkspaceUuid, Workspace>
|
||||
// workspaces: Map<WorkspaceUuid, Workspace>
|
||||
sessions: Map<string, { session: Session, socket: ConnectionSocket }>
|
||||
|
||||
createSession: SessionFactory
|
||||
|
||||
addSession: (
|
||||
ctx: MeasureContext,
|
||||
ws: ConnectionSocket,
|
||||
@@ -726,18 +692,10 @@ export interface SessionManager {
|
||||
sessionId: string | undefined
|
||||
) => Promise<AddSessionResponse>
|
||||
|
||||
broadcastAll: (workspace: Workspace, tx: Tx[], targets?: string[]) => void
|
||||
broadcastAll: (workspace: WorkspaceUuid, tx: Tx[], targets?: string[]) => void
|
||||
|
||||
close: (ctx: MeasureContext, ws: ConnectionSocket, workspaceId: WorkspaceUuid) => Promise<void>
|
||||
|
||||
closeAll: (
|
||||
wsId: WorkspaceUuid,
|
||||
workspace: Workspace,
|
||||
code: number,
|
||||
reason: 'upgrade' | 'shutdown',
|
||||
ignoreSocket?: ConnectionSocket
|
||||
) => Promise<void>
|
||||
|
||||
forceClose: (wsId: WorkspaceUuid, ignoreSocket?: ConnectionSocket) => Promise<void>
|
||||
|
||||
closeWorkspaces: (ctx: MeasureContext) => Promise<void>
|
||||
@@ -773,6 +731,8 @@ export interface SessionManager {
|
||||
service: Session,
|
||||
ws: ConnectionSocket
|
||||
) => ClientSessionCtx
|
||||
|
||||
getStatistics: () => WorkspaceStatistics[]
|
||||
}
|
||||
|
||||
export const pingConst = 'ping'
|
||||
|
||||
@@ -7,7 +7,6 @@ import core, {
|
||||
type Account,
|
||||
type AccountUuid,
|
||||
type BackupClient,
|
||||
type Branding,
|
||||
type BrandingMap,
|
||||
type BulkUpdateEvent,
|
||||
type Class,
|
||||
@@ -166,7 +165,6 @@ export class SessionDataImpl implements SessionData {
|
||||
readonly admin: boolean | undefined,
|
||||
_broadcast: SessionData['broadcast'] | undefined,
|
||||
readonly workspace: WorkspaceIds,
|
||||
readonly branding: Branding | null,
|
||||
readonly isAsyncContext: boolean,
|
||||
_removedMap: Map<Ref<Doc>, Doc> | undefined,
|
||||
_contextCache: Map<string, any> | undefined,
|
||||
@@ -244,7 +242,6 @@ export function wrapPipeline (
|
||||
true,
|
||||
{ targets: {}, txes: [] },
|
||||
wsIds,
|
||||
null,
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
@@ -559,7 +559,6 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
true,
|
||||
undefined,
|
||||
this.workspace,
|
||||
null,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
@@ -221,7 +221,6 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
|
||||
sctx.admin,
|
||||
{ txes: [], targets: {} },
|
||||
this.context.workspace,
|
||||
this.context.branding,
|
||||
true,
|
||||
sctx.removedMap,
|
||||
sctx.contextCache,
|
||||
|
||||
@@ -110,7 +110,7 @@ export function createServerPipeline (
|
||||
},
|
||||
extensions?: Partial<DbConfiguration>
|
||||
): PipelineFactory {
|
||||
return (ctx, workspace, upgrade, broadcast, branding, communicationApi) => {
|
||||
return (ctx, workspace, broadcast, branding, communicationApi) => {
|
||||
const metricsCtx = opt.usePassedCtx === true ? ctx : metrics
|
||||
const wsMetrics = metricsCtx.newChild('🧲 session', {})
|
||||
const conf = getConfig(metrics, dbUrl, wsMetrics, opt, extensions)
|
||||
@@ -176,7 +176,7 @@ export function createBackupPipeline (
|
||||
externalStorage: StorageAdapter
|
||||
}
|
||||
): PipelineFactory {
|
||||
return (ctx, workspace, upgrade, broadcast, branding, communicationApi) => {
|
||||
return (ctx, workspace, broadcast, branding, communicationApi) => {
|
||||
const metricsCtx = opt.usePassedCtx === true ? ctx : metrics
|
||||
const wsMetrics = metricsCtx.newChild('🧲 backup', {})
|
||||
const conf = getConfig(metrics, dbUrl, wsMetrics, {
|
||||
@@ -229,7 +229,7 @@ export async function getServerPipeline (
|
||||
})
|
||||
|
||||
// TODO: Communication API ??
|
||||
return await pipelineFactory(ctx, wsUrl, true, () => {}, null, null)
|
||||
return await pipelineFactory(ctx, wsUrl, () => {}, null, null)
|
||||
}
|
||||
|
||||
const txAdapterFactories: Record<string, DbAdapterFactory> = {}
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/rpc": "^0.6.5",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/server-token": "^0.6.11"
|
||||
"@hcengineering/server-token": "^0.6.11",
|
||||
"utf-8-validate": "^6.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
+27
-22
@@ -13,6 +13,21 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { LoginInfoWithWorkspaces } from '@hcengineering/account-client'
|
||||
import {
|
||||
RequestEvent as CommunicationEvent,
|
||||
SessionData as CommunicationSession,
|
||||
EventResult
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
import {
|
||||
FindLabelsParams,
|
||||
FindMessagesGroupsParams,
|
||||
FindMessagesParams,
|
||||
FindNotificationContextParams,
|
||||
FindNotificationsParams,
|
||||
Message,
|
||||
MessagesGroup
|
||||
} from '@hcengineering/communication-types'
|
||||
import {
|
||||
AccountUuid,
|
||||
generateId,
|
||||
@@ -32,11 +47,13 @@ import {
|
||||
type SearchQuery,
|
||||
type SearchResult,
|
||||
type SessionData,
|
||||
type SocialId,
|
||||
type Timestamp,
|
||||
type Tx,
|
||||
type TxCUD,
|
||||
type TxResult,
|
||||
type WorkspaceDataId
|
||||
type WorkspaceDataId,
|
||||
type WorkspaceIds
|
||||
} from '@hcengineering/core'
|
||||
import { PlatformError, unknownError } from '@hcengineering/platform'
|
||||
import {
|
||||
@@ -48,24 +65,9 @@ import {
|
||||
type Pipeline,
|
||||
type Session,
|
||||
type SessionRequest,
|
||||
type StatisticsElement,
|
||||
type Workspace
|
||||
type StatisticsElement
|
||||
} from '@hcengineering/server-core'
|
||||
import { type Token } from '@hcengineering/server-token'
|
||||
import {
|
||||
FindMessagesGroupsParams,
|
||||
FindMessagesParams,
|
||||
Message,
|
||||
MessagesGroup,
|
||||
FindNotificationContextParams,
|
||||
FindNotificationsParams,
|
||||
FindLabelsParams
|
||||
} from '@hcengineering/communication-types'
|
||||
import {
|
||||
RequestEvent as CommunicationEvent,
|
||||
SessionData as CommunicationSession,
|
||||
EventResult
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
|
||||
const useReserveContext = (process.env.USE_RESERVE_CTX ?? 'true') === 'true'
|
||||
|
||||
@@ -93,8 +95,9 @@ export class ClientSession implements Session {
|
||||
|
||||
constructor (
|
||||
protected readonly token: Token,
|
||||
readonly workspace: Workspace,
|
||||
readonly workspace: WorkspaceIds,
|
||||
readonly account: Account,
|
||||
readonly info: LoginInfoWithWorkspaces,
|
||||
readonly allowUpload: boolean
|
||||
) {
|
||||
this.isAdmin = this.token.extra?.admin === 'true'
|
||||
@@ -108,6 +111,10 @@ export class ClientSession implements Session {
|
||||
return this.account.socialIds
|
||||
}
|
||||
|
||||
getSocialIds (): SocialId[] {
|
||||
return this.info.socialIds
|
||||
}
|
||||
|
||||
getRawAccount (): Account {
|
||||
return this.account
|
||||
}
|
||||
@@ -142,18 +149,16 @@ export class ClientSession implements Session {
|
||||
}
|
||||
|
||||
includeSessionContext (ctx: ClientSessionCtx): void {
|
||||
const dataId = this.workspace.workspaceDataId ?? (this.workspace.workspaceUuid as unknown as WorkspaceDataId)
|
||||
const dataId = this.workspace.dataId ?? (this.workspace.uuid as unknown as WorkspaceDataId)
|
||||
const contextData = new SessionDataImpl(
|
||||
this.account,
|
||||
this.sessionId,
|
||||
this.isAdmin,
|
||||
undefined,
|
||||
{
|
||||
uuid: this.workspace.workspaceUuid,
|
||||
url: this.workspace.workspaceUrl,
|
||||
...this.workspace,
|
||||
dataId
|
||||
},
|
||||
this.workspace.branding,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
+359
-441
File diff suppressed because it is too large
Load Diff
@@ -20,22 +20,8 @@ export function getStatistics (ctx: MeasureContext, sessions: SessionManager, ad
|
||||
}
|
||||
data.statistics.totalClients = sessions.sessions.size
|
||||
if (admin) {
|
||||
for (const [k, vv] of sessions.workspaces) {
|
||||
data.statistics.activeSessions[k] = {
|
||||
sessions: Array.from(vv.sessions.entries()).map(([k, v]) => ({
|
||||
userId: v.session.getUser(),
|
||||
data: v.socket.data(),
|
||||
mins5: v.session.mins5,
|
||||
total: v.session.total,
|
||||
current: v.session.current,
|
||||
upgrade: v.session.isUpgradeClient()
|
||||
})),
|
||||
name: vv.workspaceName,
|
||||
wsId: vv.workspaceUuid,
|
||||
sessionsTotal: vv.sessions.size,
|
||||
upgrading: vv.upgrade,
|
||||
closing: vv.closing !== undefined
|
||||
}
|
||||
for (const wsStats of sessions.getStatistics()) {
|
||||
data.statistics.activeSessions[wsStats.wsId] = wsStats
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
//
|
||||
// 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 { Analytics } from '@hcengineering/analytics'
|
||||
import { type ServerApi as CommunicationApi } from '@hcengineering/communication-sdk-types'
|
||||
import { type Branding, type MeasureContext, type WorkspaceIds } from '@hcengineering/core'
|
||||
import type { ConnectionSocket, Pipeline, Session } from '@hcengineering/server-core'
|
||||
|
||||
interface TickHandler {
|
||||
ticks: number
|
||||
operation: () => void
|
||||
}
|
||||
|
||||
export interface PipelinePair {
|
||||
pipeline: Pipeline
|
||||
communicationApi: CommunicationApi
|
||||
}
|
||||
export type WorkspacePipelineFactory = () => Promise<PipelinePair>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class Workspace {
|
||||
pipeline?: PipelinePair | Promise<PipelinePair>
|
||||
upgrade: boolean = false
|
||||
closing?: Promise<void>
|
||||
|
||||
workspaceInitCompleted: boolean = false
|
||||
|
||||
softShutdown: number
|
||||
|
||||
sessions = new Map<string, { session: Session, socket: ConnectionSocket, tickHash: number }>()
|
||||
tickHandlers = new Map<string, TickHandler>()
|
||||
|
||||
operations: number = 0
|
||||
|
||||
constructor (
|
||||
readonly context: MeasureContext,
|
||||
readonly token: string, // Account workspace update token.
|
||||
readonly factory: WorkspacePipelineFactory,
|
||||
|
||||
readonly tickHash: number,
|
||||
|
||||
softShutdown: number,
|
||||
|
||||
readonly wsId: WorkspaceIds,
|
||||
readonly branding: Branding | null
|
||||
) {
|
||||
this.softShutdown = softShutdown
|
||||
}
|
||||
|
||||
private getPipelinePair (): PipelinePair | Promise<PipelinePair> {
|
||||
if (this.pipeline === undefined) {
|
||||
this.pipeline = this.factory()
|
||||
}
|
||||
return this.pipeline
|
||||
}
|
||||
|
||||
async with<T>(op: (pipeline: Pipeline, communicationApi: CommunicationApi) => Promise<T>): Promise<T> {
|
||||
this.operations++
|
||||
let pair = this.getPipelinePair()
|
||||
if (pair instanceof Promise) {
|
||||
pair = await pair
|
||||
this.pipeline = pair
|
||||
}
|
||||
try {
|
||||
return await op(pair.pipeline, pair.communicationApi)
|
||||
} finally {
|
||||
this.operations--
|
||||
}
|
||||
}
|
||||
|
||||
async close (ctx: MeasureContext): Promise<void> {
|
||||
if (this.pipeline === undefined) {
|
||||
return
|
||||
}
|
||||
const { pipeline, communicationApi } = await this.pipeline
|
||||
const closePipeline = async (): Promise<void> => {
|
||||
try {
|
||||
await ctx.with('close-pipeline', {}, async () => {
|
||||
await pipeline.close()
|
||||
})
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
ctx.error('close-pipeline-error', { error: err })
|
||||
}
|
||||
}
|
||||
|
||||
const closeCommunicationApi = async (): Promise<void> => {
|
||||
try {
|
||||
await ctx.with('close-communication-api', {}, async () => {
|
||||
await communicationApi.close()
|
||||
})
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
ctx.error('close-pipeline-error', { error: err })
|
||||
}
|
||||
}
|
||||
await ctx.with('closing', {}, async () => {
|
||||
const to = timeoutPromise(120000)
|
||||
const closePromises = [closePipeline(), closeCommunicationApi()]
|
||||
await Promise.race([Promise.all(closePromises), to.promise])
|
||||
to.cancelHandle()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function timeoutPromise (time: number): { promise: Promise<void>, cancelHandle: () => void } {
|
||||
let timer: any
|
||||
return {
|
||||
promise: new Promise((resolve) => {
|
||||
timer = setTimeout(resolve, time)
|
||||
}),
|
||||
cancelHandle: () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,7 +318,6 @@ export async function upgradeWorkspaceWith (
|
||||
true,
|
||||
undefined,
|
||||
wsIds,
|
||||
null,
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
Reference in New Issue
Block a user